diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 6b8ecd6..5af7f22 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -18,6 +18,7 @@ add_subdirectory(./solanaceae_tox) add_subdirectory(./sdl) add_subdirectory(./imgui) +add_subdirectory(./implot) add_subdirectory(./stb) add_subdirectory(./libwebp) diff --git a/external/implot/CMakeLists.txt b/external/implot/CMakeLists.txt new file mode 100644 index 0000000..bf3c693 --- /dev/null +++ b/external/implot/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) + +include(FetchContent) + +if (NOT TARGET implot) + FetchContent_Declare(implot + GIT_REPOSITORY https://github.com/epezent/implot.git + GIT_TAG 47522f47054d33178e7defa780042bd2a06b09f9 # 22-01-2025 + EXCLUDE_FROM_ALL + CONFIGURE_COMMAND "" # no cmake + ) + + FetchContent_GetProperties(implot) + if(NOT implot_POPULATED) + FetchContent_MakeAvailable(implot) + + add_library(implot STATIC + ${implot_SOURCE_DIR}/implot.h + ${implot_SOURCE_DIR}/implot_internal.h + + ${implot_SOURCE_DIR}/implot.cpp + ${implot_SOURCE_DIR}/implot_demo.cpp + ${implot_SOURCE_DIR}/implot_items.cpp + ) + target_include_directories(implot PUBLIC ${implot_SOURCE_DIR}) + target_compile_features(implot PUBLIC cxx_std_11) + target_link_libraries(implot PUBLIC imgui) + #target_compile_definitions(implot PUBLIC IMGUI_USE_WCHAR32) + endif() +endif() + diff --git a/flake.lock b/flake.lock index bd49f48..05cdcc3 100644 --- a/flake.lock +++ b/flake.lock @@ -18,6 +18,23 @@ "type": "github" } }, + "implot": { + "flake": false, + "locked": { + "lastModified": 1737544983, + "narHash": "sha256-/hYfmkUa5zRb2iQHdw0FhNPBaUHqzEhA0lHDeNE8bkU=", + "owner": "epezent", + "repo": "implot", + "rev": "47522f47054d33178e7defa780042bd2a06b09f9", + "type": "github" + }, + "original": { + "owner": "epezent", + "repo": "implot", + "rev": "47522f47054d33178e7defa780042bd2a06b09f9", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1720691131, @@ -54,6 +71,7 @@ "root": { "inputs": { "flake-utils": "flake-utils", + "implot": "implot", "nixpkgs": "nixpkgs", "nlohmann-json": "nlohmann-json", "sdl3": "sdl3", diff --git a/flake.nix b/flake.nix index ceeca0e..ab7a3fd 100644 --- a/flake.nix +++ b/flake.nix @@ -20,9 +20,13 @@ url = "github:libsdl-org/SDL_image/6f4584340b9b78542d11bf0232a1a0862de1f0a9"; flake = false; }; + implot = { + url = "github:epezent/implot/47522f47054d33178e7defa780042bd2a06b09f9"; + flake = false; + }; }; - outputs = { self, nixpkgs, flake-utils, nlohmann-json, sdl3, sdl3_image }: + outputs = { self, nixpkgs, flake-utils, nlohmann-json, sdl3, sdl3_image, implot }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; @@ -91,13 +95,14 @@ #"-DCMAKE_C_FLAGS:STRING=-Og" #"-DCMAKE_CXX_FLAGS:STRING=-Og" - "-DFETCHCONTENT_SOURCE_DIR_JSON=${nlohmann-json}" # we care about the version # TODO: use package instead + "-DFETCHCONTENT_SOURCE_DIR_JSON=${nlohmann-json}" # we care about the version "-DFETCHCONTENT_SOURCE_DIR_ZSTD=${pkgs.zstd.src}" # we dont care about the version (we use 1.4.x features) "-DFETCHCONTENT_SOURCE_DIR_LIBWEBP=${pkgs.libwebp.src}" "-DFETCHCONTENT_SOURCE_DIR_SDL3=${sdl3}" "-DFETCHCONTENT_SOURCE_DIR_SDL3_IMAGE=${sdl3_image}" "-DSDLIMAGE_JXL=ON" + "-DFETCHCONTENT_SOURCE_DIR_IMPLOT=${implot}" # could use pkgs.implot.src for outdated version ]; # TODO: replace with install command diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8830fe4..39de9fc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -74,6 +74,7 @@ target_sources(tomato PUBLIC ./sys_tray.hpp ./sys_tray.cpp + ./string_formatter_utils.hpp ./chat_gui/theme.hpp ./chat_gui/theme.cpp ./chat_gui/icons/direct.hpp @@ -182,6 +183,7 @@ target_link_libraries(tomato PUBLIC imgui imgui_backend_sdl3 imgui_backend_sdlrenderer3 + implot stb_image stb_image_write diff --git a/src/chat_gui4.cpp b/src/chat_gui4.cpp index 1618c88..9c955a4 100644 --- a/src/chat_gui4.cpp +++ b/src/chat_gui4.cpp @@ -26,6 +26,8 @@ #include "./sdl_clipboard_utils.hpp" #include "os_comps.hpp" +#include "./string_formatter_utils.hpp" + #include #include #include @@ -72,67 +74,6 @@ static constexpr float lerp(float a, float b, float t) { return a + t * (b - a); } -// returns divider and places static suffix string into suffix_out -static int64_t sizeToHumanReadable(int64_t file_size, const char*& suffix_out) { - static const char* suffix_arr[] { - "Bytes", - "KiB", - "MiB", - "GiB", - "TiB", - "PiB", - // TODO: fix upper bound behaviour - }; - int64_t divider = 1024; - for (size_t ij = 0; ij < std::size(suffix_arr); ij++, divider *= 1024) { - if (file_size < divider) { - suffix_out = suffix_arr[ij]; - break; - } - } - - return (divider > 1024) ? (divider / 1024) : 1; -} - -// returns divider and places static suffix string into suffix_out -static int64_t durationToHumanReadable(int64_t t, const char*& suffix_out) { - static const char* suffix_arr[] { - "ms", - "s", - "min", - "h", - "d", - "a", - }; - static const int64_t divider_arr[] { - 1000, // ms -> s - 60, // s -> min - 60, // min -> h - 24, // h -> d - 256, // d -> a // aprox - }; - - if (t <= 0) { - suffix_out = suffix_arr[0]; - return 1; - } - - int64_t divider {1}; - for (size_t i = 0; i < std::size(divider_arr); i++) { - if (t < divider * divider_arr[i]) { - suffix_out = suffix_arr[i]; - return divider; - } - - divider *= divider_arr[i]; - } - - // if we are here, we are in the last element - // 5 and 4 - suffix_out = suffix_arr[5]; - return divider; -} - static std::string file_path_url_escape(const std::string&& value) { std::ostringstream escaped; diff --git a/src/main.cpp b/src/main.cpp index 1a2b02d..109be5e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "./theme.hpp" #include "./chat_gui/theme.hpp" @@ -87,6 +88,7 @@ int main(int argc, char** argv) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); + ImPlot::CreateContext(); // TODO: test android behaviour float display_scale = SDL_GetWindowDisplayScale(window.get()); @@ -238,6 +240,7 @@ int main(int argc, char** argv) { ImGui_ImplSDLRenderer3_Shutdown(); ImGui_ImplSDL3_Shutdown(); + ImPlot::DestroyContext(); ImGui::DestroyContext(); renderer.reset(); diff --git a/src/main_screen.cpp b/src/main_screen.cpp index 02520bd..af3f450 100644 --- a/src/main_screen.cpp +++ b/src/main_screen.cpp @@ -9,6 +9,7 @@ #include "./frame_streams/sdl/sdl_video_frame_stream2.hpp" #include +#include #include @@ -123,6 +124,7 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme g_provideInstance("ImGuiMemUserData", ImGui::GetVersion(), "host", user_data); } } + g_provideInstance("ImPlotContext", IMPLOT_VERSION, "host", ImPlot::GetCurrentContext()); g_provideInstance("TextureUploaderI", "host", &sdlrtu); } diff --git a/src/string_formatter_utils.hpp b/src/string_formatter_utils.hpp new file mode 100644 index 0000000..22d8669 --- /dev/null +++ b/src/string_formatter_utils.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +// returns divider and places static suffix string into suffix_out +static int64_t sizeToHumanReadable(int64_t file_size, const char*& suffix_out) { + static const char* suffix_arr[] { + "Bytes", + "KiB", + "MiB", + "GiB", + "TiB", + "PiB", + // TODO: fix upper bound behaviour + }; + int64_t divider = 1024; + for (size_t ij = 0; ij < std::size(suffix_arr); ij++, divider *= 1024) { + if (file_size < divider) { + suffix_out = suffix_arr[ij]; + break; + } + } + + return (divider > 1024) ? (divider / 1024) : 1; +} + +// returns divider and places static suffix string into suffix_out +static int64_t durationToHumanReadable(int64_t t, const char*& suffix_out) { + static const char* suffix_arr[] { + "ms", + "s", + "min", + "h", + "d", + "a", + }; + static const int64_t divider_arr[] { + 1000, // ms -> s + 60, // s -> min + 60, // min -> h + 24, // h -> d + 256, // d -> a // aprox + }; + + if (t <= 0) { + suffix_out = suffix_arr[0]; + return 1; + } + + int64_t divider {1}; + for (size_t i = 0; i < std::size(divider_arr); i++) { + if (t < divider * divider_arr[i]) { + suffix_out = suffix_arr[i]; + return divider; + } + + divider *= divider_arr[i]; + } + + // if we are here, we are in the last element + // 5 and 4 + suffix_out = suffix_arr[5]; + return divider; +} + diff --git a/src/tox_dht_cap_histo.cpp b/src/tox_dht_cap_histo.cpp index e6233bc..64a30fb 100644 --- a/src/tox_dht_cap_histo.cpp +++ b/src/tox_dht_cap_histo.cpp @@ -1,6 +1,7 @@ #include "./tox_dht_cap_histo.hpp" #include +#include void ToxDHTCapHisto::tick(float time_delta) { if (!_enabled) { @@ -54,8 +55,19 @@ void ToxDHTCapHisto::render(void) { if (_show_window) { if (ImGui::Begin("Tox DHT announce capability histogram", &_show_window)) { - if (_enabled) { - ImGui::PlotHistogram("##histogram", _ratios.data(), _ratios.size(), 0, nullptr, 0.f, 1.f, {-1, -1}); + if (_enabled && ImPlot::BeginPlot("##caphisto")) { + ImPlot::SetupAxis(ImAxis_X1, "seconds", ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 1, ImPlotCond_Always); + + // TODO: fix colors + + ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.25f); + ImPlot::PlotShaded("##ratio_shade", _ratios.data(), _ratios.size()); + ImPlot::PopStyleVar(); + + ImPlot::PlotLine("##ratio", _ratios.data(), _ratios.size()); + + ImPlot::EndPlot(); } else { ImGui::TextUnformatted("logging disabled!"); if (ImGui::Button("enable")) { diff --git a/src/tox_netprof_ui.cpp b/src/tox_netprof_ui.cpp index daf5aec..ace710c 100644 --- a/src/tox_netprof_ui.cpp +++ b/src/tox_netprof_ui.cpp @@ -1,7 +1,12 @@ #include "./tox_netprof_ui.hpp" #include +#include + #include +#include + +#include "./string_formatter_utils.hpp" static const char* typedPkgIDToString(Tox_Netprof_Packet_Type type, uint8_t id) { // pain @@ -76,24 +81,6 @@ void ToxNetprofUI::tick(float time_delta) { _time_since_last_add -= _value_add_interval; } - if (_udp_tctx.empty()) { - _udp_tctx.push_back(0.f); - _udp_tctx_prev = _tpi.toxNetprofGetPacketTotalCount(TOX_NETPROF_PACKET_TYPE_UDP, TOX_NETPROF_DIRECTION_SENT); - } else { - const auto new_value = _tpi.toxNetprofGetPacketTotalCount(TOX_NETPROF_PACKET_TYPE_UDP, TOX_NETPROF_DIRECTION_SENT); - _udp_tctx.push_back(new_value - _udp_tctx_prev); - _udp_tctx_prev = new_value; - } - - if (_udp_tcrx.empty()) { - _udp_tcrx.push_back(0.f); - _udp_tcrx_prev = _tpi.toxNetprofGetPacketTotalCount(TOX_NETPROF_PACKET_TYPE_UDP, TOX_NETPROF_DIRECTION_RECV); - } else { - const auto new_value = _tpi.toxNetprofGetPacketTotalCount(TOX_NETPROF_PACKET_TYPE_UDP, TOX_NETPROF_DIRECTION_RECV); - _udp_tcrx.push_back(new_value - _udp_tcrx_prev); - _udp_tcrx_prev = new_value; - } - if (_udp_tbtx.empty()) { _udp_tbtx.push_back(0.f); _udp_tbtx_prev = _tpi.toxNetprofGetPacketTotalBytes(TOX_NETPROF_PACKET_TYPE_UDP, TOX_NETPROF_DIRECTION_SENT); @@ -113,14 +100,6 @@ void ToxNetprofUI::tick(float time_delta) { } // TODO: limit - while (_udp_tctx.size() > 5*60) { - _udp_tctx.erase(_udp_tctx.begin()); - } - - while (_udp_tcrx.size() > 5*60) { - _udp_tcrx.erase(_udp_tcrx.begin()); - } - while (_udp_tbtx.size() > 5*60) { _udp_tbtx.erase(_udp_tbtx.begin()); } @@ -303,31 +282,54 @@ float ToxNetprofUI::render(float time_delta) { } if (_show_window_histo) { - if (ImGui::Begin("Tox Netprof histograms", &_show_window_histo)) { - if (_enabled) { - const float line_height = ImGui::GetTextLineHeight(); - ImGui::PlotHistogram("udp total packets sent##histograms", _udp_tctx.data(), _udp_tctx.size(), 0, nullptr, 0.f, FLT_MAX, {0, 3*line_height}); - ImGui::PlotHistogram("udp total packets received##histograms", _udp_tcrx.data(), _udp_tcrx.size(), 0, nullptr, 0.f, FLT_MAX, {0, 3*line_height}); + if (ImGui::Begin("Tox Netprof graph", &_show_window_histo)) { + if (_enabled && ImPlot::BeginPlot("##plot")) { + ImPlot::SetupAxes(nullptr, "bytes", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - std::string udp_tbtx_avg_str {"avg " + std::to_string(_udp_tbtx_avg) + " B/s"}; - ImGui::PlotHistogram( - "udp total bytes sent##histograms", - _udp_tbtx.data(), _udp_tbtx.size(), - 0, - udp_tbtx_avg_str.c_str(), - 0.f, FLT_MAX, - {0, 3*line_height} + ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0.f, INFINITY); + + // TODO: bytes tick interval of 1024 (or smaller like multiples of 2) + ImPlot::SetupAxisFormat( + ImAxis_Y1, + // TODO: extract + +[](double value, char* buff, int size, void*) -> int { + if (value < 0) { + return 0; + } + + const char* byte_suffix = "???"; + int64_t byte_divider = sizeToHumanReadable(value, byte_suffix); + + return snprintf(buff, size, "%g %s/s", value/byte_divider, byte_suffix); + }, + nullptr ); - std::string udp_tbrx_avg_str {"avg " + std::to_string(_udp_tbrx_avg) + " B/s"}; - ImGui::PlotHistogram( - "udp total bytes received##histograms", - _udp_tbrx.data(), _udp_tbrx.size(), - 0, - udp_tbrx_avg_str.c_str(), - 0.f, FLT_MAX, - {0, 3*line_height} - ); + { + ImPlot::PlotLine( + "udp tx/s", + _udp_tbtx.data(), _udp_tbtx.size(), + _value_add_interval // normalize to /s + ); + auto item_color = ImPlot::GetLastItemColor(); + item_color.w *= 0.5f; + ImPlot::SetNextLineStyle(item_color); + ImPlot::PlotInfLines("udp tx avg", &_udp_tbtx_avg, 1, ImPlotInfLinesFlags_Horizontal); + } + + { + ImPlot::PlotLine( + "udp rx/s", + _udp_tbrx.data(), _udp_tbrx.size(), + _value_add_interval // normalize to /s + ); + auto item_color = ImPlot::GetLastItemColor(); + item_color.w *= 0.5f; + ImPlot::SetNextLineStyle(item_color); + ImPlot::PlotInfLines("udp rx avg", &_udp_tbrx_avg, 1, ImPlotInfLinesFlags_Horizontal); + } + + ImPlot::EndPlot(); } else { ImGui::TextUnformatted("logging disabled!"); if (ImGui::Button("enable")) { diff --git a/src/tox_netprof_ui.hpp b/src/tox_netprof_ui.hpp index d532289..b978f61 100644 --- a/src/tox_netprof_ui.hpp +++ b/src/tox_netprof_ui.hpp @@ -34,12 +34,8 @@ class ToxNetprofUI { std::map _tcp_brx_heat; // histogram totals - uint64_t _udp_tctx_prev; - uint64_t _udp_tcrx_prev; uint64_t _udp_tbtx_prev; uint64_t _udp_tbrx_prev; - std::vector _udp_tctx; - std::vector _udp_tcrx; std::vector _udp_tbtx; std::vector _udp_tbrx;