From 7db3ba912b46aa8729e531f665ef9f660b6c89af Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sun, 26 Nov 2023 21:37:59 +0100 Subject: [PATCH 001/142] wip gui --- CMakeLists.txt | 3 +- symmetri/CMakeLists.txt | 7 +- symmetri/gui/CMakeLists.txt | 49 + symmetri/gui/crude_json.cpp | 811 ++ symmetri/gui/crude_json.h | 458 + symmetri/gui/drawable.h | 28 + symmetri/gui/emo.cpp | 9975 +++++++++++++++++++ symmetri/gui/imfilebrowser.h | 929 ++ symmetri/gui/imgui | 1 + symmetri/gui/imgui_bezier_math.h | 194 + symmetri/gui/imgui_bezier_math.inl | 675 ++ symmetri/gui/imgui_canvas.cpp | 575 ++ symmetri/gui/imgui_canvas.h | 272 + symmetri/gui/imgui_extra_math.h | 67 + symmetri/gui/imgui_extra_math.inl | 191 + symmetri/gui/imgui_node_editor.cpp | 5368 ++++++++++ symmetri/gui/imgui_node_editor.h | 524 + symmetri/gui/imgui_node_editor_api.cpp | 596 ++ symmetri/gui/imgui_node_editor_internal.h | 1488 +++ symmetri/gui/imgui_node_editor_internal.inl | 65 + symmetri/gui/main.mm | 145 + symmetri/gui/menu_bar.hpp | 56 + symmetri/gui/redux.hpp | 67 + symmetri/gui/view.hpp | 42 + 24 files changed, 22582 insertions(+), 4 deletions(-) create mode 100644 symmetri/gui/CMakeLists.txt create mode 100644 symmetri/gui/crude_json.cpp create mode 100644 symmetri/gui/crude_json.h create mode 100644 symmetri/gui/drawable.h create mode 100644 symmetri/gui/emo.cpp create mode 100644 symmetri/gui/imfilebrowser.h create mode 160000 symmetri/gui/imgui create mode 100644 symmetri/gui/imgui_bezier_math.h create mode 100644 symmetri/gui/imgui_bezier_math.inl create mode 100644 symmetri/gui/imgui_canvas.cpp create mode 100644 symmetri/gui/imgui_canvas.h create mode 100644 symmetri/gui/imgui_extra_math.h create mode 100644 symmetri/gui/imgui_extra_math.inl create mode 100644 symmetri/gui/imgui_node_editor.cpp create mode 100644 symmetri/gui/imgui_node_editor.h create mode 100644 symmetri/gui/imgui_node_editor_api.cpp create mode 100644 symmetri/gui/imgui_node_editor_internal.h create mode 100644 symmetri/gui/imgui_node_editor_internal.inl create mode 100644 symmetri/gui/main.mm create mode 100644 symmetri/gui/menu_bar.hpp create mode 100644 symmetri/gui/redux.hpp create mode 100644 symmetri/gui/view.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d367d44..dbcc0f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,8 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -Wextra -pedantic") +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -Wextra -pedantic") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") if(BUILD_TESTING) enable_testing() diff --git a/symmetri/CMakeLists.txt b/symmetri/CMakeLists.txt index e0e1231..1aa8254 100644 --- a/symmetri/CMakeLists.txt +++ b/symmetri/CMakeLists.txt @@ -1,6 +1,6 @@ include_directories(include submodules) -#lib +# lib add_library(${PROJECT_NAME} SHARED types.cpp tasks.cpp @@ -52,8 +52,8 @@ write_basic_package_version_file( install( FILES - "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/${PROJECT_NAME}Config.cmake" - "${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + "${PROJECT_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/${PROJECT_NAME}Config.cmake" + "${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME} ) @@ -74,4 +74,5 @@ install( if(BUILD_TESTING) add_subdirectory(tests) + add_subdirectory(gui) endif() diff --git a/symmetri/gui/CMakeLists.txt b/symmetri/gui/CMakeLists.txt new file mode 100644 index 0000000..42ce746 --- /dev/null +++ b/symmetri/gui/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.26) +project(Farbart) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +find_package(glfw3 REQUIRED) + +include_directories(imgui imgui/backends ../externals gui) + +set(SOURCES + main.mm +) + +set(IMGUI_SOURCES + imgui/imgui.cpp + imgui/imgui_demo.cpp + imgui/imgui_draw.cpp + imgui/imgui_widgets.cpp + imgui/imgui_tables.cpp + imgui/backends/imgui_impl_glfw.cpp + imgui/backends/imgui_impl_metal.mm + crude_json.cpp + imgui_node_editor_api.cpp + imgui_node_editor.cpp + imgui_canvas.cpp + ) + +if(APPLE) + include_directories(/opt/homebrew/include) + find_library(COCOA_LIBRARY Cocoa) + find_library(OPENGL_LIBRARY OpenGL) + find_library(IOKIT_LIBRARY IOKit) + find_library(COREVIDEO_LIBRARY CoreVideo) + set(EXTRA_LIBS + ${COCOA_LIBRARY} + ${OPENGL_LIBRARY} + ${IOKIT_LIBRARY} + ${COREVIDEO_LIBRARY} + glfw + "-framework Metal" + "-framework MetalKit" + "-framework AppKit" + "-framework Foundation" + "-framework QuartzCore" + ) +endif(APPLE) + +add_executable(Farbart ${SOURCES} ${IMGUI_SOURCES}) +target_link_libraries(Farbart symmetri ${EXTRA_LIBS} ) diff --git a/symmetri/gui/crude_json.cpp b/symmetri/gui/crude_json.cpp new file mode 100644 index 0000000..ab43354 --- /dev/null +++ b/symmetri/gui/crude_json.cpp @@ -0,0 +1,811 @@ +// Crude implementation of JSON value object and parser. +// +// VERSION 0.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +#include "crude_json.h" + +#include +#include +#include +#include +#include +#include +#if CRUDE_JSON_IO +#include + +#include +#endif + +namespace crude_json { + +value::value(value&& other) : m_Type(other.m_Type) { + switch (m_Type) { + case type_t::object: + construct(m_Storage, std::move(*object_ptr(other.m_Storage))); + break; + case type_t::array: + construct(m_Storage, std::move(*array_ptr(other.m_Storage))); + break; + case type_t::string: + construct(m_Storage, std::move(*string_ptr(other.m_Storage))); + break; + case type_t::boolean: + construct(m_Storage, std::move(*boolean_ptr(other.m_Storage))); + break; + case type_t::number: + construct(m_Storage, std::move(*number_ptr(other.m_Storage))); + break; + default: + break; + } + destruct(other.m_Storage, other.m_Type); + other.m_Type = type_t::null; +} + +value::value(const value& other) : m_Type(other.m_Type) { + switch (m_Type) { + case type_t::object: + construct(m_Storage, *object_ptr(other.m_Storage)); + break; + case type_t::array: + construct(m_Storage, *array_ptr(other.m_Storage)); + break; + case type_t::string: + construct(m_Storage, *string_ptr(other.m_Storage)); + break; + case type_t::boolean: + construct(m_Storage, *boolean_ptr(other.m_Storage)); + break; + case type_t::number: + construct(m_Storage, *number_ptr(other.m_Storage)); + break; + default: + break; + } +} + +value& value::operator[](size_t index) { + if (is_null()) m_Type = construct(m_Storage, type_t::array); + + if (is_array()) { + auto& v = *array_ptr(m_Storage); + if (index >= v.size()) v.insert(v.end(), index - v.size() + 1, value()); + + return v[index]; + } + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +const value& value::operator[](size_t index) const { + if (is_array()) return (*array_ptr(m_Storage))[index]; + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +value& value::operator[](const string& key) { + if (is_null()) m_Type = construct(m_Storage, type_t::object); + + if (is_object()) return (*object_ptr(m_Storage))[key]; + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +const value& value::operator[](const string& key) const { + if (is_object()) { + auto& o = *object_ptr(m_Storage); + auto it = o.find(key); + CRUDE_ASSERT(it != o.end()); + return it->second; + } + + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); +} + +bool value::contains(const string& key) const { + if (is_object()) { + auto& o = *object_ptr(m_Storage); + auto it = o.find(key); + return it != o.end(); + } + + return false; +} + +void value::push_back(const value& value) { + if (is_null()) m_Type = construct(m_Storage, type_t::array); + + if (is_array()) { + auto& v = *array_ptr(m_Storage); + v.push_back(value); + } else { + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); + } +} + +void value::push_back(value&& value) { + if (is_null()) m_Type = construct(m_Storage, type_t::array); + + if (is_array()) { + auto& v = *array_ptr(m_Storage); + v.push_back(std::move(value)); + } else { + CRUDE_ASSERT(false && "operator[] on unsupported type"); + std::terminate(); + } +} + +size_t value::erase(const string& key) { + if (!is_object()) return 0; + + auto& o = *object_ptr(m_Storage); + auto it = o.find(key); + + if (it == o.end()) return 0; + + o.erase(it); + + return 1; +} + +void value::swap(value& other) { + using std::swap; + + if (m_Type == other.m_Type) { + switch (m_Type) { + case type_t::object: + swap(*object_ptr(m_Storage), *object_ptr(other.m_Storage)); + break; + case type_t::array: + swap(*array_ptr(m_Storage), *array_ptr(other.m_Storage)); + break; + case type_t::string: + swap(*string_ptr(m_Storage), *string_ptr(other.m_Storage)); + break; + case type_t::boolean: + swap(*boolean_ptr(m_Storage), *boolean_ptr(other.m_Storage)); + break; + case type_t::number: + swap(*number_ptr(m_Storage), *number_ptr(other.m_Storage)); + break; + default: + break; + } + } else { + value tmp(std::move(other)); + other.~value(); + new (&other) value(std::move(*this)); + this->~value(); + new (this) value(std::move(tmp)); + } +} + +string value::dump(const int indent, const char indent_char) const { + dump_context_t context(indent, indent_char); + + context.out.precision(std::numeric_limits::max_digits10 + 1); + context.out << std::defaultfloat; + + dump(context, 0); + return context.out.str(); +} + +void value::dump_context_t::write_indent(int level) { + if (indent <= 0 || level == 0) return; + + out.fill(indent_char); + out.width(indent * level); + out << indent_char; + out.width(0); +} + +void value::dump_context_t::write_separator() { + if (indent < 0) return; + + out.put(' '); +} + +void value::dump_context_t::write_newline() { + if (indent < 0) return; + + out.put('\n'); +} + +void value::dump(dump_context_t& context, int level) const { + context.write_indent(level); + + switch (m_Type) { + case type_t::null: + context.out << "null"; + break; + + case type_t::object: + context.out << '{'; + { + context.write_newline(); + bool first = true; + for (auto& entry : *object_ptr(m_Storage)) { + if (!first) { + context.out << ','; + context.write_newline(); + } else + first = false; + context.write_indent(level + 1); + context.out << '\"' << entry.first << "\":"; + if (!entry.second.is_structured()) { + context.write_separator(); + entry.second.dump(context, 0); + } else { + context.write_newline(); + entry.second.dump(context, level + 1); + } + } + if (!first) context.write_newline(); + } + context.write_indent(level); + context.out << '}'; + break; + + case type_t::array: + context.out << '['; + { + context.write_newline(); + bool first = true; + for (auto& entry : *array_ptr(m_Storage)) { + if (!first) { + context.out << ','; + context.write_newline(); + } else + first = false; + if (!entry.is_structured()) { + context.write_indent(level + 1); + entry.dump(context, 0); + } else { + entry.dump(context, level + 1); + } + } + if (!first) context.write_newline(); + } + context.write_indent(level); + context.out << ']'; + break; + + case type_t::string: + context.out << '\"'; + + if (string_ptr(m_Storage)->find_first_of("\"\\/\b\f\n\r") != + string::npos || + string_ptr(m_Storage)->find('\0') != string::npos) { + for (auto c : *string_ptr(m_Storage)) { + if (c == '\"') + context.out << "\\\""; + else if (c == '\\') + context.out << "\\\\"; + else if (c == '/') + context.out << "\\/"; + else if (c == '\b') + context.out << "\\b"; + else if (c == '\f') + context.out << "\\f"; + else if (c == '\n') + context.out << "\\n"; + else if (c == '\r') + context.out << "\\r"; + else if (c == '\t') + context.out << "\\t"; + else if (c == 0) + context.out << "\\u0000"; + else + context.out << c; + } + } else + context.out << *string_ptr(m_Storage); + context.out << '\"'; + break; + + case type_t::boolean: + if (*boolean_ptr(m_Storage)) + context.out << "true"; + else + context.out << "false"; + break; + + case type_t::number: + context.out << *number_ptr(m_Storage); + break; + + default: + break; + } +} + +struct value::parser { + parser(const char* begin, const char* end) : m_Cursor(begin), m_End(end) {} + + value parse() { + value v; + + // Switch to C locale to make strtod and strtol work as expected + auto previous_locale = std::setlocale(LC_NUMERIC, "C"); + + // Accept single value only when end of the stream is reached. + if (!accept_element(v) || !eof()) v = value(type_t::discarded); + + if (previous_locale && strcmp(previous_locale, "C") != 0) + std::setlocale(LC_NUMERIC, previous_locale); + + return v; + } + + private: + struct cursor_state { + cursor_state(parser* p) : m_Owner(p), m_LastCursor(p->m_Cursor) {} + + void reset() { m_Owner->m_Cursor = m_LastCursor; } + + bool operator()(bool accept) { + if (!accept) + reset(); + else + m_LastCursor = m_Owner->m_Cursor; + return accept; + } + + private: + parser* m_Owner; + const char* m_LastCursor; + }; + + cursor_state state() { return cursor_state(this); } + + bool accept_value(value& result) { + return accept_object(result) || accept_array(result) || + accept_string(result) || accept_number(result) || + accept_boolean(result) || accept_null(result); + } + + bool accept_object(value& result) { + auto s = state(); + + object o; + if (s(accept('{') && accept_ws() && accept('}'))) { + result = o; + return true; + } else if (s(accept('{') && accept_members(o) && accept('}'))) { + result = std::move(o); + return true; + } + + return false; + } + + bool accept_members(object& o) { + if (!accept_member(o)) return false; + + while (true) { + auto s = state(); + if (!s(accept(',') && accept_member(o))) break; + } + + return true; + } + + bool accept_member(object& o) { + auto s = state(); + + value key; + value v; + if (s(accept_ws() && accept_string(key) && accept_ws() && accept(':') && + accept_element(v))) { + o.emplace(std::move(key.get()), std::move(v)); + return true; + } + + return false; + } + + bool accept_array(value& result) { + auto s = state(); + + if (s(accept('[') && accept_ws() && accept(']'))) { + result = array(); + return true; + } + + array a; + if (s(accept('[') && accept_elements(a) && accept(']'))) { + result = std::move(a); + return true; + } + + return false; + } + + bool accept_elements(array& a) { + value v; + if (!accept_element(v)) return false; + + a.emplace_back(std::move(v)); + while (true) { + auto s = state(); + v = nullptr; + if (!s(accept(',') && accept_element(v))) break; + a.emplace_back(std::move(v)); + } + + return true; + } + + bool accept_element(value& result) { + auto s = state(); + return s(accept_ws() && accept_value(result) && accept_ws()); + } + + bool accept_string(value& result) { + auto s = state(); + + string v; + if (s(accept('\"') && accept_characters(v) && accept('\"'))) { + result = std::move(v); + return true; + } else + return false; + } + + bool accept_characters(string& result) { + int c; + while (accept_character(c)) { + CRUDE_ASSERT(c < 128); // #todo: convert characters > 127 to UTF-8 + result.push_back(static_cast(c)); + } + + return true; + } + + bool accept_character(int& c) { + auto s = state(); + + if (accept('\\')) { + return accept_escape(c); + } else if (expect('\"')) + return false; + + // #todo: Handle UTF-8 sequences. + return s((c = peek()) >= 0) && advance(); + } + + bool accept_escape(int& c) { + if (accept('\"')) { + c = '\"'; + return true; + } + if (accept('\\')) { + c = '\\'; + return true; + } + if (accept('/')) { + c = '/'; + return true; + } + if (accept('b')) { + c = '\b'; + return true; + } + if (accept('f')) { + c = '\f'; + return true; + } + if (accept('n')) { + c = '\n'; + return true; + } + if (accept('r')) { + c = '\r'; + return true; + } + if (accept('t')) { + c = '\t'; + return true; + } + + auto s = state(); + + string hex; + hex.reserve(4); + if (s(accept('u') && accept_hex(hex) && accept_hex(hex) && + accept_hex(hex) && accept_hex(hex))) { + char* end = nullptr; + auto v = std::strtol(hex.c_str(), &end, 16); + if (end != hex.c_str() + hex.size()) return false; + + c = static_cast(v); + return true; + } + + return false; + } + + bool accept_hex(string& result) { + if (accept_digit(result)) return true; + + auto c = peek(); + if ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) { + advance(); + result.push_back(static_cast(c)); + return true; + } + + return false; + } + + bool accept_number(value& result) { + auto s = state(); + + string n; + if (s(accept_int(n) && accept_frac(n) && accept_exp(n))) { + char* end = nullptr; + auto v = std::strtod(n.c_str(), &end); + if (end != n.c_str() + n.size()) return false; + + if (v != 0 && !std::isnormal(v)) return false; + + result = v; + return true; + } + + return false; + } + + bool accept_int(string& result) { + auto s = state(); + + string part; + if (s(accept_onenine(part) && accept_digits(part))) { + result += std::move(part); + return true; + } + + part.resize(0); + if (accept_digit(part)) { + result += std::move(part); + return true; + } + + part.resize(0); + if (s(accept('-') && accept_onenine(part) && accept_digits(part))) { + result += '-'; + result += std::move(part); + return true; + } + + part.resize(0); + if (s(accept('-') && accept_digit(part))) { + result += '-'; + result += std::move(part); + return true; + } + + return false; + } + + bool accept_digits(string& result) { + string part; + if (!accept_digit(part)) return false; + + while (accept_digit(part)) + ; + + result += std::move(part); + + return true; + } + + bool accept_digit(string& result) { + if (accept('0')) { + result.push_back('0'); + return true; + } else if (accept_onenine(result)) + return true; + + return false; + } + + bool accept_onenine(string& result) { + auto c = peek(); + if (c >= '1' && c <= '9') { + result.push_back(static_cast(c)); + return advance(); + } + + return false; + } + + bool accept_frac(string& result) { + auto s = state(); + + string part; + if (s(accept('.') && accept_digits(part))) { + result += '.'; + result += std::move(part); + } + + return true; + } + + bool accept_exp(string& result) { + auto s = state(); + + string part; + if (s(accept('e') && accept_sign(part) && accept_digits(part))) { + result += 'e'; + result += std::move(part); + return true; + } + part.resize(0); + if (s(accept('E') && accept_sign(part) && accept_digits(part))) { + result += 'E'; + result += std::move(part); + } + + return true; + } + + bool accept_sign(string& result) { + if (accept('+')) + result.push_back('+'); + else if (accept('-')) + result.push_back('-'); + + return true; + } + + bool accept_ws() { + while (expect('\x09') || expect('\x0A') || expect('\x0D') || expect('\x20')) + advance(); + return true; + } + + bool accept_boolean(value& result) { + if (accept("true")) { + result = true; + return true; + } else if (accept("false")) { + result = false; + return true; + } + + return false; + } + + bool accept_null(value& result) { + if (accept("null")) { + result = nullptr; + return true; + } + + return false; + } + + bool accept(char c) { + if (expect(c)) + return advance(); + else + return false; + } + + bool accept(const char* str) { + auto last = m_Cursor; + + while (*str) { + if (eof() || *str != *m_Cursor) { + m_Cursor = last; + return false; + } + + advance(); + ++str; + } + + return true; + } + + int peek() const { + if (!eof()) + return *m_Cursor; + else + return -1; + } + + bool expect(char c) { return peek() == c; } + + bool advance(int count = 1) { + if (m_Cursor + count > m_End) { + m_Cursor = m_End; + return false; + } + + m_Cursor += count; + + return true; + } + + bool eof() const { return m_Cursor == m_End; } + + const char* m_Cursor; + const char* m_End; +}; + +value value::parse(const string& data) { + auto p = parser(data.c_str(), data.c_str() + data.size()); + + auto v = p.parse(); + + return v; +} + +#if CRUDE_JSON_IO +std::pair value::load(const string& path) { + // Modern C++, so beautiful... + std::unique_ptr file{nullptr, [](FILE* file) { + if (file) fclose(file); + }}; +#if defined(_MSC_VER) || (defined(__STDC_LIB_EXT1__) && __STDC_WANT_LIB_EXT1__) + FILE* handle = nullptr; + if (fopen_s(&handle, path.c_str(), "rb") != 0) return {value{}, false}; + file.reset(handle); +#else + file.reset(fopen(path.c_str(), "rb")); +#endif + + if (!file) return {value{}, false}; + + fseek(file.get(), 0, SEEK_END); + auto size = static_cast(ftell(file.get())); + fseek(file.get(), 0, SEEK_SET); + + string data; + data.resize(size); + if (fread(const_cast(data.data()), size, 1, file.get()) != 1) + return {value{}, false}; + + return {parse(data), true}; +} + +bool value::save(const string& path, const int indent, + const char indent_char) const { + // Modern C++, so beautiful... + std::unique_ptr file{nullptr, [](FILE* file) { + if (file) fclose(file); + }}; +#if defined(_MSC_VER) || (defined(__STDC_LIB_EXT1__) && __STDC_WANT_LIB_EXT1__) + FILE* handle = nullptr; + if (fopen_s(&handle, path.c_str(), "wb") != 0) return false; + file.reset(handle); +#else + file.reset(fopen(path.c_str(), "wb")); +#endif + + if (!file) return false; + + auto data = dump(indent, indent_char); + + if (fwrite(data.data(), data.size(), 1, file.get()) != 1) return false; + + return true; +} + +#endif + +} // namespace crude_json diff --git a/symmetri/gui/crude_json.h b/symmetri/gui/crude_json.h new file mode 100644 index 0000000..1ef945b --- /dev/null +++ b/symmetri/gui/crude_json.h @@ -0,0 +1,458 @@ +// Crude implementation of JSON value object and parser. +// +// VERSION 0.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +#ifndef __CRUDE_JSON_H__ +#define __CRUDE_JSON_H__ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#ifndef CRUDE_ASSERT +#include +#define CRUDE_ASSERT(expr) assert(expr) +#endif + +#ifndef CRUDE_JSON_IO +#define CRUDE_JSON_IO 1 +#endif + +namespace crude_json { + +struct value; + +using string = std::string; +using object = std::map; +using array = std::vector; +using number = double; +using boolean = bool; +using null = std::nullptr_t; + +enum class type_t { null, object, array, string, boolean, number, discarded }; + +struct value { + value(type_t type = type_t::null) : m_Type(construct(m_Storage, type)) {} + value(value&& other); + value(const value& other); + + value(null) : m_Type(construct(m_Storage, null())) {} + value(object&& v) : m_Type(construct(m_Storage, std::move(v))) {} + value(const object& v) : m_Type(construct(m_Storage, v)) {} + value(array&& v) : m_Type(construct(m_Storage, std::move(v))) {} + value(const array& v) : m_Type(construct(m_Storage, v)) {} + value(string&& v) : m_Type(construct(m_Storage, std::move(v))) {} + value(const string& v) : m_Type(construct(m_Storage, v)) {} + value(const char* v) : m_Type(construct(m_Storage, v)) {} + value(boolean v) : m_Type(construct(m_Storage, v)) {} + value(number v) : m_Type(construct(m_Storage, v)) {} + ~value() { destruct(m_Storage, m_Type); } + + value& operator=(value&& other) { + if (this != &other) { + value(std::move(other)).swap(*this); + } + return *this; + } + value& operator=(const value& other) { + if (this != &other) { + value(other).swap(*this); + } + return *this; + } + + value& operator=(null) { + auto other = value(); + swap(other); + return *this; + } + value& operator=(object&& v) { + auto other = value(std::move(v)); + swap(other); + return *this; + } + value& operator=(const object& v) { + auto other = value(v); + swap(other); + return *this; + } + value& operator=(array&& v) { + auto other = value(std::move(v)); + swap(other); + return *this; + } + value& operator=(const array& v) { + auto other = value(v); + swap(other); + return *this; + } + value& operator=(string&& v) { + auto other = value(std::move(v)); + swap(other); + return *this; + } + value& operator=(const string& v) { + auto other = value(v); + swap(other); + return *this; + } + value& operator=(const char* v) { + auto other = value(v); + swap(other); + return *this; + } + value& operator=(boolean v) { + auto other = value(v); + swap(other); + return *this; + } + value& operator=(number v) { + auto other = value(v); + swap(other); + return *this; + } + + type_t type() const { return m_Type; } + + operator type_t() const { return m_Type; } + + value& operator[](size_t index); + const value& operator[](size_t index) const; + value& operator[](const string& key); + const value& operator[](const string& key) const; + + bool contains(const string& key) const; + + void push_back(const value& value); + void push_back(value&& value); + + size_t erase(const string& key); + + bool is_primitive() const { + return is_string() || is_number() || is_boolean() || is_null(); + } + bool is_structured() const { return is_object() || is_array(); } + bool is_null() const { return m_Type == type_t::null; } + bool is_object() const { return m_Type == type_t::object; } + bool is_array() const { return m_Type == type_t::array; } + bool is_string() const { return m_Type == type_t::string; } + bool is_boolean() const { return m_Type == type_t::boolean; } + bool is_number() const { return m_Type == type_t::number; } + bool is_discarded() const { return m_Type == type_t::discarded; } + + template + const T& get() const; + template + T& get(); + + template + const T* get_ptr() const; + template + T* get_ptr(); + + string dump(const int indent = -1, const char indent_char = ' ') const; + + void swap(value& other); + + inline friend void swap(value& lhs, value& rhs) { lhs.swap(rhs); } + + // Returns discarded value for invalid inputs. + static value parse(const string& data); + +#if CRUDE_JSON_IO + static std::pair load(const string& path); + bool save(const string& path, const int indent = -1, + const char indent_char = ' ') const; +#endif + + private: + struct parser; + + // VS2015: std::max() is not constexpr yet. +#define CRUDE_MAX2(a, b) ((a) < (b) ? (b) : (a)) +#define CRUDE_MAX3(a, b, c) CRUDE_MAX2(CRUDE_MAX2(a, b), c) +#define CRUDE_MAX4(a, b, c, d) CRUDE_MAX2(CRUDE_MAX3(a, b, c), d) +#define CRUDE_MAX5(a, b, c, d, e) CRUDE_MAX2(CRUDE_MAX4(a, b, c, d), e) + enum { + max_size = CRUDE_MAX5(sizeof(string), sizeof(object), sizeof(array), + sizeof(number), sizeof(boolean)), + max_align = CRUDE_MAX5(alignof(string), alignof(object), alignof(array), + alignof(number), alignof(boolean)) + }; +#undef CRUDE_MAX5 +#undef CRUDE_MAX4 +#undef CRUDE_MAX3 +#undef CRUDE_MAX2 + using storage_t = std::aligned_storage::type; + + static object* object_ptr(storage_t& storage) { + return reinterpret_cast(&storage); + } + static const object* object_ptr(const storage_t& storage) { + return reinterpret_cast(&storage); + } + static array* array_ptr(storage_t& storage) { + return reinterpret_cast(&storage); + } + static const array* array_ptr(const storage_t& storage) { + return reinterpret_cast(&storage); + } + static string* string_ptr(storage_t& storage) { + return reinterpret_cast(&storage); + } + static const string* string_ptr(const storage_t& storage) { + return reinterpret_cast(&storage); + } + static boolean* boolean_ptr(storage_t& storage) { + return reinterpret_cast(&storage); + } + static const boolean* boolean_ptr(const storage_t& storage) { + return reinterpret_cast(&storage); + } + static number* number_ptr(storage_t& storage) { + return reinterpret_cast(&storage); + } + static const number* number_ptr(const storage_t& storage) { + return reinterpret_cast(&storage); + } + + static type_t construct(storage_t& storage, type_t type) { + switch (type) { + case type_t::object: + new (&storage) object(); + break; + case type_t::array: + new (&storage) array(); + break; + case type_t::string: + new (&storage) string(); + break; + case type_t::boolean: + new (&storage) boolean(); + break; + case type_t::number: + new (&storage) number(); + break; + default: + break; + } + + return type; + } + + static type_t construct(storage_t& storage, null) { + (void)storage; + return type_t::null; + } + static type_t construct(storage_t& storage, object&& value) { + new (&storage) object(std::forward(value)); + return type_t::object; + } + static type_t construct(storage_t& storage, const object& value) { + new (&storage) object(value); + return type_t::object; + } + static type_t construct(storage_t& storage, array&& value) { + new (&storage) array(std::forward(value)); + return type_t::array; + } + static type_t construct(storage_t& storage, const array& value) { + new (&storage) array(value); + return type_t::array; + } + static type_t construct(storage_t& storage, string&& value) { + new (&storage) string(std::forward(value)); + return type_t::string; + } + static type_t construct(storage_t& storage, const string& value) { + new (&storage) string(value); + return type_t::string; + } + static type_t construct(storage_t& storage, const char* value) { + new (&storage) string(value); + return type_t::string; + } + static type_t construct(storage_t& storage, boolean value) { + new (&storage) boolean(value); + return type_t::boolean; + } + static type_t construct(storage_t& storage, number value) { + new (&storage) number(value); + return type_t::number; + } + + static void destruct(storage_t& storage, type_t type) { + switch (type) { + case type_t::object: + object_ptr(storage)->~object(); + break; + case type_t::array: + array_ptr(storage)->~array(); + break; + case type_t::string: + string_ptr(storage)->~string(); + break; + default: + break; + } + } + + struct dump_context_t { + std::ostringstream out; + const int indent = -1; + const char indent_char = ' '; + + // VS2015: Aggregate initialization isn't a thing yet. + dump_context_t(const int indent, const char indent_char) + : indent(indent), indent_char(indent_char) {} + + void write_indent(int level); + void write_separator(); + void write_newline(); + }; + + void dump(dump_context_t& context, int level) const; + + storage_t m_Storage; + type_t m_Type; +}; + +template <> +inline const object& value::get() const { + CRUDE_ASSERT(m_Type == type_t::object); + return *object_ptr(m_Storage); +} +template <> +inline const array& value::get() const { + CRUDE_ASSERT(m_Type == type_t::array); + return *array_ptr(m_Storage); +} +template <> +inline const string& value::get() const { + CRUDE_ASSERT(m_Type == type_t::string); + return *string_ptr(m_Storage); +} +template <> +inline const boolean& value::get() const { + CRUDE_ASSERT(m_Type == type_t::boolean); + return *boolean_ptr(m_Storage); +} +template <> +inline const number& value::get() const { + CRUDE_ASSERT(m_Type == type_t::number); + return *number_ptr(m_Storage); +} + +template <> +inline object& value::get() { + CRUDE_ASSERT(m_Type == type_t::object); + return *object_ptr(m_Storage); +} +template <> +inline array& value::get() { + CRUDE_ASSERT(m_Type == type_t::array); + return *array_ptr(m_Storage); +} +template <> +inline string& value::get() { + CRUDE_ASSERT(m_Type == type_t::string); + return *string_ptr(m_Storage); +} +template <> +inline boolean& value::get() { + CRUDE_ASSERT(m_Type == type_t::boolean); + return *boolean_ptr(m_Storage); +} +template <> +inline number& value::get() { + CRUDE_ASSERT(m_Type == type_t::number); + return *number_ptr(m_Storage); +} + +template <> +inline const object* value::get_ptr() const { + if (m_Type == type_t::object) + return object_ptr(m_Storage); + else + return nullptr; +} +template <> +inline const array* value::get_ptr() const { + if (m_Type == type_t::array) + return array_ptr(m_Storage); + else + return nullptr; +} +template <> +inline const string* value::get_ptr() const { + if (m_Type == type_t::string) + return string_ptr(m_Storage); + else + return nullptr; +} +template <> +inline const boolean* value::get_ptr() const { + if (m_Type == type_t::boolean) + return boolean_ptr(m_Storage); + else + return nullptr; +} +template <> +inline const number* value::get_ptr() const { + if (m_Type == type_t::number) + return number_ptr(m_Storage); + else + return nullptr; +} + +template <> +inline object* value::get_ptr() { + if (m_Type == type_t::object) + return object_ptr(m_Storage); + else + return nullptr; +} +template <> +inline array* value::get_ptr() { + if (m_Type == type_t::array) + return array_ptr(m_Storage); + else + return nullptr; +} +template <> +inline string* value::get_ptr() { + if (m_Type == type_t::string) + return string_ptr(m_Storage); + else + return nullptr; +} +template <> +inline boolean* value::get_ptr() { + if (m_Type == type_t::boolean) + return boolean_ptr(m_Storage); + else + return nullptr; +} +template <> +inline number* value::get_ptr() { + if (m_Type == type_t::number) + return number_ptr(m_Storage); + else + return nullptr; +} + +} // namespace crude_json + +#endif // __CRUDE_JSON_H__ diff --git a/symmetri/gui/drawable.h b/symmetri/gui/drawable.h new file mode 100644 index 0000000..6b37aa1 --- /dev/null +++ b/symmetri/gui/drawable.h @@ -0,0 +1,28 @@ +#pragma once + +/** @file drawable.h */ + +#include +#include + +class Drawable { + public: + template + Drawable(drawable_t drawable) + : self_(std::make_shared>(std::move(drawable))) {} + friend void draw(const Drawable &drawable) { return drawable.self_->draw_(); } + + private: + struct concept_t { + virtual ~concept_t() = default; + virtual void draw_() const = 0; + }; + template + struct model final : concept_t { + model(drawable_t &&x) : drawable_(std::move(x)) {} + void draw_() const override { return draw(drawable_); } + drawable_t drawable_; + }; + + std::shared_ptr self_; +}; diff --git a/symmetri/gui/emo.cpp b/symmetri/gui/emo.cpp new file mode 100644 index 0000000..829b1b3 --- /dev/null +++ b/symmetri/gui/emo.cpp @@ -0,0 +1,9975 @@ +// dear imgui, v1.90 WIP +// (demo code) + +// 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 imgui.cpp for more details, documentation and comments. +// Get the latest version at https://github.com/ocornut/imgui + +//--------------------------------------------------- +// 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 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 +// (which you won't delete) + +//-------------------------------------------- +// 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. + +//----------------------------------------- +// 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:: 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 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 IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in +// comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can +// also follow symbols in comments. + +/* + +Index of this file: + +// [SECTION] Forward Declarations +// [SECTION] Helpers +// [SECTION] Demo Window / ShowDemoWindow() +// - ShowDemoWindow() +// - sub section: ShowDemoWindowWidgets() +// - sub section: ShowDemoWindowLayout() +// - sub section: ShowDemoWindowPopups() +// - sub section: ShowDemoWindowTables() +// - sub section: ShowDemoWindowInputs() +// [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() +// [SECTION] Example App: Simple Layout / ShowExampleAppLayout() +// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() +// [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: Fullscreen window / ShowExampleAppFullscreen() +// [SECTION] Example App: Manipulating window titles / +ShowExampleAppWindowTitles() +// [SECTION] Example App: Custom Rendering using ImDrawList API / +ShowExampleAppCustomRendering() +// [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE + +// System includes +#include // toupper +#include // INT_MIN, INT_MAX +#include // sqrtf, powf, cosf, sinf, floorf, ceilf +#include // intptr_t +#include // vsnprintf, sscanf, printf +#include // NULL, malloc, free, atoi +#if !defined(_MSC_VER) || _MSC_VER >= 1800 +#include // PRId64/PRIu64, not avail in some MinGW headers. +#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 : 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 + +// 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-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 +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after + // '#pragma GCC diagnostic' kind +#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 +#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. +#endif + +// Play it nice with Windows users (Update: May 2018, Notepad now supports +// Unix-style carriage returns!) +#ifdef _WIN32 +#define IM_NEWLINE "\r\n" +#else +#define IM_NEWLINE "\n" +#endif + +// 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, Helpers +//----------------------------------------------------------------------------- + +#if !defined(IMGUI_DISABLE_DEMO_WINDOWS) + +// Forward Declarations +static void ShowExampleAppMainMenuBar(); +static void ShowExampleAppConsole(bool* p_open); +static void ShowExampleAppCustomRendering(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 ShowExampleAppSimpleOverlay(bool* p_open); +static void ShowExampleAppAutoResize(bool* p_open); +static void ShowExampleAppConstrainedResize(bool* p_open); +static void ShowExampleAppFullscreen(bool* p_open); +static void ShowExampleAppLongText(bool* p_open); +static void ShowExampleAppWindowTitles(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 grow non-linearly) +static void ShowDemoWindowWidgets(); +static void ShowDemoWindowLayout(); +static void ShowDemoWindowPopups(); +static void ShowDemoWindowTables(); +static void ShowDemoWindowColumns(); +static void ShowDemoWindowInputs(); + +//----------------------------------------------------------------------------- +// [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 docs/FONTS.md) +static void HelpMarker(const char* desc) { + ImGui::TextDisabled("(?)"); + if (ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +// 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() +//----------------------------------------------------------------------------- +// - ShowDemoWindow() +// - ShowDemoWindowWidgets() +// - ShowDemoWindowLayout() +// - ShowDemoWindowPopups() +// - ShowDemoWindowTables() +// - ShowDemoWindowColumns() +// - ShowDemoWindowInputs() +//----------------------------------------------------------------------------- + +// 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. +void ImGui::ShowDemoWindow(bool* p_open) { + // 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!"); + + // Examples Apps (accessible from the "Examples" menu) + static bool show_app_main_menu_bar = false; + static bool show_app_console = false; + static bool show_app_custom_rendering = false; + static bool show_app_documents = false; + static bool show_app_log = false; + static bool show_app_layout = false; + static bool show_app_property_editor = false; + static bool show_app_simple_overlay = false; + static bool show_app_auto_resize = false; + static bool show_app_constrained_resize = false; + static bool show_app_fullscreen = false; + static bool show_app_long_text = false; + static bool show_app_window_titles = false; + + if (show_app_main_menu_bar) ShowExampleAppMainMenuBar(); + if (show_app_documents) ShowExampleAppDocuments(&show_app_documents); + if (show_app_console) ShowExampleAppConsole(&show_app_console); + if (show_app_custom_rendering) + ShowExampleAppCustomRendering(&show_app_custom_rendering); + 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_simple_overlay) + ShowExampleAppSimpleOverlay(&show_app_simple_overlay); + if (show_app_auto_resize) ShowExampleAppAutoResize(&show_app_auto_resize); + if (show_app_constrained_resize) + ShowExampleAppConstrainedResize(&show_app_constrained_resize); + if (show_app_fullscreen) ShowExampleAppFullscreen(&show_app_fullscreen); + if (show_app_long_text) ShowExampleAppLongText(&show_app_long_text); + if (show_app_window_titles) + ShowExampleAppWindowTitles(&show_app_window_titles); + + // Dear ImGui Tools (accessible from the "Tools" menu) + static bool show_tool_metrics = false; + static bool show_tool_debug_log = false; + static bool show_tool_id_stack_tool = false; + static bool show_tool_style_editor = false; + static bool show_tool_about = false; + + if (show_tool_metrics) ImGui::ShowMetricsWindow(&show_tool_metrics); + if (show_tool_debug_log) ImGui::ShowDebugLogWindow(&show_tool_debug_log); + if (show_tool_id_stack_tool) + ImGui::ShowIDStackToolWindow(&show_tool_id_stack_tool); + if (show_tool_style_editor) { + ImGui::Begin("Dear ImGui Style Editor", &show_tool_style_editor); + ImGui::ShowStyleEditor(); + ImGui::End(); + } + if (show_tool_about) ImGui::ShowAboutWindow(&show_tool_about); + + // Demonstrate the various window flags. Typically you would just use the + // default! + static bool no_titlebar = false; + static bool no_scrollbar = false; + static bool no_menu = false; + static bool no_move = false; + static bool no_resize = false; + static bool no_collapse = false; + static bool no_close = false; + static bool no_nav = false; + static bool no_background = false; + static bool no_bring_to_front = false; + static bool unsaved_document = false; + + ImGuiWindowFlags window_flags = 0; + if (no_titlebar) window_flags |= ImGuiWindowFlags_NoTitleBar; + if (no_scrollbar) window_flags |= ImGuiWindowFlags_NoScrollbar; + if (!no_menu) window_flags |= ImGuiWindowFlags_MenuBar; + if (no_move) window_flags |= ImGuiWindowFlags_NoMove; + if (no_resize) window_flags |= ImGuiWindowFlags_NoResize; + if (no_collapse) window_flags |= ImGuiWindowFlags_NoCollapse; + if (no_nav) window_flags |= ImGuiWindowFlags_NoNav; + if (no_background) window_flags |= ImGuiWindowFlags_NoBackground; + if (no_bring_to_front) window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus; + 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. 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. + if (!ImGui::Begin("Dear ImGui Demo", p_open, window_flags)) { + // Early out if the window is collapsed, as an optimization. + ImGui::End(); + return; + } + + // Most "big" widgets share a common width settings by default. See + // 'Demo->Layout->Widgets Width' for details. e.g. Use 2/3 of the space for + // widgets and 1/3 for labels (right align) + // ImGui::PushItemWidth(-ImGui::GetWindowWidth() * 0.35f); + // e.g. Leave a fixed amount of width for labels (by passing a negative + // value), the rest goes to widgets. + ImGui::PushItemWidth(ImGui::GetFontSize() * -12); + + // Menu Bar + 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, &show_app_main_menu_bar); + + ImGui::SeparatorText("Mini apps"); + ImGui::MenuItem("Console", NULL, &show_app_console); + ImGui::MenuItem("Custom rendering", NULL, &show_app_custom_rendering); + ImGui::MenuItem("Documents", NULL, &show_app_documents); + ImGui::MenuItem("Log", NULL, &show_app_log); + ImGui::MenuItem("Property editor", NULL, &show_app_property_editor); + ImGui::MenuItem("Simple layout", NULL, &show_app_layout); + ImGui::MenuItem("Simple overlay", NULL, &show_app_simple_overlay); + + ImGui::SeparatorText("Concepts"); + ImGui::MenuItem("Auto-resizing window", NULL, &show_app_auto_resize); + ImGui::MenuItem("Constrained-resizing window", NULL, + &show_app_constrained_resize); + ImGui::MenuItem("Fullscreen window", NULL, &show_app_fullscreen); + ImGui::MenuItem("Long text display", NULL, &show_app_long_text); + ImGui::MenuItem("Manipulating window titles", NULL, + &show_app_window_titles); + + ImGui::EndMenu(); + } + // if (ImGui::MenuItem("MenuItem")) {} // You can also use MenuItem() inside + // a menu bar! + if (ImGui::BeginMenu("Tools")) { + IMGUI_DEMO_MARKER("Menu/Tools"); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + const bool has_debug_tools = true; +#else + const bool has_debug_tools = false; +#endif + ImGui::MenuItem("Metrics/Debugger", NULL, &show_tool_metrics, + has_debug_tools); + ImGui::MenuItem("Debug Log", NULL, &show_tool_debug_log, has_debug_tools); + ImGui::MenuItem("ID Stack Tool", NULL, &show_tool_id_stack_tool, + has_debug_tools); + ImGui::MenuItem("Style Editor", NULL, &show_tool_style_editor); + ImGui::MenuItem("About Dear ImGui", NULL, &show_tool_about); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + ImGui::Text("dear imgui says hello! (%s) (%d)", IMGUI_VERSION, + IMGUI_VERSION_NUM); + ImGui::Spacing(); + + IMGUI_DEMO_MARKER("Help"); + if (ImGui::CollapsingHeader("Help")) { + 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::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 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::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: NavEnableSetMousePos", + &io.ConfigFlags, + ImGuiConfigFlags_NavEnableSetMousePos); + ImGui::SameLine(); + HelpMarker( + "Instruct navigation to move the mouse cursor. See comment for " + "ImGuiConfigFlags_NavEnableSetMousePos."); + ImGui::CheckboxFlags("io.ConfigFlags: NoMouse", &io.ConfigFlags, + ImGuiConfigFlags_NoMouse); + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) { + // The "NoMouse" option can get us stuck with a disabled mouse! Let's + // provide an alternative way to fix it: + if (fmodf((float)ImGui::GetTime(), 0.40f) < 0.20f) { + ImGui::SameLine(); + ImGui::Text("<>"); + } + if (ImGui::IsKeyPressed(ImGuiKey_Space)) + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; + } + ImGui::CheckboxFlags("io.ConfigFlags: NoMouseCursorChange", + &io.ConfigFlags, + ImGuiConfigFlags_NoMouseCursorChange); + ImGui::SameLine(); + HelpMarker( + "Instruct backend to not alter mouse cursor shape and visibility."); + 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 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("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.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::Checkbox("io.ConfigMacOSXBehaviors", &io.ConfigMacOSXBehaviors); + ImGui::Text("Also see Style->Rendering for rendering options."); + + ImGui::SeparatorText("Debug"); + 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::Spacing(); + } + + IMGUI_DEMO_MARKER("Configuration/Backend Flags"); + if (ImGui::TreeNode("Backend Flags")) { + 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."); + + // 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: RendererHasVtxOffset", + &io.BackendFlags, + ImGuiBackendFlags_RendererHasVtxOffset); + ImGui::EndDisabled(); + ImGui::TreePop(); + ImGui::Spacing(); + } + + IMGUI_DEMO_MARKER("Configuration/Style"); + if (ImGui::TreeNode("Style")) { + HelpMarker( + "The same contents can be accessed in 'Tools->Style Editor' or by " + "calling the ShowStyleEditor() function."); + ImGui::ShowStyleEditor(); + ImGui::TreePop(); + ImGui::Spacing(); + } + + IMGUI_DEMO_MARKER("Configuration/Capture, Logging"); + if (ImGui::TreeNode("Capture/Logging")) { + 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(); + + 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(); + ImGui::LogText("Hello, world!"); + ImGui::LogFinish(); + } + ImGui::TreePop(); + } + } + + IMGUI_DEMO_MARKER("Window options"); + if (ImGui::CollapsingHeader("Window options")) { + 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("Unsaved document", &unsaved_document); + ImGui::EndTable(); + } + } + + // All demo contents + ShowDemoWindowWidgets(); + ShowDemoWindowLayout(); + ShowDemoWindowPopups(); + ShowDemoWindowTables(); + ShowDemoWindowInputs(); + + // End of ShowDemoWindow() + ImGui::PopItemWidth(); + ImGui::End(); +} + +static void ShowDemoWindowWidgets() { + IMGUI_DEMO_MARKER("Widgets"); + if (!ImGui::CollapsingHeader("Widgets")) return; + + static bool disable_all = false; // The Checkbox for that is inside the + // "Disabled" section at the bottom + if (disable_all) ImGui::BeginDisabled(); + + 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++; + if (clicked & 1) { + ImGui::SameLine(); + 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::Button("Click"); + ImGui::PopStyleColor(3); + ImGui::PopID(); + } + + // 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); + if (ImGui::ArrowButton("##left", ImGuiDir_Left)) { + counter--; + } + ImGui::SameLine(0.0f, spacing); + if (ImGui::ArrowButton("##right", ImGuiDir_Right)) { + counter++; + } + ImGui::PopButtonRepeat(); + ImGui::SameLine(); + ImGui::Text("%d", counter); + + ImGui::Button("Tooltip"); + ImGui::SetItemTooltip("I am a tooltip"); + + ImGui::LabelText("label", "Value"); + + 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:\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 clipboard.\n" + "CTRL+Z,CTRL+Y undo/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); + + static float f0 = 0.001f; + ImGui::InputFloat("input float", &f0, 0.01f, 1.0f, "%.3f"); + + static double d0 = 999999.00000001; + ImGui::InputDouble("input double", &d0, 0.01f, 1.0f, "%.8f"); + + 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\"."); + + static float vec4a[4] = {0.10f, 0.20f, 0.30f, 0.44f}; + ImGui::InputFloat3("input float3", vec4a); + } + + ImGui::SeparatorText("Drags"); + + { + IMGUI_DEMO_MARKER("Widgets/Basic/DragInt, DragFloat"); + static int i1 = 50, i2 = 42; + ImGui::DragInt("drag int", &i1, 1); + 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); + + 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::SeparatorText("Sliders"); + + { + 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; + ImGui::SliderFloat("slider float", &f1, 0.0f, 1.0f, "ratio = %.3f"); + 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"); + + { + 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 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); + } + + { + // 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.\nRefer 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.\nRefer to the " + "\"List boxes\" section below for an explanation of how to use the " + "more flexible and general BeginListBox/EndListBox API."); + } + + ImGui::TreePop(); + } + + 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 gamepad/keyboard 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. + // As a result, Set + ImGui::BeginDisabled(); + ImGui::Button("Disabled item", sz); + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ImGui::SetTooltip("I am a a tooltip for a disabled item."); + + 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."); + + 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); + + if (ImGui::TreeNode((void*)(intptr_t)i, "Child %d", i)) { + ImGui::Text("blah blah"); + ImGui::SameLine(); + if (ImGui::SmallButton("button")) { + } + ImGui::TreePop(); + } + } + 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_SpanAllColumns", &base_flags, + ImGuiTreeNodeFlags_SpanAllColumns); + ImGui::SameLine(); + HelpMarker("For use in Tables only."); + 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 (node_open) { + ImGui::BulletText("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 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(); + } + + 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", ImGuiTreeNodeFlags_None)) { + ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); + for (int i = 0; i < 5; i++) ImGui::Text("Some content %d", i); + } + if (ImGui::CollapsingHeader("Header with a close button", + &closable_group)) { + ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); + 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(); + } + + IMGUI_DEMO_MARKER("Widgets/Bullets"); + if (ImGui::TreeNode("Bullets")) { + ImGui::BulletText("Bullet point 1"); + ImGui::BulletText("Bullet point 2\nOn multiple lines"); + if (ImGui::TreeNode("Tree node")) { + ImGui::BulletText("Another bullet point"); + ImGui::TreePop(); + } + ImGui::Bullet(); + ImGui::Text("Bullet point 3 (two calls)"); + ImGui::Bullet(); + ImGui::SmallButton("Button"); + ImGui::TreePop(); + } + + 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)"); // 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(); + } + + 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; + { + static bool use_text_color_for_tint = false; + ImGui::Checkbox("Use Text Color for Tint", &use_text_color_for_tint); + 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 + ImVec4 tint_col = use_text_color_for_tint + ? ImGui::GetStyleColorVec4(ImGuiCol_Text) + : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // No tint + ImVec4 border_col = ImGui::GetStyleColorVec4(ImGuiCol_Border); + ImGui::Image(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, + tint_col, border_col); + 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::Image(my_tex_id, ImVec2(region_sz * zoom, region_sz * zoom), uv0, + uv1, tint_col, border_col); + ImGui::EndTooltip(); + } + } + + 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(); + } + + IMGUI_DEMO_MARKER("Widgets/Combo"); + if (ImGui::TreeNode("Combo")) { + // 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 the other flag, as we + // cannot combine both + if (ImGui::CheckboxFlags("ImGuiComboFlags_NoPreview", &flags, + ImGuiComboFlags_NoPreview)) + flags &= ~(ImGuiComboFlags_NoArrowButton | + ImGuiComboFlags_WidthFitPreview); // Clear the other flag, as + // we cannot combine both + 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_current_idx = + 0; // Here we store our selection data as an index. + const char* combo_preview_value = + items[item_current_idx]; // Pass in the preview value visible before + // opening the combo (it could be anything) + if (ImGui::BeginCombo("combo 1", combo_preview_value, flags)) { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) { + const bool is_selected = (item_current_idx == n); + if (ImGui::Selectable(items[n], is_selected)) item_current_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard + // navigation focus) + if (is_selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + ImGui::Spacing(); + ImGui::SeparatorText("One-liner variants"); + HelpMarker("Flags 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 2 (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 3 (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 4 (function)", &item_current_4, + [](void* data, int n) { return ((const char**)data)[n]; }, items, + IM_ARRAYSIZE(items)); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/List Boxes"); + if (ImGui::TreeNode("List boxes")) { + // 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_current_idx = + 0; // Here we store our selection data as an index. + if (ImGui::BeginListBox("listbox 1")) { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) { + const bool is_selected = (item_current_idx == n); + if (ImGui::Selectable(items[n], is_selected)) item_current_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard + // navigation focus) + if (is_selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndListBox(); + } + + // 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++) { + const bool is_selected = (item_current_idx == n); + if (ImGui::Selectable(items[n], is_selected)) item_current_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard + // navigation focus) + if (is_selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndListBox(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Selectables"); + if (ImGui::TreeNode("Selectables")) { + // 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")) { + 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_DEMO_MARKER("Widgets/Selectables/Single Selection"); + 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(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/Multiple Selection"); + 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(); + } + IMGUI_DEMO_MARKER( + "Widgets/Selectables/Rendering more items on the same line"); + if (ImGui::TreeNode("Rendering more items on the same line")) { + // (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 columns"); + if (ImGui::TreeNode("In columns")) { + static bool selected[10] = {}; + + if (ImGui::BeginTable("split1", 3, + ImGuiTableFlags_Resizable | + ImGuiTableFlags_NoSavedSettings | + ImGuiTableFlags_Borders)) { + for (int i = 0; i < 10; i++) { + char label[32]; + sprintf(label, "Item %d", i); + ImGui::TableNextColumn(); + ImGui::Selectable(label, + &selected[i]); // FIXME-TABLE: Selection overlap + } + ImGui::EndTable(); + } + 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(); + } + + // 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::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/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(); + } + + // Tabs + 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; + if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) { + if (ImGui::BeginTabItem("Avocado")) { + ImGui::Text("This is the Avocado tab!\nblah blah blah blah blah"); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Broccoli")) { + ImGui::Text("This is the Broccoli tab!\nblah blah blah blah blah"); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Cucumber")) { + ImGui::Text("This is the Cucumber tab!\nblah blah blah blah blah"); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::Separator(); + 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", &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); + if ((tab_bar_flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) + tab_bar_flags |= ImGuiTabBarFlags_FittingPolicyDefault_; + 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); + + // Tab Bar + 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::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. + 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], + ImGuiTabItemFlags_None)) { + ImGui::Text("This is the %s tab!", names[n]); + if (n & 1) ImGui::Text("I am an odd tab."); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + 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 "+") Note that we + // submit it before the regular tabs, but because of 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(); + } + + // 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")) { + static bool animate = true; + ImGui::Checkbox("Animate", &animate); + + // Plot as lines and plot as histogram + IMGUI_DEMO_MARKER("Widgets/Plotting/PlotLines, PlotHistogram"); + 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)); + + // 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", 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 + IMGUI_DEMO_MARKER("Widgets/Plotting/ProgressBar"); + 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) 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); + ImGui::TreePop(); + } + + 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 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::SeparatorText("Options"); + 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."); + ImGuiColorEditFlags 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_DEMO_MARKER("Widgets/Color/ColorEdit"); + ImGui::SeparatorText("Inline color editor"); + ImGui::Text("Color widget:"); + 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, misc_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_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_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.\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 | misc_flags); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (with custom Picker popup)"); + ImGui::Text("Color button with Custom Picker Popup:"); + + // Generate a default palette. The palette will persist and can be edited. + static bool saved_palette_init = true; + 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); + 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(0, ImGui::GetStyle().ItemInnerSpacing.x); + open_popup |= ImGui::Button("Palette"); + if (open_popup) { + ImGui::OpenPopup("mypicker"); + backup_color = color; + } + if (ImGui::BeginPopup("mypicker")) { + ImGui::Text("MY CUSTOM COLOR PICKER WITH AN AMAZING PALETTE!"); + ImGui::Separator(); + ImGui::ColorPicker4("##picker", (float*)&color, + misc_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::Text("Previous"); + if (ImGui::ColorButton("##previous", backup_color, + ImGuiColorEditFlags_NoPicker | + ImGuiColorEditFlags_AlphaPreviewHalf, + ImVec2(60, 40))) + color = backup_color; + ImGui::Separator(); + ImGui::Text("Palette"); + for (int n = 0; n < IM_ARRAYSIZE(saved_palette); n++) { + ImGui::PushID(n); + if ((n % 8) != 0) + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); + + 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 specifying + // the ImGuiColorEditFlags_NoDragDrop flag. + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 3); + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 4); + ImGui::EndDragDropTarget(); + } + + ImGui::PopID(); + } + ImGui::EndGroup(); + ImGui::EndPopup(); + } + + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (simple)"); + ImGui::Text("Color button only:"); + static bool no_border = false; + ImGui::Checkbox("ImGuiColorEditFlags_NoBorder", &no_border); + ImGui::ColorButton( + "MyColor##3c", *(ImVec4*)&color, + misc_flags | (no_border ? ImGuiColorEditFlags_NoBorder : 0), + ImVec2(80, 80)); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorPicker"); + ImGui::SeparatorText("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 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) { + 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::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 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()."); + ImGui::SameLine(); + HelpMarker( + "When not specified explicitly (Auto/Current mode), 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; + 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 + if (display_mode == 2) + flags |= ImGuiColorEditFlags_DisplayRGB; // Override display mode + if (display_mode == 3) flags |= ImGuiColorEditFlags_DisplayHSV; + if (display_mode == 4) flags |= ImGuiColorEditFlags_DisplayHex; + ImGui::ColorPicker4("MyColor##4", (float*)&color, flags, + ref_color ? &ref_color_v.x : NULL); + + 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 both a small version of both types of pickers (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); + + // HSV encoded support (to avoid RGB<>HSV round trips and singularities when + // S==0 or V==0) + 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::Text("Color widget with InputHSV:"); + 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(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and Slider Flags"); + if (ImGui::TreeNode("Drag/Slider Flags")) { + // 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::SameLine(); + HelpMarker( + "Always clamp value to min/max bounds (if any) when input manually " + "with CTRL+Click."); + 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."); + + // 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::DragInt("DragInt (0 -> 100)", &drag_i, 0.5f, 0, 100, "%d", flags); + + // Sliders + static float slider_f = 0.5f; + static int slider_i = 50; + ImGui::Text("Underlying float value: %f", slider_f); + ImGui::SliderFloat("SliderFloat (0 -> 1)", &slider_f, 0.0f, 1.0f, "%.3f", + flags); + ImGui::SliderInt("SliderInt (0 -> 100)", &slider_i, 0, 100, "%d", flags); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Range Widgets"); + if (ImGui::TreeNode("Range Widgets")) { + 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(); + } + + IMGUI_DEMO_MARKER("Widgets/Data Types"); + if (ImGui::TreeNode("Data Types")) { +// 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); +// } + +// 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; + ImU64 ULLONG_MAX = (2ULL * 9223372036854775807LL + 1); +#endif + const char s8_zero = 0, s8_one = 1, s8_fifty = 50, s8_min = -128, + s8_max = 127; + const ImU8 u8_zero = 0, u8_one = 1, u8_fifty = 50, u8_min = 0, u8_max = 255; + const short s16_zero = 0, s16_one = 1, s16_fifty = 50, s16_min = -32768, + s16_max = 32767; + const ImU16 u16_zero = 0, u16_one = 1, u16_fifty = 50, u16_min = 0, + u16_max = 65535; + const ImS32 s32_zero = 0, s32_one = 1, s32_fifty = 50, + s32_min = INT_MIN / 2, s32_max = INT_MAX / 2, + s32_hi_a = INT_MAX / 2 - 100, s32_hi_b = INT_MAX / 2; + const ImU32 u32_zero = 0, u32_one = 1, u32_fifty = 50, u32_min = 0, + u32_max = UINT_MAX / 2, u32_hi_a = UINT_MAX / 2 - 100, + u32_hi_b = UINT_MAX / 2; + const ImS64 s64_zero = 0, s64_one = 1, s64_fifty = 50, + s64_min = LLONG_MIN / 2, s64_max = LLONG_MAX / 2, + s64_hi_a = LLONG_MAX / 2 - 100, s64_hi_b = LLONG_MAX / 2; + const ImU64 u64_zero = 0, u64_one = 1, u64_fifty = 50, u64_min = 0, + u64_max = ULLONG_MAX / 2, u64_hi_a = ULLONG_MAX / 2 - 100, + u64_hi_b = ULLONG_MAX / 2; + const float f32_zero = 0.f, f32_one = 1.f, f32_lo_a = -10000000000.0f, + f32_hi_a = +10000000000.0f; + const double f64_zero = 0., f64_one = 1., f64_lo_a = -1000000000000000.0, + f64_hi_a = +1000000000000000.0; + + // State + static char s8_v = 127; + static ImU8 u8_v = 255; + static short s16_v = 32767; + static ImU16 u16_v = 65535; + static ImS32 s32_v = -1; + static ImU32 u32_v = (ImU32)-1; + static ImS64 s64_v = -1; + static ImU64 u64_v = (ImU64)-1; + static float f32_v = 0.123f; + static double f64_v = 90000.01234567890123456789; + + const float drag_speed = 0.2f; + static bool drag_clamp = false; + 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"); + 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_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::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, "%04X"); + 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"); + 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::TreePop(); + } + + 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::TreePop(); + } + + 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(); + } + + 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 + // '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::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and drop/Copy-swap items"); + if (ImGui::TreeNode("Drag and drop to copy/swap items")) { + enum Mode { Mode_Copy, Mode_Move, Mode_Swap }; + static int mode = 0; + 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"}; + 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)); + + // Our buttons are both drag sources and drag targets here! + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + // 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(); + } + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload("DND_DEMO_CELL")) { + IM_ASSERT(payload->DataSize == sizeof(int)); + int payload_n = *(const int*)payload->Data; + if (mode == Mode_Copy) { + names[n] = names[payload_n]; + } + if (mode == Mode_Move) { + names[n] = names[payload_n]; + names[payload_n] = ""; + } + if (mode == Mode_Swap) { + const char* tmp = names[n]; + names[n] = names[payload_n]; + names[payload_n] = tmp; + } + } + ImGui::EndDragDropTarget(); + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and Drop/Drag to reorder items (simple)"); + if (ImGui::TreeNode("Drag to reorder items (simple)")) { + // 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::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::BeginTooltip(); + ImGui::Text("Cannot drop here!"); + ImGui::EndTooltip(); + } + 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(); + } + + 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] = {}; + 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) { + ImGui::PushButtonRepeat(true); + ret = ImGui::Button("ITEM: Button"); + ImGui::PopButtonRepeat(); + } // 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(_AllowWhenOverlappedByItem) = %d\n" + "IsItemHovered(_AllowWhenOverlappedByWindow) = %d\n" + "IsItemHovered(_AllowWhenDisabled) = %d\n" + "IsItemHovered(_RectOnly) = %d\n" + "IsItemActive() = %d\n" + "IsItemEdited() = %d\n" + "IsItemActivated() = %d\n" + "IsItemDeactivated() = %d\n" + "IsItemDeactivatedAfterEdit() = %d\n" + "IsItemVisible() = %d\n" + "IsItemClicked() = %d\n" + "IsItemToggledOpen() = %d\n" + "GetItemRectMin() = (%.1f, %.1f)\n" + "GetItemRectMax() = (%.1f, %.1f)\n" + "GetItemRectSize() = (%.1f, %.1f)", + ret, ImGui::IsItemFocused(), ImGui::IsItemHovered(), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByItem), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled), + ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly), ImGui::IsItemActive(), + ImGui::IsItemEdited(), ImGui::IsItemActivated(), + ImGui::IsItemDeactivated(), 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 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.0f), + true); + + // Testing IsWindowFocused() function with its various flags. + ImGui::BulletText( + "IsWindowFocused() = %d\n" + "IsWindowFocused(_ChildWindows) = %d\n" + "IsWindowFocused(_ChildWindows|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_ChildWindows|_RootWindow) = %d\n" + "IsWindowFocused(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_RootWindow) = %d\n" + "IsWindowFocused(_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_AnyWindow) = %d\n", + ImGui::IsWindowFocused(), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | + ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | + ImGuiFocusedFlags_RootWindow), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | + ImGuiFocusedFlags_RootWindow | + ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow), + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow | + ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)); + + // 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|_RootWindow) = %d\n" + "IsWindowHovered(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_RootWindow) = %d\n" + "IsWindowHovered(_RootWindow|_NoPopupHierarchy) = %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_RootWindow), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | + ImGuiHoveredFlags_RootWindow | + ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow), + ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow | + ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | + ImGuiHoveredFlags_AllowWhenBlockedByPopup), + ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow), + ImGui::IsWindowHovered(ImGuiHoveredFlags_Stationary)); + + ImGui::BeginChild("child", ImVec2(0, 50), true); + ImGui::Text( + "This is another child window for testing the _ChildWindows flag."); + ImGui::EndChild(); + if (embed_all_inside_a_child_window) ImGui::EndChild(); + + // 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 associated to the title bar of a window. + static bool test_window = false; + ImGui::Checkbox("Hovered/Active tests after Begin() for title bar testing", + &test_window); + if (test_window) { + ImGui::Begin("Title bar Hovered/Active tests", &test_window); + if (ImGui::BeginPopupContextItem()) // <-- This is using IsItemHovered() + { + if (ImGui::MenuItem("Close")) { + test_window = false; + } + ImGui::EndPopup(); + } + ImGui::Text( + "IsItemHovered() after begin = %d (== is title bar hovered)\n" + "IsItemActive() after begin = %d (== is window being " + "clicked/moved)\n", + ImGui::IsItemHovered(), ImGui::IsItemActive()); + ImGui::End(); + } + + ImGui::TreePop(); + } + + // Demonstrate BeginDisabled/EndDisabled using a checkbox located at the + // bottom of the section (which is a bit odd: logically we'd have this + // checkbox at the top of the section, but we don't want this feature to steal + // that space) + if (disable_all) ImGui::EndDisabled(); + + IMGUI_DEMO_MARKER("Widgets/Disable Block"); + if (ImGui::TreeNode("Disable block")) { + ImGui::Checkbox("Disable entire section above", &disable_all); + ImGui::SameLine(); + HelpMarker( + "Demonstrate using BeginDisabled()/EndDisabled() across this section."); + ImGui::TreePop(); + } + + 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(); + } +} + +static void ShowDemoWindowLayout() { + 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), + false, 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), true, 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(); + } + + 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; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("Offset X", &offset_x, 1.0f, -1000, 1000); + + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (float)offset_x); + ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(255, 0, 0, 100)); + ImGui::BeginChild("Red", ImVec2(200, 100), true, ImGuiWindowFlags_None); + 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::PopStyleColor(); + 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::GetWindowPos().x + ImGui::GetWindowContentRegionMax().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( + "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(); + ImGui::Button("AAA"); + ImGui::SameLine(); + ImGui::Button("BBB"); + ImGui::SameLine(); + ImGui::BeginGroup(); + ImGui::Button("CCC"); + ImGui::Button("DDD"); + ImGui::EndGroup(); + ImGui::SameLine(); + ImGui::Button("EEE"); + ImGui::EndGroup(); + 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::SameLine(); + 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::BeginListBox("List", size)) { + ImGui::Selectable("Selected", true); + ImGui::Selectable("Not Selected", false); + ImGui::EndListBox(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Text Baseline Alignment"); + if (ImGui::TreeNode("Text Baseline Alignment")) { + { + 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("KO Blahblah"); + ImGui::SameLine(); + ImGui::Button("Some framed item"); + ImGui::SameLine(); + HelpMarker("Baseline of button will look misaligned with text.."); + + // 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"); + ImGui::SameLine(); + HelpMarker( + "We call AlignTextToFramePadding() to vertically align the text " + "baseline by +FramePadding.y"); + + // SmallButton() uses the same vertical padding as Text + ImGui::Button("TEST##1"); + ImGui::SameLine(); + ImGui::Text("TEST"); + ImGui::SameLine(); + ImGui::SmallButton("TEST##2"); + + // 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::Unindent(); + } + + ImGui::Spacing(); + + { + ImGui::BulletText("Multi-line text:"); + ImGui::Indent(); + ImGui::Text("One\nTwo\nThree"); + ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); + ImGui::SameLine(); + ImGui::Text("Banana"); + + ImGui::Text("Banana"); + ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); + ImGui::SameLine(); + ImGui::Text("One\nTwo\nThree"); + + ImGui::Button("HOP##1"); + ImGui::SameLine(); + ImGui::Text("Banana"); + ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); + ImGui::SameLine(); + ImGui::Text("Banana"); + + 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")) { + // Vertical scroll functions + IMGUI_DEMO_MARKER("Layout/Scrolling/Vertical"); + HelpMarker( + "Use SetScrollHereY() or SetScrollFromPosY() to scroll to a given " + "vertical position."); + + 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("Decoration", &enable_extra_decorations); + + ImGui::Checkbox("Track", &enable_track); + ImGui::PushItemWidth(100); + 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", &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", &scroll_to_pos_px, 1.00f, -10, + FLT_MAX, "X/Y = %.0f px"); + ImGui::PopItemWidth(); + + if (scroll_to_off || scroll_to_pos) 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(); + const char* names[] = {"Top", "25%", "Center", "75%", "Bottom"}; + ImGui::TextUnformatted(names[i]); + + 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), true, 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); + if (child_is_visible) // Avoid calling SetScrollHereY when running with + // culled items + { + for (int item = 0; item < 100; item++) { + 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(); + float scroll_max_y = ImGui::GetScrollMaxY(); + ImGui::EndChild(); + ImGui::Text("%.0f/%.0f", scroll_y, scroll_max_y); + ImGui::EndGroup(); + } + ImGui::PopID(); + + // 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), true, 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)); + ImVec2 scrolling_child_size = + ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 7 + 30); + ImGui::BeginChild("scrolling", scrolling_child_size, true, + 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. + int num_buttons = 10 + ((line & 1) ? line * 9 : line * 3); + for (int n = 0; n < num_buttons; n++) { + if (n > 0) ImGui::SameLine(); + 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; + 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)); + ImGui::Button(label, + ImVec2(40.0f + sinf((float)(line + n)) * 20.0f, 0.0f)); + ImGui::PopStyleColor(3); + ImGui::PopID(); + } + } + float scroll_x = ImGui::GetScrollX(); + float scroll_max_x = ImGui::GetScrollMaxX(); + 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::Text("Scroll from code"); + 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) { + // 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(); + } + ImGui::Spacing(); + + static bool show_horizontal_contents_size_demo_window = false; + ImGui::Checkbox("Show Horizontal contents size demo window", + &show_horizontal_contents_size_demo_window); + + if (show_horizontal_contents_size_demo_window) { + static bool show_h_scrollbar = true; + static bool show_button = true; + static bool show_tree_nodes = true; + static bool show_text_wrapped = false; + static bool show_columns = true; + static bool show_tab_bar = true; + static bool show_child = false; + static bool explicit_content_size = false; + static float contents_size_x = 300.0f; + 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."); + 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 + ImGui::Checkbox("Text wrapped", + &show_text_wrapped); // Will grow and use contents size + ImGui::Checkbox("Columns", &show_columns); // Will use contents size + ImGui::Checkbox("Tab bar", &show_tab_bar); // Will use contents size + ImGui::Checkbox("Child", &show_child); // Will grow and use contents size + ImGui::Checkbox("Explicit content size", &explicit_content_size); + ImGui::Text("Scroll %.1f/%.1f %.1f/%.1f", ImGui::GetScrollX(), + ImGui::GetScrollMaxX(), ImGui::GetScrollY(), + ImGui::GetScrollMaxY()); + if (explicit_content_size) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(100); + ImGui::DragFloat("##csx", &contents_size_x); + 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)); + } + ImGui::PopStyleVar(2); + ImGui::Separator(); + if (show_button) { + ImGui::Button("this is a 300-wide button", ImVec2(300, 0)); + } + if (show_tree_nodes) { + bool open = true; + if (ImGui::TreeNode("this is a tree node")) { + if (ImGui::TreeNode("another one of those tree node...")) { + ImGui::Text("Some tree contents"); + ImGui::TreePop(); + } + ImGui::TreePop(); + } + ImGui::CollapsingHeader("CollapsingHeader", &open); + } + if (show_text_wrapped) { + ImGui::TextWrapped( + "This text should automatically wrap on the edge of the work " + "rectangle."); + } + 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++) { + ImGui::Text("Width %.2f", ImGui::GetColumnWidth()); + ImGui::NextColumn(); + } + ImGui::Columns(1); + } + if (show_tab_bar && ImGui::BeginTabBar("Hello")) { + if (ImGui::BeginTabItem("OneOneOne")) { + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("TwoTwoTwo")) { + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("ThreeThreeThree")) { + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("FourFourFour")) { + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + if (show_child) { + ImGui::BeginChild("child", ImVec2(0, 0), true); + ImGui::EndChild(); + } + ImGui::End(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Clipping"); + if (ImGui::TreeNode("Clipping")) { + 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 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() { + 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. + + // 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(); + // Typical use for popups: + // if (ImGui::Button("Open")) ImGui::OpenPopup("MyPopup"); if + // (ImGui::BeginPopup("MyPopup") { [...] EndPopup(); } + + // 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."); + + 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) + 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::SeparatorText("Aquarium"); + for (int i = 0; i < IM_ARRAYSIZE(names); i++) + if (ImGui::Selectable(names[i])) selected_fish = i; + ImGui::EndPopup(); + } + + // Showing a menu with toggles + if (ImGui::Button("Toggle..")) ImGui::OpenPopup("my_toggle_popup"); + if (ImGui::BeginPopup("my_toggle_popup")) { + for (int i = 0; i < IM_ARRAYSIZE(names); i++) + ImGui::MenuItem(names[i], "", &toggles[i]); + if (ImGui::BeginMenu("Sub-menu")) { + ImGui::MenuItem("Click me"); + ImGui::EndMenu(); + } + + ImGui::Separator(); + ImGui::Text("Tooltip here"); + ImGui::SetItemTooltip("I am a tooltip over a popup"); + + if (ImGui::Button("Stacked Popup")) ImGui::OpenPopup("another popup"); + if (ImGui::BeginPopup("another popup")) { + for (int i = 0; i < IM_ARRAYSIZE(names); i++) + ImGui::MenuItem(names[i], "", &toggles[i]); + if (ImGui::BeginMenu("Sub-menu")) { + ImGui::MenuItem("Click me"); + if (ImGui::Button("Stacked Popup")) ImGui::OpenPopup("another popup"); + if (ImGui::BeginPopup("another popup")) { + ImGui::Text("I am the last one here."); + ImGui::EndPopup(); + } + ImGui::EndMenu(); + } + ImGui::EndPopup(); + } + ImGui::EndPopup(); + } + + // Call the more complete ShowExampleMenuFile which we use in various places + // of this demo + if (ImGui::Button("With a menu..")) ImGui::OpenPopup("my_file_popup"); + if (ImGui::BeginPopup("my_file_popup", ImGuiWindowFlags_MenuBar)) { + 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 (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. + { + 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"); + } + } + + // 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. + { + 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::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."); + + 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!"); + ImGui::Separator(); + + // 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)); + ImGui::Checkbox("Don't ask me next time", &dont_ask_me_next_time); + ImGui::PopStyleVar(); + + if (ImGui::Button("OK", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::Button("Stacked modals..")) ImGui::OpenPopup("Stacked 1"); + if (ImGui::BeginPopupModal("Stacked 1", NULL, ImGuiWindowFlags_MenuBar)) { + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Some menu item")) { + } + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + ImGui::Text( + "Hello from Stacked The First\nUsing " + "style.Colors[ImGuiCol_ModalWindowDimBg] behind it."); + + // 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}; + ImGui::Combo("Combo", &item, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0"); + 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 unused_open = true; + if (ImGui::BeginPopupModal("Stacked 2", &unused_open)) { + ImGui::Text("Hello from Stacked The Second!"); + if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + 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(); + + ImGui::MenuItem("Menu item", "CTRL+M"); + if (ImGui::BeginMenu("Menu inside a regular window")) { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + ImGui::Separator(); + ImGui::TreePop(); + } +} + +// 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 { +// 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; +} // namespace + +// Make the UI compact because there are so many fields +static void PushStyleCompact() { + ImGuiStyle& style = ImGui::GetStyle(); + ImGui::PushStyleVar( + ImGuiStyleVar_FramePadding, + ImVec2(style.FramePadding.x, (float)(int)(style.FramePadding.y * 0.60f))); + ImGui::PushStyleVar( + ImGuiStyleVar_ItemSpacing, + ImVec2(style.ItemSpacing.x, (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); +} + +static void ShowDemoWindowTables() { + // ImGui::SetNextItemOpen(true, ImGuiCond_Once); + IMGUI_DEMO_MARKER("Tables"); + if (!ImGui::CollapsingHeader("Tables & Columns")) return; + + // 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 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_BordersInnerV\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 too much. 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\nWe 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 + ScrollX together 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++) { + ImGui::Indent( + indent_step); // Add some indentation to demonstrate usage of + // per-column IndentEnable/IndentDisable flags. + 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 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\nWe 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\nPlease 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::PushStyleVar(ImGuiStyleVar_CellPadding, + ImVec2(style.CellPadding.x, 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 flags = + ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | + ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | + ImGuiTableFlags_NoBordersInBody; + + static ImGuiTreeNodeFlags tree_node_flags = + ImGuiTreeNodeFlags_SpanAllColumns; + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &tree_node_flags, + ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &tree_node_flags, + ImGuiTreeNodeFlags_SpanAllColumns); + + if (ImGui::BeginTable("3ways", 3, 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); + if (is_folder) { + bool open = ImGui::TreeNodeEx(node->Name, tree_node_flags); + 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, + tree_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", "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 + 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(); + } + + 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 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("_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); + + 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], + ImGuiTableColumnFlags_AngledHeader | + ImGuiTableColumnFlags_WidthFixed); + 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.\nUsing " + "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-colum 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_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? + ImGui::PushButtonRepeat(true); +#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 a more advanced system in your + // code 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(); + } + } + ImGui::PopButtonRepeat(); + + // 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(); + + ShowDemoWindowColumns(); + + 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 ShowDemoWindowColumns() { + 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:"); + ImGui::Columns(3, "mycolumns3", false); // 3-ways, no border + ImGui::Separator(); + for (int n = 0; n < 14; n++) { + char label[32]; + sprintf(label, "Item %d", n); + if (ImGui::Selectable(label)) { + } + // if (ImGui::Button(label, ImVec2(-FLT_MIN,0.0f))) {} + ImGui::NextColumn(); + } + ImGui::Columns(1); + ImGui::Separator(); + + ImGui::Text("With border:"); + ImGui::Columns(4, "mycolumns"); // 4-ways, with border + ImGui::Separator(); + ImGui::Text("ID"); + ImGui::NextColumn(); + ImGui::Text("Name"); + ImGui::NextColumn(); + ImGui::Text("Path"); + ImGui::NextColumn(); + ImGui::Text("Hovered"); + ImGui::NextColumn(); + ImGui::Separator(); + const char* names[3] = {"One", "Two", "Three"}; + const char* paths[3] = {"/path/one", "/path/two", "/path/three"}; + static int selected = -1; + for (int i = 0; i < 3; i++) { + char label[32]; + sprintf(label, "%04d", i); + if (ImGui::Selectable(label, selected == i, + ImGuiSelectableFlags_SpanAllColumns)) + selected = i; + bool hovered = ImGui::IsItemHovered(); + ImGui::NextColumn(); + ImGui::Text(names[i]); + ImGui::NextColumn(); + ImGui::Text(paths[i]); + ImGui::NextColumn(); + ImGui::Text("%d", hovered); + ImGui::NextColumn(); + } + ImGui::Columns(1); + ImGui::Separator(); + 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(columns_count, NULL, v_borders); + for (int i = 0; i < columns_count * lines_count; i++) { + if (h_borders && ImGui::GetColumnIndex() == 0) ImGui::Separator(); + 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(-FLT_MIN, 0.0f)); + ImGui::NextColumn(); + } + ImGui::Columns(1); + if (h_borders) ImGui::Separator(); + ImGui::TreePop(); + } + + // 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"); + ImGui::Separator(); + + ImGui::Text("Hello"); + ImGui::Button("Banana"); + ImGui::NextColumn(); + + ImGui::Text("ImGui"); + ImGui::Button("Apple"); + static float foo = 1.0f; + ImGui::InputFloat("red", &foo, 0.05f, 0, "%.3f"); + ImGui::Text("An extra line here."); + ImGui::NextColumn(); + + ImGui::Text("Sailor"); + ImGui::Button("Corniflower"); + static float bar = 1.0f; + ImGui::InputFloat("blue", &bar, 0.05f, 0, "%.3f"); + ImGui::NextColumn(); + + if (ImGui::CollapsingHeader("Category A")) { + ImGui::Text("Blah blah blah"); + } + ImGui::NextColumn(); + if (ImGui::CollapsingHeader("Category B")) { + ImGui::Text("Blah blah blah"); + } + ImGui::NextColumn(); + if (ImGui::CollapsingHeader("Category C")) { + ImGui::Text("Blah blah blah"); + } + ImGui::NextColumn(); + ImGui::Columns(1); + ImGui::Separator(); + ImGui::TreePop(); + } + + // Word wrapping + IMGUI_DEMO_MARKER("Columns (legacy API)/Word-wrapping"); + if (ImGui::TreeNode("Word-wrapping")) { + ImGui::Columns(2, "word-wrapping"); + ImGui::Separator(); + ImGui::TextWrapped("The quick brown fox jumps over the lazy dog."); + ImGui::TextWrapped("Hello Left"); + ImGui::NextColumn(); + ImGui::TextWrapped("The quick brown fox jumps over the lazy dog."); + ImGui::TextWrapped("Hello Right"); + ImGui::Columns(1); + ImGui::Separator(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Columns (legacy API)/Horizontal Scrolling"); + if (ImGui::TreeNode("Horizontal Scrolling")) { + ImGui::SetNextWindowContentSize(ImVec2(1500.0f, 0.0f)); + ImVec2 child_size = ImVec2(0, ImGui::GetFontSize() * 20.0f); + ImGui::BeginChild("##ScrollingRegion", child_size, false, + ImGuiWindowFlags_HorizontalScrollbar); + ImGui::Columns(10); + + // Also demonstrate using clipper for large vertical lists + int ITEMS_COUNT = 2000; + ImGuiListClipper clipper; + clipper.Begin(ITEMS_COUNT); + while (clipper.Step()) { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + for (int j = 0; j < 10; j++) { + ImGui::Text("Line %d Column %d...", i, j); + ImGui::NextColumn(); + } + } + ImGui::Columns(1); + ImGui::EndChild(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Columns (legacy API)/Tree"); + if (ImGui::TreeNode("Tree")) { + ImGui::Columns(2, "tree", true); + for (int x = 0; x < 3; x++) { + bool open1 = ImGui::TreeNode((void*)(intptr_t)x, "Node%d", x); + ImGui::NextColumn(); + ImGui::Text("Node contents"); + ImGui::NextColumn(); + if (open1) { + for (int y = 0; y < 3; y++) { + bool open2 = ImGui::TreeNode((void*)(intptr_t)y, "Node%d.%d", x, y); + ImGui::NextColumn(); + ImGui::Text("Node contents"); + if (open2) { + ImGui::Text("Even more contents"); + if (ImGui::TreeNode("Tree in column")) { + ImGui::Text("The quick brown fox jumps over the lazy dog"); + ImGui::TreePop(); + } + } + ImGui::NextColumn(); + if (open2) ImGui::TreePop(); + } + ImGui::TreePop(); + } + } + ImGui::Columns(1); + ImGui::TreePop(); + } + + ImGui::TreePop(); +} + +static void ShowDemoWindowInputs() { + IMGUI_DEMO_MARKER("Inputs & Focus"); + if (ImGui::CollapsingHeader("Inputs & Focus")) { + ImGuiIO& io = ImGui::GetIO(); + + // Display inputs submitted to ImGuiIO + IMGUI_DEMO_MARKER("Inputs & Focus/Inputs"); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode("Inputs")) { + HelpMarker( + "This is a simplified view. See more detailed input state:\n" + "- in 'Tools->Metrics/Debugger->Inputs'.\n" + "- in 'Tools->Debug Log->IO'."); + 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 (ImGui::IsMouseDown(i)) { + ImGui::SameLine(); + ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); + } + ImGui::Text("Mouse wheel: %.1f", io.MouseWheel); + + // We iterate both legacy native range and named ImGuiKey ranges, which is + // a little 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. +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + struct funcs { + static bool IsLegacyNativeDupe(ImGuiKey) { return false; } + }; + ImGuiKey start_key = ImGuiKey_NamedKey_BEGIN; +#else + struct funcs { + static bool IsLegacyNativeDupe(ImGuiKey key) { + return key < 512 && ImGui::GetIO().KeyMap[key] != -1; + } + }; // Hide Native<>ImGuiKey duplicates when both exists in the array + ImGuiKey start_key = (ImGuiKey)0; +#endif + 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::TreePop(); + } + + // Display ImGuiIO output flags + IMGUI_DEMO_MARKER("Inputs & Focus/Outputs"); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode("Outputs")) { + 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)."); + 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(); + } + + // 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", "NotAllowed"}; + IM_ASSERT(IM_ARRAYSIZE(mouse_cursors_names) == ImGuiMouseCursor_COUNT); + + ImGuiMouseCursor current = ImGui::GetMouseCursor(); + ImGui::Text("Current mouse cursor = %d: %s", current, + mouse_cursors_names[current]); + 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] = "hello"; + ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); + ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); + ImGui::InputText("3", buf, IM_ARRAYSIZE(buf)); + ImGui::PushTabStop(false); + ImGui::InputText("4 (tab skip)", buf, IM_ARRAYSIZE(buf)); + ImGui::SameLine(); + HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); + ImGui::PopTabStop(); + 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(); + bool focus_2 = ImGui::Button("Focus on 2"); + ImGui::SameLine(); + bool focus_3 = ImGui::Button("Focus on 3"); + int has_focus = 0; + static char buf[128] = "click on a button to set focus"; + + if (focus_1) ImGui::SetKeyboardFocusHere(); + ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemActive()) has_focus = 1; + + if (focus_2) ImGui::SetKeyboardFocusHere(); + ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemActive()) has_focus = 2; + + ImGui::PushTabStop(false); + if (focus_3) ImGui::SetKeyboardFocusHere(); + ImGui::InputText("3 (tab skip)", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemActive()) has_focus = 3; + ImGui::SameLine(); + HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); + ImGui::PopTabStop(); + + if (has_focus) + ImGui::Text("Item with focus: %d", has_focus); + else + ImGui::Text("Item with focus: "); + + // Use >= 0 parameter to SetKeyboardFocusHere() to focus an upcoming item + static float f3[3] = {0.0f, 0.0f, 0.0f}; + int focus_ahead = -1; + if (ImGui::Button("Focus on X")) { + focus_ahead = 0; + } + ImGui::SameLine(); + if (ImGui::Button("Focus on Y")) { + focus_ahead = 1; + } + ImGui::SameLine(); + if (ImGui::Button("Focus on Z")) { + focus_ahead = 2; + } + if (focus_ahead != -1) ImGui::SetKeyboardFocusHere(focus_ahead); + ImGui::SliderFloat3("Float3", &f3[0], 0.0f, 1.0f); + + ImGui::TextWrapped( + "NB: Cursor & selection are preserved when refocusing last used item " + "in code."); + 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):", 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(). + 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):"); + 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(); + } + } +} + +//----------------------------------------------------------------------------- +// [SECTION] About Window / ShowAboutWindow() +// Access from Dear ImGui Demo -> Tools -> About +//----------------------------------------------------------------------------- + +void ImGui::ShowAboutWindow(bool* p_open) { + if (!ImGui::Begin("About Dear ImGui", p_open, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::End(); + return; + } + IMGUI_DEMO_MARKER("Tools/About Dear ImGui"); + ImGui::Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); + ImGui::Separator(); + ImGui::Text("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 sponsoring the project!"); + + static bool show_config_info = false; + ImGui::Checkbox("Config/Build Information", &show_config_info); + if (show_config_info) { + ImGuiIO& io = ImGui::GetIO(); + ImGuiStyle& style = ImGui::GetStyle(); + + bool copy_to_clipboard = ImGui::Button("Copy to clipboard"); + ImVec2 child_size = ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 18); + ImGui::BeginChildFrame(ImGui::GetID("cfg_infos"), child_size, + ImGuiWindowFlags_NoMove); + 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(); + ImGui::Text( + "sizeof(size_t): %d, sizeof(ImDrawIdx): %d, sizeof(ImDrawVert): %d", + (int)sizeof(size_t), (int)sizeof(ImDrawIdx), (int)sizeof(ImDrawVert)); + ImGui::Text("define: __cplusplus=%d", (int)__cplusplus); +#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + ImGui::Text("define: IMGUI_DISABLE_OBSOLETE_KEYIO"); +#endif +#ifdef IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_WIN32_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_WIN32_FUNCTIONS"); +#endif +#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"); +#endif +#ifdef IMGUI_USE_BGRA_PACKED_COLOR + ImGui::Text("define: IMGUI_USE_BGRA_PACKED_COLOR"); +#endif +#ifdef _WIN32 + ImGui::Text("define: _WIN32"); +#endif +#ifdef _WIN64 + ImGui::Text("define: _WIN64"); +#endif +#ifdef __linux__ + ImGui::Text("define: __linux__"); +#endif +#ifdef __APPLE__ + ImGui::Text("define: __APPLE__"); +#endif +#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 +#ifdef __MINGW64__ + ImGui::Text("define: __MINGW64__"); +#endif +#ifdef __GNUC__ + ImGui::Text("define: __GNUC__=%d", (int)__GNUC__); +#endif +#ifdef __clang_version__ + ImGui::Text("define: __clang_version__=%s", __clang_version__); +#endif +#ifdef __EMSCRIPTEN__ + ImGui::Text("define: __EMSCRIPTEN__"); +#endif + ImGui::Separator(); + ImGui::Text("io.BackendPlatformName: %s", + io.BackendPlatformName ? io.BackendPlatformName : "NULL"); + ImGui::Text("io.BackendRendererName: %s", + io.BackendRendererName ? io.BackendRendererName : "NULL"); + 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.MouseDrawCursor) ImGui::Text("io.MouseDrawCursor"); + if (io.ConfigMacOSXBehaviors) ImGui::Text("io.ConfigMacOSXBehaviors"); + 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_RendererHasVtxOffset) + ImGui::Text(" RendererHasVtxOffset"); + 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); + ImGui::Text("io.DisplayFramebufferScale: %.2f,%.2f", + io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y); + ImGui::Separator(); + ImGui::Text("style.WindowPadding: %.2f,%.2f", style.WindowPadding.x, + style.WindowPadding.y); + ImGui::Text("style.WindowBorderSize: %.2f", style.WindowBorderSize); + ImGui::Text("style.FramePadding: %.2f,%.2f", style.FramePadding.x, + style.FramePadding.y); + ImGui::Text("style.FrameRounding: %.2f", style.FrameRounding); + ImGui::Text("style.FrameBorderSize: %.2f", style.FrameBorderSize); + ImGui::Text("style.ItemSpacing: %.2f,%.2f", style.ItemSpacing.x, + style.ItemSpacing.y); + 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::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Style Editor / ShowStyleEditor() +//----------------------------------------------------------------------------- +// - ShowFontSelector() +// - ShowStyleSelector() +// - ShowStyleEditor() +//----------------------------------------------------------------------------- + +// 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 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 (ImFont* font : io.Fonts->Fonts) { + ImGui::PushID((void*)font); + if (ImGui::Selectable(font->GetDebugName(), font == font_current)) + io.FontDefault = font; + ImGui::PopID(); + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + 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 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) { + 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; + + // Default to using internal storage as reference + static bool init = true; + if (init && ref == NULL) ref_saved_style = style; + init = false; + if (ref == NULL) ref = &ref_saved_style; + + ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.50f); + + if (ImGui::ShowStyleSelector("Colors##Selector")) ref_saved_style = style; + ImGui::ShowFontSelector("Fonts##Selector"); + + // 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 border = (style.WindowBorderSize > 0.0f); + if (ImGui::Checkbox("WindowBorder", &border)) { + style.WindowBorderSize = border ? 1.0f : 0.0f; + } + } + ImGui::SameLine(); + { + bool border = (style.FrameBorderSize > 0.0f); + if (ImGui::Checkbox("FrameBorder", &border)) { + style.FrameBorderSize = border ? 1.0f : 0.0f; + } + } + ImGui::SameLine(); + { + 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")) *ref = ref_saved_style = style; + ImGui::SameLine(); + 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\" below to save them somewhere."); + + ImGui::Separator(); + + if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { + if (ImGui::BeginTabItem("Sizes")) { + 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"); + ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, + 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, + 0.0f, 10.0f, "%.0f"); + 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::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::SliderFloat("TabBarBorderSize", &style.TabBarBorderSize, 0.0f, + 2.0f, "%.0f"); + + 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::SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, + "%.0f"); + + ImGui::SeparatorText("Tables"); + ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, + 20.0f, "%.0f"); + ImGui::SliderAngle("TableAngledHeadersAngle", + &style.TableAngledHeadersAngle, -50.0f, +50.0f); + + ImGui::SeparatorText("Widgets"); + ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, + 0.0f, 1.0f, "%.2f"); + int window_menu_button_position = style.WindowMenuButtonPosition + 1; + if (ImGui::Combo("WindowMenuButtonPosition", + (int*)&window_menu_button_position, + "None\0Left\0Right\0")) + style.WindowMenuButtonPosition = window_menu_button_position - 1; + 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::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("DisplaySafeAreaPadding", + (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, + "%.0f"); + 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::EndTabItem(); + } + + if (ImGui::BeginTabItem("Colors")) { + static int output_dest = 0; + static bool output_only_modified = true; + if (ImGui::Button("Export")) { + if (output_dest == 0) + ImGui::LogToClipboard(); + else + ImGui::LogToTTY(); + ImGui::LogText("ImVec4* colors = ImGui::GetStyle().Colors;" IM_NEWLINE); + for (int i = 0; i < ImGuiCol_COUNT; i++) { + 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::LogFinish(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(120); + ImGui::Combo("##output_type", &output_dest, "To Clipboard\0To TTY\0"); + ImGui::SameLine(); + ImGui::Checkbox("Only Modified Colors", &output_only_modified); + + static ImGuiTextFilter filter; + filter.Draw("Filter colors", ImGui::GetFontSize() * 16); + + static ImGuiColorEditFlags alpha_flags = 0; + if (ImGui::RadioButton("Opaque", + alpha_flags == ImGuiColorEditFlags_None)) { + alpha_flags = ImGuiColorEditFlags_None; + } + ImGui::SameLine(); + if (ImGui::RadioButton("Alpha", + alpha_flags == ImGuiColorEditFlags_AlphaPreview)) { + alpha_flags = ImGuiColorEditFlags_AlphaPreview; + } + 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(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); + 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 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); + ImGui::PopID(); + } + ImGui::PopItemWidth(); + ImGui::EndChild(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Fonts")) { + ImGuiIO& io = ImGui::GetIO(); + ImFontAtlas* atlas = io.Fonts; + 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; + 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, MIN_SCALE, + MAX_SCALE, "%.2f", + ImGuiSliderFlags_AlwaysClamp); // Scale everything + ImGui::PopItemWidth(); + + ImGui::EndTabItem(); + } + + 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 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(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 = number of segments)"); + ImGui::Spacing(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const float min_widget_width = ImGui::CalcTextSize("N: MMM\nR: 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(); + + 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\" " + "tesselation 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(); + } + + ImGui::EndTabBar(); + } + + 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 inputing 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,CTRL+Y to undo/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 (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() { + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) { + if (ImGui::MenuItem("Undo", "CTRL+Z")) { + } + if (ImGui::MenuItem("Redo", "CTRL+Y", false, false)) { + } // Disabled item + ImGui::Separator(); + if (ImGui::MenuItem("Cut", "CTRL+X")) { + } + if (ImGui::MenuItem("Copy", "CTRL+C")) { + } + if (ImGui::MenuItem("Paste", "CTRL+V")) { + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } +} + +// 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_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")) { + ImGui::MenuItem("fish_hat.c"); + ImGui::MenuItem("fish_hat.inl"); + ImGui::MenuItem("fish_hat.h"); + if (ImGui::BeginMenu("More..")) { + ImGui::MenuItem("Hello"); + ImGui::MenuItem("Sailor"); + if (ImGui::BeginMenu("Recurse..")) { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + 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); + for (int i = 0; i < 10; i++) ImGui::Text("Scrolling Text %d", i); + ImGui::EndChild(); + static float f = 0.5f; + static int n = 0; + ImGui::SliderFloat("Value", &f, 0.0f, 1.0f); + ImGui::InputFloat("Input", &f, 0.1f); + ImGui::Combo("Combo", &n, "Yes\0No\0Maybe\0\0"); + ImGui::EndMenu(); + } + + IMGUI_DEMO_MARKER("Examples/Menu/Colors"); + if (ImGui::BeginMenu("Colors")) { + float sz = ImGui::GetTextLineHeight(); + for (int i = 0; i < ImGuiCol_COUNT; i++) { + 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::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")) { + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Debug Console / ShowExampleAppConsole() +//----------------------------------------------------------------------------- + +// Demonstrate creating a simple console window, with scrolling, filtering, +// completion and history. 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]; + ImVector Items; + ImVector Commands; + ImVector History; + int HistoryPos; // -1: new line, 0..History.Size-1 browsing history. + ImGuiTextFilter Filter; + bool AutoScroll; + bool ScrollToBottom; + + 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"); + AutoScroll = true; + ScrollToBottom = false; + AddLog("Welcome to Dear ImGui!"); + } + ~ExampleAppConsole() { + ClearLog(); + for (int i = 0; i < History.Size; i++) free(History[i]); + } + + // Portable helpers + 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 = malloc(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]); + Items.clear(); + } + + void AddLog(const char* fmt, ...) IM_FMTARGS(2) { + // FIXME-OPT + char buf[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, IM_ARRAYSIZE(buf), fmt, args); + buf[IM_ARRAYSIZE(buf) - 1] = 0; + va_end(args); + Items.push_back(Strdup(buf)); + } + + void Draw(const char* title, bool* p_open) { + 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. Here we create a context menu + // only available from the title bar. + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Close Console")) *p_open = false; + ImGui::EndPopup(); + } + + 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 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(); + + // Options menu + if (ImGui::BeginPopup("Options")) { + ImGui::Checkbox("Auto-scroll", &AutoScroll); + ImGui::EndPopup(); + } + + // Options, Filter + if (ImGui::Button("Options")) ImGui::OpenPopup("Options"); + ImGui::SameLine(); + Filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180); + ImGui::Separator(); + + // 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), false, + ImGuiWindowFlags_HorizontalScrollbar)) { + 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. 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 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(); + } + ImGui::EndChild(); + ImGui::Separator(); + + // Command-line + bool reclaim_focus = false; + 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); + if (s[0]) ExecCommand(s); + strcpy(s, ""); + reclaim_focus = true; + } + + // Auto-focus on window apparition + ImGui::SetItemDefaultFocus(); + if (reclaim_focus) + ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget + + ImGui::End(); + } + + void ExecCommand(const char* command_line) { + 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. + HistoryPos = -1; + for (int i = History.Size - 1; i >= 0; i--) + if (Stricmp(History[i], command_line) == 0) { + free(History[i]); + History.erase(History.begin() + i); + break; + } + History.push_back(Strdup(command_line)); + + // Process command + if (Stricmp(command_line, "CLEAR") == 0) { + ClearLog(); + } else if (Stricmp(command_line, "HELP") == 0) { + AddLog("Commands:"); + for (int i = 0; i < Commands.Size; i++) AddLog("- %s", Commands[i]); + } else if (Stricmp(command_line, "HISTORY") == 0) { + int first = History.Size - 10; + for (int i = first > 0 ? first : 0; i < History.Size; i++) + AddLog("%3d: %s\n", i, History[i]); + } else { + AddLog("Unknown command: '%s'\n", command_line); + } + + // On command input, we scroll to bottom even if AutoScroll==false + ScrollToBottom = true; + } + + // 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); + } + + int TextEditCallback(ImGuiInputTextCallbackData* data) { + // AddLog("cursor: %d, selection: %d-%d", data->CursorPos, + // data->SelectionStart, data->SelectionEnd); + switch (data->EventFlag) { + case ImGuiInputTextFlags_CallbackCompletion: { + // Example of TEXT COMPLETION + + // Locate beginning of current word + const char* word_end = data->Buf + data->CursorPos; + const char* word_start = word_end; + while (word_start > data->Buf) { + const char c = word_start[-1]; + if (c == ' ' || c == '\t' || c == ',' || c == ';') break; + word_start--; + } + + // 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) + candidates.push_back(Commands[i]); + + if (candidates.Size == 0) { + // No match + 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)); + data->InsertChars(data->CursorPos, candidates[0]); + data->InsertChars(data->CursorPos, " "); + } else { + // Multiple matches. Complete as much as we can.. + // So inputing "C"+Tab will complete to "CL" then display "CLEAR" and + // "CLASSIFY" as matches. + int match_len = (int)(word_end - word_start); + for (;;) { + int c = 0; + bool all_candidates_matches = true; + for (int i = 0; i < candidates.Size && all_candidates_matches; i++) + if (i == 0) + c = toupper(candidates[i][match_len]); + else if (c == 0 || c != toupper(candidates[i][match_len])) + all_candidates_matches = false; + if (!all_candidates_matches) break; + match_len++; + } + + if (match_len > 0) { + data->DeleteChars((int)(word_start - data->Buf), + (int)(word_end - word_start)); + data->InsertChars(data->CursorPos, candidates[0], + candidates[0] + match_len); + } + + // List matches + AddLog("Possible matches:\n"); + for (int i = 0; i < candidates.Size; i++) + AddLog("- %s\n", candidates[i]); + } + + break; + } + case ImGuiInputTextFlags_CallbackHistory: { + // Example of HISTORY + const int prev_history_pos = HistoryPos; + if (data->EventKey == ImGuiKey_UpArrow) { + if (HistoryPos == -1) + HistoryPos = History.Size - 1; + else if (HistoryPos > 0) + HistoryPos--; + } else if (data->EventKey == ImGuiKey_DownArrow) { + if (HistoryPos != -1) + if (++HistoryPos >= History.Size) HistoryPos = -1; + } + + // A better implementation would preserve the data on the current input + // line along with cursor position. + if (prev_history_pos != HistoryPos) { + const char* history_str = + (HistoryPos >= 0) ? History[HistoryPos] : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, history_str); + } + } + } + return 0; + } +}; + +static void ShowExampleAppConsole(bool* p_open) { + static ExampleAppConsole console; + console.Draw("Example: Console", p_open); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Debug Log / ShowExampleAppLog() +//----------------------------------------------------------------------------- + +// Usage: +// static ExampleAppLog my_log; +// my_log.AddLog("Hello %d world\n", 123); +// my_log.Draw("title"); +struct ExampleAppLog { + ImGuiTextBuffer Buf; + ImGuiTextFilter Filter; + ImVector LineOffsets; // Index to lines offset. We maintain this with + // AddLog() calls. + bool AutoScroll; // Keep scrolling if already at the bottom. + + ExampleAppLog() { + AutoScroll = true; + Clear(); + } + + void Clear() { + Buf.clear(); + LineOffsets.clear(); + LineOffsets.push_back(0); + } + + void AddLog(const char* fmt, ...) IM_FMTARGS(2) { + int old_size = Buf.size(); + va_list args; + va_start(args, fmt); + Buf.appendfv(fmt, args); + va_end(args); + for (int new_size = Buf.size(); old_size < new_size; old_size++) + if (Buf[old_size] == '\n') LineOffsets.push_back(old_size + 1); + } + + void Draw(const char* title, bool* p_open = NULL) { + if (!ImGui::Begin(title, p_open)) { + ImGui::End(); + return; + } + + // Options menu + if (ImGui::BeginPopup("Options")) { + ImGui::Checkbox("Auto-scroll", &AutoScroll); + ImGui::EndPopup(); + } + + // Main window + if (ImGui::Button("Options")) ImGui::OpenPopup("Options"); + ImGui::SameLine(); + bool clear = ImGui::Button("Clear"); + ImGui::SameLine(); + bool copy = ImGui::Button("Copy"); + ImGui::SameLine(); + Filter.Draw("Filter", -100.0f); + + ImGui::Separator(); + + if (ImGui::BeginChild("scrolling", ImVec2(0, 0), false, + 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 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; + 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 + // - 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(); + + // 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(); + } +}; + +// Demonstrate creating a simple log window with basic filtering. +static void ShowExampleAppLog(bool* p_open) { + static ExampleAppLog log; + + // For the demo: add a debug button _BEFORE_ the normal log window contents + // We take advantage of a rarely used feature: multiple calls to Begin()/End() + // are appending to the _same_ window. 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* 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(), category, ImGui::GetTime(), word); + counter++; + } + } + ImGui::End(); + + // Actually call in the regular Log helper (which will Begin() into the same + // window as we just did) + log.Draw("Example: Log", p_open); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Simple Layout / ShowExampleAppLayout() +//----------------------------------------------------------------------------- + +// Demonstrate create a window with multiple child windows. +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", "Ctrl+W")) { + *p_open = false; + } + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // Left + static int selected = 0; + { + ImGui::BeginChild("left pane", ImVec2(150, 0), true); + 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::SameLine(); + + // 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)) { + 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::EndChild(); + if (ImGui::Button("Revert")) { + } + ImGui::SameLine(); + if (ImGui::Button("Save")) { + } + ImGui::EndGroup(); + } + } + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() +//----------------------------------------------------------------------------- + +static void ShowPlaceholderObject(const char* prefix, int uid) { + // Use object uid as identifier. Most commonly you could also use the object + // pointer as a base ID. + ImGui::PushID(uid); + + // Text and Tree nodes are less high than framed widgets, using + // AlignTextToFramePadding() we add vertical spacing to make the tree lines + // equal high. + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + bool node_open = ImGui::TreeNode("Object", "%s_%u", prefix, uid); + ImGui::TableSetColumnIndex(1); + ImGui::Text("my sailor is rich"); + + if (node_open) { + static float placeholder_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) { + ShowPlaceholderObject("Child", 424242); + } else { + // Here we use a TreeNode to highlight on hover (we could use e.g. + // Selectable as well) + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen | + ImGuiTreeNodeFlags_Bullet; + ImGui::TreeNodeEx("Field", flags, "Field_%d", i); + + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-FLT_MIN); + if (i >= 5) + ImGui::InputFloat("##value", &placeholder_members[i], 1.0f); + else + ImGui::DragFloat("##value", &placeholder_members[i], 0.01f); + ImGui::NextColumn(); + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + ImGui::PopID(); +} + +// Demonstrate create a simple property editor. +// This demo is a bit lackluster nowadays, would be nice to improve. +static void ShowExampleAppPropertyEditor(bool* p_open) { + ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Example: Property editor", p_open)) { + ImGui::End(); + return; + } + + IMGUI_DEMO_MARKER("Examples/Property Editor"); + HelpMarker( + "This example shows how you may implement a property editor using two " + "columns.\n" + "All objects/fields data are dummies here.\n"); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + if (ImGui::BeginTable("##split", 2, + ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_Resizable | + ImGuiTableFlags_ScrollY)) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Object"); + ImGui::TableSetupColumn("Contents"); + ImGui::TableHeadersRow(); + + // Iterate placeholder objects (all the same data) + for (int obj_i = 0; obj_i < 4; obj_i++) + ShowPlaceholderObject("Object", obj_i); + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Long Text / ShowExampleAppLongText() +//----------------------------------------------------------------------------- + +// 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); + 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()\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); + lines += 1000; + } + ImGui::BeginChild("Log"); + switch (test_type) { + case 0: + // Single call to TextUnformatted() with a big buffer + ImGui::TextUnformatted(log.begin(), log.end()); + break; + 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; + 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); + ImGui::PopStyleVar(); + break; + } + case 2: + // Multiple calls to Text(), not clipped (slow) + 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(); + break; + } + ImGui::EndChild(); + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Auto Resize / ShowExampleAppAutoResize() +//----------------------------------------------------------------------------- + +// Demonstrate creating a window which gets auto-resized according to its +// content. +static void ShowExampleAppAutoResize(bool* p_open) { + if (!ImGui::Begin("Example: Auto-resizing window", p_open, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::End(); + return; + } + IMGUI_DEMO_MARKER("Examples/Auto-resizing window"); + + static int lines = 10; + 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 + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Constrained Resize / ShowExampleAppConstrainedResize() +//----------------------------------------------------------------------------- + +// 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 + // FIXME: This doesn't take account of decoration size (e.g. title bar), + // library should make this easier. + static void AspectRatio(ImGuiSizeCallbackData* data) { + float aspect_ratio = *(float*)data->UserData; + data->DesiredSize.x = IM_MAX(data->CurrentSize.x, data->CurrentSize.y); + data->DesiredSize.y = (float)(int)(data->DesiredSize.x / aspect_ratio); + } + static void Square(ImGuiSizeCallbackData* data) { + data->DesiredSize.x = data->DesiredSize.y = + IM_MAX(data->CurrentSize.x, data->CurrentSize.y); + } + static void Step(ImGuiSizeCallbackData* data) { + float step = *(float*)data->UserData; + data->DesiredSize = + ImVec2((int)(data->CurrentSize.x / step + 0.5f) * step, + (int)(data->CurrentSize.y / step + 0.5f) * step); + } + }; + + const char* test_desc[] = { + "Between 100x100 and 500x500", "At least 100x100", + "Resize vertical only", "Resize horizontal only", + "Width Between 400 and 500", "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 = 5; // 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)); // Vertical only + if (type == 3) + ImGui::SetNextWindowSizeConstraints( + ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Horizontal only + if (type == 4) + ImGui::SetNextWindowSizeConstraints( + ImVec2(400, -1), ImVec2(500, -1)); // Width Between and 400 and 500 + if (type == 5) + ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), + CustomConstraints::AspectRatio, + (void*)&aspect_ratio); // Aspect ratio + if (type == 6) + ImGui::SetNextWindowSizeConstraints( + ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), + CustomConstraints::Square); // Always Square + if (type == 7) + 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) { + // 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::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() +//----------------------------------------------------------------------------- + +// 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) { + static int location = 0; + ImGuiIO& io = ImGui::GetIO(); + ImGuiWindowFlags window_flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav; + if (location >= 0) { + 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); + window_flags |= ImGuiWindowFlags_NoMove; + } else if (location == -2) { + // 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); + else + ImGui::Text("Mouse Position: "); + if (ImGui::BeginPopupContextWindow()) { + 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(); + } + } + 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 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(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(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(); + + // 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(base_pos.x + 100, base_pos.y + 300), + ImGuiCond_FirstUseEver); + ImGui::Begin(buf); + ImGui::Text("This window has a changing title."); + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Custom Rendering using ImDrawList API / +// ShowExampleAppCustomRendering() +//----------------------------------------------------------------------------- + +// Demonstrate using the low-level ImDrawList to draw custom shapes. +static void ShowExampleAppCustomRendering(bool* p_open) { + 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. 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")) { + 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, 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); + 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; + 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->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), 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 + 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 + + // Quadratic Bezier Curve (3 control points) + ImVec2 cp3[3] = {ImVec2(x, y + sz * 0.6f), + ImVec2(x + sz * 0.5f, y - sz * 0.4f), + ImVec2(x + sz, y + sz)}; + draw_list->AddBezierQuadratic(cp3[0], cp3[1], cp3[2], col, th, + curve_segments); + x += sz + spacing; + + // Cubic Bezier Curve (4 control points) + ImVec2 cp4[4] = {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)}; + draw_list->AddBezierCubic(cp4[0], cp4[1], cp4[2], cp4[3], col, th, + curve_segments); + + x = p.x + 4; + y += sz + spacing; + } + 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), + 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 + 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) + 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) * 11.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; + + 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."); + + // 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), true, + // 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) { + points.back() = mouse_pos_in_canvas; + if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) adding_line = false; + } + + // 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)) { + scrolling.x += io.MouseDelta.x; + scrolling.y += io.MouseDelta.y; + } + + // 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(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("BG/FG draw lists")) { + 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), 0, + 10 + 4); + if (draw_fg) + 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(); + } + + ImGui::EndTabBar(); + } + + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() +//----------------------------------------------------------------------------- + +// Simplified structure to mimic a Document model +struct MyDocument { + const char* Name; // Document 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 + bool WantClose; // Set when the document + 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)) { + Name = 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 placeholder contents for the Document + static void DisplayContents(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::SameLine(); + if (ImGui::Button("Save", ImVec2(100, 0))) doc->DoSave(); + 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) { + if (!ImGui::BeginPopupContextItem()) return; + + char buf[256]; + sprintf(buf, "Save %s", doc->Name); + if (ImGui::MenuItem(buf, "CTRL+S", false, doc->Open)) doc->DoSave(); + if (ImGui::MenuItem("Close", "CTRL+W", false, doc->Open)) + doc->DoQueueClose(); + ImGui::EndPopup(); + } +}; + +struct ExampleAppDocuments { + ImVector Documents; + + ExampleAppDocuments() { + 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)); + } +}; + +// [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 (MyDocument& doc : app.Documents) { + if (!doc.Open && doc.OpenPrev) ImGui::SetTabItemClosed(doc.Name); + doc.OpenPrev = doc.Open; + } +} + +void ShowExampleAppDocuments(bool* p_open) { + static ExampleAppDocuments app; + + // Options + static bool opt_reorderable = true; + static ImGuiTabBarFlags opt_fitting_flags = + ImGuiTabBarFlags_FittingPolicyDefault_; + + bool window_contents_visible = + ImGui::Begin("Example: Documents", p_open, ImGuiWindowFlags_MenuBar); + if (!window_contents_visible) { + ImGui::End(); + return; + } + + // Menu + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("File")) { + int open_count = 0; + for (MyDocument& doc : app.Documents) open_count += doc.Open ? 1 : 0; + + if (ImGui::BeginMenu("Open", open_count < app.Documents.Size)) { + 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 (MyDocument& doc : app.Documents) doc.DoQueueClose(); + if (ImGui::MenuItem("Exit", "Ctrl+F4") && p_open) *p_open = false; + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // [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]; + if (doc_n > 0) ImGui::SameLine(); + ImGui::PushID(&doc); + if (ImGui::Checkbox(doc.Name, &doc.Open)) + if (!doc.Open) doc.DoForceClose(); + ImGui::PopID(); + } + + ImGui::Separator(); + + // 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. + + // Submit Tab Bar and Tabs + { + ImGuiTabBarFlags tab_bar_flags = + (opt_fitting_flags) | + (opt_reorderable ? ImGuiTabBarFlags_Reorderable : 0); + if (ImGui::BeginTabBar("##tabs", tab_bar_flags)) { + if (opt_reorderable) NotifyOfDocumentsClosedElsewhere(app); + + // [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 (MyDocument& doc : app.Documents) { + if (!doc.Open) continue; + + ImGuiTabItemFlags tab_flags = + (doc.Dirty ? ImGuiTabItemFlags_UnsavedDocument : 0); + bool visible = ImGui::BeginTabItem(doc.Name, &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) { + doc.Open = true; + doc.DoQueueClose(); + } + + MyDocument::DisplayContextMenu(&doc); + if (visible) { + MyDocument::DisplayContents(&doc); + ImGui::EndTabItem(); + } + } + + ImGui::EndTabBar(); + } + } + + // Update closing queue + static ImVector close_queue; + if (close_queue.empty()) { + // Close queue is locked once we started a popup + for (MyDocument& doc : app.Documents) + if (doc.WantClose) { + doc.WantClose = false; + close_queue.push_back(&doc); + } + } + + // Display closing confirmation UI + if (!close_queue.empty()) { + int close_queue_unsaved_documents = 0; + for (int n = 0; n < close_queue.Size; n++) + if (close_queue[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(); + } else { + if (!ImGui::IsPopupOpen("Save?")) ImGui::OpenPopup("Save?"); + if (ImGui::BeginPopupModal("Save?", NULL, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Save change to the following items?"); + float item_height = ImGui::GetTextLineHeightWithSpacing(); + if (ImGui::BeginChildFrame(ImGui::GetID("frame"), + ImVec2(-FLT_MIN, 6.25f * item_height))) { + for (int n = 0; n < close_queue.Size; n++) + if (close_queue[n]->Dirty) ImGui::Text("%s", close_queue[n]->Name); + } + ImGui::EndChildFrame(); + + ImVec2 button_size(ImGui::GetFontSize() * 7.0f, 0.0f); + if (ImGui::Button("Yes", button_size)) { + for (int n = 0; n < close_queue.Size; n++) { + if (close_queue[n]->Dirty) close_queue[n]->DoSave(); + close_queue[n]->DoForceClose(); + } + close_queue.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No", button_size)) { + for (int n = 0; n < close_queue.Size; n++) + close_queue[n]->DoForceClose(); + close_queue.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", button_size)) { + close_queue.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + } + + ImGui::End(); +} + +// End of Demo code +#else + +void ImGui::ShowAboutWindow(bool*) {} +void ImGui::ShowDemoWindow(bool*) {} +void ImGui::ShowUserGuide() {} +void ImGui::ShowStyleEditor(ImGuiStyle*) {} + +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/symmetri/gui/imfilebrowser.h b/symmetri/gui/imfilebrowser.h new file mode 100644 index 0000000..35b0af8 --- /dev/null +++ b/symmetri/gui/imfilebrowser.h @@ -0,0 +1,929 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef IMGUI_VERSION +#error "include imgui.h before this header" +#endif + +using ImGuiFileBrowserFlags = int; + +enum ImGuiFileBrowserFlags_ { + ImGuiFileBrowserFlags_SelectDirectory = + 1 << 0, // select directory instead of regular file + ImGuiFileBrowserFlags_EnterNewFilename = + 1 << 1, // allow user to enter new filename when selecting regular file + ImGuiFileBrowserFlags_NoModal = + 1 << 2, // file browsing window is modal by default. specify this to use + // a popup window + ImGuiFileBrowserFlags_NoTitleBar = 1 << 3, // hide window title bar + ImGuiFileBrowserFlags_NoStatusBar = + 1 << 4, // hide status bar at the bottom of browsing window + ImGuiFileBrowserFlags_CloseOnEsc = + 1 << 5, // close file browser when pressing 'ESC' + ImGuiFileBrowserFlags_CreateNewDir = + 1 << 6, // allow user to create new directory + ImGuiFileBrowserFlags_MultipleSelection = + 1 << 7, // allow user to select multiple files. this will hide + // ImGuiFileBrowserFlags_EnterNewFilename + ImGuiFileBrowserFlags_HideRegularFiles = + 1 << 8, // hide regular files when ImGuiFileBrowserFlags_SelectDirectory + // is enabled + ImGuiFileBrowserFlags_ConfirmOnEnter = + 1 << 9, // confirm selection when pressnig 'ENTER' +}; + +namespace ImGui { +class FileBrowser { + public: + // pwd is set to current working directory by default + explicit FileBrowser(ImGuiFileBrowserFlags flags = 0); + + FileBrowser(const FileBrowser ©From); + + FileBrowser &operator=(const FileBrowser ©From); + + // set the window position (in pixels) + // default is centered + void SetWindowPos(int posx, int posy) noexcept; + + // set the window size (in pixels) + // default is (700, 450) + void SetWindowSize(int width, int height) noexcept; + + // set the window title text + void SetTitle(std::string title); + + // open the browsing window + void Open(); + + // close the browsing window + void Close(); + + // the browsing window is opened or not + bool IsOpened() const noexcept; + + // display the browsing window if opened + void Display(); + + // returns true when there is a selected filename and the "ok" button was + // clicked + bool HasSelected() const noexcept; + + // set current browsing directory + bool SetPwd( + const std::filesystem::path &pwd = std::filesystem::current_path()); + + // get current browsing directory + const std::filesystem::path &GetPwd() const noexcept; + + // returns selected filename. make sense only when HasSelected returns true + // when ImGuiFileBrowserFlags_MultipleSelection is enabled, only one of + // selected filename will be returned + std::filesystem::path GetSelected() const; + + // returns all selected filenames. + // when ImGuiFileBrowserFlags_MultipleSelection is enabled, use this + // instead of GetSelected + std::vector GetMultiSelected() const; + + // set selected filename to empty + void ClearSelected(); + + // (optional) set file type filters. eg. { ".h", ".cpp", ".hpp" } + // ".*" matches any file types + void SetTypeFilters(const std::vector &typeFilters); + + // set currently applied type filter + // default value is 0 (the first type filter) + void SetCurrentTypeFilterIndex(int index); + + // when ImGuiFileBrowserFlags_EnterNewFilename is set + // this function will pre-fill the input dialog with a filename. + void SetInputName(std::string_view input); + + private: + template + struct ScopeGuard { + ScopeGuard(Functor &&t) : func(std::move(t)) {} + + ~ScopeGuard() { func(); } + + private: + Functor func; + }; + + struct FileRecord { + bool isDir = false; + std::filesystem::path name; + std::string showName; + std::filesystem::path extension; + }; + + static std::string ToLower(const std::string &s); + + void UpdateFileRecords(); + + void SetPwdUncatched(const std::filesystem::path &pwd); + + bool IsExtensionMatched(const std::filesystem::path &extension) const; + + void ClearRangeSelectionState(); + +#ifdef _WIN32 + static std::uint32_t GetDrivesBitMask(); +#endif + + // for c++17 compatibility + +#if defined(__cpp_lib_char8_t) + static std::string u8StrToStr(std::u8string s); +#endif + static std::string u8StrToStr(std::string s); + + int width_; + int height_; + int posX_; + int posY_; + ImGuiFileBrowserFlags flags_; + + std::string title_; + std::string openLabel_; + + bool openFlag_; + bool closeFlag_; + bool isOpened_; + bool ok_; + bool posIsSet_; + + std::string statusStr_; + + std::vector typeFilters_; + unsigned int typeFilterIndex_; + bool hasAllFilter_; + + std::filesystem::path pwd_; + std::set selectedFilenames_; + unsigned int + rangeSelectionStart_; // enable range selection when shift is pressed + + std::vector fileRecords_; + + // IMPROVE: truncate when selectedFilename_.length() > inputNameBuf_.size() - + // 1 + static constexpr size_t INPUT_NAME_BUF_SIZE = 512; + std::unique_ptr> inputNameBuf_; + + std::string openNewDirLabel_; + std::unique_ptr> newDirNameBuf_; + +#ifdef _WIN32 + uint32_t drives_; +#endif +}; +} // namespace ImGui + +inline ImGui::FileBrowser::FileBrowser(ImGuiFileBrowserFlags flags) + : width_(700), + height_(450), + posX_(0), + posY_(0), + flags_(flags), + openFlag_(false), + closeFlag_(false), + isOpened_(false), + ok_(false), + posIsSet_(false), + rangeSelectionStart_(0), + inputNameBuf_(std::make_unique>()) { + if (flags_ & ImGuiFileBrowserFlags_CreateNewDir) { + newDirNameBuf_ = std::make_unique>(); + } + + inputNameBuf_->front() = '\0'; + inputNameBuf_->back() = '\0'; + SetTitle("file browser"); + SetPwd(std::filesystem::current_path()); + + typeFilters_.clear(); + typeFilterIndex_ = 0; + hasAllFilter_ = false; + +#ifdef _WIN32 + drives_ = GetDrivesBitMask(); +#endif +} + +inline ImGui::FileBrowser::FileBrowser(const FileBrowser ©From) + : FileBrowser() { + *this = copyFrom; +} + +inline ImGui::FileBrowser &ImGui::FileBrowser::operator=( + const FileBrowser ©From) { + width_ = copyFrom.width_; + height_ = copyFrom.height_; + + posX_ = copyFrom.posX_; + posY_ = copyFrom.posY_; + + flags_ = copyFrom.flags_; + SetTitle(copyFrom.title_); + + openFlag_ = copyFrom.openFlag_; + closeFlag_ = copyFrom.closeFlag_; + isOpened_ = copyFrom.isOpened_; + ok_ = copyFrom.ok_; + posIsSet_ = copyFrom.posIsSet_; + + statusStr_ = ""; + + typeFilters_ = copyFrom.typeFilters_; + typeFilterIndex_ = copyFrom.typeFilterIndex_; + hasAllFilter_ = copyFrom.hasAllFilter_; + + pwd_ = copyFrom.pwd_; + selectedFilenames_ = copyFrom.selectedFilenames_; + rangeSelectionStart_ = copyFrom.rangeSelectionStart_; + + fileRecords_ = copyFrom.fileRecords_; + + *inputNameBuf_ = *copyFrom.inputNameBuf_; + + openNewDirLabel_ = copyFrom.openNewDirLabel_; + if (flags_ & ImGuiFileBrowserFlags_CreateNewDir) { + newDirNameBuf_ = std::make_unique>(); + *newDirNameBuf_ = *copyFrom.newDirNameBuf_; + } + +#ifdef _WIN32 + drives_ = copyFrom.drives_; +#endif + + return *this; +} + +inline void ImGui::FileBrowser::SetWindowPos(int posx, int posy) noexcept { + posX_ = posx; + posY_ = posy; + posIsSet_ = true; +} + +inline void ImGui::FileBrowser::SetWindowSize(int width, int height) noexcept { + assert(width > 0 && height > 0); + width_ = width; + height_ = height; +} + +inline void ImGui::FileBrowser::SetTitle(std::string title) { + title_ = std::move(title); + openLabel_ = title_ + "##filebrowser_" + + std::to_string(reinterpret_cast(this)); + openNewDirLabel_ = + "new dir##new_dir_" + std::to_string(reinterpret_cast(this)); +} + +inline void ImGui::FileBrowser::Open() { + UpdateFileRecords(); + ClearSelected(); + statusStr_ = std::string(); + openFlag_ = true; + closeFlag_ = false; +} + +inline void ImGui::FileBrowser::Close() { + ClearSelected(); + statusStr_ = std::string(); + closeFlag_ = true; + openFlag_ = false; +} + +inline bool ImGui::FileBrowser::IsOpened() const noexcept { return isOpened_; } + +inline void ImGui::FileBrowser::Display() { + PushID(this); + ScopeGuard exitThis([this] { + openFlag_ = false; + closeFlag_ = false; + PopID(); + }); + + if (openFlag_) { + OpenPopup(openLabel_.c_str()); + } + isOpened_ = false; + + // open the popup window + + if (openFlag_ && (flags_ & ImGuiFileBrowserFlags_NoModal)) { + if (posIsSet_) { + SetNextWindowPos( + ImVec2(static_cast(posX_), static_cast(posY_))); + } + SetNextWindowSize( + ImVec2(static_cast(width_), static_cast(height_))); + } else { + if (posIsSet_) { + SetNextWindowPos( + ImVec2(static_cast(posX_), static_cast(posY_)), + ImGuiCond_FirstUseEver); + } + SetNextWindowSize( + ImVec2(static_cast(width_), static_cast(height_)), + ImGuiCond_FirstUseEver); + } + if (flags_ & ImGuiFileBrowserFlags_NoModal) { + if (!BeginPopup(openLabel_.c_str())) { + return; + } + } else if (!BeginPopupModal(openLabel_.c_str(), nullptr, + flags_ & ImGuiFileBrowserFlags_NoTitleBar + ? ImGuiWindowFlags_NoTitleBar + : 0)) { + return; + } + + isOpened_ = true; + ScopeGuard endPopup([] { EndPopup(); }); + + // display elements in pwd + +#ifdef _WIN32 + char currentDrive = static_cast(pwd_.c_str()[0]); + char driveStr[] = {currentDrive, ':', '\0'}; + + PushItemWidth(4 * GetFontSize()); + if (BeginCombo("##select_drive", driveStr)) { + ScopeGuard guard([&] { EndCombo(); }); + + for (int i = 0; i < 26; ++i) { + if (!(drives_ & (1 << i))) { + continue; + } + + char driveCh = static_cast('A' + i); + char selectableStr[] = {driveCh, ':', '\0'}; + bool selected = currentDrive == driveCh; + + if (Selectable(selectableStr, selected) && !selected) { + char newPwd[] = {driveCh, ':', '\\', '\0'}; + SetPwd(newPwd); + } + } + } + PopItemWidth(); + + SameLine(); +#endif + + int secIdx = 0, newPwdLastSecIdx = -1; + for (const auto &sec : pwd_) { +#ifdef _WIN32 + if (secIdx == 1) { + ++secIdx; + continue; + } +#endif + + PushID(secIdx); + if (secIdx > 0) { + SameLine(); + } + if (SmallButton(u8StrToStr(sec.u8string()).c_str())) { + newPwdLastSecIdx = secIdx; + } + PopID(); + + ++secIdx; + } + + if (newPwdLastSecIdx >= 0) { + int i = 0; + std::filesystem::path newPwd; + for (const auto &sec : pwd_) { + if (i++ > newPwdLastSecIdx) { + break; + } + newPwd /= sec; + } + +#ifdef _WIN32 + if (newPwdLastSecIdx == 0) { + newPwd /= "\\"; + } +#endif + + SetPwd(newPwd); + } + + SameLine(); + + if (SmallButton("*")) { + UpdateFileRecords(); + + std::set newSelectedFilenames; + for (auto &name : selectedFilenames_) { + auto it = std::find_if( + fileRecords_.begin(), fileRecords_.end(), + [&](const FileRecord &record) { return name == record.name; }); + if (it != fileRecords_.end()) { + newSelectedFilenames.insert(name); + } + } + + if (inputNameBuf_ && (*inputNameBuf_)[0]) { + newSelectedFilenames.insert(inputNameBuf_->data()); + } + } + + bool focusOnInputText = false; + if (newDirNameBuf_) { + SameLine(); + if (SmallButton("+")) { + OpenPopup(openNewDirLabel_.c_str()); + (*newDirNameBuf_)[0] = '\0'; + } + + if (BeginPopup(openNewDirLabel_.c_str())) { + ScopeGuard endNewDirPopup([] { EndPopup(); }); + + InputText("name", newDirNameBuf_->data(), newDirNameBuf_->size()); + focusOnInputText |= IsItemFocused(); + SameLine(); + + if (Button("ok") && (*newDirNameBuf_)[0] != '\0') { + ScopeGuard closeNewDirPopup([] { CloseCurrentPopup(); }); + if (create_directory(pwd_ / newDirNameBuf_->data())) { + UpdateFileRecords(); + } else { + statusStr_ = + "failed to create " + std::string(newDirNameBuf_->data()); + } + } + } + } + + // browse files in a child window + + float reserveHeight = GetFrameHeightWithSpacing(); + std::filesystem::path newPwd; + bool setNewPwd = false; + if (!(flags_ & ImGuiFileBrowserFlags_SelectDirectory) && + (flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) + reserveHeight += GetFrameHeightWithSpacing(); + + { + BeginChild("ch", ImVec2(0, -reserveHeight), true, + (flags_ & ImGuiFileBrowserFlags_NoModal) + ? ImGuiWindowFlags_AlwaysHorizontalScrollbar + : 0); + ScopeGuard endChild([] { EndChild(); }); + + const bool shouldHideRegularFiles = + (flags_ & ImGuiFileBrowserFlags_HideRegularFiles) && + (flags_ & ImGuiFileBrowserFlags_SelectDirectory); + + for (unsigned int rscIndex = 0; rscIndex < fileRecords_.size(); + ++rscIndex) { + auto &rsc = fileRecords_[rscIndex]; + if (!rsc.isDir && shouldHideRegularFiles) { + continue; + } + if (!rsc.isDir && !IsExtensionMatched(rsc.extension)) { + continue; + } + if (!rsc.name.empty() && rsc.name.c_str()[0] == '$') { + continue; + } + + const bool selected = + selectedFilenames_.find(rsc.name) != selectedFilenames_.end(); + if (Selectable(rsc.showName.c_str(), selected, + ImGuiSelectableFlags_DontClosePopups)) { + const bool wantDir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; + const bool canSelect = rsc.name != ".." && rsc.isDir == wantDir; + const bool rangeSelect = + canSelect && GetIO().KeyShift && + rangeSelectionStart_ < fileRecords_.size() && + (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && + IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + const bool multiSelect = + !rangeSelect && GetIO().KeyCtrl && + (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && + IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + + if (rangeSelect) { + const unsigned int first = (std::min)(rangeSelectionStart_, rscIndex); + const unsigned int last = (std::max)(rangeSelectionStart_, rscIndex); + selectedFilenames_.clear(); + for (unsigned int i = first; i <= last; ++i) { + if (fileRecords_[i].isDir != wantDir) { + continue; + } + if (!wantDir && !IsExtensionMatched(fileRecords_[i].extension)) { + continue; + } + selectedFilenames_.insert(fileRecords_[i].name); + } + } else if (selected) { + if (!multiSelect) { + selectedFilenames_ = {rsc.name}; + rangeSelectionStart_ = rscIndex; + } else { + selectedFilenames_.erase(rsc.name); + } + (*inputNameBuf_)[0] = '\0'; + } else if (canSelect) { + if (multiSelect) { + selectedFilenames_.insert(rsc.name); + } else { + selectedFilenames_ = {rsc.name}; + } + if (!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) { +#ifdef _MSC_VER + strcpy_s(inputNameBuf_->data(), inputNameBuf_->size(), + u8StrToStr(rsc.name.u8string()).c_str()); +#else + std::strncpy(inputNameBuf_->data(), + u8StrToStr(rsc.name.u8string()).c_str(), + inputNameBuf_->size() - 1); +#endif + } + rangeSelectionStart_ = rscIndex; + } else { + if (!multiSelect) { + selectedFilenames_.clear(); + } + } + } + + if (IsItemClicked(0) && IsMouseDoubleClicked(0)) { + if (rsc.isDir) { + setNewPwd = true; + newPwd = (rsc.name != "..") ? (pwd_ / rsc.name) : pwd_.parent_path(); + } else if (!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) { + selectedFilenames_ = {rsc.name}; + ok_ = true; + CloseCurrentPopup(); + } + } + } + } + + if (setNewPwd) { + SetPwd(newPwd); + } + + if (!(flags_ & ImGuiFileBrowserFlags_SelectDirectory) && + (flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) { + PushID(this); + ScopeGuard popTextID([] { PopID(); }); + + PushItemWidth(-1); + if (InputText("", inputNameBuf_->data(), inputNameBuf_->size()) && + inputNameBuf_->at(0) != '\0') { + selectedFilenames_ = {inputNameBuf_->data()}; + } + focusOnInputText |= IsItemFocused(); + PopItemWidth(); + } + + if (!focusOnInputText) { + const bool selectAll = + (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && + IsKeyPressed(ImGuiKey_A) && + (IsKeyDown(ImGuiKey_LeftCtrl) || IsKeyDown(ImGuiKey_RightCtrl)); + if (selectAll) { + const bool needDir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; + selectedFilenames_.clear(); + for (size_t i = 1; i < fileRecords_.size(); ++i) { + auto &record = fileRecords_[i]; + if (record.isDir == needDir && + (needDir || IsExtensionMatched(record.extension))) { + selectedFilenames_.insert(record.name); + } + } + } + } + + const bool enter = (flags_ & ImGuiFileBrowserFlags_ConfirmOnEnter) && + IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + IsKeyPressed(ImGuiKey_Enter); + if (!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) { + if ((Button(" ok ") || enter) && !selectedFilenames_.empty()) { + ok_ = true; + CloseCurrentPopup(); + } + } else { + if (Button(" ok ") || enter) { + ok_ = true; + CloseCurrentPopup(); + } + } + + SameLine(); + + bool shouldExit = Button("cancel") || closeFlag_ || + ((flags_ & ImGuiFileBrowserFlags_CloseOnEsc) && + IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + IsKeyPressed(ImGuiKey_Escape)); + if (shouldExit) { + CloseCurrentPopup(); + } + + if (!statusStr_.empty() && !(flags_ & ImGuiFileBrowserFlags_NoStatusBar)) { + SameLine(); + Text("%s", statusStr_.c_str()); + } + + if (!typeFilters_.empty()) { + SameLine(); + PushItemWidth(8 * GetFontSize()); + if (BeginCombo("##type_filters", typeFilters_[typeFilterIndex_].c_str())) { + ScopeGuard guard([&] { EndCombo(); }); + + for (size_t i = 0; i < typeFilters_.size(); ++i) { + bool selected = i == typeFilterIndex_; + if (Selectable(typeFilters_[i].c_str(), selected) && !selected) { + typeFilterIndex_ = static_cast(i); + } + } + } + PopItemWidth(); + } +} + +inline bool ImGui::FileBrowser::HasSelected() const noexcept { return ok_; } + +inline bool ImGui::FileBrowser::SetPwd(const std::filesystem::path &pwd) { + try { + SetPwdUncatched(pwd); + return true; + } catch (const std::exception &err) { + statusStr_ = std::string("last error: ") + err.what(); + } catch (...) { + statusStr_ = "last error: unknown"; + } + + SetPwdUncatched(std::filesystem::current_path()); + return false; +} + +inline const class std::filesystem::path &ImGui::FileBrowser::GetPwd() + const noexcept { + return pwd_; +} + +inline std::filesystem::path ImGui::FileBrowser::GetSelected() const { + // when ok_ is true, selectedFilenames_ may be empty if SelectDirectory + // is enabled. return pwd in that case. + if (selectedFilenames_.empty()) { + return pwd_; + } + return pwd_ / *selectedFilenames_.begin(); +} + +inline std::vector ImGui::FileBrowser::GetMultiSelected() + const { + if (selectedFilenames_.empty()) { + return {pwd_}; + } + + std::vector ret; + ret.reserve(selectedFilenames_.size()); + for (auto &s : selectedFilenames_) { + ret.push_back(pwd_ / s); + } + + return ret; +} + +inline void ImGui::FileBrowser::ClearSelected() { + selectedFilenames_.clear(); + (*inputNameBuf_)[0] = '\0'; + ok_ = false; +} + +inline void ImGui::FileBrowser::SetTypeFilters( + const std::vector &_typeFilters) { + typeFilters_.clear(); + + // remove duplicate filter names due to case unsensitivity on windows + +#ifdef _WIN32 + + std::vector typeFilters; + for (auto &rawFilter : _typeFilters) { + std::string lowerFilter = ToLower(rawFilter); + auto it = std::find(typeFilters.begin(), typeFilters.end(), lowerFilter); + if (it == typeFilters.end()) { + typeFilters.push_back(std::move(lowerFilter)); + } + } + +#else + + auto &typeFilters = _typeFilters; + +#endif + + // insert auto-generated filter + hasAllFilter_ = false; + if (typeFilters.size() > 1) { + hasAllFilter_ = true; + std::string allFiltersName = std::string(); + for (size_t i = 0; i < typeFilters.size(); ++i) { + if (typeFilters[i] == std::string_view(".*")) { + hasAllFilter_ = false; + break; + } + + if (i > 0) { + allFiltersName += ","; + } + allFiltersName += typeFilters[i]; + } + + if (hasAllFilter_) { + typeFilters_.push_back(std::move(allFiltersName)); + } + } + + std::copy(typeFilters.begin(), typeFilters.end(), + std::back_inserter(typeFilters_)); + + typeFilterIndex_ = 0; +} + +inline void ImGui::FileBrowser::SetCurrentTypeFilterIndex(int index) { + typeFilterIndex_ = static_cast(index); +} + +inline void ImGui::FileBrowser::SetInputName(std::string_view input) { + if (flags_ & ImGuiFileBrowserFlags_EnterNewFilename) { + if (input.size() >= static_cast(INPUT_NAME_BUF_SIZE)) { + // If input doesn't fit trim off characters + input = input.substr(0, INPUT_NAME_BUF_SIZE - 1); + } + std::copy(input.begin(), input.end(), inputNameBuf_->begin()); + inputNameBuf_->at(input.size()) = '\0'; + selectedFilenames_ = {inputNameBuf_->data()}; + } +} + +inline std::string ImGui::FileBrowser::ToLower(const std::string &s) { + std::string ret = s; + for (char &c : ret) { + c = static_cast(std::tolower(c)); + } + return ret; +} + +inline void ImGui::FileBrowser::UpdateFileRecords() { + fileRecords_ = {FileRecord{true, "..", "[D] ..", ""}}; + + for (auto &p : std::filesystem::directory_iterator(pwd_)) { + FileRecord rcd; + + if (p.is_regular_file()) { + rcd.isDir = false; + } else if (p.is_directory()) { + rcd.isDir = true; + } else { + continue; + } + + rcd.name = p.path().filename(); + if (rcd.name.empty()) { + continue; + } + + rcd.extension = p.path().filename().extension(); + + rcd.showName = (rcd.isDir ? "[D] " : "[F] ") + + u8StrToStr(p.path().filename().u8string()); + fileRecords_.push_back(rcd); + } + + std::sort(fileRecords_.begin(), fileRecords_.end(), + [](const FileRecord &L, const FileRecord &R) { + return (L.isDir ^ R.isDir) ? L.isDir : (L.name < R.name); + }); + + ClearRangeSelectionState(); +} + +inline void ImGui::FileBrowser::SetPwdUncatched( + const std::filesystem::path &pwd) { + pwd_ = absolute(pwd); + UpdateFileRecords(); + selectedFilenames_.clear(); + (*inputNameBuf_)[0] = '\0'; +} + +inline bool ImGui::FileBrowser::IsExtensionMatched( + const std::filesystem::path &_extension) const { +#ifdef _WIN32 + std::filesystem::path extension = ToLower(u8StrToStr(_extension.u8string())); +#else + auto &extension = _extension; +#endif + + // no type filters + if (typeFilters_.empty()) { + return true; + } + + // invalid type filter index + if (static_cast(typeFilterIndex_) >= typeFilters_.size()) { + return true; + } + + // all type filters + if (hasAllFilter_ && typeFilterIndex_ == 0) { + for (size_t i = 1; i < typeFilters_.size(); ++i) { + if (extension == typeFilters_[i]) { + return true; + } + } + return false; + } + + // universal filter + if (typeFilters_[typeFilterIndex_] == std::string_view(".*")) { + return true; + } + + // regular filter + return extension == typeFilters_[typeFilterIndex_]; +} + +inline void ImGui::FileBrowser::ClearRangeSelectionState() { + rangeSelectionStart_ = 9999999; + const bool dir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; + for (unsigned int i = 1; i < fileRecords_.size(); ++i) { + if (fileRecords_[i].isDir == dir) { + if (!dir && !IsExtensionMatched(fileRecords_[i].extension)) { + continue; + } + rangeSelectionStart_ = i; + break; + } + } +} + +#if defined(__cpp_lib_char8_t) +inline std::string ImGui::FileBrowser::u8StrToStr(std::u8string s) { + return std::string(s.begin(), s.end()); +} +#endif + +inline std::string ImGui::FileBrowser::u8StrToStr(std::string s) { return s; } + +#ifdef _WIN32 + +#ifndef _INC_WINDOWS + +#ifndef WIN32_LEAN_AND_MEAN + +#define IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN + +#endif // #ifndef WIN32_LEAN_AND_MEAN + +#include + +#ifdef IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN +#undef IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#endif // #ifdef IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN + +#endif // #ifdef _INC_WINDOWS + +inline std::uint32_t ImGui::FileBrowser::GetDrivesBitMask() { + DWORD mask = GetLogicalDrives(); + uint32_t ret = 0; + for (int i = 0; i < 26; ++i) { + if (!(mask & (1 << i))) { + continue; + } + char rootName[4] = {static_cast('A' + i), ':', '\\', '\0'}; + UINT type = GetDriveTypeA(rootName); + if (type == DRIVE_REMOVABLE || type == DRIVE_FIXED || + type == DRIVE_REMOTE) { + ret |= (1 << i); + } + } + return ret; +} + +#endif diff --git a/symmetri/gui/imgui b/symmetri/gui/imgui new file mode 160000 index 0000000..313676d --- /dev/null +++ b/symmetri/gui/imgui @@ -0,0 +1 @@ +Subproject commit 313676d200f093e2694b5cfca574f72a2b116c85 diff --git a/symmetri/gui/imgui_bezier_math.h b/symmetri/gui/imgui_bezier_math.h new file mode 100644 index 0000000..eaeb372 --- /dev/null +++ b/symmetri/gui/imgui_bezier_math.h @@ -0,0 +1,194 @@ +//------------------------------------------------------------------------------ +// VERSION 0.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +#ifndef __IMGUI_BEZIER_MATH_H__ +#define __IMGUI_BEZIER_MATH_H__ +#pragma once + +//------------------------------------------------------------------------------ +#include "imgui_extra_math.h" + +//------------------------------------------------------------------------------ +template +struct ImCubicBezierPointsT { + T P0; + T P1; + T P2; + T P3; +}; +using ImCubicBezierPoints = ImCubicBezierPointsT; + +//------------------------------------------------------------------------------ +// Low-level Bezier curve sampling. +template +inline T ImLinearBezier(const T& p0, const T& p1, float t); +template +inline T ImLinearBezierDt(const T& p0, const T& p1, float t); +template +inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t); +template +inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t); +template +inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, + float t); +template +inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, + float t); + +// High-level Bezier sampling, automatically collapse to lower level Bezier +// curves if control points overlap. +template +inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, + float t); +template +inline T ImCubicBezierSample(const ImCubicBezierPointsT& curve, float t); +template +inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, + const T& p3, float t); +template +inline T ImCubicBezierTangent(const ImCubicBezierPointsT& curve, float t); + +// Calculate approximate length of Cubic Bezier curve. +template +inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, + const T& p3); +template +inline float ImCubicBezierLength(const ImCubicBezierPointsT& curve); + +// Splits Cubic Bezier curve into two curves. +template +struct ImCubicBezierSplitResultT { + ImCubicBezierPointsT Left; + ImCubicBezierPointsT Right; +}; +using ImCubicBezierSplitResult = ImCubicBezierSplitResultT; + +template +inline ImCubicBezierSplitResultT ImCubicBezierSplit(const T& p0, const T& p1, + const T& p2, const T& p3, + float t); +template +inline ImCubicBezierSplitResultT ImCubicBezierSplit( + const ImCubicBezierPointsT& curve, float t); + +// Returns bounding rectangle of Cubic Bezier curve. +inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, + const ImVec2& p2, const ImVec2& p3); +inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve); + +// Project point on Cubic Bezier curve. +struct ImProjectResult { + ImVec2 Point; // Point on curve + float Time; // [0 - 1] + float Distance; // Distance to curve +}; + +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImVec2& p0, + const ImVec2& p1, + const ImVec2& p2, + const ImVec2& p3, + const int subdivisions = 100); +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, + const ImCubicBezierPoints& curve, + const int subdivisions = 100); + +// Calculate intersection between line and a Cubic Bezier curve. +struct ImCubicBezierIntersectResult { + int Count; + ImVec2 Points[3]; +}; + +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect( + const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, + const ImVec2& a0, const ImVec2& a1); +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect( + const ImCubicBezierPoints& curve, const ImLine& line); + +// Adaptive Cubic Bezier subdivision. +enum ImCubicBezierSubdivideFlags { + ImCubicBezierSubdivide_None = 0, + ImCubicBezierSubdivide_SkipFirst = 1 +}; + +struct ImCubicBezierSubdivideSample { + ImVec2 Point; + ImVec2 Tangent; +}; + +using ImCubicBezierSubdivideCallback = + void (*)(const ImCubicBezierSubdivideSample& p, void* user_pointer); + +inline void ImCubicBezierSubdivide( + ImCubicBezierSubdivideCallback callback, void* user_pointer, + const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, + float tess_tol = -1.0f, + ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); +inline void ImCubicBezierSubdivide( + ImCubicBezierSubdivideCallback callback, void* user_pointer, + const ImCubicBezierPoints& curve, float tess_tol = -1.0f, + ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); + +// F has signature void(const ImCubicBezierSubdivideSample& p) +template +inline void ImCubicBezierSubdivide( + F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, + const ImVec2& p3, float tess_tol = -1.0f, + ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); +template +inline void ImCubicBezierSubdivide( + F& callback, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, + ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); + +// Fixed step Cubic Bezier subdivision. +struct ImCubicBezierFixedStepSample { + float T; + float Length; + ImVec2 Point; + bool BreakSearch; +}; + +using ImCubicBezierFixedStepCallback = + void (*)(ImCubicBezierFixedStepSample& sample, void* user_pointer); + +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, + void* user_pointer, const ImVec2& p0, + const ImVec2& p1, const ImVec2& p2, + const ImVec2& p3, float step, + bool overshoot = false, + float max_value_error = 1e-3f, + float max_t_error = 1e-5f); +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, + void* user_pointer, + const ImCubicBezierPoints& curve, float step, + bool overshoot = false, + float max_value_error = 1e-3f, + float max_t_error = 1e-5f); + +// F has signature void(const ImCubicBezierFixedStepSample& p) +template +inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, + const ImVec2& p1, const ImVec2& p2, + const ImVec2& p3, float step, + bool overshoot = false, + float max_value_error = 1e-3f, + float max_t_error = 1e-5f); +template +inline void ImCubicBezierFixedStep(F& callback, + const ImCubicBezierPoints& curve, float step, + bool overshoot = false, + float max_value_error = 1e-3f, + float max_t_error = 1e-5f); + +//------------------------------------------------------------------------------ +#include "imgui_bezier_math.inl" + +//------------------------------------------------------------------------------ +#endif // __IMGUI_BEZIER_MATH_H__ diff --git a/symmetri/gui/imgui_bezier_math.inl b/symmetri/gui/imgui_bezier_math.inl new file mode 100644 index 0000000..3020bdb --- /dev/null +++ b/symmetri/gui/imgui_bezier_math.inl @@ -0,0 +1,675 @@ +//------------------------------------------------------------------------------ +// VERSION 0.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_BEZIER_MATH_INL__ +# define __IMGUI_BEZIER_MATH_INL__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_bezier_math.h" +# include // used in ImCubicBezierFixedStep + + +//------------------------------------------------------------------------------ +template +inline T ImLinearBezier(const T& p0, const T& p1, float t) +{ + return p0 + t * (p1 - p0); +} + +template +inline T ImLinearBezierDt(const T& p0, const T& p1, float t) +{ + IM_UNUSED(t); + + return p1 - p0; +} + +template +inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t) +{ + const auto a = 1 - t; + + return a * a * p0 + 2 * t * a * p1 + t * t * p2; +} + +template +inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t) +{ + return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1); +} + +template +inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto a = 1 - t; + const auto b = a * a * a; + const auto c = t * t * t; + + return b * p0 + 3 * t * a * a * p1 + 3 * t * t * a * p2 + c * p3; +} + +template +inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto a = 1 - t; + const auto b = a * a; + const auto c = t * t; + const auto d = 2 * t * a; + + return -3 * p0 * b + 3 * p1 * (b - d) + 3 * p2 * (d - c) + 3 * p3 * c; +} + +template +inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f; + const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f; + + if (cp0_zero && cp1_zero) + return ImLinearBezier(p0, p3, t); + else if (cp0_zero) + return ImQuadraticBezier(p0, p2, p3, t); + else if (cp1_zero) + return ImQuadraticBezier(p0, p1, p3, t); + else + return ImCubicBezier(p0, p1, p2, p3, t); +} + +template +inline T ImCubicBezierSample(const ImCubicBezierPointsT& curve, float t) +{ + return ImCubicBezierSample(curve.P0, curve.P1, curve.P2, curve.P3, t); +} + +template +inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f; + const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f; + + if (cp0_zero && cp1_zero) + return ImLinearBezierDt(p0, p3, t); + else if (cp0_zero) + return ImQuadraticBezierDt(p0, p2, p3, t); + else if (cp1_zero) + return ImQuadraticBezierDt(p0, p1, p3, t); + else + return ImCubicBezierDt(p0, p1, p2, p3, t); +} + +template +inline T ImCubicBezierTangent(const ImCubicBezierPointsT& curve, float t) +{ + return ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, t); +} + +template +inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3) +{ + // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) + static const float t_values[] = + { + -0.0640568928626056260850430826247450385909f, + 0.0640568928626056260850430826247450385909f, + -0.1911188674736163091586398207570696318404f, + 0.1911188674736163091586398207570696318404f, + -0.3150426796961633743867932913198102407864f, + 0.3150426796961633743867932913198102407864f, + -0.4337935076260451384870842319133497124524f, + 0.4337935076260451384870842319133497124524f, + -0.5454214713888395356583756172183723700107f, + 0.5454214713888395356583756172183723700107f, + -0.6480936519369755692524957869107476266696f, + 0.6480936519369755692524957869107476266696f, + -0.7401241915785543642438281030999784255232f, + 0.7401241915785543642438281030999784255232f, + -0.8200019859739029219539498726697452080761f, + 0.8200019859739029219539498726697452080761f, + -0.8864155270044010342131543419821967550873f, + 0.8864155270044010342131543419821967550873f, + -0.9382745520027327585236490017087214496548f, + 0.9382745520027327585236490017087214496548f, + -0.9747285559713094981983919930081690617411f, + 0.9747285559713094981983919930081690617411f, + -0.9951872199970213601799974097007368118745f, + 0.9951872199970213601799974097007368118745f + }; + + // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article) + static const float c_values[] = + { + 0.1279381953467521569740561652246953718517f, + 0.1279381953467521569740561652246953718517f, + 0.1258374563468282961213753825111836887264f, + 0.1258374563468282961213753825111836887264f, + 0.1216704729278033912044631534762624256070f, + 0.1216704729278033912044631534762624256070f, + 0.1155056680537256013533444839067835598622f, + 0.1155056680537256013533444839067835598622f, + 0.1074442701159656347825773424466062227946f, + 0.1074442701159656347825773424466062227946f, + 0.0976186521041138882698806644642471544279f, + 0.0976186521041138882698806644642471544279f, + 0.0861901615319532759171852029837426671850f, + 0.0861901615319532759171852029837426671850f, + 0.0733464814110803057340336152531165181193f, + 0.0733464814110803057340336152531165181193f, + 0.0592985849154367807463677585001085845412f, + 0.0592985849154367807463677585001085845412f, + 0.0442774388174198061686027482113382288593f, + 0.0442774388174198061686027482113382288593f, + 0.0285313886289336631813078159518782864491f, + 0.0285313886289336631813078159518782864491f, + 0.0123412297999871995468056670700372915759f, + 0.0123412297999871995468056670700372915759f + }; + + static_assert(sizeof(t_values) / sizeof(*t_values) == sizeof(c_values) / sizeof(*c_values), ""); + + auto arc = [p0, p1, p2, p3](float t) + { + const auto p = ImCubicBezierDt(p0, p1, p2, p3, t); + const auto l = ImLength(p); + return l; + }; + + const auto z = 0.5f; + const auto n = sizeof(t_values) / sizeof(*t_values); + + auto accumulator = 0.0f; + for (size_t i = 0; i < n; ++i) + { + const auto t = z * t_values[i] + z; + accumulator += c_values[i] * arc(t); + } + + return z * accumulator; +} + +template +inline float ImCubicBezierLength(const ImCubicBezierPointsT& curve) +{ + return ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3); +} + +template +inline ImCubicBezierSplitResultT ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t) +{ + const auto z1 = t; + const auto z2 = z1 * z1; + const auto z3 = z1 * z1 * z1; + const auto s1 = z1 - 1; + const auto s2 = s1 * s1; + const auto s3 = s1 * s1 * s1; + + return ImCubicBezierSplitResultT + { + ImCubicBezierPointsT + { + p0, + z1 * p1 - s1 * p0, + z2 * p2 - 2 * z1 * s1 * p1 + s2 * p0, + z3 * p3 - 3 * z2 * s1 * p2 + 3 * z1 * s2 * p1 - s3 * p0 + }, + ImCubicBezierPointsT + { + z3 * p0 - 3 * z2 * s1 * p1 + 3 * z1 * s2 * p2 - s3 * p3, + z2 * p1 - 2 * z1 * s1 * p2 + s2 * p3, + z1 * p2 - s1 * p3, + p3, + } + }; +} + +template +inline ImCubicBezierSplitResultT ImCubicBezierSplit(const ImCubicBezierPointsT& curve, float t) +{ + return ImCubicBezierSplit(curve.P0, curve.P1, curve.P2, curve.P3, t); +} + +inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3) +{ + auto a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0; + auto b = 6 * p0 - 12 * p1 + 6 * p2; + auto c = 3 * p1 - 3 * p0; + auto delta_squared = ImMul(b, b) - 4 * ImMul(a, c); + + auto tl = ImMin(p0, p3); + auto rb = ImMax(p0, p3); + +# define IM_VEC2_INDEX(v, i) *(&v.x + i) + + for (int i = 0; i < 2; ++i) + { + if (IM_VEC2_INDEX(a, i) == 0.0f) + continue; + + if (IM_VEC2_INDEX(delta_squared, i) >= 0) + { + auto delta = ImSqrt(IM_VEC2_INDEX(delta_squared, i)); + + auto t0 = (-IM_VEC2_INDEX(b, i) + delta) / (2 * IM_VEC2_INDEX(a, i)); + if (t0 > 0 && t0 < 1) + { + auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t0); + IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p); + IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p); + } + + auto t1 = (-IM_VEC2_INDEX(b, i) - delta) / (2 * IM_VEC2_INDEX(a, i)); + if (t1 > 0 && t1 < 1) + { + auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t1); + IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p); + IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p); + } + } + } + +# undef IM_VEC2_INDEX + + return ImRect(tl, rb); +} + +inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve) +{ + return ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3); +} + +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& point, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions) +{ + // http://pomax.github.io/bezierinfo/#projections + + const float epsilon = 1e-5f; + const float fixed_step = 1.0f / static_cast(subdivisions - 1); + + ImProjectResult result; + result.Point = point; + result.Time = 0.0f; + result.Distance = FLT_MAX; + + // Step 1: Coarse check + for (int i = 0; i < subdivisions; ++i) + { + auto t = i * fixed_step; + auto p = ImCubicBezier(p0, p1, p2, p3, t); + auto s = point - p; + auto d = ImDot(s, s); + + if (d < result.Distance) + { + result.Point = p; + result.Time = t; + result.Distance = d; + } + } + + if (result.Time == 0.0f || ImFabs(result.Time - 1.0f) <= epsilon) + { + result.Distance = ImSqrt(result.Distance); + return result; + } + + // Step 2: Fine check + auto left = result.Time - fixed_step; + auto right = result.Time + fixed_step; + auto step = fixed_step * 0.1f; + + for (auto t = left; t < right + step; t += step) + { + auto p = ImCubicBezier(p0, p1, p2, p3, t); + auto s = point - p; + auto d = ImDot(s, s); + + if (d < result.Distance) + { + result.Point = p; + result.Time = t; + result.Distance = d; + } + } + + result.Distance = ImSqrt(result.Distance); + + return result; +} + +inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions) +{ + return ImProjectOnCubicBezier(p, curve.P0, curve.P1, curve.P2, curve.P3, subdivisions); +} + +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1) +{ + auto cubic_roots = [](float a, float b, float c, float d, float* roots) -> int + { + int count = 0; + + auto sign = [](float x) -> float { return x < 0 ? -1.0f : 1.0f; }; + + auto A = b / a; + auto B = c / a; + auto C = d / a; + + auto Q = (3 * B - ImPow(A, 2)) / 9; + auto R = (9 * A * B - 27 * C - 2 * ImPow(A, 3)) / 54; + auto D = ImPow(Q, 3) + ImPow(R, 2); // polynomial discriminant + + if (D >= 0) // complex or duplicate roots + { + auto S = sign(R + ImSqrt(D)) * ImPow(ImFabs(R + ImSqrt(D)), (1.0f / 3.0f)); + auto T = sign(R - ImSqrt(D)) * ImPow(ImFabs(R - ImSqrt(D)), (1.0f / 3.0f)); + + roots[0] = -A / 3 + (S + T); // real root + roots[1] = -A / 3 - (S + T) / 2; // real part of complex root + roots[2] = -A / 3 - (S + T) / 2; // real part of complex root + auto Im = ImFabs(ImSqrt(3) * (S - T) / 2); // complex part of root pair + + // discard complex roots + if (Im != 0) + count = 1; + else + count = 3; + } + else // distinct real roots + { + auto th = ImAcos(R / ImSqrt(-ImPow(Q, 3))); + + roots[0] = 2 * ImSqrt(-Q) * ImCos(th / 3) - A / 3; + roots[1] = 2 * ImSqrt(-Q) * ImCos((th + 2 * IM_PI) / 3) - A / 3; + roots[2] = 2 * ImSqrt(-Q) * ImCos((th + 4 * IM_PI) / 3) - A / 3; + + count = 3; + } + + return count; + }; + + // https://github.com/kaishiqi/Geometric-Bezier/blob/master/GeometricBezier/src/kaishiqi/geometric/intersection/Intersection.as + // + // Start with Bezier using Bernstein polynomials for weighting functions: + // (1-t^3)P0 + 3t(1-t)^2P1 + 3t^2(1-t)P2 + t^3P3 + // + // Expand and collect terms to form linear combinations of original Bezier + // controls. This ends up with a vector cubic in t: + // (-P0+3P1-3P2+P3)t^3 + (3P0-6P1+3P2)t^2 + (-3P0+3P1)t + P0 + // /\ /\ /\ /\ + // || || || || + // c3 c2 c1 c0 + + // Calculate the coefficients + auto c3 = -p0 + 3 * p1 - 3 * p2 + p3; + auto c2 = 3 * p0 - 6 * p1 + 3 * p2; + auto c1 = -3 * p0 + 3 * p1; + auto c0 = p0; + + // Convert line to normal form: ax + by + c = 0 + auto a = a1.y - a0.y; + auto b = a0.x - a1.x; + auto c = a0.x * (a0.y - a1.y) + a0.y * (a1.x - a0.x); + + // Rotate each cubic coefficient using line for new coordinate system? + // Find roots of rotated cubic + float roots[3]; + auto rootCount = cubic_roots( + a * c3.x + b * c3.y, + a * c2.x + b * c2.y, + a * c1.x + b * c1.y, + a * c0.x + b * c0.y + c, + roots); + + // Any roots in closed interval [0,1] are intersections on Bezier, but + // might not be on the line segment. + // Find intersections and calculate point coordinates + + auto min = ImMin(a0, a1); + auto max = ImMax(a0, a1); + + ImCubicBezierIntersectResult result; + auto points = result.Points; + + for (int i = 0; i < rootCount; ++i) + { + auto root = roots[i]; + + if (0 <= root && root <= 1) + { + // We're within the Bezier curve + // Find point on Bezier + auto p = ImCubicBezier(p0, p1, p2, p3, root); + + // See if point is on line segment + // Had to make special cases for vertical and horizontal lines due + // to slight errors in calculation of p00 + if (a0.x == a1.x) + { + if (min.y <= p.y && p.y <= max.y) + *points++ = p; + } + else if (a0.y == a1.y) + { + if (min.x <= p.x && p.x <= max.x) + *points++ = p; + } + else if (p.x >= min.x && p.y >= min.y && p.x <= max.x && p.y <= max.y) + { + *points++ = p; + } + } + } + + result.Count = static_cast(points - result.Points); + + return result; +} + +inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line) +{ + return ImCubicBezierLineIntersect(curve.P0, curve.P1, curve.P2, curve.P3, line.A, line.B); +} + +inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + return ImCubicBezierSubdivide(callback, user_pointer, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags); +} + +inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + struct Tesselator + { + ImCubicBezierSubdivideCallback Callback; + void* UserPointer; + float TesselationTollerance; + ImCubicBezierSubdivideFlags Flags; + + void Commit(const ImVec2& p, const ImVec2& t) + { + ImCubicBezierSubdivideSample sample; + sample.Point = p; + sample.Tangent = t; + Callback(sample, UserPointer); + } + + void Subdivide(const ImCubicBezierPoints& curve, int level = 0) + { + float dx = curve.P3.x - curve.P0.x; + float dy = curve.P3.y - curve.P0.y; + float d2 = ((curve.P1.x - curve.P3.x) * dy - (curve.P1.y - curve.P3.y) * dx); + float d3 = ((curve.P2.x - curve.P3.x) * dy - (curve.P2.y - curve.P3.y) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < TesselationTollerance * (dx * dx + dy * dy)) + { + Commit(curve.P3, ImCubicBezierTangent(curve, 1.0f)); + } + else if (level < 10) + { + const auto p12 = (curve.P0 + curve.P1) * 0.5f; + const auto p23 = (curve.P1 + curve.P2) * 0.5f; + const auto p34 = (curve.P2 + curve.P3) * 0.5f; + const auto p123 = (p12 + p23) * 0.5f; + const auto p234 = (p23 + p34) * 0.5f; + const auto p1234 = (p123 + p234) * 0.5f; + + Subdivide(ImCubicBezierPoints { curve.P0, p12, p123, p1234 }, level + 1); + Subdivide(ImCubicBezierPoints { p1234, p234, p34, curve.P3 }, level + 1); + } + } + }; + + if (tess_tol < 0) + tess_tol = 1.118f; // sqrtf(1.25f) + + Tesselator tesselator; + tesselator.Callback = callback; + tesselator.UserPointer = user_pointer; + tesselator.TesselationTollerance = tess_tol * tess_tol; + tesselator.Flags = flags; + + if (!(tesselator.Flags & ImCubicBezierSubdivide_SkipFirst)) + tesselator.Commit(curve.P0, ImCubicBezierTangent(curve, 0.0f)); + + tesselator.Subdivide(curve, 0); +} + +template inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(p); + }; + + ImCubicBezierSubdivide(handler, &callback, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags); +} + +template inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags) +{ + auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(p); + }; + + ImCubicBezierSubdivide(handler, &callback, curve, tess_tol, flags); +} + +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error) +{ + if (step <= 0.0f || !callback || max_value_error <= 0 || max_t_error <= 0) + return; + + ImCubicBezierFixedStepSample sample; + sample.T = 0.0f; + sample.Length = 0.0f; + sample.Point = p0; + sample.BreakSearch = false; + + callback(sample, user_pointer); + if (sample.BreakSearch) + return; + + const auto total_length = ImCubicBezierLength(p0, p1, p2, p3); + const auto point_count = static_cast(total_length / step) + (overshoot ? 2 : 1); + const auto t_min = 0.0f; + const auto t_max = step * point_count / total_length; + const auto t_0 = (t_min + t_max) * 0.5f; + + // #todo: replace map with ImVector + binary search + std::map cache; + for (int point_index = 1; point_index < point_count; ++point_index) + { + const auto targetLength = point_index * step; + + float t_start = t_min; + float t_end = t_max; + float t = t_0; + + float t_best = t; + float error_best = total_length; + + while (true) + { + auto cacheIt = cache.find(t); + if (cacheIt == cache.end()) + { + const auto front = ImCubicBezierSplit(p0, p1, p2, p3, t).Left; + const auto split_length = ImCubicBezierLength(front); + + cacheIt = cache.emplace(t, split_length).first; + } + + const auto length = cacheIt->second; + const auto error = targetLength - length; + + if (error < error_best) + { + error_best = error; + t_best = t; + } + + if (ImFabs(error) <= max_value_error || ImFabs(t_start - t_end) <= max_t_error) + { + sample.T = t; + sample.Length = length; + sample.Point = ImCubicBezier(p0, p1, p2, p3, t); + + callback(sample, user_pointer); + if (sample.BreakSearch) + return; + + break; + } + else if (error < 0.0f) + t_end = t; + else // if (error > 0.0f) + t_start = t; + + t = (t_start + t_end) * 0.5f; + } + } +} + +inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error) +{ + ImCubicBezierFixedStep(callback, user_pointer, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error); +} + +// F has signature void(const ImCubicBezierFixedStepSample& p) +template +inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error) +{ + auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(sample); + }; + + ImCubicBezierFixedStep(handler, &callback, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error); +} + +template +inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error) +{ + auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer) + { + auto& callback = *reinterpret_cast(user_pointer); + callback(sample); + }; + + ImCubicBezierFixedStep(handler, &callback, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error); +} + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_BEZIER_MATH_INL__ diff --git a/symmetri/gui/imgui_canvas.cpp b/symmetri/gui/imgui_canvas.cpp new file mode 100644 index 0000000..bfa5957 --- /dev/null +++ b/symmetri/gui/imgui_canvas.cpp @@ -0,0 +1,575 @@ +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include "imgui_canvas.h" + +#include + +// https://stackoverflow.com/a/36079786 +#define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \ + \ + template \ + class __trait_name__ { \ + using check_type = ::std::remove_const_t<__boost_has_member_T__>; \ + struct no_type { \ + char x[2]; \ + }; \ + using yes_type = char; \ + \ + struct base { \ + void __member_name__() {} \ + }; \ + struct mixin : public base, public check_type {}; \ + \ + template \ + struct aux {}; \ + \ + template \ + static no_type test(aux<&U::__member_name__>*); \ + template \ + static yes_type test(...); \ + \ + public: \ + static constexpr bool value = \ + (sizeof(yes_type) == sizeof(test(0))); \ + } + +// Special sentinel value. This needs to be unique, so allow it to be overridden +// in the user's ImGui config +#ifndef ImDrawCallback_ImCanvas +#define ImDrawCallback_ImCanvas (ImDrawCallback)(-2) +#endif + +namespace ImCanvasDetails { + +DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale); + +struct FringeScaleRef { + // Overload is present when ImDrawList does have _FringeScale member variable. + template + static float& Get( + typename std::enable_if::value, T>::type* drawList) { + return drawList->_FringeScale; + } + + // Overload is present when ImDrawList does not have _FringeScale member + // variable. + template + static float& Get( + typename std::enable_if::value, T>::type*) { + static float placeholder = 1.0f; + return placeholder; + } +}; + +DECLARE_HAS_MEMBER(HasVtxCurrentOffset, _VtxCurrentOffset); + +struct VtxCurrentOffsetRef { + // Overload is present when ImDrawList does have _FringeScale member variable. + template + static unsigned int& Get( + typename std::enable_if::value, T>::type* + drawList) { + return drawList->_VtxCurrentOffset; + } + + // Overload is present when ImDrawList does not have _FringeScale member + // variable. + template + static unsigned int& Get( + typename std::enable_if::value, T>::type* + drawList) { + return drawList->_CmdHeader.VtxOffset; + } +}; + +} // namespace ImCanvasDetails + +// Returns a reference to _FringeScale extension to ImDrawList +// +// If ImDrawList does not have _FringeScale a placeholder is returned. +static inline float& ImFringeScaleRef(ImDrawList* drawList) { + using namespace ImCanvasDetails; + return FringeScaleRef::Get(drawList); +} + +static inline unsigned int& ImVtxOffsetRef(ImDrawList* drawList) { + using namespace ImCanvasDetails; + return VtxCurrentOffsetRef::Get(drawList); +} + +static inline ImVec2 ImSelectPositive(const ImVec2& lhs, const ImVec2& rhs) { + return ImVec2(lhs.x > 0.0f ? lhs.x : rhs.x, lhs.y > 0.0f ? lhs.y : rhs.y); +} + +bool ImGuiEx::Canvas::Begin(const char* id, const ImVec2& size) { + return Begin(ImGui::GetID(id), size); +} + +bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size) { + IM_ASSERT(m_InBeginEnd == false); + + m_WidgetPosition = ImGui::GetCursorScreenPos(); + m_WidgetSize = ImSelectPositive(size, ImGui::GetContentRegionAvail()); + m_WidgetRect = ImRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize); + m_DrawList = ImGui::GetWindowDrawList(); + + UpdateViewTransformPosition(); + +#if IMGUI_VERSION_NUM > 18415 + if (ImGui::IsClippedEx(m_WidgetRect, id)) return false; +#else + if (ImGui::IsClippedEx(m_WidgetRect, id, false)) return false; +#endif + + // Save current channel, so we can assert when user + // call canvas API with different one. + m_ExpectedChannel = m_DrawList->_Splitter._Current; + + // #debug: Canvas content. + // m_DrawList->AddRectFilled(m_StartPos, m_StartPos + m_CurrentSize, + // IM_COL32(0, 0, 0, 64)); m_DrawList->AddRect(m_WidgetRect.Min, + // m_WidgetRect.Max, IM_COL32(255, 0, 255, 64)); + + ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f)); + +#if IMGUI_EX_CANVAS_DEFERED() + m_Ranges.resize(0); +#endif + + SaveInputState(); + SaveViewportState(); + + // Record cursor max to prevent scrollbars from appearing. + m_WindowCursorMaxBackup = ImGui::GetCurrentWindow()->DC.CursorMaxPos; + + EnterLocalSpace(); + +#if IMGUI_VERSION_NUM >= 18967 + ImGui::SetNextItemAllowOverlap(); +#endif + + // Emit dummy widget matching bounds of the canvas. + ImGui::SetCursorScreenPos(m_ViewRect.Min); + ImGui::Dummy(m_ViewRect.GetSize()); + + ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f)); + + m_InBeginEnd = true; + + return true; +} + +void ImGuiEx::Canvas::End() { + // If you're here your call to Begin() returned false, + // or Begin() wasn't called at all. + IM_ASSERT(m_InBeginEnd == true); + + // If you're here, please make sure you do not interleave + // channel splitter with canvas. + // Always call canvas function with using same channel. + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + + // auto& io = ImGui::GetIO(); + + // Check: Unmatched calls to Suspend() / Resume(). Please check your code. + IM_ASSERT(m_SuspendCounter == 0); + + LeaveLocalSpace(); + + ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup; + +#if IMGUI_VERSION_NUM < 18967 + ImGui::SetItemAllowOverlap(); +#endif + + // Emit dummy widget matching bounds of the canvas. + ImGui::SetCursorScreenPos(m_WidgetPosition); + ImGui::Dummy(m_WidgetSize); + + // #debug: Rect around canvas. Content should be inside these bounds. + // m_DrawList->AddRect(m_WidgetPosition - ImVec2(1.0f, 1.0f), m_WidgetPosition + // + m_WidgetSize + ImVec2(1.0f, 1.0f), IM_COL32(196, 0, 0, 255)); + + m_InBeginEnd = false; +} + +void ImGuiEx::Canvas::SetView(const ImVec2& origin, float scale) { + SetView(CanvasView(origin, scale)); +} + +void ImGuiEx::Canvas::SetView(const CanvasView& view) { + if (m_InBeginEnd) LeaveLocalSpace(); + + if (m_View.Origin.x != view.Origin.x || m_View.Origin.y != view.Origin.y) { + m_View.Origin = view.Origin; + + UpdateViewTransformPosition(); + } + + if (m_View.Scale != view.Scale) { + m_View.Scale = view.Scale; + m_View.InvScale = view.InvScale; + } + + if (m_InBeginEnd) EnterLocalSpace(); +} + +void ImGuiEx::Canvas::CenterView(const ImVec2& canvasPoint) { + auto view = CalcCenterView(canvasPoint); + SetView(view); +} + +ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView( + const ImVec2& canvasPoint) const { + auto localCenter = ToLocal(m_WidgetPosition + m_WidgetSize * 0.5f); + auto localOffset = canvasPoint - localCenter; + auto offset = FromLocalV(localOffset); + + return CanvasView{m_View.Origin - offset, m_View.Scale}; +} + +void ImGuiEx::Canvas::CenterView(const ImRect& canvasRect) { + auto view = CalcCenterView(canvasRect); + + SetView(view); +} + +ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView( + const ImRect& canvasRect) const { + auto canvasRectSize = canvasRect.GetSize(); + + if (canvasRectSize.x <= 0.0f || canvasRectSize.y <= 0.0f) return View(); + + auto widgetAspectRatio = + m_WidgetSize.y > 0.0f ? m_WidgetSize.x / m_WidgetSize.y : 0.0f; + auto canvasRectAspectRatio = + canvasRectSize.y > 0.0f ? canvasRectSize.x / canvasRectSize.y : 0.0f; + + if (widgetAspectRatio <= 0.0f || canvasRectAspectRatio <= 0.0f) return View(); + + auto newOrigin = m_View.Origin; + auto newScale = m_View.Scale; + if (canvasRectAspectRatio > widgetAspectRatio) { + // width span across view + newScale = m_WidgetSize.x / canvasRectSize.x; + newOrigin = canvasRect.Min * -newScale; + newOrigin.y += (m_WidgetSize.y - canvasRectSize.y * newScale) * 0.5f; + } else { + // height span across view + newScale = m_WidgetSize.y / canvasRectSize.y; + newOrigin = canvasRect.Min * -newScale; + newOrigin.x += (m_WidgetSize.x - canvasRectSize.x * newScale) * 0.5f; + } + + return CanvasView{newOrigin, newScale}; +} + +void ImGuiEx::Canvas::Suspend() { + // If you're here, please make sure you do not interleave + // channel splitter with canvas. + // Always call canvas function with using same channel. + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + + if (m_SuspendCounter == 0) LeaveLocalSpace(); + + ++m_SuspendCounter; +} + +void ImGuiEx::Canvas::Resume() { + // If you're here, please make sure you do not interleave + // channel splitter with canvas. + // Always call canvas function with using same channel. + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + + // Check: Number of calls to Resume() do not match calls to Suspend(). Please + // check your code. + IM_ASSERT(m_SuspendCounter > 0); + if (--m_SuspendCounter == 0) EnterLocalSpace(); +} + +ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point) const { + return point * m_View.Scale + m_ViewTransformPosition; +} + +ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point, + const CanvasView& view) const { + return point * view.Scale + view.Origin + m_WidgetPosition; +} + +ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector) const { + return vector * m_View.Scale; +} + +ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector, + const CanvasView& view) const { + return vector * view.Scale; +} + +ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point) const { + return (point - m_ViewTransformPosition) * m_View.InvScale; +} + +ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point, + const CanvasView& view) const { + return (point - view.Origin - m_WidgetPosition) * view.InvScale; +} + +ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector) const { + return vector * m_View.InvScale; +} + +ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector, + const CanvasView& view) const { + return vector * view.InvScale; +} + +ImRect ImGuiEx::Canvas::CalcViewRect(const CanvasView& view) const { + ImRect result; + result.Min = ImVec2(-view.Origin.x, -view.Origin.y) * view.InvScale; + result.Max = (m_WidgetSize - view.Origin) * view.InvScale; + return result; +} + +void ImGuiEx::Canvas::UpdateViewTransformPosition() { + m_ViewTransformPosition = m_View.Origin + m_WidgetPosition; +} + +void ImGuiEx::Canvas::SaveInputState() { + auto& io = ImGui::GetIO(); + m_MousePosBackup = io.MousePos; + m_MousePosPrevBackup = io.MousePosPrev; + for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) + m_MouseClickedPosBackup[i] = io.MouseClickedPos[i]; +} + +void ImGuiEx::Canvas::RestoreInputState() { + auto& io = ImGui::GetIO(); + io.MousePos = m_MousePosBackup; + io.MousePosPrev = m_MousePosPrevBackup; + for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) + io.MouseClickedPos[i] = m_MouseClickedPosBackup[i]; +} + +void ImGuiEx::Canvas::SaveViewportState() { +#if defined(IMGUI_HAS_VIEWPORT) + auto window = ImGui::GetCurrentWindow(); + auto viewport = ImGui::GetWindowViewport(); + + m_WindowPosBackup = window->Pos; + m_ViewportPosBackup = viewport->Pos; + m_ViewportSizeBackup = viewport->Size; +#if IMGUI_VERSION_NUM > 18002 + m_ViewportWorkPosBackup = viewport->WorkPos; + m_ViewportWorkSizeBackup = viewport->WorkSize; +#else + m_ViewportWorkOffsetMinBackup = viewport->WorkOffsetMin; + m_ViewportWorkOffsetMaxBackup = viewport->WorkOffsetMax; +#endif +#endif +} + +void ImGuiEx::Canvas::RestoreViewportState() { +#if defined(IMGUI_HAS_VIEWPORT) + auto window = ImGui::GetCurrentWindow(); + auto viewport = ImGui::GetWindowViewport(); + + window->Pos = m_WindowPosBackup; + viewport->Pos = m_ViewportPosBackup; + viewport->Size = m_ViewportSizeBackup; +#if IMGUI_VERSION_NUM > 18002 + viewport->WorkPos = m_ViewportWorkPosBackup; + viewport->WorkSize = m_ViewportWorkSizeBackup; +#else + viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup; + viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup; +#endif +#endif +} + +void ImGuiEx::Canvas::EnterLocalSpace() { + // Prepare ImDrawList for drawing in local coordinate system: + // - determine visible part of the canvas + // - start unique draw command + // - add clip rect matching canvas size + // - record current command index + // - record current vertex write index + + // Determine visible part of the canvas. Make it before + // adding new command, to avoid round rip where command + // is removed in PopClipRect() and added again next PushClipRect(). + ImGui::PushClipRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize, true); + auto clipped_clip_rect = m_DrawList->_ClipRectStack.back(); + ImGui::PopClipRect(); + +#if IMGUI_EX_CANVAS_DEFERED() + m_Ranges.resize(m_Ranges.Size + 1); + m_CurrentRange = &m_Ranges.back(); + m_CurrentRange->BeginComandIndex = ImMax(m_DrawList->CmdBuffer.Size, 0); + m_CurrentRange->BeginVertexIndex = + m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList); +#endif + m_DrawListCommadBufferSize = ImMax(m_DrawList->CmdBuffer.Size, 0); + m_DrawListStartVertexIndex = + m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList); + + // Make sure we do not share draw command with anyone. We don't want to mess + // with someones clip rectangle. + + // #FIXME: + // This condition is not enough to avoid when user choose + // to use channel splitter. + // + // To deal with Suspend()/Resume() calls empty draw command + // is always added then splitter is active. Otherwise + // channel merger will collapse our draw command one with + // different clip rectangle. + // + // More investigation is needed. To get to the bottom of this. + if ((!m_DrawList->CmdBuffer.empty() && + m_DrawList->CmdBuffer.back().ElemCount > 0) || + m_DrawList->_Splitter._Count > 1) + m_DrawList->AddCallback(ImDrawCallback_ImCanvas, nullptr); + +#if defined(IMGUI_HAS_VIEWPORT) + auto window = ImGui::GetCurrentWindow(); + window->Pos = ImVec2(0.0f, 0.0f); + + auto viewport_min = m_ViewportPosBackup; + auto viewport_max = m_ViewportPosBackup + m_ViewportSizeBackup; + + viewport_min.x = + (viewport_min.x - m_ViewTransformPosition.x) * m_View.InvScale; + viewport_min.y = + (viewport_min.y - m_ViewTransformPosition.y) * m_View.InvScale; + viewport_max.x = + (viewport_max.x - m_ViewTransformPosition.x) * m_View.InvScale; + viewport_max.y = + (viewport_max.y - m_ViewTransformPosition.y) * m_View.InvScale; + + auto viewport = ImGui::GetWindowViewport(); + viewport->Pos = viewport_min; + viewport->Size = viewport_max - viewport_min; + +#if IMGUI_VERSION_NUM > 18002 + viewport->WorkPos = m_ViewportWorkPosBackup * m_View.InvScale; + viewport->WorkSize = m_ViewportWorkSizeBackup * m_View.InvScale; +#else + viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup * m_View.InvScale; + viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup * m_View.InvScale; +#endif +#endif + + // Clip rectangle in parent canvas space and move it to local space. + clipped_clip_rect.x = + (clipped_clip_rect.x - m_ViewTransformPosition.x) * m_View.InvScale; + clipped_clip_rect.y = + (clipped_clip_rect.y - m_ViewTransformPosition.y) * m_View.InvScale; + clipped_clip_rect.z = + (clipped_clip_rect.z - m_ViewTransformPosition.x) * m_View.InvScale; + clipped_clip_rect.w = + (clipped_clip_rect.w - m_ViewTransformPosition.y) * m_View.InvScale; + ImGui::PushClipRect(ImVec2(clipped_clip_rect.x, clipped_clip_rect.y), + ImVec2(clipped_clip_rect.z, clipped_clip_rect.w), false); + + // Transform mouse position to local space. + auto& io = ImGui::GetIO(); + io.MousePos = (m_MousePosBackup - m_ViewTransformPosition) * m_View.InvScale; + io.MousePosPrev = + (m_MousePosPrevBackup - m_ViewTransformPosition) * m_View.InvScale; + for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) + io.MouseClickedPos[i] = + (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * + m_View.InvScale; + + m_ViewRect = CalcViewRect(m_View); + ; + + auto& fringeScale = ImFringeScaleRef(m_DrawList); + m_LastFringeScale = fringeScale; + fringeScale *= m_View.InvScale; +} + +void ImGuiEx::Canvas::LeaveLocalSpace() { + IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); + +#if IMGUI_EX_CANVAS_DEFERED() + IM_ASSERT(m_CurrentRange != nullptr); + + m_CurrentRange->EndVertexIndex = + m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList); + m_CurrentRange->EndCommandIndex = m_DrawList->CmdBuffer.size(); + if (m_CurrentRange->BeginVertexIndex == m_CurrentRange->EndVertexIndex) { + // Drop empty range + m_Ranges.resize(m_Ranges.Size - 1); + } + m_CurrentRange = nullptr; +#endif + + // Move vertices to screen space. + auto vertex = m_DrawList->VtxBuffer.Data + m_DrawListStartVertexIndex; + auto vertexEnd = m_DrawList->VtxBuffer.Data + m_DrawList->_VtxCurrentIdx + + ImVtxOffsetRef(m_DrawList); + + // If canvas view is not scaled take a faster path. + if (m_View.Scale != 1.0f) { + while (vertex < vertexEnd) { + vertex->pos.x = vertex->pos.x * m_View.Scale + m_ViewTransformPosition.x; + vertex->pos.y = vertex->pos.y * m_View.Scale + m_ViewTransformPosition.y; + ++vertex; + } + + // Move clip rectangles to screen space. + for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); + ++i) { + auto& command = m_DrawList->CmdBuffer[i]; + command.ClipRect.x = + command.ClipRect.x * m_View.Scale + m_ViewTransformPosition.x; + command.ClipRect.y = + command.ClipRect.y * m_View.Scale + m_ViewTransformPosition.y; + command.ClipRect.z = + command.ClipRect.z * m_View.Scale + m_ViewTransformPosition.x; + command.ClipRect.w = + command.ClipRect.w * m_View.Scale + m_ViewTransformPosition.y; + } + } else { + while (vertex < vertexEnd) { + vertex->pos.x = vertex->pos.x + m_ViewTransformPosition.x; + vertex->pos.y = vertex->pos.y + m_ViewTransformPosition.y; + ++vertex; + } + + // Move clip rectangles to screen space. + for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); + ++i) { + auto& command = m_DrawList->CmdBuffer[i]; + command.ClipRect.x = command.ClipRect.x + m_ViewTransformPosition.x; + command.ClipRect.y = command.ClipRect.y + m_ViewTransformPosition.y; + command.ClipRect.z = command.ClipRect.z + m_ViewTransformPosition.x; + command.ClipRect.w = command.ClipRect.w + m_ViewTransformPosition.y; + } + } + + // Remove sentinel draw command if present + if (m_DrawListCommadBufferSize > 0) { + if (m_DrawList->CmdBuffer.size() > m_DrawListCommadBufferSize && + m_DrawList->CmdBuffer[m_DrawListCommadBufferSize].UserCallback == + ImDrawCallback_ImCanvas) + m_DrawList->CmdBuffer.erase(m_DrawList->CmdBuffer.Data + + m_DrawListCommadBufferSize); + else if (m_DrawList->CmdBuffer.size() >= m_DrawListCommadBufferSize && + m_DrawList->CmdBuffer[m_DrawListCommadBufferSize - 1] + .UserCallback == ImDrawCallback_ImCanvas) + m_DrawList->CmdBuffer.erase(m_DrawList->CmdBuffer.Data + + m_DrawListCommadBufferSize - 1); + } + + auto& fringeScale = ImFringeScaleRef(m_DrawList); + fringeScale = m_LastFringeScale; + + // And pop \o/ + ImGui::PopClipRect(); + + RestoreInputState(); + RestoreViewportState(); +} diff --git a/symmetri/gui/imgui_canvas.h b/symmetri/gui/imgui_canvas.h new file mode 100644 index 0000000..0ef5cf5 --- /dev/null +++ b/symmetri/gui/imgui_canvas.h @@ -0,0 +1,272 @@ +// Canvas widget - view over infinite virtual space. +// +// Canvas allows you to draw your widgets anywhere over infinite space and +// provide view over it with support for panning and scaling. +// +// When you enter a canvas ImGui is moved to virtual space which mean: +// - ImGui::GetCursorScreenPos() return (0, 0) and which correspond to top +// left corner +// of the canvas on the screen (this can be changed using CanvasView()). +// - Mouse input is brought to canvas space, so widgets works as usual. +// - Everything you draw with ImDrawList will be in virtual space. +// +// By default origin point is on top left corner of canvas widget. It can be +// changed with call to CanvasView() where you can specify what part of space +// should be viewed by setting viewport origin point and scale. Current state +// can be queried with CanvasViewOrigin() and CanvasViewScale(). +// +// Viewport size is controlled by 'size' parameter in BeginCanvas(). You can +// query it using CanvasContentMin/Max/Size functions. They are useful if you to +// not specify canvas size in which case all free space is used. +// +// Bounds of visible region of infinite space can be queried using +// CanvasViewMin/Max/Size functions. Everything that is drawn outside of this +// region will be clipped as usual in ImGui. +// +// While drawing inside canvas you can translate position from world (usual +// ImGui space) to virtual space and back using +// CanvasFromWorld()/CanvasToWorld(). +// +// Canvas can be nested in each other (they are regular widgets after all). +// There is a way to transform position between current and parent canvas with +// CanvasFromParent()/CanvasToParent(). +// +// Sometimes in more elaborate scenarios you want to move out canvas virtual +// space, do something and came back. You can do that with SuspendCanvas() and +// ResumeCanvas(). +// +// Note: +// It is not valid to call canvas API outside of BeginCanvas() / EndCanvas() +// scope. +// +// VERSION 0.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +#ifndef __IMGUI_EX_CANVAS_H__ +#define __IMGUI_EX_CANVAS_H__ +#pragma once + +#include +#include // ImRect, ImFloor + +#ifndef IMGUIEX_CANVAS_API +#define IMGUIEX_CANVAS_API +#endif + +namespace ImGuiEx { + +struct CanvasView { + ImVec2 Origin; + float Scale = 1.0f; + float InvScale = 1.0f; + + CanvasView() = default; + CanvasView(const ImVec2& origin, float scale) + : Origin(origin), Scale(scale), InvScale(scale ? 1.0f / scale : 0.0f) {} + + void Set(const ImVec2& origin, float scale) { + *this = CanvasView(origin, scale); + } +}; + +// Canvas widget represent view over infinite plane. +// +// It acts like a child window without scroll bars with +// ability to zoom to specific part of canvas plane. +// +// Widgets are clipped according to current view exactly +// same way ImGui do. To avoid `missing widgets` artifacts first +// setup visible region with SetView() then draw content. +// +// Everything drawn with ImDrawList betwen calls to Begin()/End() +// will be drawn on canvas plane. This behavior can be suspended +// by calling Suspend() and resumed by calling Resume(). +// +// Warning: +// Please do not interleave canvas with use of channel splitter. +// Keep channel splitter contained inside canvas or always +// call canvas functions from same channel. +struct Canvas { + // Begins drawing content of canvas plane. + // + // When false is returned that mean canvas is not visible to the + // user can drawing should be skipped and End() not called. + // When true is returned drawing must be ended with call to End(). + // + // If any size component is equal to zero or less canvas will + // automatically expand to all available area on that axis. + // So (0, 300) will take horizontal space and have height + // of 300 points. (0, 0) will take all remaining space of + // the window. + // + // You can query size of the canvas while it is being drawn + // by calling Rect(). + IMGUIEX_CANVAS_API bool Begin(const char* id, const ImVec2& size); + IMGUIEX_CANVAS_API bool Begin(ImGuiID id, const ImVec2& size); + + // Ends interaction with canvas plane. + // + // Must be called only when Begin() retuned true. + IMGUIEX_CANVAS_API void End(); + + // Sets visible region of canvas plane. + // + // Origin is an offset of infinite plane origin from top left + // corner of the canvas. + // + // Scale greater than 1 make canvas content be bigger, less than 1 smaller. + IMGUIEX_CANVAS_API void SetView(const ImVec2& origin, float scale); + IMGUIEX_CANVAS_API void SetView(const CanvasView& view); + + // Centers view over specific point on canvas plane. + // + // View will be centered on specific point by changing origin + // but not scale. + IMGUIEX_CANVAS_API void CenterView(const ImVec2& canvasPoint); + + // Calculates view over specific point on canvas plane. + IMGUIEX_CANVAS_API CanvasView CalcCenterView(const ImVec2& canvasPoint) const; + + // Centers view over specific rectangle on canvas plane. + // + // Whole rectangle will fit in canvas view. This will affect both + // origin and scale. + IMGUIEX_CANVAS_API void CenterView(const ImRect& canvasRect); + + // Calculates view over specific rectangle on canvas plane. + IMGUIEX_CANVAS_API CanvasView CalcCenterView(const ImRect& canvasRect) const; + + // Suspends canvas by returning to normal ImGui transformation space. + // While suspended UI will not be drawn on canvas plane. + // + // Calls to Suspend()/Resume() are symetrical. Each call to Suspend() + // must be matched with call to Resume(). + IMGUIEX_CANVAS_API void Suspend(); + IMGUIEX_CANVAS_API void Resume(); + + // Transforms point from canvas plane to ImGui. + IMGUIEX_CANVAS_API ImVec2 FromLocal(const ImVec2& point) const; + IMGUIEX_CANVAS_API ImVec2 FromLocal(const ImVec2& point, + const CanvasView& view) const; + + // Transforms vector from canvas plant to ImGui. + IMGUIEX_CANVAS_API ImVec2 FromLocalV(const ImVec2& vector) const; + IMGUIEX_CANVAS_API ImVec2 FromLocalV(const ImVec2& vector, + const CanvasView& view) const; + + // Transforms point from ImGui to canvas plane. + IMGUIEX_CANVAS_API ImVec2 ToLocal(const ImVec2& point) const; + IMGUIEX_CANVAS_API ImVec2 ToLocal(const ImVec2& point, + const CanvasView& view) const; + + // Transforms vector from ImGui to canvas plane. + IMGUIEX_CANVAS_API ImVec2 ToLocalV(const ImVec2& vector) const; + IMGUIEX_CANVAS_API ImVec2 ToLocalV(const ImVec2& vector, + const CanvasView& view) const; + + // Returns widget bounds. + // + // Note: + // Rect is valid after call to Begin(). + const ImRect& Rect() const { return m_WidgetRect; } + + // Returns visible region on canvas plane (in canvas plane coordinates). + const ImRect& ViewRect() const { return m_ViewRect; } + + // Calculates visible region for view. + IMGUIEX_CANVAS_API ImRect CalcViewRect(const CanvasView& view) const; + + // Returns current view. + const CanvasView& View() const { return m_View; } + + // Returns origin of the view. + // + // Origin is an offset of infinite plane origin from top left + // corner of the canvas. + const ImVec2& ViewOrigin() const { return m_View.Origin; } + + // Returns scale of the view. + float ViewScale() const { return m_View.Scale; } + + // Returns true if canvas is suspended. + // + // See: Suspend()/Resume() + bool IsSuspended() const { return m_SuspendCounter > 0; } + + private: +#define IMGUI_EX_CANVAS_DEFERED() 0 + +#if IMGUI_EX_CANVAS_DEFERED() + struct Range { + int BeginVertexIndex = 0; + int EndVertexIndex = 0; + int BeginComandIndex = 0; + int EndCommandIndex = 0; + }; +#endif + + void UpdateViewTransformPosition(); + + void SaveInputState(); + void RestoreInputState(); + + void SaveViewportState(); + void RestoreViewportState(); + + void EnterLocalSpace(); + void LeaveLocalSpace(); + + bool m_InBeginEnd = false; + + ImVec2 m_WidgetPosition; + ImVec2 m_WidgetSize; + ImRect m_WidgetRect; + + ImDrawList* m_DrawList = nullptr; + int m_ExpectedChannel = 0; + +#if IMGUI_EX_CANVAS_DEFERED() + ImVector m_Ranges; + Range* m_CurrentRange = nullptr; +#endif + + int m_DrawListCommadBufferSize = 0; + int m_DrawListStartVertexIndex = 0; + + CanvasView m_View; + ImRect m_ViewRect; + + ImVec2 m_ViewTransformPosition; + + int m_SuspendCounter = 0; + + float m_LastFringeScale = 1.0f; + + ImVec2 m_MousePosBackup; + ImVec2 m_MousePosPrevBackup; + ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)]; + ImVec2 m_WindowCursorMaxBackup; + +#if defined(IMGUI_HAS_VIEWPORT) + ImVec2 m_WindowPosBackup; + ImVec2 m_ViewportPosBackup; + ImVec2 m_ViewportSizeBackup; +#if IMGUI_VERSION_NUM > 18002 + ImVec2 m_ViewportWorkPosBackup; + ImVec2 m_ViewportWorkSizeBackup; +#else + ImVec2 m_ViewportWorkOffsetMinBackup; + ImVec2 m_ViewportWorkOffsetMaxBackup; +#endif +#endif +}; + +} // namespace ImGuiEx + +#endif // __IMGUI_EX_CANVAS_H__ diff --git a/symmetri/gui/imgui_extra_math.h b/symmetri/gui/imgui_extra_math.h new file mode 100644 index 0000000..d28e2ca --- /dev/null +++ b/symmetri/gui/imgui_extra_math.h @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +// VERSION 0.9.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +#ifndef __IMGUI_EXTRA_MATH_H__ +#define __IMGUI_EXTRA_MATH_H__ +#pragma once + +//------------------------------------------------------------------------------ +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include +#include + +//------------------------------------------------------------------------------ +struct ImLine { + ImVec2 A, B; +}; + +//------------------------------------------------------------------------------ +inline bool operator==(const ImVec2& lhs, const ImVec2& rhs); +inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs); +inline ImVec2 operator*(const float lhs, const ImVec2& rhs); +#if IMGUI_VERSION_NUM < 18955 +inline ImVec2 operator-(const ImVec2& lhs); +#endif + +//------------------------------------------------------------------------------ +inline float ImLength(float v); +inline float ImLength(const ImVec2& v); +inline float ImLengthSqr(float v); +inline ImVec2 ImNormalized(const ImVec2& v); + +//------------------------------------------------------------------------------ +inline bool ImRect_IsEmpty(const ImRect& rect); +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, + bool snap_to_edge); +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, + bool snap_to_edge, float radius); +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& b); +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b); +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, + float radius_a, float radius_b); + +//------------------------------------------------------------------------------ +namespace ImEasing { + +template +inline V EaseOutQuad(V b, V c, T t) { + return b - c * (t * (t - 2)); +} + +} // namespace ImEasing + +//------------------------------------------------------------------------------ +#include "imgui_extra_math.inl" + +//------------------------------------------------------------------------------ +#endif // __IMGUI_EXTRA_MATH_H__ diff --git a/symmetri/gui/imgui_extra_math.inl b/symmetri/gui/imgui_extra_math.inl new file mode 100644 index 0000000..8b1b071 --- /dev/null +++ b/symmetri/gui/imgui_extra_math.inl @@ -0,0 +1,191 @@ +//------------------------------------------------------------------------------ +// VERSION 0.9.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_EXTRA_MATH_INL__ +# define __IMGUI_EXTRA_MATH_INL__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_extra_math.h" + + +//------------------------------------------------------------------------------ +inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) +{ + return lhs.x == rhs.x && lhs.y == rhs.y; +} + +inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) +{ + return lhs.x != rhs.x || lhs.y != rhs.y; +} + +inline ImVec2 operator*(const float lhs, const ImVec2& rhs) +{ + return ImVec2(lhs * rhs.x, lhs * rhs.y); +} + +# if IMGUI_VERSION_NUM < 18955 +inline ImVec2 operator-(const ImVec2& lhs) +{ + return ImVec2(-lhs.x, -lhs.y); +} +# endif + + +//------------------------------------------------------------------------------ +inline float ImLength(float v) +{ + return v; +} + +inline float ImLength(const ImVec2& v) +{ + return ImSqrt(ImLengthSqr(v)); +} + +inline float ImLengthSqr(float v) +{ + return v * v; +} + +inline ImVec2 ImNormalized(const ImVec2& v) +{ + return v * ImInvLength(v, 0.0f); +} + + + + +//------------------------------------------------------------------------------ +inline bool ImRect_IsEmpty(const ImRect& rect) +{ + return rect.Min.x >= rect.Max.x + || rect.Min.y >= rect.Max.y; +} + +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge) +{ + if (!snap_to_edge && rect.Contains(p)) + return p; + + return ImVec2( + (p.x > rect.Max.x) ? rect.Max.x : (p.x < rect.Min.x ? rect.Min.x : p.x), + (p.y > rect.Max.y) ? rect.Max.y : (p.y < rect.Min.y ? rect.Min.y : p.y) + ); +} + +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius) +{ + auto point = ImRect_ClosestPoint(rect, p, snap_to_edge); + + const auto offset = p - point; + const auto distance_sq = offset.x * offset.x + offset.y * offset.y; + if (distance_sq <= 0) + return point; + + const auto distance = ImSqrt(distance_sq); + + return point + offset * (ImMin(distance, radius) * (1.0f / distance)); +} + +inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& other) +{ + ImVec2 result; + if (other.Min.x >= rect.Max.x) + result.x = rect.Max.x; + else if (other.Max.x <= rect.Min.x) + result.x = rect.Min.x; + else + result.x = (ImMax(rect.Min.x, other.Min.x) + ImMin(rect.Max.x, other.Max.x)) / 2; + + if (other.Min.y >= rect.Max.y) + result.y = rect.Max.y; + else if (other.Max.y <= rect.Min.y) + result.y = rect.Min.y; + else + result.y = (ImMax(rect.Min.y, other.Min.y) + ImMin(rect.Max.y, other.Max.y)) / 2; + + return result; +} + +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b) +{ + ImLine result; + result.A = ImRect_ClosestPoint(rect_a, rect_b); + result.B = ImRect_ClosestPoint(rect_b, rect_a); + + auto distribute = [](float& a, float& b, float a0, float a1, float b0, float b1) + { + if (a0 >= b1 || a1 <= b0) + return; + + const auto aw = a1 - a0; + const auto bw = b1 - b0; + + if (aw > bw) + { + b = b0 + bw - bw * (a - a0) / aw; + a = b; + } + else if (aw < bw) + { + a = a0 + aw - aw * (b - b0) / bw; + b = a; + } + }; + + distribute(result.A.x, result.B.x, rect_a.Min.x, rect_a.Max.x, rect_b.Min.x, rect_b.Max.x); + distribute(result.A.y, result.B.y, rect_a.Min.y, rect_a.Max.y, rect_b.Min.y, rect_b.Max.y); + + return result; +} + +inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b) +{ + auto line = ImRect_ClosestLine(rect_a, rect_b); + if (radius_a < 0) + radius_a = 0; + if (radius_b < 0) + radius_b = 0; + + if (radius_a == 0 && radius_b == 0) + return line; + + const auto offset = line.B - line.A; + const auto length_sq = offset.x * offset.x + offset.y * offset.y; + const auto radius_a_sq = radius_a * radius_a; + const auto radius_b_sq = radius_b * radius_b; + + if (length_sq <= 0) + return line; + + const auto length = ImSqrt(length_sq); + const auto direction = ImVec2(offset.x / length, offset.y / length); + + const auto total_radius_sq = radius_a_sq + radius_b_sq; + if (total_radius_sq > length_sq) + { + const auto scale = length / (radius_a + radius_b); + radius_a *= scale; + radius_b *= scale; + } + + line.A = line.A + (direction * radius_a); + line.B = line.B - (direction * radius_b); + + return line; +} + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_EXTRA_MATH_INL__ diff --git a/symmetri/gui/imgui_node_editor.cpp b/symmetri/gui/imgui_node_editor.cpp new file mode 100644 index 0000000..7959a68 --- /dev/null +++ b/symmetri/gui/imgui_node_editor.cpp @@ -0,0 +1,5368 @@ +//------------------------------------------------------------------------------ +// VERSION 0.9.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +#include +#include +#include +#include // snprintf +#include +#include +#include +#include +#include + +#include "imgui_node_editor_internal.h" + +// https://stackoverflow.com/a/8597498 +#define DECLARE_HAS_NESTED(Name, Member) \ + \ + template \ + struct has_nested_##Name { \ + typedef char yes; \ + typedef yes (&no)[2]; \ + \ + template \ + static yes test(decltype(U::Member)*); \ + template \ + static no test(...); \ + \ + static bool const value = sizeof(test(0)) == sizeof(yes); \ + }; + +namespace ax { +namespace NodeEditor { +namespace Detail { + +#if !defined(IMGUI_VERSION_NUM) || (IMGUI_VERSION_NUM < 18822) +#define DECLARE_KEY_TESTER(Key) \ + DECLARE_HAS_NESTED(Key, Key) \ + struct KeyTester_##Key { \ + template \ + static int Get(typename std::enable_if::value, \ + T>::type*) { \ + return ImGui::GetKeyIndex(T::Key); \ + } \ + \ + template \ + static int Get(typename std::enable_if< \ + !has_nested_##Key::value, T>::type*) { \ + return -1; \ + } \ + } + +DECLARE_KEY_TESTER(ImGuiKey_F); +DECLARE_KEY_TESTER(ImGuiKey_D); + +static inline int GetKeyIndexForF() { + return KeyTester_ImGuiKey_F::Get(nullptr); +} + +static inline int GetKeyIndexForD() { + return KeyTester_ImGuiKey_D::Get(nullptr); +} +#else +static inline ImGuiKey GetKeyIndexForF() { return ImGuiKey_F; } + +static inline ImGuiKey GetKeyIndexForD() { return ImGuiKey_D; } +#endif + +} // namespace Detail +} // namespace NodeEditor +} // namespace ax + +//------------------------------------------------------------------------------ +namespace ed = ax::NodeEditor::Detail; + +//------------------------------------------------------------------------------ +static const int c_BackgroundChannelCount = 1; +static const int c_LinkChannelCount = 4; +static const int c_UserLayersCount = 5; + +static const int c_UserLayerChannelStart = 0; +static const int c_BackgroundChannelStart = + c_UserLayerChannelStart + c_UserLayersCount; +static const int c_LinkStartChannel = + c_BackgroundChannelStart + c_BackgroundChannelCount; +static const int c_NodeStartChannel = c_LinkStartChannel + c_LinkChannelCount; + +static const int c_BackgroundChannel_SelectionRect = + c_BackgroundChannelStart + 0; + +static const int c_UserChannel_Content = c_UserLayerChannelStart + 1; +static const int c_UserChannel_Grid = c_UserLayerChannelStart + 2; +static const int c_UserChannel_HintsBackground = c_UserLayerChannelStart + 3; +static const int c_UserChannel_Hints = c_UserLayerChannelStart + 4; + +static const int c_LinkChannel_Selection = c_LinkStartChannel + 0; +static const int c_LinkChannel_Links = c_LinkStartChannel + 1; +static const int c_LinkChannel_Flow = c_LinkStartChannel + 2; +static const int c_LinkChannel_NewLink = c_LinkStartChannel + 3; + +static const int c_ChannelsPerNode = 5; +static const int c_NodeBaseChannel = 0; +static const int c_NodeBackgroundChannel = 1; +static const int c_NodeUserBackgroundChannel = 2; +static const int c_NodePinChannel = 3; +static const int c_NodeContentChannel = 4; + +static const float c_GroupSelectThickness = 6.0f; // canvas pixels +static const float c_LinkSelectThickness = 5.0f; // canvas pixels +static const float c_NavigationZoomMargin = + 0.1f; // percentage of visible bounds +static const float c_MouseZoomDuration = 0.15f; // seconds +static const float c_SelectionFadeOutDuration = 0.15f; // seconds + +static const auto c_MaxMoveOverEdgeSpeed = 10.0f; +static const auto c_MaxMoveOverEdgeDistance = 300.0f; + +#if IMGUI_VERSION_NUM > 18101 +static const auto c_AllRoundCornersFlags = ImDrawFlags_RoundCornersAll; +#else +static const auto c_AllRoundCornersFlags = 15; +#endif + +//------------------------------------------------------------------------------ +#if defined(_DEBUG) && defined(_WIN32) +extern "C" __declspec(dllimport) void __stdcall OutputDebugStringA( + const char* string); + +static void LogV(const char* fmt, va_list args) { + const int buffer_size = 1024; + static char buffer[1024]; + + vsnprintf(buffer, buffer_size - 1, fmt, args); + buffer[buffer_size - 1] = 0; + + ImGui::LogText("\nNode Editor: %s", buffer); + + OutputDebugStringA("NodeEditor: "); + OutputDebugStringA(buffer); + OutputDebugStringA("\n"); +} +#endif + +void ed::Log(const char* fmt, ...) { +#if defined(_DEBUG) && defined(_WIN32) + va_list args; + va_start(args, fmt); + LogV(fmt, args); + va_end(args); +#endif +} + +//------------------------------------------------------------------------------ +static bool IsGroup(const ed::Node* node) { + if (node && node->m_Type == ed::NodeType::Group) + return true; + else + return false; +} + +//------------------------------------------------------------------------------ +static void ImDrawListSplitter_Grow(ImDrawList* draw_list, + ImDrawListSplitter* splitter, + int channels_count) { + IM_ASSERT(splitter != nullptr); + IM_ASSERT(splitter->_Count <= channels_count); + + if (splitter->_Count == 1) { + splitter->Split(draw_list, channels_count); + return; + } + + int old_channels_count = splitter->_Channels.Size; + if (old_channels_count < channels_count) { + splitter->_Channels.reserve(channels_count); + splitter->_Channels.resize(channels_count); + } + int old_used_channels_count = splitter->_Count; + splitter->_Count = channels_count; + + for (int i = old_used_channels_count; i < channels_count; i++) { + if (i >= old_channels_count) { + IM_PLACEMENT_NEW(&splitter->_Channels[i]) ImDrawChannel(); + } else { + splitter->_Channels[i]._CmdBuffer.resize(0); + splitter->_Channels[i]._IdxBuffer.resize(0); + } + } +} + +static void ImDrawList_ChannelsGrow(ImDrawList* draw_list, int channels_count) { + ImDrawListSplitter_Grow(draw_list, &draw_list->_Splitter, channels_count); +} + +static void ImDrawListSplitter_SwapChannels(ImDrawListSplitter* splitter, + int left, int right) { + IM_ASSERT(left < splitter->_Count && right < splitter->_Count); + if (left == right) return; + + auto currentChannel = splitter->_Current; + + auto* leftCmdBuffer = &splitter->_Channels[left]._CmdBuffer; + auto* leftIdxBuffer = &splitter->_Channels[left]._IdxBuffer; + auto* rightCmdBuffer = &splitter->_Channels[right]._CmdBuffer; + auto* rightIdxBuffer = &splitter->_Channels[right]._IdxBuffer; + + leftCmdBuffer->swap(*rightCmdBuffer); + leftIdxBuffer->swap(*rightIdxBuffer); + + if (currentChannel == left) + splitter->_Current = right; + else if (currentChannel == right) + splitter->_Current = left; +} + +static void ImDrawList_SwapChannels(ImDrawList* drawList, int left, int right) { + ImDrawListSplitter_SwapChannels(&drawList->_Splitter, left, right); +} + +static void ImDrawList_SwapSplitter(ImDrawList* drawList, + ImDrawListSplitter& splitter) { + auto& currentSplitter = drawList->_Splitter; + + std::swap(currentSplitter._Current, splitter._Current); + std::swap(currentSplitter._Count, splitter._Count); + currentSplitter._Channels.swap(splitter._Channels); +} + +// static void ImDrawList_TransformChannel_Inner(ImVector& +// vtxBuffer, const ImVector& idxBuffer, const ImVector& +// cmdBuffer, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& +// postOffset) +//{ +// auto idxRead = idxBuffer.Data; +// +// int indexOffset = 0; +// for (auto& cmd : cmdBuffer) +// { +// auto idxCount = cmd.ElemCount; +// +// if (idxCount == 0) continue; +// +// auto minIndex = idxRead[indexOffset]; +// auto maxIndex = idxRead[indexOffset]; +// +// for (auto i = 1u; i < idxCount; ++i) +// { +// auto idx = idxRead[indexOffset + i]; +// minIndex = std::min(minIndex, idx); +// maxIndex = ImMax(maxIndex, idx); +// } +// +// for (auto vtx = vtxBuffer.Data + minIndex, vtxEnd = vtxBuffer.Data + +// maxIndex + 1; vtx < vtxEnd; ++vtx) +// { +// vtx->pos.x = (vtx->pos.x + preOffset.x) * scale.x + postOffset.x; +// vtx->pos.y = (vtx->pos.y + preOffset.y) * scale.y + postOffset.y; +// } +// +// indexOffset += idxCount; +// } +// } + +// static void ImDrawList_TransformChannels(ImDrawList* drawList, int begin, int +// end, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset) +//{ +// int lastCurrentChannel = drawList->_ChannelsCurrent; +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(0); +// +// auto& vtxBuffer = drawList->VtxBuffer; +// +// if (begin == 0 && begin != end) +// { +// ImDrawList_TransformChannel_Inner(vtxBuffer, drawList->IdxBuffer, +// drawList->CmdBuffer, preOffset, scale, postOffset); +// ++begin; +// } +// +// for (int channelIndex = begin; channelIndex < end; ++channelIndex) +// { +// auto& channel = drawList->_Channels[channelIndex]; +// ImDrawList_TransformChannel_Inner(vtxBuffer, channel.IdxBuffer, +// channel.CmdBuffer, preOffset, scale, postOffset); +// } +// +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(lastCurrentChannel); +// } + +// static void ImDrawList_ClampClipRects_Inner(ImVector& cmdBuffer, +// const ImVec4& clipRect, const ImVec2& offset) +//{ +// for (auto& cmd : cmdBuffer) +// { +// cmd.ClipRect.x = ImMax(cmd.ClipRect.x + offset.x, clipRect.x); +// cmd.ClipRect.y = ImMax(cmd.ClipRect.y + offset.y, clipRect.y); +// cmd.ClipRect.z = std::min(cmd.ClipRect.z + offset.x, clipRect.z); +// cmd.ClipRect.w = std::min(cmd.ClipRect.w + offset.y, clipRect.w); +// } +// } + +// static void ImDrawList_TranslateAndClampClipRects(ImDrawList* drawList, int +// begin, int end, const ImVec2& offset) +//{ +// int lastCurrentChannel = drawList->_ChannelsCurrent; +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(0); +// +// auto clipRect = drawList->_ClipRectStack.back(); +// +// if (begin == 0 && begin != end) +// { +// ImDrawList_ClampClipRects_Inner(drawList->CmdBuffer, clipRect, +// offset); +// ++begin; +// } +// +// for (int channelIndex = begin; channelIndex < end; ++channelIndex) +// { +// auto& channel = drawList->_Channels[channelIndex]; +// ImDrawList_ClampClipRects_Inner(channel.CmdBuffer, clipRect, offset); +// } +// +// if (lastCurrentChannel != 0) +// drawList->ChannelsSetCurrent(lastCurrentChannel); +// } + +static void ImDrawList_PathBezierOffset(ImDrawList* drawList, float offset, + const ImVec2& p0, const ImVec2& p1, + const ImVec2& p2, const ImVec2& p3) { + using namespace ed; + + auto acceptPoint = [drawList, offset](const ImCubicBezierSubdivideSample& r) { + drawList->PathLineTo( + r.Point + ImNormalized(ImVec2(-r.Tangent.y, r.Tangent.x)) * offset); + }; + + ImCubicBezierSubdivide(acceptPoint, p0, p1, p2, p3); +} + +/* +static void ImDrawList_PolyFillScanFlood(ImDrawList *draw, std::vector* +poly, ImColor color, int gap = 1, float strokeWidth = 1.0f) +{ + std::vector scanHits; + ImVec2 min, max; // polygon min/max points + auto io = ImGui::GetIO(); + float y; + bool isMinMaxDone = false; + unsigned int polysize = poly->size(); + + // find the orthagonal bounding box + // probably can put this as a predefined + if (!isMinMaxDone) + { + min.x = min.y = FLT_MAX; + max.x = max.y = FLT_MIN; + for (auto p : *poly) + { + if (p.x < min.x) min.x = p.x; + if (p.y < min.y) min.y = p.y; + if (p.x > max.x) max.x = p.x; + if (p.y > max.y) max.y = p.y; + } + isMinMaxDone = true; + } + + // Bounds check + if ((max.x < 0) || (min.x > io.DisplaySize.x) || (max.y < 0) || (min.y > +io.DisplaySize.y)) return; + + // Vertically clip + if (min.y < 0) min.y = 0; + if (max.y > io.DisplaySize.y) max.y = io.DisplaySize.y; + + // so we know we start on the outside of the object we step out by 1. + min.x -= 1; + max.x += 1; + + // Initialise our starting conditions + y = min.y; + + // Go through each scan line iteratively, jumping by 'gap' pixels each time + while (y < max.y) + { + scanHits.clear(); + + { + int jump = 1; + ImVec2 fp = poly->at(0); + + for (size_t i = 0; i < polysize - 1; i++) + { + ImVec2 pa = poly->at(i); + ImVec2 pb = poly->at(i + 1); + + // jump double/dud points + if (pa.x == pb.x && pa.y == pb.y) continue; + + // if we encounter our hull/poly start point, then we've now +created the + // closed + // hull, jump the next segment and reset the first-point + if ((!jump) && (fp.x == pb.x) && (fp.y == pb.y)) + { + if (i < polysize - 2) + { + fp = poly->at(i + 2); + jump = 1; + i++; + } + } + else + { + jump = 0; + } + + // test to see if this segment makes the scan-cut. + if ((pa.y > pb.y && y < pa.y && y > pb.y) || (pa.y < pb.y && y > +pa.y && y < pb.y)) + { + ImVec2 intersect; + + intersect.y = y; + if (pa.x == pb.x) + { + intersect.x = pa.x; + } + else + { + intersect.x = (pb.x - pa.x) / (pb.y - pa.y) * (y - pa.y) ++ pa.x; + } + scanHits.push_back(intersect); + } + } + + // Sort the scan hits by X, so we have a proper left->right ordering + sort(scanHits.begin(), scanHits.end(), [](ImVec2 const &a, ImVec2 +const &b) { return a.x < b.x; }); + + // generate the line segments. + { + int i = 0; + int l = scanHits.size() - 1; // we need pairs of points, this +prevents segfault. for (i = 0; i < l; i += 2) + { + draw->AddLine(scanHits[i], scanHits[i + 1], color, +strokeWidth); + } + } + } + y += gap; + } // for each scan line + scanHits.clear(); +} +*/ + +static void ImDrawList_AddBezierWithArrows( + ImDrawList* drawList, const ImCubicBezierPoints& curve, float thickness, + float startArrowSize, float startArrowWidth, float endArrowSize, + float endArrowWidth, bool fill, ImU32 color, float strokeThickness, + const ImVec2* startDirHint = nullptr, const ImVec2* endDirHint = nullptr) { + using namespace ax; + + if ((color >> 24) == 0) return; + + const auto half_thickness = thickness * 0.5f; + + if (fill) { + drawList->AddBezierCubic(curve.P0, curve.P1, curve.P2, curve.P3, color, + thickness); + + if (startArrowSize > 0.0f) { + const auto start_dir = ImNormalized( + startDirHint ? *startDirHint + : ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, + curve.P3, 0.0f)); + const auto start_n = ImVec2(-start_dir.y, start_dir.x); + const auto half_width = startArrowWidth * 0.5f; + const auto tip = curve.P0 - start_dir * startArrowSize; + + drawList->PathLineTo(curve.P0 - + start_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(curve.P0 + + start_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(tip); + drawList->PathFillConvex(color); + } + + if (endArrowSize > 0.0f) { + const auto end_dir = ImNormalized( + endDirHint ? -*endDirHint + : ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, + curve.P3, 1.0f)); + const auto end_n = ImVec2(-end_dir.y, end_dir.x); + const auto half_width = endArrowWidth * 0.5f; + const auto tip = curve.P3 + end_dir * endArrowSize; + + drawList->PathLineTo(curve.P3 + + end_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(curve.P3 - + end_n * ImMax(half_width, half_thickness)); + drawList->PathLineTo(tip); + drawList->PathFillConvex(color); + } + } else { + if (startArrowSize > 0.0f) { + const auto start_dir = ImNormalized( + ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f)); + const auto start_n = ImVec2(-start_dir.y, start_dir.x); + const auto half_width = startArrowWidth * 0.5f; + const auto tip = curve.P0 - start_dir * startArrowSize; + + if (half_width > half_thickness) + drawList->PathLineTo(curve.P0 - start_n * half_width); + drawList->PathLineTo(tip); + if (half_width > half_thickness) + drawList->PathLineTo(curve.P0 + start_n * half_width); + } + + ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P0, curve.P1, + curve.P2, curve.P3); + + if (endArrowSize > 0.0f) { + const auto end_dir = ImNormalized( + ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f)); + const auto end_n = ImVec2(-end_dir.y, end_dir.x); + const auto half_width = endArrowWidth * 0.5f; + const auto tip = curve.P3 + end_dir * endArrowSize; + + if (half_width > half_thickness) + drawList->PathLineTo(curve.P3 + end_n * half_width); + drawList->PathLineTo(tip); + if (half_width > half_thickness) + drawList->PathLineTo(curve.P3 - end_n * half_width); + } + + ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P3, curve.P2, + curve.P1, curve.P0); + + drawList->PathStroke(color, true, strokeThickness); + } +} + +//------------------------------------------------------------------------------ +// +// Pin +// +//------------------------------------------------------------------------------ +void ed::Pin::Draw(ImDrawList* drawList, DrawFlags flags) { + if (flags & Hovered) { + drawList->ChannelsSetCurrent(m_Node->m_Channel + c_NodePinChannel); + + drawList->AddRectFilled(m_Bounds.Min, m_Bounds.Max, m_Color, m_Rounding, + m_Corners); + + if (m_BorderWidth > 0.0f) { + FringeScaleScope fringe(1.0f); + drawList->AddRect(m_Bounds.Min, m_Bounds.Max, m_BorderColor, m_Rounding, + m_Corners, m_BorderWidth); + } + + if (!Editor->IsSelected(m_Node)) m_Node->Draw(drawList, flags); + } +} + +ImVec2 ed::Pin::GetClosestPoint(const ImVec2& p) const { + auto pivot = m_Pivot; + auto extent = m_Radius + m_ArrowSize; + + if (m_SnapLinkToDir && extent > 0.0f) { + pivot.Min += m_Dir * extent; + pivot.Max += m_Dir * extent; + + extent = 0; + } + + return ImRect_ClosestPoint(pivot, p, true, extent); +} + +ImLine ed::Pin::GetClosestLine(const Pin* pin) const { + auto pivotA = m_Pivot; + auto pivotB = pin->m_Pivot; + auto extentA = m_Radius + m_ArrowSize; + auto extentB = pin->m_Radius + pin->m_ArrowSize; + + if (m_SnapLinkToDir && extentA > 0.0f) { + pivotA.Min += m_Dir * extentA; + pivotA.Max += m_Dir * extentA; + + extentA = 0; + } + + if (pin->m_SnapLinkToDir && extentB > 0.0f) { + pivotB.Min += pin->m_Dir * extentB; + pivotB.Max += pin->m_Dir * extentB; + + extentB = 0; + } + + return ImRect_ClosestLine(pivotA, pivotB, extentA, extentB); +} + +//------------------------------------------------------------------------------ +// +// Node +// +//------------------------------------------------------------------------------ +bool ed::Node::AcceptDrag() { + m_DragStart = m_Bounds.Min; + return true; +} + +void ed::Node::UpdateDrag(const ImVec2& offset) { + auto size = m_Bounds.GetSize(); + m_Bounds.Min = ImFloor(m_DragStart + offset); + m_Bounds.Max = m_Bounds.Min + size; +} + +bool ed::Node::EndDrag() { return m_Bounds.Min != m_DragStart; } + +void ed::Node::Draw(ImDrawList* drawList, DrawFlags flags) { + if (flags == Detail::Object::None) { + drawList->ChannelsSetCurrent(m_Channel + c_NodeBackgroundChannel); + + drawList->AddRectFilled(m_Bounds.Min, m_Bounds.Max, m_Color, m_Rounding); + + if (IsGroup(this)) { + drawList->AddRectFilled(m_GroupBounds.Min, m_GroupBounds.Max, + m_GroupColor, m_GroupRounding); + + if (m_GroupBorderWidth > 0.0f) { + FringeScaleScope fringe(1.0f); + + drawList->AddRect(m_GroupBounds.Min, m_GroupBounds.Max, + m_GroupBorderColor, m_GroupRounding, + c_AllRoundCornersFlags, m_GroupBorderWidth); + } + } + +#if 0 + // #debug: highlight group regions + auto drawRect = [drawList](const ImRect& rect, ImU32 color) + { + if (ImRect_IsEmpty(rect)) return; + drawList->AddRectFilled(rect.Min, rect.Max, color); + }; + + drawRect(GetRegionBounds(NodeRegion::Top), IM_COL32(255, 0, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::Bottom), IM_COL32(255, 0, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::Left), IM_COL32(0, 255, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::Right), IM_COL32(0, 255, 0, 64)); + drawRect(GetRegionBounds(NodeRegion::TopLeft), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::TopRight), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::BottomLeft), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::BottomRight), IM_COL32(255, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::Center), IM_COL32(0, 0, 255, 64)); + drawRect(GetRegionBounds(NodeRegion::Header), IM_COL32(0, 255, 255, 64)); +#endif + + DrawBorder(drawList, m_BorderColor, m_BorderWidth); + } else if (flags & Selected) { + const auto borderColor = Editor->GetColor(StyleColor_SelNodeBorder); + const auto& editorStyle = Editor->GetStyle(); + + drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel); + + DrawBorder(drawList, borderColor, editorStyle.SelectedNodeBorderWidth, + editorStyle.SelectedNodeBorderOffset); + } else if (!IsGroup(this) && (flags & Hovered)) { + const auto borderColor = Editor->GetColor(StyleColor_HovNodeBorder); + const auto& editorStyle = Editor->GetStyle(); + + drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel); + + DrawBorder(drawList, borderColor, editorStyle.HoveredNodeBorderWidth, + editorStyle.HoverNodeBorderOffset); + } +} + +void ed::Node::DrawBorder(ImDrawList* drawList, ImU32 color, float thickness, + float offset) { + if (thickness > 0.0f) { + const ImVec2 extraOffset = ImVec2(offset, offset); + + drawList->AddRect(m_Bounds.Min - extraOffset, m_Bounds.Max + extraOffset, + color, ImMax(0.0f, m_Rounding + offset), + c_AllRoundCornersFlags, thickness); + } +} + +void ed::Node::GetGroupedNodes(std::vector& result, bool append) { + if (!append) result.resize(0); + + if (!IsGroup(this)) return; + + const auto firstNodeIndex = result.size(); + Editor->FindNodesInRect(m_GroupBounds, result, true, false); + + for (auto index = firstNodeIndex; index < result.size(); ++index) + result[index]->GetGroupedNodes(result, true); +} + +ImRect ed::Node::GetRegionBounds(NodeRegion region) const { + if (m_Type == NodeType::Node) { + if (region == NodeRegion::Header) return m_Bounds; + } else if (m_Type == NodeType::Group) { + const float activeAreaMinimumSize = + ImMax(ImMax(Editor->GetView().InvScale * c_GroupSelectThickness, + m_GroupBorderWidth), + c_GroupSelectThickness); + const float minimumSize = activeAreaMinimumSize * 5; + + auto bounds = m_Bounds; + if (bounds.GetWidth() < minimumSize) + bounds.Expand(ImVec2(minimumSize - bounds.GetWidth(), 0.0f)); + if (bounds.GetHeight() < minimumSize) + bounds.Expand(ImVec2(0.0f, minimumSize - bounds.GetHeight())); + + if (region == NodeRegion::Top) { + bounds.Max.y = bounds.Min.y + activeAreaMinimumSize; + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.x -= activeAreaMinimumSize; + return bounds; + } else if (region == NodeRegion::Bottom) { + bounds.Min.y = bounds.Max.y - activeAreaMinimumSize; + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.x -= activeAreaMinimumSize; + return bounds; + } else if (region == NodeRegion::Left) { + bounds.Max.x = bounds.Min.x + activeAreaMinimumSize; + bounds.Min.y += activeAreaMinimumSize; + bounds.Max.y -= activeAreaMinimumSize; + return bounds; + } else if (region == NodeRegion::Right) { + bounds.Min.x = bounds.Max.x - activeAreaMinimumSize; + bounds.Min.y += activeAreaMinimumSize; + bounds.Max.y -= activeAreaMinimumSize; + return bounds; + } else if (region == NodeRegion::TopLeft) { + bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2; + bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2; + return bounds; + } else if (region == NodeRegion::TopRight) { + bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2; + bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2; + return bounds; + } else if (region == NodeRegion::BottomRight) { + bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2; + bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2; + return bounds; + } else if (region == NodeRegion::BottomLeft) { + bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2; + bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2; + return bounds; + } else if (region == NodeRegion::Header) { + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.x -= activeAreaMinimumSize; + bounds.Min.y += activeAreaMinimumSize; + bounds.Max.y = + ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y); + return bounds; + } else if (region == NodeRegion::Center) { + bounds.Max.x -= activeAreaMinimumSize; + bounds.Min.y = + ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y); + bounds.Min.x += activeAreaMinimumSize; + bounds.Max.y -= activeAreaMinimumSize; + return bounds; + } + } + + return ImRect(); +} + +ed::NodeRegion ed::Node::GetRegion(const ImVec2& point) const { + if (m_Type == NodeType::Node) { + if (m_Bounds.Contains(point)) + return NodeRegion::Header; + else + return NodeRegion::None; + } else if (m_Type == NodeType::Group) { + static const NodeRegion c_Regions[] = { + // Corners first, they may overlap other regions. + NodeRegion::TopLeft, NodeRegion::TopRight, NodeRegion::BottomLeft, + NodeRegion::BottomRight, NodeRegion::Header, NodeRegion::Top, + NodeRegion::Bottom, NodeRegion::Left, NodeRegion::Right, + NodeRegion::Center}; + + for (auto region : c_Regions) { + auto bounds = GetRegionBounds(region); + if (bounds.Contains(point)) return region; + } + } + + return NodeRegion::None; +} + +//------------------------------------------------------------------------------ +// +// Link +// +//------------------------------------------------------------------------------ +void ed::Link::Draw(ImDrawList* drawList, DrawFlags flags) { + if (flags == None) { + drawList->ChannelsSetCurrent(c_LinkChannel_Links); + + Draw(drawList, m_Color, 0.0f); + } else if (flags & Selected) { + const auto borderColor = Editor->GetColor(StyleColor_SelLinkBorder); + + drawList->ChannelsSetCurrent(c_LinkChannel_Selection); + + Draw(drawList, borderColor, 4.5f); + } else if (flags & Hovered) { + const auto borderColor = Editor->GetColor(StyleColor_HovLinkBorder); + + drawList->ChannelsSetCurrent(c_LinkChannel_Selection); + + Draw(drawList, borderColor, 2.0f); + } else if (flags & Highlighted) { + drawList->ChannelsSetCurrent(c_LinkChannel_Selection); + + Draw(drawList, m_HighlightColor, 3.5f); + } +} + +void ed::Link::Draw(ImDrawList* drawList, ImU32 color, + float extraThickness) const { + if (!m_IsLive) return; + + const auto curve = GetCurve(); + + ImDrawList_AddBezierWithArrows( + drawList, curve, m_Thickness + extraThickness, + m_StartPin && m_StartPin->m_ArrowSize > 0.0f + ? m_StartPin->m_ArrowSize + extraThickness + : 0.0f, + m_StartPin && m_StartPin->m_ArrowWidth > 0.0f + ? m_StartPin->m_ArrowWidth + extraThickness + : 0.0f, + m_EndPin && m_EndPin->m_ArrowSize > 0.0f + ? m_EndPin->m_ArrowSize + extraThickness + : 0.0f, + m_EndPin && m_EndPin->m_ArrowWidth > 0.0f + ? m_EndPin->m_ArrowWidth + extraThickness + : 0.0f, + true, color, 1.0f, + m_StartPin && m_StartPin->m_SnapLinkToDir ? &m_StartPin->m_Dir : nullptr, + m_EndPin && m_EndPin->m_SnapLinkToDir ? &m_EndPin->m_Dir : nullptr); +} + +void ed::Link::UpdateEndpoints() { + const auto line = m_StartPin->GetClosestLine(m_EndPin); + m_Start = line.A; + m_End = line.B; +} + +ImCubicBezierPoints ed::Link::GetCurve() const { + auto easeLinkStrength = [](const ImVec2& a, const ImVec2& b, float strength) { + const auto distanceX = b.x - a.x; + const auto distanceY = b.y - a.y; + const auto distance = ImSqrt(distanceX * distanceX + distanceY * distanceY); + const auto halfDistance = distance * 0.5f; + + if (halfDistance < strength) + strength = strength * ImSin(IM_PI * 0.5f * halfDistance / strength); + + return strength; + }; + + const auto startStrength = + easeLinkStrength(m_Start, m_End, m_StartPin->m_Strength); + const auto endStrength = + easeLinkStrength(m_Start, m_End, m_EndPin->m_Strength); + const auto cp0 = m_Start + m_StartPin->m_Dir * startStrength; + const auto cp1 = m_End + m_EndPin->m_Dir * endStrength; + + ImCubicBezierPoints result; + result.P0 = m_Start; + result.P1 = cp0; + result.P2 = cp1; + result.P3 = m_End; + + return result; +} + +bool ed::Link::TestHit(const ImVec2& point, float extraThickness) const { + if (!m_IsLive) return false; + + auto bounds = GetBounds(); + if (extraThickness > 0.0f) bounds.Expand(extraThickness); + + if (!bounds.Contains(point)) return false; + + const auto bezier = GetCurve(); + const auto result = ImProjectOnCubicBezier(point, bezier.P0, bezier.P1, + bezier.P2, bezier.P3, 50); + + return result.Distance <= m_Thickness + extraThickness; +} + +bool ed::Link::TestHit(const ImRect& rect, bool allowIntersect) const { + if (!m_IsLive) return false; + + const auto bounds = GetBounds(); + + if (rect.Contains(bounds)) return true; + + if (!allowIntersect || !rect.Overlaps(bounds)) return false; + + const auto bezier = GetCurve(); + + const auto p0 = rect.GetTL(); + const auto p1 = rect.GetTR(); + const auto p2 = rect.GetBR(); + const auto p3 = rect.GetBL(); + + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p0, + p1) + .Count > 0) + return true; + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p1, + p2) + .Count > 0) + return true; + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p2, + p3) + .Count > 0) + return true; + if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p3, + p0) + .Count > 0) + return true; + + return false; +} + +ImRect ed::Link::GetBounds() const { + if (m_IsLive) { + const auto curve = GetCurve(); + auto bounds = + ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3); + + if (bounds.GetWidth() == 0.0f) { + bounds.Min.x -= 0.5f; + bounds.Max.x += 0.5f; + } + + if (bounds.GetHeight() == 0.0f) { + bounds.Min.y -= 0.5f; + bounds.Max.y += 0.5f; + } + + if (m_StartPin->m_ArrowSize) { + const auto start_dir = ImNormalized( + ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f)); + const auto p0 = curve.P0; + const auto p1 = curve.P0 - start_dir * m_StartPin->m_ArrowSize; + const auto min = ImMin(p0, p1); + const auto max = ImMax(p0, p1); + auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1))); + bounds.Add(arrowBounds); + } + + if (m_EndPin->m_ArrowSize) { + const auto end_dir = ImNormalized( + ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f)); + const auto p0 = curve.P3; + const auto p1 = curve.P3 + end_dir * m_EndPin->m_ArrowSize; + const auto min = ImMin(p0, p1); + const auto max = ImMax(p0, p1); + auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1))); + bounds.Add(arrowBounds); + } + + return bounds; + } else + return ImRect(); +} + +//------------------------------------------------------------------------------ +// +// Editor Context +// +//------------------------------------------------------------------------------ +ed::EditorContext::EditorContext(const ax::NodeEditor::Config* config) + : m_Config(config), + m_EditorActiveId(0), + m_IsFirstFrame(true), + m_IsFocused(false), + m_IsHovered(false), + m_IsHoveredWithoutOverlapp(false), + m_ShortcutsEnabled(true), + m_Style(), + m_Nodes(), + m_Pins(), + m_Links(), + m_SelectionId(1), + m_LastActiveLink(nullptr), + m_Canvas(), + m_IsCanvasVisible(false), + m_NodeBuilder(this), + m_HintBuilder(this), + m_CurrentAction(nullptr), + m_NavigateAction(this, m_Canvas), + m_SizeAction(this), + m_DragAction(this), + m_SelectAction(this), + m_ContextMenuAction(this), + m_ShortcutAction(this), + m_CreateItemAction(this), + m_DeleteItemsAction(this), + m_AnimationControllers{&m_FlowAnimationController}, + m_FlowAnimationController(this), + m_HoveredNode(0), + m_HoveredPin(0), + m_HoveredLink(0), + m_DoubleClickedNode(0), + m_DoubleClickedPin(0), + m_DoubleClickedLink(0), + m_BackgroundClickButtonIndex(-1), + m_BackgroundDoubleClickButtonIndex(-1), + m_IsInitialized(false), + m_Settings(), + m_DrawList(nullptr), + m_ExternalChannel(0) {} + +ed::EditorContext::~EditorContext() { + if (m_IsInitialized) SaveSettings(); + + for (auto link : m_Links) delete link.m_Object; + for (auto pin : m_Pins) delete pin.m_Object; + for (auto node : m_Nodes) delete node.m_Object; + + m_Splitter.ClearFreeMemory(); +} + +void ed::EditorContext::Begin(const char* id, const ImVec2& size) { + m_EditorActiveId = ImGui::GetID(id); + ImGui::PushID(id); + + auto availableContentSize = ImGui::GetContentRegionAvail(); + ImVec2 canvasSize = ImFloor(size); + if (canvasSize.x <= 0.0f) canvasSize.x = ImMax(4.0f, availableContentSize.x); + if (canvasSize.y <= 0.0f) canvasSize.y = ImMax(4.0f, availableContentSize.y); + + if (!m_IsInitialized) { + // Cycle canvas, so it has a chance to initialize its size before settings + // are loaded + if (m_Canvas.Begin(id, canvasSize)) m_Canvas.End(); + + LoadSettings(); + m_IsInitialized = true; + } + + // ImGui::LogToClipboard(); + // Log("---- begin ----"); + + static auto resetAndCollect = [](auto& objects) { + objects.erase(std::remove_if(objects.begin(), objects.end(), + [](auto objectWrapper) { + if (objectWrapper->m_DeleteOnNewFrame) { + delete objectWrapper.m_Object; + return true; + } else { + objectWrapper->Reset(); + return false; + } + }), + objects.end()); + }; + + resetAndCollect(m_Nodes); + resetAndCollect(m_Pins); + resetAndCollect(m_Links); + + m_DrawList = ImGui::GetWindowDrawList(); + + ImDrawList_SwapSplitter(m_DrawList, m_Splitter); + m_ExternalChannel = m_DrawList->_Splitter._Current; + + if (m_CurrentAction && m_CurrentAction->IsDragging() && + m_NavigateAction.MoveOverEdge(canvasSize)) { + auto& io = ImGui::GetIO(); + auto offset = m_NavigateAction.GetMoveScreenOffset(); + for (int i = 0; i < 5; ++i) + io.MouseClickedPos[i] = io.MouseClickedPos[i] - offset; + } else + m_NavigateAction.StopMoveOverEdge(); + + auto previousSize = m_Canvas.Rect().GetSize(); + auto previousVisibleRect = m_Canvas.ViewRect(); + m_IsCanvasVisible = m_Canvas.Begin(id, canvasSize); + + // ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); + // ImGui::BeginChild(id, size, false, + // ImGuiWindowFlags_NoMove | + // ImGuiWindowFlags_NoScrollbar | + // ImGuiWindowFlags_NoScrollWithMouse); + + m_IsFocused = ImGui::IsWindowFocused(); + + // + m_NavigateAction.SetWindow(m_Canvas.ViewRect().Min, + m_Canvas.ViewRect().GetSize()); + + // Handle canvas size change. Scale to Y axis, center on X. + if (!ImRect_IsEmpty(previousVisibleRect) && previousSize != canvasSize) { + m_NavigateAction.FinishNavigation(); + + auto centerX = + (previousVisibleRect.Max.x + previousVisibleRect.Min.x) * 0.5f; + auto centerY = + (previousVisibleRect.Max.y + previousVisibleRect.Min.y) * 0.5f; + auto currentVisibleRect = m_Canvas.ViewRect(); + auto currentAspectRatio = + currentVisibleRect.GetHeight() + ? (currentVisibleRect.GetWidth() / currentVisibleRect.GetHeight()) + : 0.0f; + auto width = previousVisibleRect.GetWidth(); + auto height = previousVisibleRect.GetHeight(); + + if (m_Config.CanvasSizeMode == + ax::NodeEditor::CanvasSizeMode::FitVerticalView) { + height = previousVisibleRect.GetHeight(); + width = height * currentAspectRatio; + } else if (m_Config.CanvasSizeMode == + ax::NodeEditor::CanvasSizeMode::FitHorizontalView) { + width = previousVisibleRect.GetWidth(); + height = width / currentAspectRatio; + } else if (m_Config.CanvasSizeMode == + ax::NodeEditor::CanvasSizeMode::CenterOnly) { + width = currentVisibleRect.GetWidth(); + height = currentVisibleRect.GetHeight(); + } + + previousVisibleRect.Min.x = centerX - 0.5f * width; + previousVisibleRect.Max.x = centerX + 0.5f * width; + previousVisibleRect.Min.y = centerY - 0.5f * height; + previousVisibleRect.Max.y = centerY + 0.5f * height; + + m_NavigateAction.NavigateTo(previousVisibleRect, + Detail::NavigateAction::ZoomMode::Exact, 0.0f); + } + + m_Canvas.SetView(m_NavigateAction.GetView()); + + // #debug #clip + // ImGui::Text("CLIP = { x=%g y=%g w=%g h=%g r=%g b=%g }", + // clipMin.x, clipMin.y, clipMax.x - clipMin.x, clipMax.y - clipMin.y, + // clipMax.x, clipMax.y); + + // Reserve channels for background and links + ImDrawList_ChannelsGrow(m_DrawList, c_NodeStartChannel); + + if (HasSelectionChanged()) ++m_SelectionId; + + m_LastSelectedObjects = m_SelectedObjects; +} + +void ed::EditorContext::End() { + // auto& io = ImGui::GetIO(); + auto control = BuildControl( + m_CurrentAction && + m_CurrentAction->IsDragging()); // NavigateAction.IsMovingOverEdge() + // auto& editorStyle = GetStyle(); + + m_HoveredNode = + control.HotNode && m_CurrentAction == nullptr ? control.HotNode->m_ID : 0; + m_HoveredPin = + control.HotPin && m_CurrentAction == nullptr ? control.HotPin->m_ID : 0; + m_HoveredLink = + control.HotLink && m_CurrentAction == nullptr ? control.HotLink->m_ID : 0; + m_DoubleClickedNode = + control.DoubleClickedNode ? control.DoubleClickedNode->m_ID : 0; + m_DoubleClickedPin = + control.DoubleClickedPin ? control.DoubleClickedPin->m_ID : 0; + m_DoubleClickedLink = + control.DoubleClickedLink ? control.DoubleClickedLink->m_ID : 0; + m_BackgroundClickButtonIndex = control.BackgroundClickButtonIndex; + m_BackgroundDoubleClickButtonIndex = control.BackgroundDoubleClickButtonIndex; + + // if (DoubleClickedNode) LOG_TRACE(0, "DOUBLE CLICK NODE: %d", + // DoubleClickedNode); if (DoubleClickedPin) LOG_TRACE(0, "DOUBLE CLICK PIN: + // %d", DoubleClickedPin); if (DoubleClickedLink) LOG_TRACE(0, "DOUBLE CLICK + // LINK: %d", DoubleClickedLink); if (BackgroundDoubleClicked) LOG_TRACE(0, + // "DOUBLE CLICK BACKGROUND", DoubleClickedLink); + + const bool isSelecting = + m_CurrentAction && m_CurrentAction->AsSelect() != nullptr; + const bool isDragging = + m_CurrentAction && m_CurrentAction->AsDrag() != nullptr; + // const bool isSizing = CurrentAction && CurrentAction->AsSize() != + // nullptr; + + // Draw nodes + for (auto node : m_Nodes) + if (node->m_IsLive && node->IsVisible()) node->Draw(m_DrawList); + + // Draw links + for (auto link : m_Links) + if (link->m_IsLive && link->IsVisible()) link->Draw(m_DrawList); + + // Highlight selected objects + { + auto selectedObjects = &m_SelectedObjects; + if (auto selectAction = + m_CurrentAction ? m_CurrentAction->AsSelect() : nullptr) + selectedObjects = &selectAction->m_CandidateObjects; + + for (auto selectedObject : *selectedObjects) { + if (selectedObject->IsVisible()) + selectedObject->Draw(m_DrawList, Object::Selected); + } + + // Highlight adjacent links + static auto isLinkHighlightedForPin = [](const Pin& pin) { + return pin.m_Node->m_HighlightConnectedLinks && pin.m_Node->m_IsSelected; + }; + + for (auto& link : m_Links) { + if (!link->m_IsLive || !link->IsVisible()) continue; + + auto isLinkHighlighted = isLinkHighlightedForPin(*link->m_StartPin) || + isLinkHighlightedForPin(*link->m_EndPin); + if (!isLinkHighlighted) continue; + + link->Draw(m_DrawList, Object::Highlighted); + } + } + + if (!isSelecting) { + auto hoveredObject = control.HotObject; + if (auto dragAction = m_CurrentAction ? m_CurrentAction->AsDrag() : nullptr) + hoveredObject = dragAction->m_DraggedObject; + if (auto sizeAction = m_CurrentAction ? m_CurrentAction->AsSize() : nullptr) + hoveredObject = sizeAction->m_SizedNode; + + if (hoveredObject && !IsSelected(hoveredObject) && + hoveredObject->IsVisible()) + hoveredObject->Draw(m_DrawList, Object::Hovered); + } + + // Draw animations + for (auto controller : m_AnimationControllers) controller->Draw(m_DrawList); + + if (m_CurrentAction && !m_CurrentAction->Process(control)) + m_CurrentAction = nullptr; + + if (m_NavigateAction.m_IsActive) + m_NavigateAction.Process(control); + else + m_NavigateAction.Accept(control); + + if (nullptr == m_CurrentAction) { + EditorAction* possibleAction = nullptr; + + auto accept = [&possibleAction, &control](EditorAction& action) { + auto result = action.Accept(control); + + if (result == EditorAction::True) + return true; + else if (/*!possibleAction &&*/ result == EditorAction::Possible) + possibleAction = &action; + else if (result == EditorAction::Possible) + action.Reject(); + + return false; + }; + + if (accept(m_ContextMenuAction)) + m_CurrentAction = &m_ContextMenuAction; + else if (accept(m_ShortcutAction)) + m_CurrentAction = &m_ShortcutAction; + else if (accept(m_SizeAction)) + m_CurrentAction = &m_SizeAction; + else if (accept(m_DragAction)) + m_CurrentAction = &m_DragAction; + else if (accept(m_CreateItemAction)) + m_CurrentAction = &m_CreateItemAction; + else if (accept(m_DeleteItemsAction)) + m_CurrentAction = &m_DeleteItemsAction; + else if (accept(m_SelectAction)) + m_CurrentAction = &m_SelectAction; + + if (possibleAction) ImGui::SetMouseCursor(possibleAction->GetCursor()); + + if (m_CurrentAction && possibleAction) possibleAction->Reject(); + } + + if (m_CurrentAction) ImGui::SetMouseCursor(m_CurrentAction->GetCursor()); + + // Draw selection rectangle + m_SelectAction.Draw(m_DrawList); + + bool sortGroups = false; + if (control.ActiveNode) { + if (!IsGroup(control.ActiveNode)) { + // Bring active node to front + auto activeNodeIt = + std::find(m_Nodes.begin(), m_Nodes.end(), control.ActiveNode); + std::rotate(activeNodeIt, activeNodeIt + 1, m_Nodes.end()); + } else if (!isDragging && m_CurrentAction && m_CurrentAction->AsDrag()) { + // Bring content of dragged group to front + std::vector nodes; + control.ActiveNode->GetGroupedNodes(nodes); + + std::stable_partition( + m_Nodes.begin(), m_Nodes.end(), [&nodes](Node* node) { + return std::find(nodes.begin(), nodes.end(), node) == nodes.end(); + }); + + sortGroups = true; + } + } + + // Sort nodes if bounds of node changed + if (sortGroups || ((m_Settings.m_DirtyReason & + (SaveReasonFlags::Position | SaveReasonFlags::Size)) != + SaveReasonFlags::None)) { + // Bring all groups before regular nodes + auto groupsItEnd = + std::stable_partition(m_Nodes.begin(), m_Nodes.end(), IsGroup); + + // Sort groups by area + std::sort(m_Nodes.begin(), groupsItEnd, [this](Node* lhs, Node* rhs) { + const auto& lhsSize = lhs == m_SizeAction.m_SizedNode + ? m_SizeAction.GetStartGroupBounds().GetSize() + : lhs->m_GroupBounds.GetSize(); + const auto& rhsSize = rhs == m_SizeAction.m_SizedNode + ? m_SizeAction.GetStartGroupBounds().GetSize() + : rhs->m_GroupBounds.GetSize(); + + const auto lhsArea = lhsSize.x * lhsSize.y; + const auto rhsArea = rhsSize.x * rhsSize.y; + + return lhsArea > rhsArea; + }); + } + + // Apply Z order + std::stable_sort(m_Nodes.begin(), m_Nodes.end(), + [](const auto& lhs, const auto& rhs) { + return lhs->m_ZPosition < rhs->m_ZPosition; + }); + +#if 1 + // Every node has few channels assigned. Grow channel list + // to hold twice as much of channels and place them in + // node drawing order. + { + // Copy group nodes + auto liveNodeCount = static_cast( + std::count_if(m_Nodes.begin(), m_Nodes.end(), + [](Node* node) { return node->m_IsLive; })); + + // Reserve two additional channels for sorted list of channels + auto nodeChannelCount = m_DrawList->_Splitter._Count; + ImDrawList_ChannelsGrow(m_DrawList, m_DrawList->_Splitter._Count + + c_ChannelsPerNode * liveNodeCount + + c_LinkChannelCount); + + int targetChannel = nodeChannelCount; + + auto copyNode = [this, &targetChannel](Node* node) { + if (!node->m_IsLive) return; + + for (int i = 0; i < c_ChannelsPerNode; ++i) + ImDrawList_SwapChannels(m_DrawList, node->m_Channel + i, + targetChannel + i); + + node->m_Channel = targetChannel; + targetChannel += c_ChannelsPerNode; + }; + + auto groupsItEnd = std::find_if(m_Nodes.begin(), m_Nodes.end(), + [](Node* node) { return !IsGroup(node); }); + + // Copy group nodes + std::for_each(m_Nodes.begin(), groupsItEnd, copyNode); + + // Copy links + for (int i = 0; i < c_LinkChannelCount; ++i, ++targetChannel) + ImDrawList_SwapChannels(m_DrawList, c_LinkStartChannel + i, + targetChannel); + + // Copy normal nodes + std::for_each(groupsItEnd, m_Nodes.end(), copyNode); + } +#endif + + // ImGui::PopClipRect(); + + // Draw grid +#if 1 // #FIXME + { + // auto& style = ImGui::GetStyle(); + + m_DrawList->ChannelsSetCurrent(c_UserChannel_Grid); + + ImVec2 offset = m_Canvas.ViewOrigin() * (1.0f / m_Canvas.ViewScale()); + ImU32 GRID_COLOR = GetColor( + StyleColor_Grid, + ImClamp(m_Canvas.ViewScale() * m_Canvas.ViewScale(), 0.0f, 1.0f)); + float GRID_SX = 32.0f; // * m_Canvas.ViewScale(); + float GRID_SY = 32.0f; // * m_Canvas.ViewScale(); + ImVec2 VIEW_POS = m_Canvas.ViewRect().Min; + ImVec2 VIEW_SIZE = m_Canvas.ViewRect().GetSize(); + + m_DrawList->AddRectFilled(VIEW_POS, VIEW_POS + VIEW_SIZE, + GetColor(StyleColor_Bg)); + + for (float x = fmodf(offset.x, GRID_SX); x < VIEW_SIZE.x; x += GRID_SX) + m_DrawList->AddLine(ImVec2(x, 0.0f) + VIEW_POS, + ImVec2(x, VIEW_SIZE.y) + VIEW_POS, GRID_COLOR); + for (float y = fmodf(offset.y, GRID_SY); y < VIEW_SIZE.y; y += GRID_SY) + m_DrawList->AddLine(ImVec2(0.0f, y) + VIEW_POS, + ImVec2(VIEW_SIZE.x, y) + VIEW_POS, GRID_COLOR); + } +#endif + +#if 0 + { + auto userChannel = drawList->_Splitter._Count; + auto channelsToCopy = c_UserLayersCount; + ImDrawList_ChannelsGrow(drawList, userChannel + channelsToCopy); + for (int i = 0; i < channelsToCopy; ++i) + ImDrawList_SwapChannels(drawList, userChannel + i, c_UserLayerChannelStart + i); + } +#endif + +#if 0 + { + auto preOffset = ImVec2(0, 0); + auto postOffset = m_OldCanvas.WindowScreenPos + m_OldCanvas.ClientOrigin; + auto scale = m_OldCanvas.Zoom; + + ImDrawList_TransformChannels(drawList, 0, 1, preOffset, scale, postOffset); + ImDrawList_TransformChannels(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, preOffset, scale, postOffset); + + auto clipTranslation = m_OldCanvas.WindowScreenPos - m_OldCanvas.FromScreen(m_OldCanvas.WindowScreenPos); + ImGui::PushClipRect(m_OldCanvas.WindowScreenPos + ImVec2(1, 1), m_OldCanvas.WindowScreenPos + m_OldCanvas.WindowScreenSize - ImVec2(1, 1), false); + ImDrawList_TranslateAndClampClipRects(drawList, 0, 1, clipTranslation); + ImDrawList_TranslateAndClampClipRects(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, clipTranslation); + ImGui::PopClipRect(); + + // #debug: Static grid in local space + //for (float x = 0; x < Canvas.WindowScreenSize.x; x += 100) + // drawList->AddLine(ImVec2(x, 0.0f) + Canvas.WindowScreenPos, ImVec2(x, Canvas.WindowScreenSize.y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128)); + //for (float y = 0; y < Canvas.WindowScreenSize.y; y += 100) + // drawList->AddLine(ImVec2(0.0f, y) + Canvas.WindowScreenPos, ImVec2(Canvas.WindowScreenSize.x, y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128)); + } +#endif + +#if 1 + // Move user and hint channels to top + { + // Clip plane is transformed to global space. + // These channels already have clip planes in global space, so + // we move them to clip plane. Batch transformation in canvas + // will bring them back to global space. + auto preTransformClipRect = [this](int channelIndex) { + ImDrawChannel& channel = m_DrawList->_Splitter._Channels[channelIndex]; + for (ImDrawCmd& cmd : channel._CmdBuffer) { + auto a = ToCanvas(ImVec2(cmd.ClipRect.x, cmd.ClipRect.y)); + auto b = ToCanvas(ImVec2(cmd.ClipRect.z, cmd.ClipRect.w)); + cmd.ClipRect = ImVec4(a.x, a.y, b.x, b.y); + } + }; + + m_DrawList->ChannelsSetCurrent(0); + + auto channelCount = m_DrawList->_Splitter._Count; + ImDrawList_ChannelsGrow(m_DrawList, channelCount + 3); + ImDrawList_SwapChannels(m_DrawList, c_UserChannel_HintsBackground, + channelCount + 0); + ImDrawList_SwapChannels(m_DrawList, c_UserChannel_Hints, channelCount + 1); + ImDrawList_SwapChannels(m_DrawList, c_UserChannel_Content, + channelCount + 2); + + preTransformClipRect(channelCount + 0); + preTransformClipRect(channelCount + 1); + preTransformClipRect(channelCount + 2); + } +#endif + + UpdateAnimations(); + + m_DrawList->ChannelsMerge(); + + // #debug + // drawList->AddRectFilled(ImVec2(-10.0f, -10.0f), ImVec2(10.0f, 10.0f), + // IM_COL32(255, 0, 255, 255)); + + // ImGui::EndChild(); + // ImGui::PopStyleColor(); + if (m_IsCanvasVisible) m_Canvas.End(); + + ImDrawList_SwapSplitter(m_DrawList, m_Splitter); + + // Draw border + { + auto& style = ImGui::GetStyle(); + auto borderShadoColor = style.Colors[ImGuiCol_BorderShadow]; + auto borderColor = style.Colors[ImGuiCol_Border]; + m_DrawList->AddRect(m_Canvas.Rect().Min + ImVec2(1, 1), + m_Canvas.Rect().Max - ImVec2(1, 1), + ImColor(borderShadoColor)); + m_DrawList->AddRect(m_Canvas.Rect().Min, m_Canvas.Rect().Max, + ImColor(borderColor)); + } + + // #metrics + // ShowMetrics(control); + + ImGui::PopID(); + + if (!m_CurrentAction && m_IsFirstFrame && !m_Settings.m_Selection.empty()) { + ClearSelection(); + for (auto id : m_Settings.m_Selection) + if (auto object = FindObject(id)) SelectObject(object); + } + + if (HasSelectionChanged()) MakeDirty(SaveReasonFlags::Selection); + + if (m_Settings.m_IsDirty && !m_CurrentAction) SaveSettings(); + + m_DrawList = nullptr; + m_IsFirstFrame = false; +} + +bool ed::EditorContext::DoLink(LinkId id, PinId startPinId, PinId endPinId, + ImU32 color, float thickness) { + // auto& editorStyle = GetStyle(); + + auto startPin = FindPin(startPinId); + auto endPin = FindPin(endPinId); + + if (!startPin || !startPin->m_IsLive || !endPin || !endPin->m_IsLive) + return false; + + startPin->m_HasConnection = true; + endPin->m_HasConnection = true; + + auto link = GetLink(id); + link->m_StartPin = startPin; + link->m_EndPin = endPin; + link->m_Color = color; + link->m_HighlightColor = GetColor(StyleColor_HighlightLinkBorder); + link->m_Thickness = thickness; + link->m_IsLive = true; + + link->UpdateEndpoints(); + + return true; +} + +void ed::EditorContext::SetNodePosition(NodeId nodeId, const ImVec2& position) { + auto node = FindNode(nodeId); + if (!node) { + node = CreateNode(nodeId); + node->m_IsLive = false; + } + + if (node->m_Bounds.Min != position) { + node->m_Bounds.Translate(position - node->m_Bounds.Min); + node->m_Bounds.Floor(); + MakeDirty(NodeEditor::SaveReasonFlags::Position, node); + } +} + +void ed::EditorContext::SetGroupSize(NodeId nodeId, const ImVec2& size) { + auto node = FindNode(nodeId); + if (!node) { + node = CreateNode(nodeId); + node->m_IsLive = false; + } + + node->m_Type = NodeType::Group; + + if (node->m_GroupBounds.GetSize() != size) { + node->m_GroupBounds.Min = node->m_Bounds.Min; + node->m_GroupBounds.Max = node->m_Bounds.Min + size; + node->m_GroupBounds.Floor(); + MakeDirty(NodeEditor::SaveReasonFlags::Size, node); + } +} + +ImVec2 ed::EditorContext::GetNodePosition(NodeId nodeId) { + auto node = FindNode(nodeId); + if (!node) return ImVec2(FLT_MAX, FLT_MAX); + + return node->m_Bounds.Min; +} + +ImVec2 ed::EditorContext::GetNodeSize(NodeId nodeId) { + auto node = FindNode(nodeId); + if (!node) return ImVec2(0, 0); + + return node->m_Bounds.GetSize(); +} + +void ed::EditorContext::SetNodeZPosition(NodeId nodeId, float z) { + auto node = FindNode(nodeId); + if (!node) { + node = CreateNode(nodeId); + node->m_IsLive = false; + } + + node->m_ZPosition = z; +} + +float ed::EditorContext::GetNodeZPosition(NodeId nodeId) { + auto node = FindNode(nodeId); + if (!node) return 0.0f; + + return node->m_ZPosition; +} + +void ed::EditorContext::MarkNodeToRestoreState(Node* node) { + node->m_RestoreState = true; +} + +void ed::EditorContext::UpdateNodeState(Node* node) { + bool tryLoadState = node->m_RestoreState; + + node->m_RestoreState = false; + + auto settings = m_Settings.FindNode(node->m_ID); + if (!settings) return; + + if (!tryLoadState && settings->m_WasUsed) return; + + if (!settings->m_WasUsed) { + MakeDirty(SaveReasonFlags::AddNode, node); + settings->m_WasUsed = true; + } + + // Load state from config (if possible) + if (tryLoadState) { + NodeSettings newSettings = *settings; + if (NodeSettings::Parse(m_Config.LoadNode(node->m_ID), newSettings)) + *settings = newSettings; + } + + node->m_Bounds.Min = settings->m_Location; + node->m_Bounds.Max = node->m_Bounds.Min + settings->m_Size; + node->m_Bounds.Floor(); + node->m_GroupBounds.Min = settings->m_Location; + node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize; + node->m_GroupBounds.Floor(); +} + +void ed::EditorContext::RemoveSettings(Object* object) { + if (auto node = object->AsNode()) { + m_Settings.RemoveNode(node->m_ID); + MakeDirty(SaveReasonFlags::RemoveNode, node); + } +} + +void ed::EditorContext::ClearSelection() { + for (auto& object : m_SelectedObjects) object->m_IsSelected = false; + + m_SelectedObjects.clear(); +} + +void ed::EditorContext::SelectObject(Object* object) { + m_SelectedObjects.push_back(object); + object->m_IsSelected = true; +} + +void ed::EditorContext::DeselectObject(Object* object) { + auto objectIt = + std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object); + if (objectIt == m_SelectedObjects.end()) return; + + object->m_IsSelected = false; + m_SelectedObjects.erase(objectIt); +} + +void ed::EditorContext::SetSelectedObject(Object* object) { + ClearSelection(); + SelectObject(object); +} + +void ed::EditorContext::ToggleObjectSelection(Object* object) { + if (IsSelected(object)) + DeselectObject(object); + else + SelectObject(object); +} + +bool ed::EditorContext::IsSelected(Object* object) { + return object && object->m_IsSelected; + // return std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), + // object) != m_SelectedObjects.end(); +} + +const ed::vector& ed::EditorContext::GetSelectedObjects() { + return m_SelectedObjects; +} + +bool ed::EditorContext::IsAnyNodeSelected() { + for (auto object : m_SelectedObjects) + if (object->AsNode()) return true; + + return false; +} + +bool ed::EditorContext::IsAnyLinkSelected() { + for (auto object : m_SelectedObjects) + if (object->AsLink()) return true; + + return false; +} + +bool ed::EditorContext::HasSelectionChanged() { + return m_LastSelectedObjects != m_SelectedObjects; +} + +ed::Node* ed::EditorContext::FindNodeAt(const ImVec2& p) { + for (auto node : m_Nodes) + if (node->TestHit(p)) return node; + + return nullptr; +} + +void ed::EditorContext::FindNodesInRect(const ImRect& r, vector& result, + bool append, bool includeIntersecting) { + if (!append) result.resize(0); + + if (ImRect_IsEmpty(r)) return; + + for (auto node : m_Nodes) + if (node->TestHit(r, includeIntersecting)) result.push_back(node); +} + +void ed::EditorContext::FindLinksInRect(const ImRect& r, vector& result, + bool append) { + if (!append) result.resize(0); + + if (ImRect_IsEmpty(r)) return; + + for (auto link : m_Links) + if (link->TestHit(r)) result.push_back(link); +} + +bool ed::EditorContext::HasAnyLinks(NodeId nodeId) const { + for (auto link : m_Links) { + if (!link->m_IsLive) continue; + + if (link->m_StartPin->m_Node->m_ID == nodeId || + link->m_EndPin->m_Node->m_ID == nodeId) + return true; + } + + return false; +} + +bool ed::EditorContext::HasAnyLinks(PinId pinId) const { + for (auto link : m_Links) { + if (!link->m_IsLive) continue; + + if (link->m_StartPin->m_ID == pinId || link->m_EndPin->m_ID == pinId) + return true; + } + + return false; +} + +int ed::EditorContext::BreakLinks(NodeId nodeId) { + int result = 0; + for (auto link : m_Links) { + if (!link->m_IsLive) continue; + + if (link->m_StartPin->m_Node->m_ID == nodeId || + link->m_EndPin->m_Node->m_ID == nodeId) { + if (GetItemDeleter().Add(link)) ++result; + } + } + return result; +} + +int ed::EditorContext::BreakLinks(PinId pinId) { + int result = 0; + for (auto link : m_Links) { + if (!link->m_IsLive) continue; + + if (link->m_StartPin->m_ID == pinId || link->m_EndPin->m_ID == pinId) { + if (GetItemDeleter().Add(link)) ++result; + } + } + return result; +} + +void ed::EditorContext::FindLinksForNode(NodeId nodeId, vector& result, + bool add) { + if (!add) result.clear(); + + for (auto link : m_Links) { + if (!link->m_IsLive) continue; + + if (link->m_StartPin->m_Node->m_ID == nodeId || + link->m_EndPin->m_Node->m_ID == nodeId) + result.push_back(link); + } +} + +bool ed::EditorContext::PinHadAnyLinks(PinId pinId) { + auto pin = FindPin(pinId); + if (!pin || !pin->m_IsLive) return false; + + return pin->m_HasConnection || pin->m_HadConnection; +} + +void ed::EditorContext::NotifyLinkDeleted(Link* link) { + if (m_LastActiveLink == link) m_LastActiveLink = nullptr; +} + +void ed::EditorContext::Suspend(SuspendFlags flags) { + IM_ASSERT(m_DrawList != nullptr && + "Suspend was called outiside of Begin/End."); + auto lastChannel = m_DrawList->_Splitter._Current; + m_DrawList->ChannelsSetCurrent(m_ExternalChannel); + if (m_IsCanvasVisible) m_Canvas.Suspend(); + m_DrawList->ChannelsSetCurrent(lastChannel); + if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter) + ImDrawList_SwapSplitter(m_DrawList, m_Splitter); +} + +void ed::EditorContext::Resume(SuspendFlags flags) { + IM_ASSERT(m_DrawList != nullptr && + "Reasume was called outiside of Begin/End."); + if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter) + ImDrawList_SwapSplitter(m_DrawList, m_Splitter); + auto lastChannel = m_DrawList->_Splitter._Current; + m_DrawList->ChannelsSetCurrent(m_ExternalChannel); + if (m_IsCanvasVisible) m_Canvas.Resume(); + m_DrawList->ChannelsSetCurrent(lastChannel); +} + +bool ed::EditorContext::IsSuspended() { return m_Canvas.IsSuspended(); } + +bool ed::EditorContext::IsFocused() { return m_IsFocused; } + +bool ed::EditorContext::IsHovered() const { return m_IsHovered; } + +bool ed::EditorContext::IsHoveredWithoutOverlapp() const { + return m_IsHoveredWithoutOverlapp; +} + +bool ed::EditorContext::CanAcceptUserInput() const { + return m_IsFocused && m_IsHovered; +} + +int ed::EditorContext::CountLiveNodes() const { + return (int)std::count_if(m_Nodes.begin(), m_Nodes.end(), + [](const Node* node) { return node->m_IsLive; }); +} + +int ed::EditorContext::CountLivePins() const { + return (int)std::count_if(m_Pins.begin(), m_Pins.end(), + [](const Pin* pin) { return pin->m_IsLive; }); +} + +int ed::EditorContext::CountLiveLinks() const { + return (int)std::count_if(m_Links.begin(), m_Links.end(), + [](const Link* link) { return link->m_IsLive; }); +} + +ed::Pin* ed::EditorContext::CreatePin(PinId id, PinKind kind) { + IM_ASSERT(nullptr == FindObject(id)); + auto pin = new Pin(this, id, kind); + m_Pins.push_back({id, pin}); + std::sort(m_Pins.begin(), m_Pins.end()); + return pin; +} + +ed::Node* ed::EditorContext::CreateNode(NodeId id) { + IM_ASSERT(nullptr == FindObject(id)); + auto node = new Node(this, id); + m_Nodes.push_back({id, node}); + // std::sort(Nodes.begin(), Nodes.end()); + + auto settings = m_Settings.FindNode(id); + if (!settings) settings = m_Settings.AddNode(id); + + UpdateNodeState(node); + + if (settings->m_GroupSize.x > 0 || settings->m_GroupSize.y > 0) + node->m_Type = NodeType::Group; + + node->m_IsLive = false; + + return node; +} + +ed::Link* ed::EditorContext::CreateLink(LinkId id) { + IM_ASSERT(nullptr == FindObject(id)); + auto link = new Link(this, id); + m_Links.push_back({id, link}); + std::sort(m_Links.begin(), m_Links.end()); + + return link; +} + +template +static inline auto FindItemInLinear(C& container, Id id) { +#if defined(_DEBUG) + auto start = container.data(); + auto end = container.data() + container.size(); + for (auto it = start; it < end; ++it) + if ((*it).m_ID == id) return it->m_Object; +#else + for (auto item : container) + if (item.m_ID == id) return item.m_Object; +#endif + + return static_cast(nullptr); +} + +template +static inline auto FindItemIn(C& container, Id id) { + // # if defined(_DEBUG) + // auto start = container.data(); + // auto end = container.data() + container.size(); + // for (auto it = start; it < end; ++it) + // if ((*it)->ID == id) + // return *it; + // # else + // for (auto item : container) + // if (item->ID == id) + // return item; + // # endif + auto key = typename C::value_type{id, nullptr}; + auto first = container.cbegin(); + auto last = container.cend(); + auto it = std::lower_bound(first, last, key); + if (it != last && (key.m_ID == it->m_ID)) + return it->m_Object; + else + return static_castm_Object)>(nullptr); +} + +ed::Node* ed::EditorContext::FindNode(NodeId id) { + return FindItemInLinear(m_Nodes, id); +} + +ed::Pin* ed::EditorContext::FindPin(PinId id) { return FindItemIn(m_Pins, id); } + +ed::Link* ed::EditorContext::FindLink(LinkId id) { + return FindItemIn(m_Links, id); +} + +ed::Object* ed::EditorContext::FindObject(ObjectId id) { + if (id.IsNodeId()) + return FindNode(id.AsNodeId()); + else if (id.IsLinkId()) + return FindLink(id.AsLinkId()); + else if (id.IsPinId()) + return FindPin(id.AsPinId()); + else + return nullptr; +} + +ed::Node* ed::EditorContext::GetNode(NodeId id) { + auto node = FindNode(id); + if (!node) node = CreateNode(id); + return node; +} + +ed::Pin* ed::EditorContext::GetPin(PinId id, PinKind kind) { + if (auto pin = FindPin(id)) { + pin->m_Kind = kind; + return pin; + } else + return CreatePin(id, kind); +} + +ed::Link* ed::EditorContext::GetLink(LinkId id) { + if (auto link = FindLink(id)) + return link; + else + return CreateLink(id); +} + +void ed::EditorContext::LoadSettings() { + ed::Settings::Parse(m_Config.Load(), m_Settings); + + if (ImRect_IsEmpty(m_Settings.m_VisibleRect)) { + m_NavigateAction.m_Scroll = m_Settings.m_ViewScroll; + m_NavigateAction.m_Zoom = m_Settings.m_ViewZoom; + } else { + m_NavigateAction.NavigateTo(m_Settings.m_VisibleRect, + NavigateAction::ZoomMode::Exact, 0.0f); + } +} + +void ed::EditorContext::SaveSettings() { + m_Config.BeginSave(); + + for (auto& node : m_Nodes) { + auto settings = m_Settings.FindNode(node->m_ID); + settings->m_Location = node->m_Bounds.Min; + settings->m_Size = node->m_Bounds.GetSize(); + if (IsGroup(node)) settings->m_GroupSize = node->m_GroupBounds.GetSize(); + + if (!node->m_RestoreState && settings->m_IsDirty && + m_Config.SaveNodeSettings) { + if (m_Config.SaveNode(node->m_ID, settings->Serialize().dump(), + settings->m_DirtyReason)) + settings->ClearDirty(); + } + } + + m_Settings.m_Selection.resize(0); + for (auto& object : m_SelectedObjects) + m_Settings.m_Selection.push_back(object->ID()); + + m_Settings.m_ViewScroll = m_NavigateAction.m_Scroll; + m_Settings.m_ViewZoom = m_NavigateAction.m_Zoom; + m_Settings.m_VisibleRect = m_NavigateAction.m_VisibleRect; + + if (m_Config.Save(m_Settings.Serialize(), m_Settings.m_DirtyReason)) + m_Settings.ClearDirty(); + + m_Config.EndSave(); +} + +void ed::EditorContext::MakeDirty(SaveReasonFlags reason) { + m_Settings.MakeDirty(reason); +} + +void ed::EditorContext::MakeDirty(SaveReasonFlags reason, Node* node) { + m_Settings.MakeDirty(reason, node); +} + +ed::Link* ed::EditorContext::FindLinkAt(const ImVec2& p) { + for (auto& link : m_Links) + if (link->TestHit(p, c_LinkSelectThickness)) return link; + + return nullptr; +} + +ImU32 ed::EditorContext::GetColor(StyleColor colorIndex) const { + return ImColor(m_Style.Colors[colorIndex]); +} + +ImU32 ed::EditorContext::GetColor(StyleColor colorIndex, float alpha) const { + auto color = m_Style.Colors[colorIndex]; + return ImColor(color.x, color.y, color.z, color.w * alpha); +} + +int ed::EditorContext::GetNodeIds(NodeId* nodes, int size) const { + if (size <= 0) return 0; + + int result = 0; + for (auto node : m_Nodes) { + if (!node->m_IsLive) continue; + + *nodes++ = node->m_ID; + ++result; + if (--size == 0) break; + } + + return result; +} + +void ed::EditorContext::RegisterAnimation(Animation* animation) { + m_LiveAnimations.push_back(animation); +} + +void ed::EditorContext::UnregisterAnimation(Animation* animation) { + auto it = + std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation); + if (it != m_LiveAnimations.end()) m_LiveAnimations.erase(it); +} + +void ed::EditorContext::UpdateAnimations() { + m_LastLiveAnimations = m_LiveAnimations; + + for (auto animation : m_LastLiveAnimations) { + const bool isLive = + (std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), + animation) != m_LiveAnimations.end()); + + if (isLive) animation->Update(); + } +} + +void ed::EditorContext::Flow(Link* link, FlowDirection direction) { + m_FlowAnimationController.Flow(link, direction); +} + +void ed::EditorContext::SetUserContext(bool globalSpace) { + const auto mousePos = ImGui::GetMousePos(); + + // Move drawing cursor to mouse location and prepare layer for + // content added by user. + if (globalSpace) + ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); + else + ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); + // ImGui::SetCursorScreenPos(ImFloor(mousePos)); + // ImGui::SetCursorScreenPos(ImVec2(floorf(mousePos.x), floorf(mousePos.y))); + + if (!IsSuspended()) { + m_DrawList->ChannelsSetCurrent(c_UserChannel_Content); + } + + // #debug + // drawList->AddCircleFilled(ImGui::GetMousePos(), 4, IM_COL32(0, 255, 0, + // 255)); +} + +void ed::EditorContext::EnableShortcuts(bool enable) { + m_ShortcutsEnabled = enable; +} + +bool ed::EditorContext::AreShortcutsEnabled() { return m_ShortcutsEnabled; } + +ed::Control ed::EditorContext::BuildControl(bool allowOffscreen) { + m_IsHovered = false; + m_IsHoveredWithoutOverlapp = false; + + const auto windowHovered = ImGui::IsWindowHovered(); + const auto widgetHovered = ImGui::IsMouseHoveringRect( + m_Canvas.ViewRect().Min, m_Canvas.ViewRect().Max, true); + + if (!allowOffscreen && !windowHovered && !widgetHovered) return Control(); + + const auto mousePos = ImGui::GetMousePos(); + + // Expand clip rectangle to always contain cursor + auto editorRect = m_Canvas.ViewRect(); + auto isMouseOffscreen = allowOffscreen && !editorRect.Contains(mousePos); + if (isMouseOffscreen) { + // Extend clip rect to capture off-screen mouse cursor + editorRect.Add(ImFloor(mousePos)); + editorRect.Add(ImVec2(ImCeil(mousePos.x), ImCeil(mousePos.y))); + + ImGui::PushClipRect(editorRect.Min, editorRect.Max, false); + } + + ImGuiID activeId = 0; + Object* hotObject = nullptr; + Object* activeObject = nullptr; + Object* clickedObject = nullptr; + Object* doubleClickedObject = nullptr; + + ImGuiButtonFlags extraFlags = ImGuiButtonFlags_None; + extraFlags |= ImGuiButtonFlags_MouseButtonLeft; + extraFlags |= ImGuiButtonFlags_MouseButtonRight; + extraFlags |= ImGuiButtonFlags_MouseButtonMiddle; + + static auto invisibleButtonEx = [](const char* str_id, const ImVec2& size_arg, + ImGuiButtonFlags extraFlags) -> int { + using namespace ImGui; + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) return -1; + + if (size_arg.x == 0.0f || size_arg.y == 0.0f) return false; + + const ImGuiID id = window->GetID(str_id); + 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)) return -1; + + auto buttonIndex = ImGui::GetCurrentContext()->ActiveIdMouseButton; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, extraFlags); + + return pressed ? buttonIndex : -1; + }; + + // Emits invisible button and returns true if it is clicked. + auto emitInteractiveAreaEx = [&activeId](ObjectId id, const ImRect& rect, + ImGuiButtonFlags extraFlags) -> int { + char idString[33] = {0}; // itoa can output 33 bytes maximum + snprintf(idString, 32, "%p", id.AsPointer()); + ImGui::SetCursorScreenPos(rect.Min); + + // debug + // if (id < 0) return ImGui::Button(idString, to_imvec(rect.size)); + + auto buttonIndex = invisibleButtonEx(idString, rect.GetSize(), extraFlags); + + // #debug + // ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), + // ImGui::GetItemRectMax(), IM_COL32(0, 255, 0, 64)); + + if (ImGui::IsItemActive()) activeId = ImGui::GetActiveID(); + + return buttonIndex; + }; + + auto emitInteractiveArea = [&emitInteractiveAreaEx, extraFlags]( + ObjectId id, const ImRect& rect) { + return emitInteractiveAreaEx(id, rect, extraFlags); + }; + + // Check input interactions over area. + auto checkInteractionsInArea = + [this, &emitInteractiveArea, &hotObject, &activeObject, &clickedObject, + &doubleClickedObject](ObjectId id, const ImRect& rect, Object* object) { + if (emitInteractiveArea(id, rect) >= 0) clickedObject = object; + if (!doubleClickedObject && + ImGui::IsMouseDoubleClicked(m_Config.DragButtonIndex) && + ImGui::IsItemHovered()) + doubleClickedObject = object; + + if (!hotObject && ImGui::IsItemHovered( + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + hotObject = object; + + if (ImGui::IsItemActive()) activeObject = object; + }; + + // Process live nodes and pins. + for (auto nodeIt = m_Nodes.rbegin(), nodeItEnd = m_Nodes.rend(); + nodeIt != nodeItEnd; ++nodeIt) { + auto node = *nodeIt; + + if (!node->m_IsLive) continue; + + // Check for interactions with live pins in node before + // processing node itself. Pins does not overlap each other + // and all are within node bounds. + for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin) { + if (!pin->m_IsLive) continue; + + checkInteractionsInArea(pin->m_ID, pin->m_Bounds, pin); + } + + // Check for interactions with node. + if (node->m_Type == NodeType::Group) { + // Node with a hole + ImGui::PushID(node->m_ID.AsPointer()); + + static const NodeRegion c_Regions[] = { + NodeRegion::TopLeft, NodeRegion::TopRight, NodeRegion::BottomLeft, + NodeRegion::BottomRight, NodeRegion::Top, NodeRegion::Bottom, + NodeRegion::Left, NodeRegion::Right, NodeRegion::Header, + }; + + for (auto region : c_Regions) { + auto bounds = node->GetRegionBounds(region); + if (ImRect_IsEmpty(bounds)) continue; + checkInteractionsInArea(NodeId(static_cast(region)), bounds, node); + } + + ImGui::PopID(); + } else + checkInteractionsInArea(node->m_ID, node->m_Bounds, node); + } + + // Links are not regular widgets and must be done manually since + // ImGui does not support interactive elements with custom hit maps. + // + // Links can steal input from background. + + // Links are just over background. So if anything else + // is hovered we can skip them. + if (nullptr == hotObject) hotObject = FindLinkAt(mousePos); + + ImGuiButtonFlags backgroundExtraFlags = ImGuiButtonFlags_None; + if (m_Config.DragButtonIndex == 0 || m_Config.SelectButtonIndex == 0 || + m_Config.NavigateButtonIndex == 0) + backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonLeft; + if (m_Config.DragButtonIndex == 1 || m_Config.SelectButtonIndex == 1 || + m_Config.NavigateButtonIndex == 1) + backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonRight; + if (m_Config.DragButtonIndex == 2 || m_Config.SelectButtonIndex == 2 || + m_Config.NavigateButtonIndex == 2) + backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonMiddle; + + auto isMouseDoubleClickOverBackground = [doubleClickedObject, + backgroundExtraFlags]() -> int { + if (doubleClickedObject) return -1; + + if (!ImGui::IsItemHovered()) return -1; + + if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonLeft) && + ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + return ImGuiButtonFlags_MouseButtonLeft; + if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonRight) && + ImGui::IsMouseDoubleClicked(ImGuiButtonFlags_MouseButtonRight)) + return ImGuiButtonFlags_MouseButtonRight; + if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonMiddle) && + ImGui::IsMouseDoubleClicked(ImGuiButtonFlags_MouseButtonMiddle)) + return ImGuiButtonFlags_MouseButtonMiddle; + + return -1; + }; + + // Check for interaction with background. + auto backgroundClickButonIndex = + emitInteractiveAreaEx(NodeId(0), editorRect, backgroundExtraFlags); + auto backgroundDoubleClickButtonIndex = isMouseDoubleClickOverBackground(); + auto isBackgroundActive = ImGui::IsItemActive(); + auto isBackgroundHot = !hotObject; + auto isDragging = ImGui::IsMouseDragging(0, 1) || + ImGui::IsMouseDragging(1, 1) || + ImGui::IsMouseDragging(2, 1); + + if (backgroundDoubleClickButtonIndex >= 0) backgroundClickButonIndex = -1; + + if (isMouseOffscreen) ImGui::PopClipRect(); + + // Process link input using background interactions. + auto hotLink = hotObject ? hotObject->AsLink() : nullptr; + + // ImGui take care of tracking active items. With link + // we must do this ourself. + if (!isDragging && isBackgroundActive && hotLink && !m_LastActiveLink) + m_LastActiveLink = hotLink; + if (isBackgroundActive && m_LastActiveLink) { + activeObject = m_LastActiveLink; + isBackgroundActive = false; + } else if (!isBackgroundActive && m_LastActiveLink) + m_LastActiveLink = nullptr; + + // Steal click from backgrounds if link is hovered. + if (!isDragging && backgroundClickButonIndex >= 0 && hotLink) { + clickedObject = hotLink; + backgroundClickButonIndex = -1; + } + + // Steal double-click from backgrounds if link is hovered. + if (!isDragging && backgroundDoubleClickButtonIndex >= 0 && hotLink) { + doubleClickedObject = hotLink; + backgroundDoubleClickButtonIndex = -1; + } + + if (activeId) m_EditorActiveId = activeId; + + if (ImGui::IsAnyItemActive() && ImGui::GetActiveID() != m_EditorActiveId) + return Control(); + + m_IsHovered = ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly); + m_IsHoveredWithoutOverlapp = ImGui::IsItemHovered(); + if (!allowOffscreen && !m_IsHovered) return Control(); + +#if IMGUI_VERSION_NUM >= 18836 + if (m_IsHoveredWithoutOverlapp) ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY); +#elif IMGUI_VERSION_NUM >= 17909 + if (m_IsHoveredWithoutOverlapp) ImGui::SetItemUsingMouseWheel(); +#endif + + return Control(hotObject, activeObject, clickedObject, doubleClickedObject, + isBackgroundHot, isBackgroundActive, backgroundClickButonIndex, + backgroundDoubleClickButtonIndex); +} + +void ed::EditorContext::ShowMetrics(const Control& control) { + auto& io = ImGui::GetIO(); + + auto getObjectName = [](Object* object) { + if (!object) + return ""; + else if (object->AsNode()) + return "Node"; + else if (object->AsPin()) + return "Pin"; + else if (object->AsLink()) + return "Link"; + else + return ""; + }; + + auto getHotObjectName = [&control, &getObjectName]() { + if (control.HotObject) + return getObjectName(control.HotObject); + else if (control.BackgroundHot) + return "Background"; + else + return ""; + }; + + auto getActiveObjectName = [&control, &getObjectName]() { + if (control.ActiveObject) + return getObjectName(control.ActiveObject); + else if (control.BackgroundActive) + return "Background"; + else + return ""; + }; + + auto liveNodeCount = CountLiveNodes(); + auto livePinCount = CountLivePins(); + auto liveLinkCount = CountLiveLinks(); + + auto canvasRect = m_Canvas.Rect(); + auto viewRect = m_Canvas.ViewRect(); + auto localMousePos = m_Canvas.ToLocal(io.MousePos); + auto globalMousePos = io.MousePos; + + ImGui::SetCursorScreenPos(canvasRect.Min + ImVec2(5, 5)); + ImGui::BeginGroup(); + ImGui::Text("Is Focused: %s", m_IsFocused ? "true" : "false"); + ImGui::Text("Is Hovered: %s", m_IsHovered ? "true" : "false"); + ImGui::Text("Is Hovered (without overlapp): %s", + m_IsHoveredWithoutOverlapp ? "true" : "false"); + ImGui::Text("Accept Input: %s", CanAcceptUserInput() ? "true" : "false"); + ImGui::Text("View Position: { x=%g y=%g }", viewRect.Min.x, viewRect.Min.y); + ImGui::Text("View Size: { w=%g h=%g }", viewRect.GetWidth(), + viewRect.GetHeight()); + ImGui::Text("Canvas Size: { w=%g h=%g }", canvasRect.GetWidth(), + canvasRect.GetHeight()); + ImGui::Text("Mouse: { x=%.0f y=%.0f } global: { x=%g y=%g }", localMousePos.x, + localMousePos.y, globalMousePos.x, globalMousePos.y); + ImGui::Text("Live Nodes: %d", liveNodeCount); + ImGui::Text("Live Pins: %d", livePinCount); + ImGui::Text("Live Links: %d", liveLinkCount); + ImGui::Text( + "Hot Object: %s (%p)", getHotObjectName(), + control.HotObject ? control.HotObject->ID().AsPointer() : nullptr); + if (auto node = control.HotObject ? control.HotObject->AsNode() : nullptr) { + ImGui::SameLine(); + ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, + node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), + node->m_Bounds.GetHeight()); + } + ImGui::Text( + "Active Object: %s (%p)", getActiveObjectName(), + control.ActiveObject ? control.ActiveObject->ID().AsPointer() : nullptr); + if (auto node = + control.ActiveObject ? control.ActiveObject->AsNode() : nullptr) { + ImGui::SameLine(); + ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, + node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), + node->m_Bounds.GetHeight()); + } + ImGui::Text("Action: %s", + m_CurrentAction ? m_CurrentAction->GetName() : ""); + ImGui::Text("Action Is Dragging: %s", + m_CurrentAction && m_CurrentAction->IsDragging() ? "Yes" : "No"); + m_NavigateAction.ShowMetrics(); + m_SizeAction.ShowMetrics(); + m_DragAction.ShowMetrics(); + m_SelectAction.ShowMetrics(); + m_ContextMenuAction.ShowMetrics(); + m_CreateItemAction.ShowMetrics(); + m_DeleteItemsAction.ShowMetrics(); + ImGui::EndGroup(); +} + +//------------------------------------------------------------------------------ +// +// Node Settings +// +//------------------------------------------------------------------------------ +void ed::NodeSettings::ClearDirty() { + m_IsDirty = false; + m_DirtyReason = SaveReasonFlags::None; +} + +void ed::NodeSettings::MakeDirty(SaveReasonFlags reason) { + m_IsDirty = true; + m_DirtyReason = m_DirtyReason | reason; +} + +ed::json::value ed::NodeSettings::Serialize() { + json::value result; + result["location"]["x"] = m_Location.x; + result["location"]["y"] = m_Location.y; + + if (m_GroupSize.x > 0 || m_GroupSize.y > 0) { + result["group_size"]["x"] = m_GroupSize.x; + result["group_size"]["y"] = m_GroupSize.y; + } + + return result; +} + +bool ed::NodeSettings::Parse(const std::string& string, + NodeSettings& settings) { + auto settingsValue = json::value::parse(string); + if (settingsValue.is_discarded()) return false; + + return Parse(settingsValue, settings); +} + +bool ed::NodeSettings::Parse(const json::value& data, NodeSettings& result) { + if (!data.is_object()) return false; + + auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool { + if (v.is_object()) { + auto xValue = v["x"]; + auto yValue = v["y"]; + + if (xValue.is_number() && yValue.is_number()) { + result.x = static_cast(xValue.get()); + result.y = static_cast(yValue.get()); + + return true; + } + } + + return false; + }; + + if (!tryParseVector(data["location"], result.m_Location)) return false; + + if (data.contains("group_size") && + !tryParseVector(data["group_size"], result.m_GroupSize)) + return false; + + return true; +} + +//------------------------------------------------------------------------------ +// +// Settings +// +//------------------------------------------------------------------------------ +ed::NodeSettings* ed::Settings::AddNode(NodeId id) { + m_Nodes.push_back(NodeSettings(id)); + return &m_Nodes.back(); +} + +ed::NodeSettings* ed::Settings::FindNode(NodeId id) { + for (auto& settings : m_Nodes) + if (settings.m_ID == id) return &settings; + + return nullptr; +} + +void ed::Settings::RemoveNode(NodeId id) { + auto node = FindNode(id); + if (!node) return; + + *node = NodeSettings(id); +} + +void ed::Settings::ClearDirty(Node* node) { + if (node) { + auto settings = FindNode(node->m_ID); + IM_ASSERT(settings); + settings->ClearDirty(); + } else { + m_IsDirty = false; + m_DirtyReason = SaveReasonFlags::None; + + for (auto& knownNode : m_Nodes) knownNode.ClearDirty(); + } +} + +void ed::Settings::MakeDirty(SaveReasonFlags reason, Node* node) { + m_IsDirty = true; + m_DirtyReason = m_DirtyReason | reason; + + if (node) { + auto settings = FindNode(node->m_ID); + IM_ASSERT(settings); + + settings->MakeDirty(reason); + } +} + +std::string ed::Settings::Serialize() { + json::value result; + + auto serializeObjectId = [](ObjectId id) { + auto value = std::to_string(reinterpret_cast(id.AsPointer())); + switch (id.Type()) { + default: + case NodeEditor::Detail::ObjectType::None: + return value; + case NodeEditor::Detail::ObjectType::Node: + return "node:" + value; + case NodeEditor::Detail::ObjectType::Link: + return "link:" + value; + case NodeEditor::Detail::ObjectType::Pin: + return "pin:" + value; + } + }; + + auto& nodes = result["nodes"]; + for (auto& node : m_Nodes) { + if (node.m_WasUsed) nodes[serializeObjectId(node.m_ID)] = node.Serialize(); + } + + auto& selection = result["selection"]; + for (auto& id : m_Selection) selection.push_back(serializeObjectId(id)); + + auto& view = result["view"]; + view["scroll"]["x"] = m_ViewScroll.x; + view["scroll"]["y"] = m_ViewScroll.y; + view["zoom"] = m_ViewZoom; + view["visible_rect"]["min"]["x"] = m_VisibleRect.Min.x; + view["visible_rect"]["min"]["y"] = m_VisibleRect.Min.y; + view["visible_rect"]["max"]["x"] = m_VisibleRect.Max.x; + view["visible_rect"]["max"]["y"] = m_VisibleRect.Max.y; + + return result.dump(); +} + +bool ed::Settings::Parse(const std::string& string, Settings& settings) { + Settings result = settings; + + auto settingsValue = json::value::parse(string); + if (settingsValue.is_discarded()) return false; + + if (!settingsValue.is_object()) return false; + + auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool { + if (v.is_object() && v.contains("x") && v.contains("y")) { + auto xValue = v["x"]; + auto yValue = v["y"]; + + if (xValue.is_number() && yValue.is_number()) { + result.x = static_cast(xValue.get()); + result.y = static_cast(yValue.get()); + + return true; + } + } + + return false; + }; + + auto deserializeObjectId = [](const std::string& str) { + auto separator = str.find_first_of(':'); + auto idStart = + str.c_str() + ((separator != std::string::npos) ? separator + 1 : 0); + auto id = reinterpret_cast(strtoull(idStart, nullptr, 10)); + if (str.compare(0, separator, "node") == 0) + return ObjectId(NodeId(id)); + else if (str.compare(0, separator, "link") == 0) + return ObjectId(LinkId(id)); + else if (str.compare(0, separator, "pin") == 0) + return ObjectId(PinId(id)); + else + // fallback to old format + return ObjectId(NodeId(id)); // return ObjectId(); + }; + + // auto& settingsObject = settingsValue.get(); + + auto& nodesValue = settingsValue["nodes"]; + if (nodesValue.is_object()) { + for (auto& node : nodesValue.get()) { + auto id = deserializeObjectId(node.first.c_str()).AsNodeId(); + + auto nodeSettings = result.FindNode(id); + if (!nodeSettings) nodeSettings = result.AddNode(id); + + NodeSettings::Parse(node.second, *nodeSettings); + } + } + + auto& selectionValue = settingsValue["selection"]; + if (selectionValue.is_array()) { + const auto selectionArray = selectionValue.get(); + + result.m_Selection.reserve(selectionArray.size()); + result.m_Selection.resize(0); + for (auto& selection : selectionArray) { + if (selection.is_string()) + result.m_Selection.push_back( + deserializeObjectId(selection.get())); + } + } + + auto& viewValue = settingsValue["view"]; + if (viewValue.is_object()) { + auto& viewScrollValue = viewValue["scroll"]; + auto& viewZoomValue = viewValue["zoom"]; + + if (!tryParseVector(viewScrollValue, result.m_ViewScroll)) + result.m_ViewScroll = ImVec2(0, 0); + + result.m_ViewZoom = viewZoomValue.is_number() + ? static_cast(viewZoomValue.get()) + : 1.0f; + + if (!viewValue.contains("visible_rect") || + !tryParseVector(viewValue["visible_rect"]["min"], + result.m_VisibleRect.Min) || + !tryParseVector(viewValue["visible_rect"]["max"], + result.m_VisibleRect.Max)) + result.m_VisibleRect = {}; + } + + settings = std::move(result); + + return true; +} + +//------------------------------------------------------------------------------ +// +// Animation +// +//------------------------------------------------------------------------------ +ed::Animation::Animation(EditorContext* editor) + : Editor(editor), m_State(Stopped), m_Time(0.0f), m_Duration(0.0f) {} + +ed::Animation::~Animation() { Stop(); } + +void ed::Animation::Play(float duration) { + if (IsPlaying()) Stop(); + + m_State = Playing; + if (duration < 0) duration = 0.0f; + + m_Time = 0.0f; + m_Duration = duration; + + OnPlay(); + + Editor->RegisterAnimation(this); + + if (duration == 0.0f) Finish(); +} + +void ed::Animation::Stop() { + if (!IsPlaying()) return; + + m_State = Stopped; + + Editor->UnregisterAnimation(this); + + OnStop(); +} + +void ed::Animation::Finish() { + if (!IsPlaying()) return; + + OnFinish(); + + Stop(); +} + +void ed::Animation::Update() { + if (!IsPlaying()) return; + + m_Time += ImMax(0.0f, ImGui::GetIO().DeltaTime); + if (m_Time < m_Duration) { + const float progress = GetProgress(); + OnUpdate(progress); + } else { + OnFinish(); + Stop(); + } +} + +//------------------------------------------------------------------------------ +// +// Navigate Animation +// +//------------------------------------------------------------------------------ +ed::NavigateAnimation::NavigateAnimation(EditorContext* editor, + NavigateAction& scrollAction) + : Animation(editor), Action(scrollAction) {} + +void ed::NavigateAnimation::NavigateTo(const ImRect& target, float duration) { + Stop(); + + m_Start = Action.GetViewRect(); + m_Target = target; + + // Skip tiny animations + auto minoffset = m_Target.Min - m_Start.Min; + auto maxOffset = m_Target.Max - m_Start.Max; + auto epsilon = 1e-4f; + if (ImFabs(minoffset.x) < epsilon && ImFabs(minoffset.y) < epsilon && + ImFabs(maxOffset.x) < epsilon && ImFabs(maxOffset.y) < epsilon) { + duration = 0; + } + + Play(duration); +} + +void ed::NavigateAnimation::OnUpdate(float progress) { + ImRect current; + current.Min = + ImEasing::EaseOutQuad(m_Start.Min, m_Target.Min - m_Start.Min, progress); + current.Max = + ImEasing::EaseOutQuad(m_Start.Max, m_Target.Max - m_Start.Max, progress); + Action.SetViewRect(current); +} + +void ed::NavigateAnimation::OnStop() { + Editor->MakeDirty(SaveReasonFlags::Navigation); +} + +void ed::NavigateAnimation::OnFinish() { + Action.SetViewRect(m_Target); + + Editor->MakeDirty(SaveReasonFlags::Navigation); +} + +//------------------------------------------------------------------------------ +// +// Flow Animation +// +//------------------------------------------------------------------------------ +ed::FlowAnimation::FlowAnimation(FlowAnimationController* controller) + : Animation(controller->Editor), + Controller(controller), + m_Link(nullptr), + m_Offset(0.0f), + m_PathLength(0.0f) {} + +void ed::FlowAnimation::Flow(ed::Link* link, float markerDistance, float speed, + float duration) { + Stop(); + + if (m_Link != link) { + m_Offset = 0.0f; + ClearPath(); + } + + if (m_MarkerDistance != markerDistance) ClearPath(); + + m_MarkerDistance = markerDistance; + m_Speed = speed; + m_Link = link; + + Play(duration); +} + +void ed::FlowAnimation::Draw(ImDrawList* drawList) { + if (!IsPlaying() || !IsLinkValid() || !m_Link->IsVisible()) return; + + if (!IsPathValid()) UpdatePath(); + + m_Offset = fmodf(m_Offset, m_MarkerDistance); + if (m_Offset < 0) m_Offset += m_MarkerDistance; + + const auto progress = GetProgress(); + + const auto flowAlpha = 1.0f - progress * progress; + const auto flowColor = Editor->GetColor(StyleColor_Flow, flowAlpha); + // const auto flowPath = Link->GetCurve(); + + m_Link->Draw(drawList, flowColor, 2.0f); + + if (IsPathValid()) { + // Offset = 0; + + const auto markerAlpha = powf(1.0f - progress, 0.35f); + const auto markerRadius = 4.0f * (1.0f - progress) + 2.0f; + const auto markerColor = + Editor->GetColor(StyleColor_FlowMarker, markerAlpha); + + for (float d = m_Offset; d < m_PathLength; d += m_MarkerDistance) + drawList->AddCircleFilled(SamplePath(d), markerRadius, markerColor); + } +} + +bool ed::FlowAnimation::IsLinkValid() const { + return m_Link && m_Link->m_IsLive; +} + +bool ed::FlowAnimation::IsPathValid() const { + return m_Path.size() > 1 && m_PathLength > 0.0f && + m_Link->m_Start == m_LastStart && m_Link->m_End == m_LastEnd; +} + +void ed::FlowAnimation::UpdatePath() { + if (!IsLinkValid()) { + ClearPath(); + return; + } + + const auto curve = m_Link->GetCurve(); + + m_LastStart = m_Link->m_Start; + m_LastEnd = m_Link->m_End; + m_PathLength = ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3); + + auto collectPointsCallback = [this](ImCubicBezierFixedStepSample& result) { + m_Path.push_back(CurvePoint{result.Length, result.Point}); + }; + + const auto step = ImMax(m_MarkerDistance * 0.5f, 15.0f); + + m_Path.resize(0); + ImCubicBezierFixedStep(collectPointsCallback, curve, step, false, 0.5f, + 0.001f); +} + +void ed::FlowAnimation::ClearPath() { + vector().swap(m_Path); + m_PathLength = 0.0f; +} + +ImVec2 ed::FlowAnimation::SamplePath(float distance) const { + // distance = ImMax(0.0f, std::min(distance, PathLength)); + + auto endPointIt = std::find_if( + m_Path.begin(), m_Path.end(), + [distance](const CurvePoint& p) { return distance < p.Distance; }); + if (endPointIt == m_Path.end()) + endPointIt = m_Path.end() - 1; + else if (endPointIt == m_Path.begin()) + endPointIt = m_Path.begin() + 1; + + const auto& start = endPointIt[-1]; + const auto& end = *endPointIt; + const auto t = (distance - start.Distance) / (end.Distance - start.Distance); + + return start.Point + (end.Point - start.Point) * t; +} + +void ed::FlowAnimation::OnUpdate(float progress) { + IM_UNUSED(progress); + + m_Offset += m_Speed * ImGui::GetIO().DeltaTime; +} + +void ed::FlowAnimation::OnStop() { Controller->Release(this); } + +//------------------------------------------------------------------------------ +// +// Flow Animation Controller +// +//------------------------------------------------------------------------------ +ed::FlowAnimationController::FlowAnimationController(EditorContext* editor) + : AnimationController(editor) {} + +ed::FlowAnimationController::~FlowAnimationController() { + for (auto animation : m_Animations) delete animation; +} + +void ed::FlowAnimationController::Flow(Link* link, FlowDirection direction) { + if (!link || !link->m_IsLive) return; + + auto& editorStyle = GetStyle(); + + auto animation = GetOrCreate(link); + + float speedDirection = 1.0f; + if (direction == FlowDirection::Backward) speedDirection = -1.0f; + + animation->Flow(link, editorStyle.FlowMarkerDistance, + editorStyle.FlowSpeed * speedDirection, + editorStyle.FlowDuration); +} + +void ed::FlowAnimationController::Draw(ImDrawList* drawList) { + if (m_Animations.empty()) return; + + drawList->ChannelsSetCurrent(c_LinkChannel_Flow); + + for (auto animation : m_Animations) animation->Draw(drawList); +} + +ed::FlowAnimation* ed::FlowAnimationController::GetOrCreate(Link* link) { + // Return live animation which match target link + { + auto animationIt = std::find_if( + m_Animations.begin(), m_Animations.end(), + [link](FlowAnimation* animation) { return animation->m_Link == link; }); + if (animationIt != m_Animations.end()) return *animationIt; + } + + // There are no live animations for target link, try to reuse inactive old one + if (!m_FreePool.empty()) { + auto animation = m_FreePool.back(); + m_FreePool.pop_back(); + return animation; + } + + // Cache miss, allocate new one + auto animation = new FlowAnimation(this); + m_Animations.push_back(animation); + + return animation; +} + +void ed::FlowAnimationController::Release(FlowAnimation* animation) { + IM_UNUSED(animation); +} + +//------------------------------------------------------------------------------ +// +// Navigate Action +// +//------------------------------------------------------------------------------ +const float ed::NavigateAction::s_DefaultZoomLevels[] = { + 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f, 1.25f, + 1.50f, 2.0f, 2.5f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f}; + +const int ed::NavigateAction::s_DefaultZoomLevelCount = + sizeof(s_DefaultZoomLevels) / sizeof(*s_DefaultZoomLevels); + +ed::NavigateAction::NavigateAction(EditorContext* editor, + ImGuiEx::Canvas& canvas) + : EditorAction(editor), + m_IsActive(false), + m_Zoom(1), + m_VisibleRect(), + m_Scroll(0, 0), + m_ScrollStart(0, 0), + m_ScrollDelta(0, 0), + m_Canvas(canvas), + m_WindowScreenPos(0, 0), + m_WindowScreenSize(0, 0), + m_Animation(editor, *this), + m_Reason(NavigationReason::Unknown), + m_LastSelectionId(0), + m_LastObject(nullptr), + m_MovingOverEdge(false), + m_MoveScreenOffset(0, 0), + m_ZoomLevels(editor->GetConfig().CustomZoomLevels.Size > 0 + ? editor->GetConfig().CustomZoomLevels.Data + : s_DefaultZoomLevels), + m_ZoomLevelCount(editor->GetConfig().CustomZoomLevels.Size > 0 + ? editor->GetConfig().CustomZoomLevels.Size + : s_DefaultZoomLevelCount) {} + +ed::EditorAction::AcceptResult ed::NavigateAction::Accept( + const Control& control) { + IM_ASSERT(!m_IsActive); + + if (m_IsActive) return False; + + if (Editor->CanAcceptUserInput() /*&& !ImGui::IsAnyItemActive()*/ && + ImGui::IsMouseDragging(Editor->GetConfig().NavigateButtonIndex, 0.0f)) { + m_IsActive = true; + m_ScrollStart = m_Scroll; + m_ScrollDelta = + ImGui::GetMouseDragDelta(Editor->GetConfig().NavigateButtonIndex); + m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom; + } + + auto& io = ImGui::GetIO(); + + if (Editor->CanAcceptUserInput() && ImGui::IsKeyPressed(GetKeyIndexForF()) && + Editor->AreShortcutsEnabled()) { + const auto zoomMode = io.KeyShift ? NavigateAction::ZoomMode::WithMargin + : NavigateAction::ZoomMode::None; + + auto findHotObjectToZoom = [this, &control, &io]() -> Object* { + if (control.HotObject) { + if (auto pin = control.HotObject->AsPin()) + return pin->m_Node; + else + return control.HotObject; + } else if (control.BackgroundHot) { + auto node = Editor->FindNodeAt(io.MousePos); + if (IsGroup(node)) return node; + } + + return nullptr; + }; + + bool navigateToContent = false; + if (!Editor->GetSelectedObjects().empty()) { + if (m_Reason != NavigationReason::Selection || + m_LastSelectionId != Editor->GetSelectionId() || + (zoomMode != NavigateAction::ZoomMode::None)) { + m_LastSelectionId = Editor->GetSelectionId(); + NavigateTo(Editor->GetSelectionBounds(), zoomMode, -1.0f, + NavigationReason::Selection); + } else + navigateToContent = true; + } else if (auto hotObject = findHotObjectToZoom()) { + if (m_Reason != NavigationReason::Object || m_LastObject != hotObject || + (zoomMode != NavigateAction::ZoomMode::None)) { + m_LastObject = hotObject; + auto bounds = hotObject->GetBounds(); + NavigateTo(bounds, zoomMode, -1.0f, NavigationReason::Object); + } else + navigateToContent = true; + } else + navigateToContent = true; + + if (navigateToContent) + NavigateTo(Editor->GetContentBounds(), + NavigateAction::ZoomMode::WithMargin, -1.0f, + NavigationReason::Content); + } + + auto visibleRect = GetViewRect(); + if (m_VisibleRect.Min != visibleRect.Min || + m_VisibleRect.Max != visibleRect.Max) { + m_VisibleRect = visibleRect; + Editor->MakeDirty(SaveReasonFlags::Navigation); + } + + // // #debug + // if (m_DrawList) + // m_DrawList->AddCircleFilled(io.MousePos, 4.0f, IM_COL32(255, 0, 255, + // 255)); + + if (HandleZoom(control)) return True; + + return m_IsActive ? True : False; +} + +bool ed::NavigateAction::Process(const Control& control) { + IM_UNUSED(control); + + if (!m_IsActive) return false; + + if (ImGui::IsMouseDragging(Editor->GetConfig().NavigateButtonIndex, 0.0f)) { + m_ScrollDelta = + ImGui::GetMouseDragDelta(Editor->GetConfig().NavigateButtonIndex); + m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom; + m_VisibleRect = GetViewRect(); + // if (IsActive && Animation.IsPlaying()) + // Animation.Target = Animation.Target - ScrollDelta * + // Animation.TargetZoom; + } else { + if (m_Scroll != m_ScrollStart) + Editor->MakeDirty(SaveReasonFlags::Navigation); + + m_IsActive = false; + } + + // #TODO: Handle zoom while scrolling + // HandleZoom(control); + + return m_IsActive; +} + +bool ed::NavigateAction::HandleZoom(const Control& control) { + IM_UNUSED(control); + + const auto currentAction = Editor->GetCurrentAction(); + const auto allowOffscreen = currentAction && currentAction->IsDragging(); + + auto& io = ImGui::GetIO(); + + if (!io.MouseWheel || + (!allowOffscreen && + !Editor->IsHoveredWithoutOverlapp())) // && !ImGui::IsAnyItemActive()) + return false; + + auto savedScroll = m_Scroll; + auto savedZoom = m_Zoom; + + m_Animation.Finish(); + + auto mousePos = io.MousePos; + auto steps = (int)io.MouseWheel; + auto newZoom = + MatchZoom(steps, m_ZoomLevels[steps < 0 ? 0 : m_ZoomLevelCount - 1]); + + auto oldView = GetView(); + m_Zoom = newZoom; + auto newView = GetView(); + + auto screenPos = m_Canvas.FromLocal(mousePos, oldView); + auto canvasPos = m_Canvas.ToLocal(screenPos, newView); + + auto offset = (canvasPos - mousePos) * m_Zoom; + auto targetScroll = m_Scroll - offset; + + auto visibleRect = GetViewRect(); + + if (m_Scroll != savedScroll || m_Zoom != savedZoom || + m_VisibleRect.Min != visibleRect.Min || + m_VisibleRect.Max != visibleRect.Max) { + m_Scroll = savedScroll; + m_Zoom = savedZoom; + m_VisibleRect = visibleRect; + + Editor->MakeDirty(SaveReasonFlags::Navigation); + } + + auto targetRect = + m_Canvas.CalcViewRect(ImGuiEx::CanvasView(-targetScroll, newZoom)); + + NavigateTo(targetRect, c_MouseZoomDuration, NavigationReason::MouseZoom); + + return true; +} + +void ed::NavigateAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + ImGui::Text(" Scroll: { x=%g y=%g }", m_Scroll.x, m_Scroll.y); + ImGui::Text(" Zoom: %g", m_Zoom); + ImGui::Text(" Visible Rect: { l=%g t=%g, r=%g b=%g w=%g h=%g }", + m_VisibleRect.Min.x, m_VisibleRect.Min.y, m_VisibleRect.Max.x, + m_VisibleRect.Max.y, m_VisibleRect.Max.x - m_VisibleRect.Min.x, + m_VisibleRect.Max.y - m_VisibleRect.Min.y); +} + +void ed::NavigateAction::NavigateTo(const ImRect& bounds, ZoomMode zoomMode, + float duration, NavigationReason reason) { + if (ImRect_IsEmpty(bounds)) return; + + if (duration < 0.0f) duration = GetStyle().ScrollDuration; + + if (zoomMode == ZoomMode::None) { + auto viewRect = m_Canvas.ViewRect(); + auto viewRectCenter = viewRect.GetCenter(); + auto targetCenter = bounds.GetCenter(); + + viewRect.Translate(targetCenter - viewRectCenter); + + NavigateTo(viewRect, duration, reason); + } else { + // Grow rect by 5% to leave some reasonable margin + // from the edges of the canvas. + auto rect = bounds; + + if (zoomMode == ZoomMode::WithMargin) { + auto extend = ImMax(rect.GetWidth(), rect.GetHeight()); + rect.Expand(extend * c_NavigationZoomMargin * 0.5f); + } + + NavigateTo(rect, duration, reason); + } +} + +void ed::NavigateAction::NavigateTo(const ImRect& target, float duration, + NavigationReason reason) { + m_Reason = reason; + + m_Animation.NavigateTo(target, duration); +} + +void ed::NavigateAction::StopNavigation() { m_Animation.Stop(); } + +void ed::NavigateAction::FinishNavigation() { m_Animation.Finish(); } + +bool ed::NavigateAction::MoveOverEdge(const ImVec2& canvasSize) { + // Don't interrupt non-edge animations + if (m_Animation.IsPlaying()) return false; + + auto& io = ImGui::GetIO(); + + const auto screenMousePos = io.MousePos; + const auto screenRect = ImRect(ImGui::GetCursorScreenPos(), + ImGui::GetCursorScreenPos() + canvasSize); + + // Mouse is over screen, do nothing + if (screenRect.Contains(screenMousePos)) return false; + + // Several backend move mouse position to -FLT_MAX to indicate + // uninitialized/unknown state. To prevent all sorts + // of math problems, we just ignore such state. + if (screenMousePos.x <= -FLT_MAX || screenMousePos.y <= -FLT_MAX) + return false; + + const auto minDistance = + ImVec2(-c_MaxMoveOverEdgeDistance, -c_MaxMoveOverEdgeDistance); + const auto maxDistance = + ImVec2(c_MaxMoveOverEdgeDistance, c_MaxMoveOverEdgeDistance); + + const auto screenPointOnEdge = + ImRect_ClosestPoint(screenRect, screenMousePos, true); + const auto offset = ImMin( + ImMax(screenPointOnEdge - screenMousePos, minDistance), maxDistance); + const auto relativeOffset = -offset * io.DeltaTime * c_MaxMoveOverEdgeSpeed; + + m_Scroll = m_Scroll + relativeOffset; + + m_MoveScreenOffset = relativeOffset; + m_MovingOverEdge = true; + + return true; +} + +void ed::NavigateAction::StopMoveOverEdge() { + if (m_MovingOverEdge) { + Editor->MakeDirty(SaveReasonFlags::Navigation); + + m_MoveScreenOffset = ImVec2(0, 0); + m_MovingOverEdge = false; + } +} + +void ed::NavigateAction::SetWindow(ImVec2 position, ImVec2 size) { + m_WindowScreenPos = position; + m_WindowScreenSize = size; +} + +ImGuiEx::CanvasView ed::NavigateAction::GetView() const { + return ImGuiEx::CanvasView(-m_Scroll, m_Zoom); +} + +ImVec2 ed::NavigateAction::GetViewOrigin() const { return -m_Scroll; } + +float ed::NavigateAction::GetViewScale() const { return m_Zoom; } + +void ed::NavigateAction::SetViewRect(const ImRect& rect) { + auto view = m_Canvas.CalcCenterView(rect); + m_Scroll = -view.Origin; + m_Zoom = view.Scale; +} + +ImRect ed::NavigateAction::GetViewRect() const { + return m_Canvas.CalcViewRect(GetView()); +} + +float ed::NavigateAction::MatchZoom(int steps, float fallbackZoom) { + auto currentZoomIndex = MatchZoomIndex(steps); + if (currentZoomIndex < 0) return fallbackZoom; + + auto currentZoom = m_ZoomLevels[currentZoomIndex]; + if (fabsf(currentZoom - m_Zoom) > 0.001f) return currentZoom; + + auto newIndex = currentZoomIndex + steps; + if (newIndex >= 0 && newIndex < m_ZoomLevelCount) + return m_ZoomLevels[newIndex]; + else + return fallbackZoom; +} + +int ed::NavigateAction::MatchZoomIndex(int direction) { + int bestIndex = -1; + float bestDistance = 0.0f; + + for (int i = 0; i < m_ZoomLevelCount; ++i) { + auto distance = fabsf(m_ZoomLevels[i] - m_Zoom); + if (distance < bestDistance || bestIndex < 0) { + bestDistance = distance; + bestIndex = i; + } + } + + if (bestDistance > 0.001f) { + if (direction > 0) { + ++bestIndex; + + if (bestIndex >= m_ZoomLevelCount) bestIndex = m_ZoomLevelCount - 1; + } else if (direction < 0) { + --bestIndex; + + if (bestIndex < 0) bestIndex = 0; + } + } + + return bestIndex; +} + +//------------------------------------------------------------------------------ +// +// Size Action +// +//------------------------------------------------------------------------------ +ed::SizeAction::SizeAction(EditorContext* editor) + : EditorAction(editor), + m_IsActive(false), + m_Clean(false), + m_SizedNode(nullptr), + m_Pivot(NodeRegion::None), + m_Cursor(ImGuiMouseCursor_Arrow) {} + +ed::EditorAction::AcceptResult ed::SizeAction::Accept(const Control& control) { + IM_ASSERT(!m_IsActive); + + if (m_IsActive) return False; + + if (control.ActiveNode && IsGroup(control.ActiveNode) && + ImGui::IsMouseDragging(Editor->GetConfig().DragButtonIndex, 1)) { + // const auto mousePos = to_point(ImGui::GetMousePos()); + // const auto closestPoint = + // control.ActiveNode->Bounds.get_closest_point_hollow(mousePos, + // static_cast(control.ActiveNode->Rounding)); + + auto pivot = GetRegion(control.ActiveNode); + if (pivot != NodeRegion::Header && pivot != NodeRegion::Center) { + m_StartBounds = control.ActiveNode->m_Bounds; + m_StartGroupBounds = control.ActiveNode->m_GroupBounds; + m_LastSize = control.ActiveNode->m_Bounds.GetSize(); + m_MinimumSize = ImVec2(0, 0); + m_LastDragOffset = ImVec2(0, 0); + m_Pivot = pivot; + m_Cursor = ChooseCursor(m_Pivot); + m_SizedNode = control.ActiveNode; + m_IsActive = true; + } + } else if (control.HotNode && IsGroup(control.HotNode)) { + m_Cursor = ChooseCursor(GetRegion(control.HotNode)); + return Possible; + } + + return m_IsActive ? True : False; +} + +bool ed::SizeAction::Process(const Control& control) { + if (m_Clean) { + m_Clean = false; + + if (m_SizedNode->m_Bounds.Min != m_StartBounds.Min || + m_SizedNode->m_GroupBounds.Min != m_StartGroupBounds.Min) + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, + m_SizedNode); + + if (m_SizedNode->m_Bounds.GetSize() != m_StartBounds.GetSize() || + m_SizedNode->m_GroupBounds.GetSize() != m_StartGroupBounds.GetSize()) + Editor->MakeDirty(SaveReasonFlags::Size | SaveReasonFlags::User, + m_SizedNode); + + m_SizedNode = nullptr; + } + + if (!m_IsActive) return false; + + if (control.ActiveNode == m_SizedNode) { + const auto dragOffset = (control.ActiveNode == m_SizedNode) + ? ImGui::GetMouseDragDelta(0, 0.0f) + : m_LastDragOffset; + m_LastDragOffset = dragOffset; + + if (m_MinimumSize.x == 0.0f && + m_LastSize.x != m_SizedNode->m_Bounds.GetWidth()) + m_MinimumSize.x = m_SizedNode->m_Bounds.GetWidth(); + if (m_MinimumSize.y == 0.0f && + m_LastSize.y != m_SizedNode->m_Bounds.GetHeight()) + m_MinimumSize.y = m_SizedNode->m_Bounds.GetHeight(); + + auto minimumSize = ImMax( + m_MinimumSize, m_StartBounds.GetSize() - m_StartGroupBounds.GetSize()); + + auto newBounds = m_StartBounds; + + if ((m_Pivot & NodeRegion::Top) == NodeRegion::Top) + newBounds.Min.y = + ImMin(newBounds.Max.y - minimumSize.y, + Editor->AlignPointToGrid(newBounds.Min.y + dragOffset.y)); + if ((m_Pivot & NodeRegion::Bottom) == NodeRegion::Bottom) + newBounds.Max.y = + ImMax(newBounds.Min.y + minimumSize.y, + Editor->AlignPointToGrid(newBounds.Max.y + dragOffset.y)); + if ((m_Pivot & NodeRegion::Left) == NodeRegion::Left) + newBounds.Min.x = + ImMin(newBounds.Max.x - minimumSize.x, + Editor->AlignPointToGrid(newBounds.Min.x + dragOffset.x)); + if ((m_Pivot & NodeRegion::Right) == NodeRegion::Right) + newBounds.Max.x = + ImMax(newBounds.Min.x + minimumSize.x, + Editor->AlignPointToGrid(newBounds.Max.x + dragOffset.x)); + + newBounds.Floor(); + + m_LastSize = newBounds.GetSize(); + + m_SizedNode->m_Bounds = newBounds; + m_SizedNode->m_GroupBounds = newBounds; + m_SizedNode->m_GroupBounds.Min.x -= + m_StartBounds.Min.x - m_StartGroupBounds.Min.x; + m_SizedNode->m_GroupBounds.Min.y -= + m_StartBounds.Min.y - m_StartGroupBounds.Min.y; + m_SizedNode->m_GroupBounds.Max.x -= + m_StartBounds.Max.x - m_StartGroupBounds.Max.x; + m_SizedNode->m_GroupBounds.Max.y -= + m_StartBounds.Max.y - m_StartGroupBounds.Max.y; + } else if (!control.ActiveNode) { + m_Clean = true; + m_IsActive = false; + return true; + } + + return m_IsActive; +} + +void ed::SizeAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + auto getObjectName = [](Object* object) { + if (!object) + return ""; + else if (object->AsNode()) + return "Node"; + else if (object->AsPin()) + return "Pin"; + else if (object->AsLink()) + return "Link"; + else + return ""; + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + ImGui::Text(" Node: %s (%p)", getObjectName(m_SizedNode), + m_SizedNode ? m_SizedNode->m_ID.AsPointer() : nullptr); + if (m_SizedNode && m_IsActive) { + ImGui::Text(" Bounds: { x=%g y=%g w=%g h=%g }", + m_SizedNode->m_Bounds.Min.x, m_SizedNode->m_Bounds.Min.y, + m_SizedNode->m_Bounds.GetWidth(), + m_SizedNode->m_Bounds.GetHeight()); + ImGui::Text(" Group Bounds: { x=%g y=%g w=%g h=%g }", + m_SizedNode->m_GroupBounds.Min.x, + m_SizedNode->m_GroupBounds.Min.y, + m_SizedNode->m_GroupBounds.GetWidth(), + m_SizedNode->m_GroupBounds.GetHeight()); + ImGui::Text(" Start Bounds: { x=%g y=%g w=%g h=%g }", + m_StartBounds.Min.x, m_StartBounds.Min.y, + m_StartBounds.GetWidth(), m_StartBounds.GetHeight()); + ImGui::Text(" Start Group Bounds: { x=%g y=%g w=%g h=%g }", + m_StartGroupBounds.Min.x, m_StartGroupBounds.Min.y, + m_StartGroupBounds.GetWidth(), m_StartGroupBounds.GetHeight()); + ImGui::Text(" Minimum Size: { w=%g h=%g }", m_MinimumSize.x, + m_MinimumSize.y); + ImGui::Text(" Last Size: { w=%g h=%g }", m_LastSize.x, m_LastSize.y); + } +} + +ed::NodeRegion ed::SizeAction::GetRegion(Node* node) { + return node->GetRegion(ImGui::GetMousePos()); +} + +ImGuiMouseCursor ed::SizeAction::ChooseCursor(NodeRegion region) { + switch (region) { + default: + case NodeRegion::Center: + return ImGuiMouseCursor_Arrow; + + case NodeRegion::Top: + case NodeRegion::Bottom: + return ImGuiMouseCursor_ResizeNS; + + case NodeRegion::Left: + case NodeRegion::Right: + return ImGuiMouseCursor_ResizeEW; + + case NodeRegion::TopLeft: + case NodeRegion::BottomRight: + return ImGuiMouseCursor_ResizeNWSE; + + case NodeRegion::TopRight: + case NodeRegion::BottomLeft: + return ImGuiMouseCursor_ResizeNESW; + } +} + +//------------------------------------------------------------------------------ +// +// Drag Action +// +//------------------------------------------------------------------------------ +ed::DragAction::DragAction(EditorContext* editor) + : EditorAction(editor), + m_IsActive(false), + m_Clear(false), + m_DraggedObject(nullptr) {} + +ed::EditorAction::AcceptResult ed::DragAction::Accept(const Control& control) { + IM_ASSERT(!m_IsActive); + + if (m_IsActive) return False; + + if (Editor->CanAcceptUserInput() && control.ActiveObject && + ImGui::IsMouseDragging(Editor->GetConfig().DragButtonIndex, 1)) { + if (!control.ActiveObject->AcceptDrag()) return False; + + m_DraggedObject = control.ActiveObject; + + m_Objects.resize(0); + m_Objects.push_back(m_DraggedObject); + + if (Editor->IsSelected(m_DraggedObject)) { + for (auto selectedObject : Editor->GetSelectedObjects()) + if (auto selectedNode = selectedObject->AsNode()) + if (selectedNode != m_DraggedObject && selectedNode->AcceptDrag()) + m_Objects.push_back(selectedNode); + } + + auto& io = ImGui::GetIO(); + if (!io.KeyShift) { + std::vector groupedNodes; + for (auto object : m_Objects) + if (auto node = object->AsNode()) + node->GetGroupedNodes(groupedNodes, true); + + auto isAlreadyPicked = [this](Node* node) { + return std::find(m_Objects.begin(), m_Objects.end(), node) != + m_Objects.end(); + }; + + for (auto candidate : groupedNodes) + if (!isAlreadyPicked(candidate) && candidate->AcceptDrag()) + m_Objects.push_back(candidate); + } + + m_IsActive = true; + } else if (control.HotNode && IsGroup(control.HotNode) && + control.HotNode->GetRegion(ImGui::GetMousePos()) == + NodeRegion::Header) { + return Possible; + } + + return m_IsActive ? True : False; +} + +bool ed::DragAction::Process(const Control& control) { + if (m_Clear) { + m_Clear = false; + + for (auto object : m_Objects) { + if (object->EndDrag()) + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, + object->AsNode()); + } + + m_Objects.resize(0); + + m_DraggedObject = nullptr; + } + + if (!m_IsActive) return false; + + if (control.ActiveObject == m_DraggedObject) { + auto dragOffset = + ImGui::GetMouseDragDelta(Editor->GetConfig().DragButtonIndex, 0.0f); + + auto draggedOrigin = m_DraggedObject->DragStartLocation(); + auto alignPivot = ImVec2(0, 0); + + // TODO: Move this experimental alignment to closes pivot out of internals + // to node API + if (auto draggedNode = m_DraggedObject->AsNode()) { + float x = FLT_MAX; + float y = FLT_MAX; + + auto testPivot = [this, &x, &y, &draggedOrigin, &dragOffset, + &alignPivot](const ImVec2& pivot) { + auto initial = draggedOrigin + dragOffset + pivot; + auto candidate = + Editor->AlignPointToGrid(initial) - draggedOrigin - pivot; + + if (ImFabs(candidate.x) < ImFabs(ImMin(x, FLT_MAX))) { + x = candidate.x; + alignPivot.x = pivot.x; + } + + if (ImFabs(candidate.y) < ImFabs(ImMin(y, FLT_MAX))) { + y = candidate.y; + alignPivot.y = pivot.y; + } + }; + + for (auto pin = draggedNode->m_LastPin; pin; pin = pin->m_PreviousPin) { + auto pivot = pin->m_Pivot.GetCenter() - draggedNode->m_Bounds.Min; + testPivot(pivot); + } + + // testPivot(point(0, 0)); + } + + auto alignedOffset = + Editor->AlignPointToGrid(draggedOrigin + dragOffset + alignPivot) - + draggedOrigin - alignPivot; + + if (!ImGui::GetIO().KeyAlt) dragOffset = alignedOffset; + + for (auto object : m_Objects) object->UpdateDrag(dragOffset); + } else if (!control.ActiveObject) { + m_Clear = true; + + m_IsActive = false; + return true; + } + + return m_IsActive; +} + +void ed::DragAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + auto getObjectName = [](Object* object) { + if (!object) + return ""; + else if (object->AsNode()) + return "Node"; + else if (object->AsPin()) + return "Pin"; + else if (object->AsLink()) + return "Link"; + else + return ""; + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + ImGui::Text(" Node: %s (%p)", getObjectName(m_DraggedObject), + m_DraggedObject ? m_DraggedObject->ID().AsPointer() : nullptr); +} + +//------------------------------------------------------------------------------ +// +// Select Action +// +//------------------------------------------------------------------------------ +ed::SelectAction::SelectAction(EditorContext* editor) + : EditorAction(editor), + m_IsActive(false), + m_SelectGroups(false), + m_SelectLinkMode(false), + m_CommitSelection(false), + m_StartPoint(), + m_Animation(editor) {} + +ed::EditorAction::AcceptResult ed::SelectAction::Accept( + const Control& control) { + IM_ASSERT(!m_IsActive); + + if (m_IsActive) return False; + + auto& io = ImGui::GetIO(); + m_SelectGroups = io.KeyShift; + m_SelectLinkMode = io.KeyAlt; + + m_SelectedObjectsAtStart.clear(); + + if (Editor->CanAcceptUserInput() && control.BackgroundHot && + ImGui::IsMouseDragging(Editor->GetConfig().SelectButtonIndex, 1)) { + m_IsActive = true; + m_StartPoint = + ImGui_GetMouseClickPos(Editor->GetConfig().SelectButtonIndex); + m_EndPoint = m_StartPoint; + + // Links and nodes cannot be selected together + if ((m_SelectLinkMode && Editor->IsAnyNodeSelected()) || + (!m_SelectLinkMode && Editor->IsAnyLinkSelected())) { + Editor->ClearSelection(); + } + + if (io.KeyCtrl) m_SelectedObjectsAtStart = Editor->GetSelectedObjects(); + } else if (control.BackgroundClickButtonIndex == + Editor->GetConfig().SelectButtonIndex) { + Editor->ClearSelection(); + } else { + Object* clickedObject = control.ClickedNode + ? static_cast(control.ClickedNode) + : static_cast(control.ClickedLink); + + if (clickedObject) { + // Links and nodes cannot be selected together + if ((clickedObject->AsLink() && Editor->IsAnyNodeSelected()) || + (clickedObject->AsNode() && Editor->IsAnyLinkSelected())) { + Editor->ClearSelection(); + } + + if (io.KeyCtrl) + Editor->ToggleObjectSelection(clickedObject); + else + Editor->SetSelectedObject(clickedObject); + } + } + + if (m_IsActive) m_Animation.Stop(); + + return m_IsActive ? True : False; +} + +bool ed::SelectAction::Process(const Control& control) { + IM_UNUSED(control); + + if (m_CommitSelection) { + Editor->ClearSelection(); + for (auto object : m_CandidateObjects) Editor->SelectObject(object); + + m_CandidateObjects.clear(); + + m_CommitSelection = false; + } + + if (!m_IsActive) return false; + + if (ImGui::IsMouseDragging(Editor->GetConfig().SelectButtonIndex, 0)) { + m_EndPoint = ImGui::GetMousePos(); + + auto topLeft = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), + std::min(m_StartPoint.y, m_EndPoint.y)); + auto bottomRight = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), + ImMax(m_StartPoint.y, m_EndPoint.y)); + auto rect = ImRect(topLeft, bottomRight); + if (rect.GetWidth() <= 0) rect.Max.x = rect.Min.x + 1; + if (rect.GetHeight() <= 0) rect.Max.y = rect.Min.y + 1; + + vector nodes; + vector links; + + if (m_SelectLinkMode) { + Editor->FindLinksInRect(rect, links); + m_CandidateObjects.assign(links.begin(), links.end()); + } else { + Editor->FindNodesInRect(rect, nodes); + m_CandidateObjects.assign(nodes.begin(), nodes.end()); + + if (m_SelectGroups) { + auto endIt = std::remove_if( + m_CandidateObjects.begin(), m_CandidateObjects.end(), + [](Object* object) { return !IsGroup(object->AsNode()); }); + m_CandidateObjects.erase(endIt, m_CandidateObjects.end()); + } else { + auto endIt = std::remove_if( + m_CandidateObjects.begin(), m_CandidateObjects.end(), + [](Object* object) { return IsGroup(object->AsNode()); }); + m_CandidateObjects.erase(endIt, m_CandidateObjects.end()); + } + } + + m_CandidateObjects.insert(m_CandidateObjects.end(), + m_SelectedObjectsAtStart.begin(), + m_SelectedObjectsAtStart.end()); + std::sort(m_CandidateObjects.begin(), m_CandidateObjects.end()); + m_CandidateObjects.erase( + std::unique(m_CandidateObjects.begin(), m_CandidateObjects.end()), + m_CandidateObjects.end()); + } else { + m_IsActive = false; + + m_Animation.Play(c_SelectionFadeOutDuration); + + m_CommitSelection = true; + + return true; + } + + return m_IsActive; +} + +void ed::SelectAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); +} + +void ed::SelectAction::Draw(ImDrawList* drawList) { + if (!m_IsActive && !m_Animation.IsPlaying()) return; + + const auto alpha = + m_Animation.IsPlaying() + ? ImEasing::EaseOutQuad(1.0f, -1.0f, m_Animation.GetProgress()) + : 1.0f; + + const auto fillColor = Editor->GetColor( + m_SelectLinkMode ? StyleColor_LinkSelRect : StyleColor_NodeSelRect, + alpha); + const auto outlineColor = + Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRectBorder + : StyleColor_NodeSelRectBorder, + alpha); + + drawList->ChannelsSetCurrent(c_BackgroundChannel_SelectionRect); + + auto min = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), + std::min(m_StartPoint.y, m_EndPoint.y)); + auto max = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), + ImMax(m_StartPoint.y, m_EndPoint.y)); + + drawList->AddRectFilled(min, max, fillColor); + drawList->AddRect(min, max, outlineColor); +} + +//------------------------------------------------------------------------------ +// +// Context Menu Action +// +//------------------------------------------------------------------------------ +ed::ContextMenuAction::ContextMenuAction(EditorContext* editor) + : EditorAction(editor), + m_CandidateMenu(Menu::None), + m_CurrentMenu(Menu::None), + m_ContextId() {} + +ed::EditorAction::AcceptResult ed::ContextMenuAction::Accept( + const Control& control) { + const auto isPressed = + ImGui::IsMouseClicked(Editor->GetConfig().ContextMenuButtonIndex); + const auto isReleased = + ImGui::IsMouseReleased(Editor->GetConfig().ContextMenuButtonIndex); + const auto isDragging = + ImGui::IsMouseDragging(Editor->GetConfig().ContextMenuButtonIndex, 1); + + if (isPressed || isReleased || isDragging) { + Menu candidateMenu = ContextMenuAction::None; + ObjectId contextId; + + if (auto hotObejct = control.HotObject) { + if (hotObejct->AsNode()) + candidateMenu = Node; + else if (hotObejct->AsPin()) + candidateMenu = Pin; + else if (hotObejct->AsLink()) + candidateMenu = Link; + + if (candidateMenu != None) contextId = hotObejct->ID(); + } else if (control.BackgroundHot) + candidateMenu = Background; + + if (isPressed) { + m_CandidateMenu = candidateMenu; + m_ContextId = contextId; + return Possible; + } else if (isReleased && m_CandidateMenu == candidateMenu && + m_ContextId == contextId) { + m_CurrentMenu = m_CandidateMenu; + m_CandidateMenu = ContextMenuAction::None; + return True; + } else { + m_CandidateMenu = None; + m_CurrentMenu = None; + m_ContextId = ObjectId(); + return False; + } + } + + return False; +} + +bool ed::ContextMenuAction::Process(const Control& control) { + IM_UNUSED(control); + + m_CandidateMenu = None; + m_CurrentMenu = None; + m_ContextId = ObjectId(); + return false; +} + +void ed::ContextMenuAction::Reject() { + m_CandidateMenu = None; + m_CurrentMenu = None; + m_ContextId = ObjectId(); +} + +void ed::ContextMenuAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + auto getMenuName = [](Menu menu) { + switch (menu) { + default: + case None: + return "None"; + case Node: + return "Node"; + case Pin: + return "Pin"; + case Link: + return "Link"; + case Background: + return "Background"; + } + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Menu: %s", getMenuName(m_CurrentMenu)); +} + +bool ed::ContextMenuAction::ShowNodeContextMenu(NodeId* nodeId) { + if (m_CurrentMenu != Node) return false; + + *nodeId = m_ContextId.AsNodeId(); + Editor->SetUserContext(); + return true; +} + +bool ed::ContextMenuAction::ShowPinContextMenu(PinId* pinId) { + if (m_CurrentMenu != Pin) return false; + + *pinId = m_ContextId.AsPinId(); + Editor->SetUserContext(); + return true; +} + +bool ed::ContextMenuAction::ShowLinkContextMenu(LinkId* linkId) { + if (m_CurrentMenu != Link) return false; + + *linkId = m_ContextId.AsLinkId(); + Editor->SetUserContext(); + return true; +} + +bool ed::ContextMenuAction::ShowBackgroundContextMenu() { + if (m_CurrentMenu != Background) return false; + + Editor->SetUserContext(); + return true; +} + +//------------------------------------------------------------------------------ +// +// Cut/Copy/Paste Action +// +//------------------------------------------------------------------------------ +ed::ShortcutAction::ShortcutAction(EditorContext* editor) + : EditorAction(editor), + m_IsActive(false), + m_InAction(false), + m_CurrentAction(Action::None), + m_Context() {} + +ed::EditorAction::AcceptResult ed::ShortcutAction::Accept( + const Control& control) { + if (!Editor->IsFocused() || !Editor->AreShortcutsEnabled()) return False; + + Action candidateAction = None; + + auto& io = ImGui::GetIO(); + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X))) + candidateAction = Cut; + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C))) + candidateAction = Copy; + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V))) + candidateAction = Paste; + if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && + ImGui::IsKeyPressed(GetKeyIndexForD())) + candidateAction = Duplicate; + if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Space))) + candidateAction = CreateNode; + + if (candidateAction != None) { + if (candidateAction != Paste && candidateAction != CreateNode) { + auto& selection = Editor->GetSelectedObjects(); + if (!selection.empty()) { + // #TODO: Find a way to simplify logic. + + m_Context.assign(selection.begin(), selection.end()); + + // Expand groups + vector extra; + for (auto object : m_Context) { + auto node = object->AsNode(); + if (IsGroup(node)) node->GetGroupedNodes(extra, true); + } + + // Apply groups and remove duplicates + if (!extra.empty()) { + m_Context.insert(m_Context.end(), extra.begin(), extra.end()); + std::sort(m_Context.begin(), m_Context.end()); + m_Context.erase(std::unique(m_Context.begin(), m_Context.end()), + m_Context.end()); + } + } else if (control.HotObject && control.HotObject->IsSelectable() && + !IsGroup(control.HotObject->AsNode())) { + m_Context.push_back(control.HotObject); + } + + if (m_Context.empty()) return False; + + // Does copying only links make sense? + // const auto hasOnlyLinks = std::all_of(Context.begin(), Context.end(), + // [](Object* object) { return object->AsLink() != nullptr; }); if + // (hasOnlyLinks) + // return False; + + // If no links are selected, pick all links between nodes within context + const auto hasAnyLinks = std::any_of( + m_Context.begin(), m_Context.end(), + [](Object* object) { return object->AsLink() != nullptr; }); + if (!hasAnyLinks && + m_Context.size() > 1) // one node cannot make connection to anything + { + // Collect nodes in sorted vector viable for binary search + std::vector> nodes; + + nodes.reserve(m_Context.size()); + std::for_each(m_Context.begin(), m_Context.end(), + [&nodes](Object* object) { + if (auto node = object->AsNode()) + nodes.push_back({node->m_ID, node}); + }); + + std::sort(nodes.begin(), nodes.end()); + + auto isNodeInContext = [&nodes](NodeId nodeId) { + return std::binary_search(nodes.begin(), nodes.end(), + ObjectWrapper{nodeId, nullptr}); + }; + + // Collect links connected to nodes and drop those reaching out of + // context + std::vector links; + + for (auto node : nodes) + Editor->FindLinksForNode(node.m_ID, links, true); + + // Remove duplicates + std::sort(links.begin(), links.end()); + links.erase(std::unique(links.begin(), links.end()), links.end()); + + // Drop out of context links + links.erase( + std::remove_if( + links.begin(), links.end(), + [&isNodeInContext](Link* link) { + return !isNodeInContext(link->m_StartPin->m_Node->m_ID) || + !isNodeInContext(link->m_EndPin->m_Node->m_ID); + }), + links.end()); + + // Append links and remove duplicates + m_Context.insert(m_Context.end(), links.begin(), links.end()); + } + } else + m_Context.resize(0); + + m_IsActive = true; + m_CurrentAction = candidateAction; + + return True; + } + + return False; +} + +bool ed::ShortcutAction::Process(const Control& control) { + IM_UNUSED(control); + + m_IsActive = false; + m_CurrentAction = None; + m_Context.resize(0); + return false; +} + +void ed::ShortcutAction::Reject() { + m_IsActive = false; + m_CurrentAction = None; + m_Context.resize(0); +} + +void ed::ShortcutAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + auto getActionName = [](Action action) { + switch (action) { + default: + case None: + return "None"; + case Cut: + return "Cut"; + case Copy: + return "Copy"; + case Paste: + return "Paste"; + case Duplicate: + return "Duplicate"; + case CreateNode: + return "CreateNode"; + } + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Action: %s", getActionName(m_CurrentAction)); +} + +bool ed::ShortcutAction::Begin() { + if (m_IsActive) m_InAction = true; + return m_IsActive; +} + +void ed::ShortcutAction::End() { + if (m_IsActive) m_InAction = false; +} + +bool ed::ShortcutAction::AcceptCut() { + IM_ASSERT(m_InAction); + return m_CurrentAction == Cut; +} + +bool ed::ShortcutAction::AcceptCopy() { + IM_ASSERT(m_InAction); + return m_CurrentAction == Copy; +} + +bool ed::ShortcutAction::AcceptPaste() { + IM_ASSERT(m_InAction); + return m_CurrentAction == Paste; +} + +bool ed::ShortcutAction::AcceptDuplicate() { + IM_ASSERT(m_InAction); + return m_CurrentAction == Duplicate; +} + +bool ed::ShortcutAction::AcceptCreateNode() { + IM_ASSERT(m_InAction); + return m_CurrentAction == CreateNode; +} + +//------------------------------------------------------------------------------ +// +// Create Item Action +// +//------------------------------------------------------------------------------ +ed::CreateItemAction::CreateItemAction(EditorContext* editor) + : EditorAction(editor), + m_InActive(false), + m_NextStage(None), + m_CurrentStage(None), + m_ItemType(NoItem), + m_UserAction(Unknown), + m_LinkColor(IM_COL32_WHITE), + m_LinkThickness(1.0f), + m_LinkStart(nullptr), + m_LinkEnd(nullptr), + + m_IsActive(false), + m_DraggedPin(nullptr), + + m_IsInGlobalSpace(false) {} + +ed::EditorAction::AcceptResult ed::CreateItemAction::Accept( + const Control& control) { + IM_ASSERT(!m_IsActive); + + if (m_IsActive) return EditorAction::False; + + if (control.ActivePin && + ImGui::IsMouseDragging(Editor->GetConfig().DragButtonIndex, 1)) { + m_DraggedPin = control.ActivePin; + DragStart(m_DraggedPin); + + Editor->ClearSelection(); + } else if (control.HotPin) { + return EditorAction::Possible; + } else + return EditorAction::False; + + m_IsActive = true; + + return EditorAction::True; +} + +bool ed::CreateItemAction::Process(const Control& control) { + IM_ASSERT(m_IsActive); + + if (!m_IsActive) return false; + + if (m_DraggedPin && control.ActivePin == m_DraggedPin && + (m_CurrentStage == Possible)) { + const auto draggingFromSource = (m_DraggedPin->m_Kind == PinKind::Output); + + ed::Pin cursorPin(Editor, 0, + draggingFromSource ? PinKind::Input : PinKind::Output); + cursorPin.m_Pivot = ImRect(ImGui::GetMousePos(), ImGui::GetMousePos()); + cursorPin.m_Dir = -m_DraggedPin->m_Dir; + cursorPin.m_Strength = m_DraggedPin->m_Strength; + + ed::Link candidate(Editor, 0); + candidate.m_Color = m_LinkColor; + candidate.m_StartPin = draggingFromSource ? m_DraggedPin : &cursorPin; + candidate.m_EndPin = draggingFromSource ? &cursorPin : m_DraggedPin; + + ed::Pin*& freePin = + draggingFromSource ? candidate.m_EndPin : candidate.m_StartPin; + + if (control.HotPin) { + DropPin(control.HotPin); + + if (m_UserAction == UserAccept) freePin = control.HotPin; + } else if (control.BackgroundHot) + DropNode(); + else + DropNothing(); + + auto drawList = Editor->GetDrawList(); + drawList->ChannelsSetCurrent(c_LinkChannel_NewLink); + + candidate.UpdateEndpoints(); + candidate.Draw(drawList, m_LinkColor, m_LinkThickness); + } else if (m_CurrentStage == Possible || !control.ActivePin) { + if (!Editor->CanAcceptUserInput()) { + m_DraggedPin = nullptr; + DropNothing(); + } + + DragEnd(); + m_IsActive = false; + } + + return m_IsActive; +} + +void ed::CreateItemAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + auto getStageName = [](Stage stage) { + switch (stage) { + case None: + return "None"; + case Possible: + return "Possible"; + case Create: + return "Create"; + default: + return ""; + } + }; + + auto getActionName = [](Action action) { + switch (action) { + default: + case Unknown: + return "Unknown"; + case UserReject: + return "Reject"; + case UserAccept: + return "Accept"; + } + }; + + auto getItemName = [](Type item) { + switch (item) { + default: + case NoItem: + return "None"; + case Node: + return "Node"; + case Link: + return "Link"; + } + }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Stage: %s", getStageName(m_CurrentStage)); + ImGui::Text(" User Action: %s", getActionName(m_UserAction)); + ImGui::Text(" Item Type: %s", getItemName(m_ItemType)); +} + +void ed::CreateItemAction::SetStyle(ImU32 color, float thickness) { + m_LinkColor = color; + m_LinkThickness = thickness; +} + +bool ed::CreateItemAction::Begin() { + IM_ASSERT(false == m_InActive); + + m_InActive = true; + m_CurrentStage = m_NextStage; + m_UserAction = Unknown; + m_LinkColor = IM_COL32_WHITE; + m_LinkThickness = 1.0f; + + if (m_CurrentStage == None) return false; + + m_LastChannel = Editor->GetDrawList()->_Splitter._Current; + + return true; +} + +void ed::CreateItemAction::End() { + IM_ASSERT(m_InActive); + + if (m_IsInGlobalSpace) { + ImGui::PopClipRect(); + Editor->Resume(SuspendFlags::KeepSplitter); + + auto currentChannel = Editor->GetDrawList()->_Splitter._Current; + if (currentChannel != m_LastChannel) + Editor->GetDrawList()->ChannelsSetCurrent(m_LastChannel); + + m_IsInGlobalSpace = false; + } + + m_InActive = false; +} + +void ed::CreateItemAction::DragStart(Pin* startPin) { + IM_ASSERT(!m_InActive); + + m_NextStage = Possible; + m_LinkStart = startPin; + m_LinkEnd = nullptr; +} + +void ed::CreateItemAction::DragEnd() { + IM_ASSERT(!m_InActive); + + if (m_CurrentStage == Possible && m_UserAction == UserAccept) { + m_NextStage = Create; + } else { + m_NextStage = None; + m_ItemType = NoItem; + m_LinkStart = nullptr; + m_LinkEnd = nullptr; + } +} + +void ed::CreateItemAction::DropPin(Pin* endPin) { + IM_ASSERT(!m_InActive); + + m_ItemType = Link; + m_LinkEnd = endPin; +} + +void ed::CreateItemAction::DropNode() { + IM_ASSERT(!m_InActive); + + m_ItemType = Node; + m_LinkEnd = nullptr; +} + +void ed::CreateItemAction::DropNothing() { + IM_ASSERT(!m_InActive); + + m_ItemType = NoItem; + m_LinkEnd = nullptr; +} + +ed::CreateItemAction::Result ed::CreateItemAction::RejectItem() { + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem) + return Indeterminate; + + m_UserAction = UserReject; + + return True; +} + +ed::CreateItemAction::Result ed::CreateItemAction::AcceptItem() { + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem) + return Indeterminate; + + m_UserAction = UserAccept; + + if (m_CurrentStage == Create) { + m_NextStage = None; + m_ItemType = NoItem; + m_LinkStart = nullptr; + m_LinkEnd = nullptr; + return True; + } else + return False; +} + +ed::CreateItemAction::Result ed::CreateItemAction::QueryLink(PinId* startId, + PinId* endId) { + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType != Link) + return Indeterminate; + + auto linkStartId = m_LinkStart->m_ID; + auto linkEndId = m_LinkEnd->m_ID; + + *startId = linkStartId; + *endId = linkEndId; + + Editor->SetUserContext(true); + + if (!m_IsInGlobalSpace) { + Editor->Suspend(SuspendFlags::KeepSplitter); + + auto rect = Editor->GetRect(); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), + false); + m_IsInGlobalSpace = true; + } + + return True; +} + +ed::CreateItemAction::Result ed::CreateItemAction::QueryNode(PinId* pinId) { + IM_ASSERT(m_InActive); + + if (!m_InActive || m_CurrentStage == None || m_ItemType != Node) + return Indeterminate; + + *pinId = m_LinkStart ? m_LinkStart->m_ID : 0; + + Editor->SetUserContext(true); + + if (!m_IsInGlobalSpace) { + Editor->Suspend(SuspendFlags::KeepSplitter); + + auto rect = Editor->GetRect(); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), + false); + m_IsInGlobalSpace = true; + } + + return True; +} + +//------------------------------------------------------------------------------ +// +// Delete Items Action +// +//------------------------------------------------------------------------------ +ed::DeleteItemsAction::DeleteItemsAction(EditorContext* editor) + : EditorAction(editor), + m_IsActive(false), + m_InInteraction(false), + m_CurrentItemType(Unknown), + m_UserAction(Undetermined) {} + +void ed::DeleteItemsAction::DeleteDeadLinks(NodeId nodeId) { + vector links; + Editor->FindLinksForNode(nodeId, links, true); + for (auto link : links) { + link->m_DeleteOnNewFrame = true; + + auto it = + std::find(m_CandidateObjects.begin(), m_CandidateObjects.end(), link); + if (it != m_CandidateObjects.end()) continue; + + m_CandidateObjects.push_back(link); + } +} + +void ed::DeleteItemsAction::DeleteDeadPins(NodeId nodeId) { + auto node = Editor->FindNode(nodeId); + if (!node) return; + + for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin) + pin->m_DeleteOnNewFrame = true; +} + +ed::EditorAction::AcceptResult ed::DeleteItemsAction::Accept( + const Control& control) { + IM_ASSERT(!m_IsActive); + + if (m_IsActive) return False; + + auto& io = ImGui::GetIO(); + if (Editor->CanAcceptUserInput() && + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)) && + Editor->AreShortcutsEnabled()) { + auto& selection = Editor->GetSelectedObjects(); + if (!selection.empty()) { + m_CandidateObjects = selection; + m_IsActive = true; + return True; + } + } else if (control.ClickedLink && io.KeyAlt) { + m_CandidateObjects.clear(); + m_CandidateObjects.push_back(control.ClickedLink); + m_IsActive = true; + return True; + } + + else if (!m_ManuallyDeletedObjects.empty()) { + m_CandidateObjects = m_ManuallyDeletedObjects; + m_ManuallyDeletedObjects.clear(); + m_IsActive = true; + return True; + } + + return m_IsActive ? True : False; +} + +bool ed::DeleteItemsAction::Process(const Control& control) { + IM_UNUSED(control); + + if (!m_IsActive) return false; + + m_IsActive = false; + return true; +} + +void ed::DeleteItemsAction::ShowMetrics() { + EditorAction::ShowMetrics(); + + // auto getObjectName = [](Object* object) + //{ + // if (!object) return ""; + // else if (object->AsNode()) return "Node"; + // else if (object->AsPin()) return "Pin"; + // else if (object->AsLink()) return "Link"; + // else return ""; + // }; + + ImGui::Text("%s:", GetName()); + ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); + // ImGui::Text(" Node: %s (%d)", getObjectName(DeleteItemsgedNode), + // DeleteItemsgedNode ? DeleteItemsgedNode->ID : 0); +} + +bool ed::DeleteItemsAction::Add(Object* object) { + if (Editor->GetCurrentAction() != nullptr) return false; + + m_ManuallyDeletedObjects.push_back(object); + + return true; +} + +bool ed::DeleteItemsAction::Begin() { + if (!m_IsActive) return false; + + IM_ASSERT(!m_InInteraction); + m_InInteraction = true; + + m_CurrentItemType = Unknown; + m_UserAction = Undetermined; + + return m_IsActive; +} + +void ed::DeleteItemsAction::End() { + if (!m_IsActive) return; + + IM_ASSERT(m_InInteraction); + m_InInteraction = false; +} + +bool ed::DeleteItemsAction::QueryLink(LinkId* linkId, PinId* startId, + PinId* endId) { + ObjectId objectId; + if (!QueryItem(&objectId, Link)) return false; + + if (auto id = objectId.AsLinkId()) + *linkId = id; + else + return false; + + if (startId || endId) { + auto link = Editor->FindLink(*linkId); + if (startId) *startId = link->m_StartPin->m_ID; + if (endId) *endId = link->m_EndPin->m_ID; + } + + return true; +} + +bool ed::DeleteItemsAction::QueryNode(NodeId* nodeId) { + ObjectId objectId; + if (!QueryItem(&objectId, Node)) return false; + + if (auto id = objectId.AsNodeId()) + *nodeId = id; + else + return false; + + return true; +} + +bool ed::DeleteItemsAction::QueryItem(ObjectId* itemId, IteratorType itemType) { + if (!m_InInteraction) return false; + + if (m_CurrentItemType != itemType) { + m_CurrentItemType = itemType; + m_CandidateItemIndex = 0; + } else if (m_UserAction == Undetermined) { + RejectItem(); + } + + m_UserAction = Undetermined; + + auto itemCount = (int)m_CandidateObjects.size(); + while (m_CandidateItemIndex < itemCount) { + auto item = m_CandidateObjects[m_CandidateItemIndex]; + if (itemType == Node) { + if (auto node = item->AsNode()) { + *itemId = node->m_ID; + return true; + } + } else if (itemType == Link) { + if (auto link = item->AsLink()) { + *itemId = link->m_ID; + return true; + } + } + + ++m_CandidateItemIndex; + } + + if (m_CandidateItemIndex == itemCount) m_CurrentItemType = Unknown; + + return false; +} + +bool ed::DeleteItemsAction::AcceptItem(bool deleteDependencies) { + if (!m_InInteraction) return false; + + m_UserAction = Accepted; + + RemoveItem(deleteDependencies); + + return true; +} + +void ed::DeleteItemsAction::RejectItem() { + if (!m_InInteraction) return; + + m_UserAction = Rejected; + + DropCurrentItem(); +} + +void ed::DeleteItemsAction::RemoveItem(bool deleteDependencies) { + auto item = DropCurrentItem(); + + Editor->DeselectObject(item); + + Editor->RemoveSettings(item); + + item->m_DeleteOnNewFrame = true; + + if (deleteDependencies && m_CurrentItemType == Node) { + auto node = item->ID().AsNodeId(); + DeleteDeadLinks(node); + DeleteDeadPins(node); + } + + if (m_CurrentItemType == Link) Editor->NotifyLinkDeleted(item->AsLink()); +} + +ed::Object* ed::DeleteItemsAction::DropCurrentItem() { + auto item = m_CandidateObjects[m_CandidateItemIndex]; + m_CandidateObjects.erase(m_CandidateObjects.begin() + m_CandidateItemIndex); + + return item; +} + +//------------------------------------------------------------------------------ +// +// Node Builder +// +//------------------------------------------------------------------------------ +ed::NodeBuilder::NodeBuilder(EditorContext* editor) + : Editor(editor), m_CurrentNode(nullptr), m_CurrentPin(nullptr) {} + +ed::NodeBuilder::~NodeBuilder() { + m_Splitter.ClearFreeMemory(); + m_PinSplitter.ClearFreeMemory(); +} + +void ed::NodeBuilder::Begin(NodeId nodeId) { + IM_ASSERT(nullptr == m_CurrentNode); + + m_CurrentNode = Editor->GetNode(nodeId); + + Editor->UpdateNodeState(m_CurrentNode); + + if (m_CurrentNode->m_CenterOnScreen) { + auto bounds = Editor->GetViewRect(); + auto offset = bounds.GetCenter() - m_CurrentNode->m_Bounds.GetCenter(); + + if (ImLengthSqr(offset) > 0) { + if (::IsGroup(m_CurrentNode)) { + std::vector groupedNodes; + m_CurrentNode->GetGroupedNodes(groupedNodes); + groupedNodes.push_back(m_CurrentNode); + + for (auto node : groupedNodes) { + node->m_Bounds.Translate(ImFloor(offset)); + node->m_GroupBounds.Translate(ImFloor(offset)); + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, + node); + } + } else { + m_CurrentNode->m_Bounds.Translate(ImFloor(offset)); + m_CurrentNode->m_GroupBounds.Translate(ImFloor(offset)); + Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, + m_CurrentNode); + } + } + + m_CurrentNode->m_CenterOnScreen = false; + } + + // Position node on screen + ImGui::SetCursorScreenPos(m_CurrentNode->m_Bounds.Min); + + auto& editorStyle = Editor->GetStyle(); + + const auto alpha = ImGui::GetStyle().Alpha; + + m_CurrentNode->m_IsLive = true; + m_CurrentNode->m_LastPin = nullptr; + m_CurrentNode->m_Color = Editor->GetColor(StyleColor_NodeBg, alpha); + m_CurrentNode->m_BorderColor = Editor->GetColor(StyleColor_NodeBorder, alpha); + m_CurrentNode->m_BorderWidth = editorStyle.NodeBorderWidth; + m_CurrentNode->m_Rounding = editorStyle.NodeRounding; + m_CurrentNode->m_GroupColor = Editor->GetColor(StyleColor_GroupBg, alpha); + m_CurrentNode->m_GroupBorderColor = + Editor->GetColor(StyleColor_GroupBorder, alpha); + m_CurrentNode->m_GroupBorderWidth = editorStyle.GroupBorderWidth; + m_CurrentNode->m_GroupRounding = editorStyle.GroupRounding; + m_CurrentNode->m_HighlightConnectedLinks = + editorStyle.HighlightConnectedLinks != 0.0f; + + m_IsGroup = false; + + // Grow channel list and select user channel + if (auto drawList = Editor->GetDrawList()) { + m_CurrentNode->m_Channel = drawList->_Splitter._Count; + ImDrawList_ChannelsGrow(drawList, + drawList->_Splitter._Count + c_ChannelsPerNode); + drawList->ChannelsSetCurrent(m_CurrentNode->m_Channel + + c_NodeContentChannel); + + m_Splitter.Clear(); + ImDrawList_SwapSplitter(drawList, m_Splitter); + } + + // Begin outer group + ImGui::BeginGroup(); + + // Apply frame padding. Begin inner group if necessary. + if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || + editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0) { + ImGui::SetCursorPos( + ImGui::GetCursorPos() + + ImVec2(editorStyle.NodePadding.x, editorStyle.NodePadding.y)); + ImGui::BeginGroup(); + } +} + +void ed::NodeBuilder::End() { + IM_ASSERT(nullptr != m_CurrentNode); + + if (auto drawList = Editor->GetDrawList()) { + IM_ASSERT(drawList->_Splitter._Count == + 1); // Did you forgot to call drawList->ChannelsMerge()? + ImDrawList_SwapSplitter(drawList, m_Splitter); + } + + // Apply frame padding. This must be done in this convoluted way if outer + // group size must contain inner group padding. + auto& editorStyle = Editor->GetStyle(); + if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || + editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0) { + ImGui::EndGroup(); + ImGui::SameLine(0, editorStyle.NodePadding.z); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + ImGui::Dummy(ImVec2( + 0, 0)); // bump cursor at the end of the line and move to next one + ImGui::Dummy(ImVec2(0, editorStyle.NodePadding.w)); // apply padding + ImGui::PopStyleVar(); + } + + // End outer group. + ImGui::EndGroup(); + + m_NodeRect = ImGui_GetItemRect(); + m_NodeRect.Floor(); + + if (m_CurrentNode->m_Bounds.GetSize() != m_NodeRect.GetSize()) { + m_CurrentNode->m_Bounds.Max = + m_CurrentNode->m_Bounds.Min + m_NodeRect.GetSize(); + Editor->MakeDirty(SaveReasonFlags::Size, m_CurrentNode); + } + + if (m_IsGroup) { + // Groups cannot have pins. Discard them. + for (auto pin = m_CurrentNode->m_LastPin; pin; pin = pin->m_PreviousPin) + pin->Reset(); + + m_CurrentNode->m_Type = NodeType::Group; + m_CurrentNode->m_GroupBounds = m_GroupBounds; + m_CurrentNode->m_LastPin = nullptr; + } else + m_CurrentNode->m_Type = NodeType::Node; + + m_CurrentNode = nullptr; +} + +void ed::NodeBuilder::BeginPin(PinId pinId, PinKind kind) { + IM_ASSERT(nullptr != m_CurrentNode); + IM_ASSERT(nullptr == m_CurrentPin); + IM_ASSERT(false == m_IsGroup); + + auto& editorStyle = Editor->GetStyle(); + + m_CurrentPin = Editor->GetPin(pinId, kind); + m_CurrentPin->m_Node = m_CurrentNode; + + m_CurrentPin->m_IsLive = true; + m_CurrentPin->m_Color = Editor->GetColor(StyleColor_PinRect); + m_CurrentPin->m_BorderColor = Editor->GetColor(StyleColor_PinRectBorder); + m_CurrentPin->m_BorderWidth = editorStyle.PinBorderWidth; + m_CurrentPin->m_Rounding = editorStyle.PinRounding; + m_CurrentPin->m_Corners = static_cast(editorStyle.PinCorners); + m_CurrentPin->m_Radius = editorStyle.PinRadius; + m_CurrentPin->m_ArrowSize = editorStyle.PinArrowSize; + m_CurrentPin->m_ArrowWidth = editorStyle.PinArrowWidth; + m_CurrentPin->m_Dir = kind == PinKind::Output ? editorStyle.SourceDirection + : editorStyle.TargetDirection; + m_CurrentPin->m_Strength = editorStyle.LinkStrength; + m_CurrentPin->m_SnapLinkToDir = editorStyle.SnapLinkToPinDir != 0.0f; + + m_CurrentPin->m_PreviousPin = m_CurrentNode->m_LastPin; + m_CurrentNode->m_LastPin = m_CurrentPin; + + m_PivotAlignment = editorStyle.PivotAlignment; + m_PivotSize = editorStyle.PivotSize; + m_PivotScale = editorStyle.PivotScale; + m_ResolvePinRect = true; + m_ResolvePivot = true; + + if (auto drawList = Editor->GetDrawList()) { + m_PinSplitter.Clear(); + ImDrawList_SwapSplitter(drawList, m_PinSplitter); + } + + ImGui::BeginGroup(); +} + +void ed::NodeBuilder::EndPin() { + IM_ASSERT(nullptr != m_CurrentPin); + + if (auto drawList = Editor->GetDrawList()) { + IM_ASSERT(drawList->_Splitter._Count == + 1); // Did you forgot to call drawList->ChannelsMerge()? + ImDrawList_SwapSplitter(drawList, m_PinSplitter); + } + + ImGui::EndGroup(); + + if (m_ResolvePinRect) m_CurrentPin->m_Bounds = ImGui_GetItemRect(); + + if (m_ResolvePivot) { + auto& pinRect = m_CurrentPin->m_Bounds; + + if (m_PivotSize.x < 0) m_PivotSize.x = pinRect.GetWidth(); + if (m_PivotSize.y < 0) m_PivotSize.y = pinRect.GetHeight(); + + m_CurrentPin->m_Pivot.Min = + pinRect.Min + ImMul(pinRect.GetSize(), m_PivotAlignment); + m_CurrentPin->m_Pivot.Max = + m_CurrentPin->m_Pivot.Min + ImMul(m_PivotSize, m_PivotScale); + } + + // #debug: Draw pin bounds + // Editor->GetDrawList()->AddRect(m_CurrentPin->m_Bounds.Min, + // m_CurrentPin->m_Bounds.Max, IM_COL32(255, 255, 0, 255)); + + // #debug: Draw pin pivot rectangle + // Editor->GetDrawList()->AddRect(m_CurrentPin->m_Pivot.Min, + // m_CurrentPin->m_Pivot.Max, IM_COL32(255, 0, 255, 255)); + + m_CurrentPin = nullptr; +} + +void ed::NodeBuilder::PinRect(const ImVec2& a, const ImVec2& b) { + IM_ASSERT(nullptr != m_CurrentPin); + + m_CurrentPin->m_Bounds = ImRect(a, b); + m_CurrentPin->m_Bounds.Floor(); + m_ResolvePinRect = false; +} + +void ed::NodeBuilder::PinPivotRect(const ImVec2& a, const ImVec2& b) { + IM_ASSERT(nullptr != m_CurrentPin); + + m_CurrentPin->m_Pivot = ImRect(a, b); + m_ResolvePivot = false; +} + +void ed::NodeBuilder::PinPivotSize(const ImVec2& size) { + IM_ASSERT(nullptr != m_CurrentPin); + + m_PivotSize = size; + m_ResolvePivot = true; +} + +void ed::NodeBuilder::PinPivotScale(const ImVec2& scale) { + IM_ASSERT(nullptr != m_CurrentPin); + + m_PivotScale = scale; + m_ResolvePivot = true; +} + +void ed::NodeBuilder::PinPivotAlignment(const ImVec2& alignment) { + IM_ASSERT(nullptr != m_CurrentPin); + + m_PivotAlignment = alignment; + m_ResolvePivot = true; +} + +void ed::NodeBuilder::Group(const ImVec2& size) { + IM_ASSERT(nullptr != m_CurrentNode); + IM_ASSERT(nullptr == m_CurrentPin); + IM_ASSERT(false == m_IsGroup); + + m_IsGroup = true; + + if (IsGroup(m_CurrentNode)) + ImGui::Dummy(m_CurrentNode->m_GroupBounds.GetSize()); + else + ImGui::Dummy(size); + + m_GroupBounds = ImGui_GetItemRect(); + m_GroupBounds.Floor(); +} + +ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList() const { + return GetUserBackgroundDrawList(m_CurrentNode); +} + +ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList(Node* node) const { + if (node && node->m_IsLive) { + auto drawList = Editor->GetDrawList(); + drawList->ChannelsSetCurrent(node->m_Channel + c_NodeUserBackgroundChannel); + return drawList; + } else + return nullptr; +} + +//------------------------------------------------------------------------------ +// +// Node Builder +// +//------------------------------------------------------------------------------ +ed::HintBuilder::HintBuilder(EditorContext* editor) + : Editor(editor), m_IsActive(false), m_CurrentNode(nullptr) {} + +bool ed::HintBuilder::Begin(NodeId nodeId) { + IM_ASSERT(nullptr == m_CurrentNode); + + auto& view = Editor->GetView(); + auto& rect = Editor->GetRect(); + + const float c_min_zoom = 0.75f; + const float c_max_zoom = 0.50f; + + if (view.Scale > 0.75f) return false; + + auto node = Editor->FindNode(nodeId); + if (!IsGroup(node)) return false; + + m_CurrentNode = node; + + m_LastChannel = Editor->GetDrawList()->_Splitter._Current; + + Editor->Suspend(SuspendFlags::KeepSplitter); + + const auto alpha = ImMax(0.0f, std::min(1.0f, (view.Scale - c_min_zoom) / + (c_max_zoom - c_min_zoom))); + + Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); + + Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_Hints); + ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); + + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); + + m_IsActive = true; + + return true; +} + +void ed::HintBuilder::End() { + if (!m_IsActive) return; + + ImGui::PopStyleVar(); + + Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_Hints); + ImGui::PopClipRect(); + + Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground); + ImGui::PopClipRect(); + + Editor->GetDrawList()->ChannelsSetCurrent(m_LastChannel); + + Editor->Resume(SuspendFlags::KeepSplitter); + + m_IsActive = false; + m_CurrentNode = nullptr; +} + +ImVec2 ed::HintBuilder::GetGroupMin() { + IM_ASSERT(nullptr != m_CurrentNode); + + return Editor->ToScreen(m_CurrentNode->m_Bounds.Min); +} + +ImVec2 ed::HintBuilder::GetGroupMax() { + IM_ASSERT(nullptr != m_CurrentNode); + + return Editor->ToScreen(m_CurrentNode->m_Bounds.Max); +} + +ImDrawList* ed::HintBuilder::GetForegroundDrawList() { + IM_ASSERT(nullptr != m_CurrentNode); + + auto drawList = Editor->GetDrawList(); + + drawList->ChannelsSetCurrent(c_UserChannel_Hints); + + return drawList; +} + +ImDrawList* ed::HintBuilder::GetBackgroundDrawList() { + IM_ASSERT(nullptr != m_CurrentNode); + + auto drawList = Editor->GetDrawList(); + + drawList->ChannelsSetCurrent(c_UserChannel_HintsBackground); + + return drawList; +} + +//------------------------------------------------------------------------------ +// +// Style +// +//------------------------------------------------------------------------------ +void ed::Style::PushColor(StyleColor colorIndex, const ImVec4& color) { + ColorModifier modifier; + modifier.Index = colorIndex; + modifier.Value = Colors[colorIndex]; + m_ColorStack.push_back(modifier); + Colors[colorIndex] = color; +} + +void ed::Style::PopColor(int count) { + while (count > 0) { + auto& modifier = m_ColorStack.back(); + Colors[modifier.Index] = modifier.Value; + m_ColorStack.pop_back(); + --count; + } +} + +void ed::Style::PushVar(StyleVar varIndex, float value) { + auto* var = GetVarFloatAddr(varIndex); + IM_ASSERT(var != nullptr); + VarModifier modifier; + modifier.Index = varIndex; + modifier.Value = ImVec4(*var, 0, 0, 0); + *var = value; + m_VarStack.push_back(modifier); +} + +void ed::Style::PushVar(StyleVar varIndex, const ImVec2& value) { + auto* var = GetVarVec2Addr(varIndex); + IM_ASSERT(var != nullptr); + VarModifier modifier; + modifier.Index = varIndex; + modifier.Value = ImVec4(var->x, var->y, 0, 0); + *var = value; + m_VarStack.push_back(modifier); +} + +void ed::Style::PushVar(StyleVar varIndex, const ImVec4& value) { + auto* var = GetVarVec4Addr(varIndex); + IM_ASSERT(var != nullptr); + VarModifier modifier; + modifier.Index = varIndex; + modifier.Value = *var; + *var = value; + m_VarStack.push_back(modifier); +} + +void ed::Style::PopVar(int count) { + while (count > 0) { + auto& modifier = m_VarStack.back(); + if (auto floatValue = GetVarFloatAddr(modifier.Index)) + *floatValue = modifier.Value.x; + else if (auto vec2Value = GetVarVec2Addr(modifier.Index)) + *vec2Value = ImVec2(modifier.Value.x, modifier.Value.y); + else if (auto vec4Value = GetVarVec4Addr(modifier.Index)) + *vec4Value = modifier.Value; + m_VarStack.pop_back(); + --count; + } +} + +const char* ed::Style::GetColorName(StyleColor colorIndex) const { + switch (colorIndex) { + case StyleColor_Bg: + return "Bg"; + case StyleColor_Grid: + return "Grid"; + case StyleColor_NodeBg: + return "NodeBg"; + case StyleColor_NodeBorder: + return "NodeBorder"; + case StyleColor_HovNodeBorder: + return "HoveredNodeBorder"; + case StyleColor_SelNodeBorder: + return "SelNodeBorder"; + case StyleColor_NodeSelRect: + return "NodeSelRect"; + case StyleColor_NodeSelRectBorder: + return "NodeSelRectBorder"; + case StyleColor_HovLinkBorder: + return "HoveredLinkBorder"; + case StyleColor_SelLinkBorder: + return "SelLinkBorder"; + case StyleColor_HighlightLinkBorder: + return "HighlightLinkBorder"; + case StyleColor_LinkSelRect: + return "LinkSelRect"; + case StyleColor_LinkSelRectBorder: + return "LinkSelRectBorder"; + case StyleColor_PinRect: + return "PinRect"; + case StyleColor_PinRectBorder: + return "PinRectBorder"; + case StyleColor_Flow: + return "Flow"; + case StyleColor_FlowMarker: + return "FlowMarker"; + case StyleColor_GroupBg: + return "GroupBg"; + case StyleColor_GroupBorder: + return "GroupBorder"; + case StyleColor_Count: + break; + } + + IM_ASSERT(0); + return "Unknown"; +} + +float* ed::Style::GetVarFloatAddr(StyleVar idx) { + switch (idx) { + case StyleVar_NodeRounding: + return &NodeRounding; + case StyleVar_NodeBorderWidth: + return &NodeBorderWidth; + case StyleVar_HoveredNodeBorderWidth: + return &HoveredNodeBorderWidth; + case StyleVar_SelectedNodeBorderWidth: + return &SelectedNodeBorderWidth; + case StyleVar_PinRounding: + return &PinRounding; + case StyleVar_PinBorderWidth: + return &PinBorderWidth; + case StyleVar_LinkStrength: + return &LinkStrength; + case StyleVar_ScrollDuration: + return &ScrollDuration; + case StyleVar_FlowMarkerDistance: + return &FlowMarkerDistance; + case StyleVar_FlowSpeed: + return &FlowSpeed; + case StyleVar_FlowDuration: + return &FlowDuration; + case StyleVar_PinCorners: + return &PinCorners; + case StyleVar_PinRadius: + return &PinRadius; + case StyleVar_PinArrowSize: + return &PinArrowSize; + case StyleVar_PinArrowWidth: + return &PinArrowWidth; + case StyleVar_GroupRounding: + return &GroupRounding; + case StyleVar_GroupBorderWidth: + return &GroupBorderWidth; + case StyleVar_HighlightConnectedLinks: + return &HighlightConnectedLinks; + case StyleVar_SnapLinkToPinDir: + return &SnapLinkToPinDir; + case StyleVar_HoveredNodeBorderOffset: + return &HoverNodeBorderOffset; + case StyleVar_SelectedNodeBorderOffset: + return &SelectedNodeBorderOffset; + default: + return nullptr; + } +} + +ImVec2* ed::Style::GetVarVec2Addr(StyleVar idx) { + switch (idx) { + case StyleVar_SourceDirection: + return &SourceDirection; + case StyleVar_TargetDirection: + return &TargetDirection; + case StyleVar_PivotAlignment: + return &PivotAlignment; + case StyleVar_PivotSize: + return &PivotSize; + case StyleVar_PivotScale: + return &PivotScale; + default: + return nullptr; + } +} + +ImVec4* ed::Style::GetVarVec4Addr(StyleVar idx) { + switch (idx) { + case StyleVar_NodePadding: + return &NodePadding; + default: + return nullptr; + } +} + +//------------------------------------------------------------------------------ +// +// Config +// +//------------------------------------------------------------------------------ +ed::Config::Config(const ax::NodeEditor::Config* config) { + if (config) *static_cast(this) = *config; +} + +std::string ed::Config::Load() { + std::string data; + + if (LoadSettings) { + const auto size = LoadSettings(nullptr, UserPointer); + if (size > 0) { + data.resize(size); + LoadSettings(const_cast(data.data()), UserPointer); + } + } else if (SettingsFile) { + std::ifstream file(SettingsFile); + if (file) { + file.seekg(0, std::ios_base::end); + auto size = static_cast(file.tellg()); + file.seekg(0, std::ios_base::beg); + + data.reserve(size); + data.assign(std::istreambuf_iterator(file), + std::istreambuf_iterator()); + } + } + + return data; +} + +std::string ed::Config::LoadNode(NodeId nodeId) { + std::string data; + + if (LoadNodeSettings) { + const auto size = LoadNodeSettings(nodeId, nullptr, UserPointer); + if (size > 0) { + data.resize(size); + LoadNodeSettings(nodeId, const_cast(data.data()), UserPointer); + } + } + + return data; +} + +void ed::Config::BeginSave() { + if (BeginSaveSession) BeginSaveSession(UserPointer); +} + +bool ed::Config::Save(const std::string& data, SaveReasonFlags flags) { + if (SaveSettings) { + return SaveSettings(data.c_str(), data.size(), flags, UserPointer); + } else if (SettingsFile) { + std::ofstream settingsFile(SettingsFile); + if (settingsFile) settingsFile << data; + + return !!settingsFile; + } + + return false; +} + +bool ed::Config::SaveNode(NodeId nodeId, const std::string& data, + SaveReasonFlags flags) { + if (SaveNodeSettings) + return SaveNodeSettings(nodeId, data.c_str(), data.size(), flags, + UserPointer); + + return false; +} + +void ed::Config::EndSave() { + if (EndSaveSession) EndSaveSession(UserPointer); +} diff --git a/symmetri/gui/imgui_node_editor.h b/symmetri/gui/imgui_node_editor.h new file mode 100644 index 0000000..4a77361 --- /dev/null +++ b/symmetri/gui/imgui_node_editor.h @@ -0,0 +1,524 @@ +//------------------------------------------------------------------------------ +// VERSION 0.9.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +#ifndef __IMGUI_NODE_EDITOR_H__ +#define __IMGUI_NODE_EDITOR_H__ +#pragma once + +//------------------------------------------------------------------------------ +#include + +#include // std::uintXX_t +#include // std::move + +//------------------------------------------------------------------------------ +#define IMGUI_NODE_EDITOR_VERSION "0.9.3" +#define IMGUI_NODE_EDITOR_VERSION_NUM 000903 + +//------------------------------------------------------------------------------ +#ifndef IMGUI_NODE_EDITOR_API +#define IMGUI_NODE_EDITOR_API +#endif + +//------------------------------------------------------------------------------ +namespace ax { +namespace NodeEditor { + +//------------------------------------------------------------------------------ +struct NodeId; +struct LinkId; +struct PinId; + +//------------------------------------------------------------------------------ +enum class PinKind { Input, Output }; + +enum class FlowDirection { Forward, Backward }; + +enum class CanvasSizeMode { + FitVerticalView, // Previous view will be scaled to fit new view on Y axis + FitHorizontalView, // Previous view will be scaled to fit new view on X axis + CenterOnly, // Previous view will be centered on new view +}; + +//------------------------------------------------------------------------------ +enum class SaveReasonFlags : uint32_t { + None = 0x00000000, + Navigation = 0x00000001, + Position = 0x00000002, + Size = 0x00000004, + Selection = 0x00000008, + AddNode = 0x00000010, + RemoveNode = 0x00000020, + User = 0x00000040 +}; + +inline SaveReasonFlags operator|(SaveReasonFlags lhs, SaveReasonFlags rhs) { + return static_cast(static_cast(lhs) | + static_cast(rhs)); +} +inline SaveReasonFlags operator&(SaveReasonFlags lhs, SaveReasonFlags rhs) { + return static_cast(static_cast(lhs) & + static_cast(rhs)); +} + +using ConfigSaveSettings = bool (*)(const char* data, size_t size, + SaveReasonFlags reason, void* userPointer); +using ConfigLoadSettings = size_t (*)(char* data, void* userPointer); + +using ConfigSaveNodeSettings = bool (*)(NodeId nodeId, const char* data, + size_t size, SaveReasonFlags reason, + void* userPointer); +using ConfigLoadNodeSettings = size_t (*)(NodeId nodeId, char* data, + void* userPointer); + +using ConfigSession = void (*)(void* userPointer); + +struct Config { + using CanvasSizeModeAlias = ax::NodeEditor::CanvasSizeMode; + + const char* SettingsFile; + ConfigSession BeginSaveSession; + ConfigSession EndSaveSession; + ConfigSaveSettings SaveSettings; + ConfigLoadSettings LoadSettings; + ConfigSaveNodeSettings SaveNodeSettings; + ConfigLoadNodeSettings LoadNodeSettings; + void* UserPointer; + ImVector CustomZoomLevels; + CanvasSizeModeAlias CanvasSizeMode; + int DragButtonIndex; // Mouse button index drag action will react to (0-left, + // 1-right, 2-middle) + int SelectButtonIndex; // Mouse button index select action will react to + // (0-left, 1-right, 2-middle) + int NavigateButtonIndex; // Mouse button index navigate action will react to + // (0-left, 1-right, 2-middle) + int ContextMenuButtonIndex; // Mouse button index context menu action will + // react to (0-left, 1-right, 2-middle) + + Config() + : SettingsFile("NodeEditor.json"), + BeginSaveSession(nullptr), + EndSaveSession(nullptr), + SaveSettings(nullptr), + LoadSettings(nullptr), + SaveNodeSettings(nullptr), + LoadNodeSettings(nullptr), + UserPointer(nullptr), + CustomZoomLevels(), + CanvasSizeMode(CanvasSizeModeAlias::FitVerticalView), + DragButtonIndex(0), + SelectButtonIndex(0), + NavigateButtonIndex(1), + ContextMenuButtonIndex(1) {} +}; + +//------------------------------------------------------------------------------ +enum StyleColor { + StyleColor_Bg, + StyleColor_Grid, + StyleColor_NodeBg, + StyleColor_NodeBorder, + StyleColor_HovNodeBorder, + StyleColor_SelNodeBorder, + StyleColor_NodeSelRect, + StyleColor_NodeSelRectBorder, + StyleColor_HovLinkBorder, + StyleColor_SelLinkBorder, + StyleColor_HighlightLinkBorder, + StyleColor_LinkSelRect, + StyleColor_LinkSelRectBorder, + StyleColor_PinRect, + StyleColor_PinRectBorder, + StyleColor_Flow, + StyleColor_FlowMarker, + StyleColor_GroupBg, + StyleColor_GroupBorder, + + StyleColor_Count +}; + +enum StyleVar { + StyleVar_NodePadding, + StyleVar_NodeRounding, + StyleVar_NodeBorderWidth, + StyleVar_HoveredNodeBorderWidth, + StyleVar_SelectedNodeBorderWidth, + StyleVar_PinRounding, + StyleVar_PinBorderWidth, + StyleVar_LinkStrength, + StyleVar_SourceDirection, + StyleVar_TargetDirection, + StyleVar_ScrollDuration, + StyleVar_FlowMarkerDistance, + StyleVar_FlowSpeed, + StyleVar_FlowDuration, + StyleVar_PivotAlignment, + StyleVar_PivotSize, + StyleVar_PivotScale, + StyleVar_PinCorners, + StyleVar_PinRadius, + StyleVar_PinArrowSize, + StyleVar_PinArrowWidth, + StyleVar_GroupRounding, + StyleVar_GroupBorderWidth, + StyleVar_HighlightConnectedLinks, + StyleVar_SnapLinkToPinDir, + StyleVar_HoveredNodeBorderOffset, + StyleVar_SelectedNodeBorderOffset, + + StyleVar_Count +}; + +struct Style { + ImVec4 NodePadding; + float NodeRounding; + float NodeBorderWidth; + float HoveredNodeBorderWidth; + float HoverNodeBorderOffset; + float SelectedNodeBorderWidth; + float SelectedNodeBorderOffset; + float PinRounding; + float PinBorderWidth; + float LinkStrength; + ImVec2 SourceDirection; + ImVec2 TargetDirection; + float ScrollDuration; + float FlowMarkerDistance; + float FlowSpeed; + float FlowDuration; + ImVec2 PivotAlignment; + ImVec2 PivotSize; + ImVec2 PivotScale; + float PinCorners; + float PinRadius; + float PinArrowSize; + float PinArrowWidth; + float GroupRounding; + float GroupBorderWidth; + float HighlightConnectedLinks; + float SnapLinkToPinDir; // when true link will start on the line defined by + // pin direction + ImVec4 Colors[StyleColor_Count]; + + Style() { + NodePadding = ImVec4(8, 8, 8, 8); + NodeRounding = 12.0f; + NodeBorderWidth = 1.5f; + HoveredNodeBorderWidth = 3.5f; + HoverNodeBorderOffset = 0.0f; + SelectedNodeBorderWidth = 3.5f; + SelectedNodeBorderOffset = 0.0f; + PinRounding = 4.0f; + PinBorderWidth = 0.0f; + LinkStrength = 100.0f; + SourceDirection = ImVec2(1.0f, 0.0f); + TargetDirection = ImVec2(-1.0f, 0.0f); + ScrollDuration = 0.35f; + FlowMarkerDistance = 30.0f; + FlowSpeed = 150.0f; + FlowDuration = 2.0f; + PivotAlignment = ImVec2(0.5f, 0.5f); + PivotSize = ImVec2(0.0f, 0.0f); + PivotScale = ImVec2(1, 1); +#if IMGUI_VERSION_NUM > 18101 + PinCorners = ImDrawFlags_RoundCornersAll; +#else + PinCorners = ImDrawCornerFlags_All; +#endif + PinRadius = 0.0f; + PinArrowSize = 0.0f; + PinArrowWidth = 0.0f; + GroupRounding = 6.0f; + GroupBorderWidth = 1.0f; + HighlightConnectedLinks = 0.0f; + SnapLinkToPinDir = 0.0f; + + Colors[StyleColor_Bg] = ImColor(60, 60, 70, 200); + Colors[StyleColor_Grid] = ImColor(120, 120, 120, 40); + Colors[StyleColor_NodeBg] = ImColor(32, 32, 32, 200); + Colors[StyleColor_NodeBorder] = ImColor(255, 255, 255, 96); + Colors[StyleColor_HovNodeBorder] = ImColor(50, 176, 255, 255); + Colors[StyleColor_SelNodeBorder] = ImColor(255, 176, 50, 255); + Colors[StyleColor_NodeSelRect] = ImColor(5, 130, 255, 64); + Colors[StyleColor_NodeSelRectBorder] = ImColor(5, 130, 255, 128); + Colors[StyleColor_HovLinkBorder] = ImColor(50, 176, 255, 255); + Colors[StyleColor_SelLinkBorder] = ImColor(255, 176, 50, 255); + Colors[StyleColor_HighlightLinkBorder] = ImColor(204, 105, 0, 255); + Colors[StyleColor_LinkSelRect] = ImColor(5, 130, 255, 64); + Colors[StyleColor_LinkSelRectBorder] = ImColor(5, 130, 255, 128); + Colors[StyleColor_PinRect] = ImColor(60, 180, 255, 100); + Colors[StyleColor_PinRectBorder] = ImColor(60, 180, 255, 128); + Colors[StyleColor_Flow] = ImColor(255, 128, 64, 255); + Colors[StyleColor_FlowMarker] = ImColor(255, 128, 64, 255); + Colors[StyleColor_GroupBg] = ImColor(0, 0, 0, 160); + Colors[StyleColor_GroupBorder] = ImColor(255, 255, 255, 32); + } +}; + +//------------------------------------------------------------------------------ +struct EditorContext; + +//------------------------------------------------------------------------------ +IMGUI_NODE_EDITOR_API void SetCurrentEditor(EditorContext* ctx); +IMGUI_NODE_EDITOR_API EditorContext* GetCurrentEditor(); +IMGUI_NODE_EDITOR_API EditorContext* CreateEditor( + const Config* config = nullptr); +IMGUI_NODE_EDITOR_API void DestroyEditor(EditorContext* ctx); +IMGUI_NODE_EDITOR_API const Config& GetConfig(EditorContext* ctx = nullptr); + +IMGUI_NODE_EDITOR_API Style& GetStyle(); +IMGUI_NODE_EDITOR_API const char* GetStyleColorName(StyleColor colorIndex); + +IMGUI_NODE_EDITOR_API void PushStyleColor(StyleColor colorIndex, + const ImVec4& color); +IMGUI_NODE_EDITOR_API void PopStyleColor(int count = 1); + +IMGUI_NODE_EDITOR_API void PushStyleVar(StyleVar varIndex, float value); +IMGUI_NODE_EDITOR_API void PushStyleVar(StyleVar varIndex, const ImVec2& value); +IMGUI_NODE_EDITOR_API void PushStyleVar(StyleVar varIndex, const ImVec4& value); +IMGUI_NODE_EDITOR_API void PopStyleVar(int count = 1); + +IMGUI_NODE_EDITOR_API void Begin(const char* id, + const ImVec2& size = ImVec2(0, 0)); +IMGUI_NODE_EDITOR_API void End(); + +IMGUI_NODE_EDITOR_API void BeginNode(NodeId id); +IMGUI_NODE_EDITOR_API void BeginPin(PinId id, PinKind kind); +IMGUI_NODE_EDITOR_API void PinRect(const ImVec2& a, const ImVec2& b); +IMGUI_NODE_EDITOR_API void PinPivotRect(const ImVec2& a, const ImVec2& b); +IMGUI_NODE_EDITOR_API void PinPivotSize(const ImVec2& size); +IMGUI_NODE_EDITOR_API void PinPivotScale(const ImVec2& scale); +IMGUI_NODE_EDITOR_API void PinPivotAlignment(const ImVec2& alignment); +IMGUI_NODE_EDITOR_API void EndPin(); +IMGUI_NODE_EDITOR_API void Group(const ImVec2& size); +IMGUI_NODE_EDITOR_API void EndNode(); + +IMGUI_NODE_EDITOR_API bool BeginGroupHint(NodeId nodeId); +IMGUI_NODE_EDITOR_API ImVec2 GetGroupMin(); +IMGUI_NODE_EDITOR_API ImVec2 GetGroupMax(); +IMGUI_NODE_EDITOR_API ImDrawList* GetHintForegroundDrawList(); +IMGUI_NODE_EDITOR_API ImDrawList* GetHintBackgroundDrawList(); +IMGUI_NODE_EDITOR_API void EndGroupHint(); + +// TODO: Add a way to manage node background channels +IMGUI_NODE_EDITOR_API ImDrawList* GetNodeBackgroundDrawList(NodeId nodeId); + +IMGUI_NODE_EDITOR_API bool Link(LinkId id, PinId startPinId, PinId endPinId, + const ImVec4& color = ImVec4(1, 1, 1, 1), + float thickness = 1.0f); + +IMGUI_NODE_EDITOR_API void Flow( + LinkId linkId, FlowDirection direction = FlowDirection::Forward); + +IMGUI_NODE_EDITOR_API bool BeginCreate(const ImVec4& color = ImVec4(1, 1, 1, 1), + float thickness = 1.0f); +IMGUI_NODE_EDITOR_API bool QueryNewLink(PinId* startId, PinId* endId); +IMGUI_NODE_EDITOR_API bool QueryNewLink(PinId* startId, PinId* endId, + const ImVec4& color, + float thickness = 1.0f); +IMGUI_NODE_EDITOR_API bool QueryNewNode(PinId* pinId); +IMGUI_NODE_EDITOR_API bool QueryNewNode(PinId* pinId, const ImVec4& color, + float thickness = 1.0f); +IMGUI_NODE_EDITOR_API bool AcceptNewItem(); +IMGUI_NODE_EDITOR_API bool AcceptNewItem(const ImVec4& color, + float thickness = 1.0f); +IMGUI_NODE_EDITOR_API void RejectNewItem(); +IMGUI_NODE_EDITOR_API void RejectNewItem(const ImVec4& color, + float thickness = 1.0f); +IMGUI_NODE_EDITOR_API void EndCreate(); + +IMGUI_NODE_EDITOR_API bool BeginDelete(); +IMGUI_NODE_EDITOR_API bool QueryDeletedLink(LinkId* linkId, + PinId* startId = nullptr, + PinId* endId = nullptr); +IMGUI_NODE_EDITOR_API bool QueryDeletedNode(NodeId* nodeId); +IMGUI_NODE_EDITOR_API bool AcceptDeletedItem(bool deleteDependencies = true); +IMGUI_NODE_EDITOR_API void RejectDeletedItem(); +IMGUI_NODE_EDITOR_API void EndDelete(); + +IMGUI_NODE_EDITOR_API void SetNodePosition(NodeId nodeId, + const ImVec2& editorPosition); +IMGUI_NODE_EDITOR_API void SetGroupSize(NodeId nodeId, const ImVec2& size); +IMGUI_NODE_EDITOR_API ImVec2 GetNodePosition(NodeId nodeId); +IMGUI_NODE_EDITOR_API ImVec2 GetNodeSize(NodeId nodeId); +IMGUI_NODE_EDITOR_API void CenterNodeOnScreen(NodeId nodeId); +IMGUI_NODE_EDITOR_API void SetNodeZPosition( + NodeId nodeId, float z); // Sets node z position, nodes with higher value + // are drawn over nodes with lower value +IMGUI_NODE_EDITOR_API float GetNodeZPosition( + NodeId nodeId); // Returns node z position, defaults is 0.0f + +IMGUI_NODE_EDITOR_API void RestoreNodeState(NodeId nodeId); + +IMGUI_NODE_EDITOR_API void Suspend(); +IMGUI_NODE_EDITOR_API void Resume(); +IMGUI_NODE_EDITOR_API bool IsSuspended(); + +IMGUI_NODE_EDITOR_API bool IsActive(); + +IMGUI_NODE_EDITOR_API bool HasSelectionChanged(); +IMGUI_NODE_EDITOR_API int GetSelectedObjectCount(); +IMGUI_NODE_EDITOR_API int GetSelectedNodes(NodeId* nodes, int size); +IMGUI_NODE_EDITOR_API int GetSelectedLinks(LinkId* links, int size); +IMGUI_NODE_EDITOR_API bool IsNodeSelected(NodeId nodeId); +IMGUI_NODE_EDITOR_API bool IsLinkSelected(LinkId linkId); +IMGUI_NODE_EDITOR_API void ClearSelection(); +IMGUI_NODE_EDITOR_API void SelectNode(NodeId nodeId, bool append = false); +IMGUI_NODE_EDITOR_API void SelectLink(LinkId linkId, bool append = false); +IMGUI_NODE_EDITOR_API void DeselectNode(NodeId nodeId); +IMGUI_NODE_EDITOR_API void DeselectLink(LinkId linkId); + +IMGUI_NODE_EDITOR_API bool DeleteNode(NodeId nodeId); +IMGUI_NODE_EDITOR_API bool DeleteLink(LinkId linkId); + +IMGUI_NODE_EDITOR_API bool HasAnyLinks( + NodeId nodeId); // Returns true if node has any link connected +IMGUI_NODE_EDITOR_API bool HasAnyLinks( + PinId pinId); // Return true if pin has any link connected +IMGUI_NODE_EDITOR_API int BreakLinks( + NodeId nodeId); // Break all links connected to this node +IMGUI_NODE_EDITOR_API int BreakLinks( + PinId pinId); // Break all links connected to this pin + +IMGUI_NODE_EDITOR_API void NavigateToContent(float duration = -1); +IMGUI_NODE_EDITOR_API void NavigateToSelection(bool zoomIn = false, + float duration = -1); + +IMGUI_NODE_EDITOR_API bool ShowNodeContextMenu(NodeId* nodeId); +IMGUI_NODE_EDITOR_API bool ShowPinContextMenu(PinId* pinId); +IMGUI_NODE_EDITOR_API bool ShowLinkContextMenu(LinkId* linkId); +IMGUI_NODE_EDITOR_API bool ShowBackgroundContextMenu(); + +IMGUI_NODE_EDITOR_API void EnableShortcuts(bool enable); +IMGUI_NODE_EDITOR_API bool AreShortcutsEnabled(); + +IMGUI_NODE_EDITOR_API bool BeginShortcut(); +IMGUI_NODE_EDITOR_API bool AcceptCut(); +IMGUI_NODE_EDITOR_API bool AcceptCopy(); +IMGUI_NODE_EDITOR_API bool AcceptPaste(); +IMGUI_NODE_EDITOR_API bool AcceptDuplicate(); +IMGUI_NODE_EDITOR_API bool AcceptCreateNode(); +IMGUI_NODE_EDITOR_API int GetActionContextSize(); +IMGUI_NODE_EDITOR_API int GetActionContextNodes(NodeId* nodes, int size); +IMGUI_NODE_EDITOR_API int GetActionContextLinks(LinkId* links, int size); +IMGUI_NODE_EDITOR_API void EndShortcut(); + +IMGUI_NODE_EDITOR_API float GetCurrentZoom(); + +IMGUI_NODE_EDITOR_API NodeId GetHoveredNode(); +IMGUI_NODE_EDITOR_API PinId GetHoveredPin(); +IMGUI_NODE_EDITOR_API LinkId GetHoveredLink(); +IMGUI_NODE_EDITOR_API NodeId GetDoubleClickedNode(); +IMGUI_NODE_EDITOR_API PinId GetDoubleClickedPin(); +IMGUI_NODE_EDITOR_API LinkId GetDoubleClickedLink(); +IMGUI_NODE_EDITOR_API bool IsBackgroundClicked(); +IMGUI_NODE_EDITOR_API bool IsBackgroundDoubleClicked(); +IMGUI_NODE_EDITOR_API ImGuiMouseButton +GetBackgroundClickButtonIndex(); // -1 if none +IMGUI_NODE_EDITOR_API ImGuiMouseButton +GetBackgroundDoubleClickButtonIndex(); // -1 if none + +IMGUI_NODE_EDITOR_API bool GetLinkPins( + LinkId linkId, PinId* startPinId, + PinId* endPinId); // pass nullptr if particular pin do not interest you + +IMGUI_NODE_EDITOR_API bool PinHadAnyLinks(PinId pinId); + +IMGUI_NODE_EDITOR_API ImVec2 GetScreenSize(); +IMGUI_NODE_EDITOR_API ImVec2 ScreenToCanvas(const ImVec2& pos); +IMGUI_NODE_EDITOR_API ImVec2 CanvasToScreen(const ImVec2& pos); + +IMGUI_NODE_EDITOR_API int +GetNodeCount(); // Returns number of submitted nodes since Begin() call +IMGUI_NODE_EDITOR_API int GetOrderedNodeIds( + NodeId* nodes, + int size); // Fills an array with node id's in order they're drawn; up to + // 'size` elements are set. Returns actual size of filled id's. + +//------------------------------------------------------------------------------ +namespace Details { + +template +struct SafeType { + SafeType(T t) : m_Value(std::move(t)) {} + + SafeType(const SafeType&) = default; + + template + SafeType(const SafeType< + typename std::enable_if::value, T2>::type, + typename std::enable_if::value, + Tag2>::type>&) = delete; + + SafeType& operator=(const SafeType&) = default; + + explicit operator T() const { return Get(); } + + T Get() const { return m_Value; } + + private: + T m_Value; +}; + +template +struct SafePointerType : SafeType { + static const Tag Invalid; + + using SafeType::SafeType; + + SafePointerType() : SafePointerType(Invalid) {} + + template + explicit SafePointerType(T* ptr) + : SafePointerType(reinterpret_cast(ptr)) {} + template + T* AsPointer() const { + return reinterpret_cast(this->Get()); + } + + explicit operator bool() const { return *this != Invalid; } +}; + +template +const Tag SafePointerType::Invalid = {0}; + +template +inline bool operator==(const SafePointerType& lhs, + const SafePointerType& rhs) { + return lhs.Get() == rhs.Get(); +} + +template +inline bool operator!=(const SafePointerType& lhs, + const SafePointerType& rhs) { + return lhs.Get() != rhs.Get(); +} + +} // namespace Details + +struct NodeId final : Details::SafePointerType { + using SafePointerType::SafePointerType; +}; + +struct LinkId final : Details::SafePointerType { + using SafePointerType::SafePointerType; +}; + +struct PinId final : Details::SafePointerType { + using SafePointerType::SafePointerType; +}; + +//------------------------------------------------------------------------------ +} // namespace NodeEditor +} // namespace ax + +//------------------------------------------------------------------------------ +#endif // __IMGUI_NODE_EDITOR_H__ diff --git a/symmetri/gui/imgui_node_editor_api.cpp b/symmetri/gui/imgui_node_editor_api.cpp new file mode 100644 index 0000000..21084a8 --- /dev/null +++ b/symmetri/gui/imgui_node_editor_api.cpp @@ -0,0 +1,596 @@ +//------------------------------------------------------------------------------ +// VERSION 0.9.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +#include + +#include "imgui_node_editor_internal.h" + +//------------------------------------------------------------------------------ +static ax::NodeEditor::Detail::EditorContext* s_Editor = nullptr; + +//------------------------------------------------------------------------------ +template +static int BuildIdList(C& container, I* list, int listSize, F&& accept) { + if (list != nullptr) { + int count = 0; + for (auto object : container) { + if (listSize <= 0) break; + + if (accept(object)) { + list[count] = I(object->ID().AsPointer()); + ++count; + --listSize; + } + } + + return count; + } else + return static_cast( + std::count_if(container.begin(), container.end(), accept)); +} + +//------------------------------------------------------------------------------ +ax::NodeEditor::EditorContext* ax::NodeEditor::CreateEditor( + const Config* config) { + return reinterpret_cast( + new ax::NodeEditor::Detail::EditorContext(config)); +} + +void ax::NodeEditor::DestroyEditor(EditorContext* ctx) { + auto lastContext = GetCurrentEditor(); + + // Set context we're about to destroy as current, to give callback valid + // context + if (lastContext != ctx) SetCurrentEditor(ctx); + + auto editor = reinterpret_cast(ctx); + + delete editor; + + if (lastContext != ctx) SetCurrentEditor(lastContext); +} + +const ax::NodeEditor::Config& ax::NodeEditor::GetConfig(EditorContext* ctx) { + if (ctx == nullptr) ctx = GetCurrentEditor(); + + if (ctx) { + auto editor = reinterpret_cast(ctx); + + return editor->GetConfig(); + } else { + static Config s_EmptyConfig; + return s_EmptyConfig; + } +} + +void ax::NodeEditor::SetCurrentEditor(EditorContext* ctx) { + s_Editor = reinterpret_cast(ctx); +} + +ax::NodeEditor::EditorContext* ax::NodeEditor::GetCurrentEditor() { + return reinterpret_cast(s_Editor); +} + +ax::NodeEditor::Style& ax::NodeEditor::GetStyle() { + return s_Editor->GetStyle(); +} + +const char* ax::NodeEditor::GetStyleColorName(StyleColor colorIndex) { + return s_Editor->GetStyle().GetColorName(colorIndex); +} + +void ax::NodeEditor::PushStyleColor(StyleColor colorIndex, + const ImVec4& color) { + s_Editor->GetStyle().PushColor(colorIndex, color); +} + +void ax::NodeEditor::PopStyleColor(int count) { + s_Editor->GetStyle().PopColor(count); +} + +void ax::NodeEditor::PushStyleVar(StyleVar varIndex, float value) { + s_Editor->GetStyle().PushVar(varIndex, value); +} + +void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec2& value) { + s_Editor->GetStyle().PushVar(varIndex, value); +} + +void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec4& value) { + s_Editor->GetStyle().PushVar(varIndex, value); +} + +void ax::NodeEditor::PopStyleVar(int count) { + s_Editor->GetStyle().PopVar(count); +} + +void ax::NodeEditor::Begin(const char* id, const ImVec2& size) { + s_Editor->Begin(id, size); +} + +void ax::NodeEditor::End() { s_Editor->End(); } + +void ax::NodeEditor::BeginNode(NodeId id) { + s_Editor->GetNodeBuilder().Begin(id); +} + +void ax::NodeEditor::BeginPin(PinId id, PinKind kind) { + s_Editor->GetNodeBuilder().BeginPin(id, kind); +} + +void ax::NodeEditor::PinRect(const ImVec2& a, const ImVec2& b) { + s_Editor->GetNodeBuilder().PinRect(a, b); +} + +void ax::NodeEditor::PinPivotRect(const ImVec2& a, const ImVec2& b) { + s_Editor->GetNodeBuilder().PinPivotRect(a, b); +} + +void ax::NodeEditor::PinPivotSize(const ImVec2& size) { + s_Editor->GetNodeBuilder().PinPivotSize(size); +} + +void ax::NodeEditor::PinPivotScale(const ImVec2& scale) { + s_Editor->GetNodeBuilder().PinPivotScale(scale); +} + +void ax::NodeEditor::PinPivotAlignment(const ImVec2& alignment) { + s_Editor->GetNodeBuilder().PinPivotAlignment(alignment); +} + +void ax::NodeEditor::EndPin() { s_Editor->GetNodeBuilder().EndPin(); } + +void ax::NodeEditor::Group(const ImVec2& size) { + s_Editor->GetNodeBuilder().Group(size); +} + +void ax::NodeEditor::EndNode() { s_Editor->GetNodeBuilder().End(); } + +bool ax::NodeEditor::BeginGroupHint(NodeId nodeId) { + return s_Editor->GetHintBuilder().Begin(nodeId); +} + +ImVec2 ax::NodeEditor::GetGroupMin() { + return s_Editor->GetHintBuilder().GetGroupMin(); +} + +ImVec2 ax::NodeEditor::GetGroupMax() { + return s_Editor->GetHintBuilder().GetGroupMax(); +} + +ImDrawList* ax::NodeEditor::GetHintForegroundDrawList() { + return s_Editor->GetHintBuilder().GetForegroundDrawList(); +} + +ImDrawList* ax::NodeEditor::GetHintBackgroundDrawList() { + return s_Editor->GetHintBuilder().GetBackgroundDrawList(); +} + +void ax::NodeEditor::EndGroupHint() { s_Editor->GetHintBuilder().End(); } + +ImDrawList* ax::NodeEditor::GetNodeBackgroundDrawList(NodeId nodeId) { + if (auto node = s_Editor->FindNode(nodeId)) + return s_Editor->GetNodeBuilder().GetUserBackgroundDrawList(node); + else + return nullptr; +} + +bool ax::NodeEditor::Link(LinkId id, PinId startPinId, PinId endPinId, + const ImVec4& color /* = ImVec4(1, 1, 1, 1)*/, + float thickness /* = 1.0f*/) { + return s_Editor->DoLink(id, startPinId, endPinId, ImColor(color), thickness); +} + +void ax::NodeEditor::Flow(LinkId linkId, FlowDirection direction) { + if (auto link = s_Editor->FindLink(linkId)) s_Editor->Flow(link, direction); +} + +bool ax::NodeEditor::BeginCreate(const ImVec4& color, float thickness) { + auto& context = s_Editor->GetItemCreator(); + + if (context.Begin()) { + context.SetStyle(ImColor(color), thickness); + return true; + } else + return false; +} + +bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId) { + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + return context.QueryLink(startId, endId) == Result::True; +} + +bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId, + const ImVec4& color, float thickness) { + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + auto result = context.QueryLink(startId, endId); + if (result != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); + + return result == Result::True; +} + +bool ax::NodeEditor::QueryNewNode(PinId* pinId) { + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + return context.QueryNode(pinId) == Result::True; +} + +bool ax::NodeEditor::QueryNewNode(PinId* pinId, const ImVec4& color, + float thickness) { + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + auto result = context.QueryNode(pinId); + if (result != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); + + return result == Result::True; +} + +bool ax::NodeEditor::AcceptNewItem() { + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + return context.AcceptItem() == Result::True; +} + +bool ax::NodeEditor::AcceptNewItem(const ImVec4& color, float thickness) { + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + auto result = context.AcceptItem(); + if (result != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); + + return result == Result::True; +} + +void ax::NodeEditor::RejectNewItem() { + auto& context = s_Editor->GetItemCreator(); + + context.RejectItem(); +} + +void ax::NodeEditor::RejectNewItem(const ImVec4& color, float thickness) { + using Result = ax::NodeEditor::Detail::CreateItemAction::Result; + + auto& context = s_Editor->GetItemCreator(); + + if (context.RejectItem() != Result::Indeterminate) + context.SetStyle(ImColor(color), thickness); +} + +void ax::NodeEditor::EndCreate() { + auto& context = s_Editor->GetItemCreator(); + + context.End(); +} + +bool ax::NodeEditor::BeginDelete() { + auto& context = s_Editor->GetItemDeleter(); + + return context.Begin(); +} + +bool ax::NodeEditor::QueryDeletedLink(LinkId* linkId, PinId* startId, + PinId* endId) { + auto& context = s_Editor->GetItemDeleter(); + + return context.QueryLink(linkId, startId, endId); +} + +bool ax::NodeEditor::QueryDeletedNode(NodeId* nodeId) { + auto& context = s_Editor->GetItemDeleter(); + + return context.QueryNode(nodeId); +} + +bool ax::NodeEditor::AcceptDeletedItem(bool deleteDependencies) { + auto& context = s_Editor->GetItemDeleter(); + + return context.AcceptItem(deleteDependencies); +} + +void ax::NodeEditor::RejectDeletedItem() { + auto& context = s_Editor->GetItemDeleter(); + + context.RejectItem(); +} + +void ax::NodeEditor::EndDelete() { + auto& context = s_Editor->GetItemDeleter(); + + context.End(); +} + +void ax::NodeEditor::SetNodePosition(NodeId nodeId, const ImVec2& position) { + s_Editor->SetNodePosition(nodeId, position); +} + +void ax::NodeEditor::SetGroupSize(NodeId nodeId, const ImVec2& size) { + s_Editor->SetGroupSize(nodeId, size); +} + +ImVec2 ax::NodeEditor::GetNodePosition(NodeId nodeId) { + return s_Editor->GetNodePosition(nodeId); +} + +ImVec2 ax::NodeEditor::GetNodeSize(NodeId nodeId) { + return s_Editor->GetNodeSize(nodeId); +} + +void ax::NodeEditor::CenterNodeOnScreen(NodeId nodeId) { + if (auto node = s_Editor->FindNode(nodeId)) node->CenterOnScreenInNextFrame(); +} + +void ax::NodeEditor::SetNodeZPosition(NodeId nodeId, float z) { + s_Editor->SetNodeZPosition(nodeId, z); +} + +float ax::NodeEditor::GetNodeZPosition(NodeId nodeId) { + return s_Editor->GetNodeZPosition(nodeId); +} + +void ax::NodeEditor::RestoreNodeState(NodeId nodeId) { + if (auto node = s_Editor->FindNode(nodeId)) + s_Editor->MarkNodeToRestoreState(node); +} + +void ax::NodeEditor::Suspend() { s_Editor->Suspend(); } + +void ax::NodeEditor::Resume() { s_Editor->Resume(); } + +bool ax::NodeEditor::IsSuspended() { return s_Editor->IsSuspended(); } + +bool ax::NodeEditor::IsActive() { return s_Editor->IsFocused(); } + +bool ax::NodeEditor::HasSelectionChanged() { + return s_Editor->HasSelectionChanged(); +} + +int ax::NodeEditor::GetSelectedObjectCount() { + return (int)s_Editor->GetSelectedObjects().size(); +} + +int ax::NodeEditor::GetSelectedNodes(NodeId* nodes, int size) { + return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, + [](auto object) { return object->AsNode() != nullptr; }); +} + +int ax::NodeEditor::GetSelectedLinks(LinkId* links, int size) { + return BuildIdList(s_Editor->GetSelectedObjects(), links, size, + [](auto object) { return object->AsLink() != nullptr; }); +} + +bool ax::NodeEditor::IsNodeSelected(NodeId nodeId) { + if (auto node = s_Editor->FindNode(nodeId)) + return s_Editor->IsSelected(node); + else + return false; +} + +bool ax::NodeEditor::IsLinkSelected(LinkId linkId) { + if (auto link = s_Editor->FindLink(linkId)) + return s_Editor->IsSelected(link); + else + return false; +} + +void ax::NodeEditor::ClearSelection() { s_Editor->ClearSelection(); } + +void ax::NodeEditor::SelectNode(NodeId nodeId, bool append) { + if (auto node = s_Editor->FindNode(nodeId)) { + if (append) + s_Editor->SelectObject(node); + else + s_Editor->SetSelectedObject(node); + } +} + +void ax::NodeEditor::SelectLink(LinkId linkId, bool append) { + if (auto link = s_Editor->FindLink(linkId)) { + if (append) + s_Editor->SelectObject(link); + else + s_Editor->SetSelectedObject(link); + } +} + +void ax::NodeEditor::DeselectNode(NodeId nodeId) { + if (auto node = s_Editor->FindNode(nodeId)) s_Editor->DeselectObject(node); +} + +void ax::NodeEditor::DeselectLink(LinkId linkId) { + if (auto link = s_Editor->FindLink(linkId)) s_Editor->DeselectObject(link); +} + +bool ax::NodeEditor::DeleteNode(NodeId nodeId) { + if (auto node = s_Editor->FindNode(nodeId)) + return s_Editor->GetItemDeleter().Add(node); + else + return false; +} + +bool ax::NodeEditor::DeleteLink(LinkId linkId) { + if (auto link = s_Editor->FindLink(linkId)) + return s_Editor->GetItemDeleter().Add(link); + else + return false; +} + +bool ax::NodeEditor::HasAnyLinks(NodeId nodeId) { + return s_Editor->HasAnyLinks(nodeId); +} + +bool ax::NodeEditor::HasAnyLinks(PinId pinId) { + return s_Editor->HasAnyLinks(pinId); +} + +int ax::NodeEditor::BreakLinks(NodeId nodeId) { + return s_Editor->BreakLinks(nodeId); +} + +int ax::NodeEditor::BreakLinks(PinId pinId) { + return s_Editor->BreakLinks(pinId); +} + +void ax::NodeEditor::NavigateToContent(float duration) { + s_Editor->NavigateTo(s_Editor->GetContentBounds(), true, duration); +} + +void ax::NodeEditor::NavigateToSelection(bool zoomIn, float duration) { + s_Editor->NavigateTo(s_Editor->GetSelectionBounds(), zoomIn, duration); +} + +bool ax::NodeEditor::ShowNodeContextMenu(NodeId* nodeId) { + return s_Editor->GetContextMenu().ShowNodeContextMenu(nodeId); +} + +bool ax::NodeEditor::ShowPinContextMenu(PinId* pinId) { + return s_Editor->GetContextMenu().ShowPinContextMenu(pinId); +} + +bool ax::NodeEditor::ShowLinkContextMenu(LinkId* linkId) { + return s_Editor->GetContextMenu().ShowLinkContextMenu(linkId); +} + +bool ax::NodeEditor::ShowBackgroundContextMenu() { + return s_Editor->GetContextMenu().ShowBackgroundContextMenu(); +} + +void ax::NodeEditor::EnableShortcuts(bool enable) { + s_Editor->EnableShortcuts(enable); +} + +bool ax::NodeEditor::AreShortcutsEnabled() { + return s_Editor->AreShortcutsEnabled(); +} + +bool ax::NodeEditor::BeginShortcut() { return s_Editor->GetShortcut().Begin(); } + +bool ax::NodeEditor::AcceptCut() { return s_Editor->GetShortcut().AcceptCut(); } + +bool ax::NodeEditor::AcceptCopy() { + return s_Editor->GetShortcut().AcceptCopy(); +} + +bool ax::NodeEditor::AcceptPaste() { + return s_Editor->GetShortcut().AcceptPaste(); +} + +bool ax::NodeEditor::AcceptDuplicate() { + return s_Editor->GetShortcut().AcceptDuplicate(); +} + +bool ax::NodeEditor::AcceptCreateNode() { + return s_Editor->GetShortcut().AcceptCreateNode(); +} + +int ax::NodeEditor::GetActionContextSize() { + return static_cast(s_Editor->GetShortcut().m_Context.size()); +} + +int ax::NodeEditor::GetActionContextNodes(NodeId* nodes, int size) { + return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, + [](auto object) { return object->AsNode() != nullptr; }); +} + +int ax::NodeEditor::GetActionContextLinks(LinkId* links, int size) { + return BuildIdList(s_Editor->GetSelectedObjects(), links, size, + [](auto object) { return object->AsLink() != nullptr; }); +} + +void ax::NodeEditor::EndShortcut() { return s_Editor->GetShortcut().End(); } + +float ax::NodeEditor::GetCurrentZoom() { return s_Editor->GetView().InvScale; } + +ax::NodeEditor::NodeId ax::NodeEditor::GetHoveredNode() { + return s_Editor->GetHoveredNode(); +} + +ax::NodeEditor::PinId ax::NodeEditor::GetHoveredPin() { + return s_Editor->GetHoveredPin(); +} + +ax::NodeEditor::LinkId ax::NodeEditor::GetHoveredLink() { + return s_Editor->GetHoveredLink(); +} + +ax::NodeEditor::NodeId ax::NodeEditor::GetDoubleClickedNode() { + return s_Editor->GetDoubleClickedNode(); +} + +ax::NodeEditor::PinId ax::NodeEditor::GetDoubleClickedPin() { + return s_Editor->GetDoubleClickedPin(); +} + +ax::NodeEditor::LinkId ax::NodeEditor::GetDoubleClickedLink() { + return s_Editor->GetDoubleClickedLink(); +} + +bool ax::NodeEditor::IsBackgroundClicked() { + return s_Editor->IsBackgroundClicked(); +} + +bool ax::NodeEditor::IsBackgroundDoubleClicked() { + return s_Editor->IsBackgroundDoubleClicked(); +} + +ImGuiMouseButton ax::NodeEditor::GetBackgroundClickButtonIndex() { + return s_Editor->GetBackgroundClickButtonIndex(); +} + +ImGuiMouseButton ax::NodeEditor::GetBackgroundDoubleClickButtonIndex() { + return s_Editor->GetBackgroundDoubleClickButtonIndex(); +} + +bool ax::NodeEditor::GetLinkPins(LinkId linkId, PinId* startPinId, + PinId* endPinId) { + auto link = s_Editor->FindLink(linkId); + if (!link) return false; + + if (startPinId) *startPinId = link->m_StartPin->m_ID; + if (endPinId) *endPinId = link->m_EndPin->m_ID; + + return true; +} + +bool ax::NodeEditor::PinHadAnyLinks(PinId pinId) { + return s_Editor->PinHadAnyLinks(pinId); +} + +ImVec2 ax::NodeEditor::GetScreenSize() { return s_Editor->GetRect().GetSize(); } + +ImVec2 ax::NodeEditor::ScreenToCanvas(const ImVec2& pos) { + return s_Editor->ToCanvas(pos); +} + +ImVec2 ax::NodeEditor::CanvasToScreen(const ImVec2& pos) { + return s_Editor->ToScreen(pos); +} + +int ax::NodeEditor::GetNodeCount() { return s_Editor->CountLiveNodes(); } + +int ax::NodeEditor::GetOrderedNodeIds(NodeId* nodes, int size) { + return s_Editor->GetNodeIds(nodes, size); +} diff --git a/symmetri/gui/imgui_node_editor_internal.h b/symmetri/gui/imgui_node_editor_internal.h new file mode 100644 index 0000000..e4d158e --- /dev/null +++ b/symmetri/gui/imgui_node_editor_internal.h @@ -0,0 +1,1488 @@ +//------------------------------------------------------------------------------ +// VERSION 0.9.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +#ifndef __IMGUI_NODE_EDITOR_INTERNAL_H__ +#define __IMGUI_NODE_EDITOR_INTERNAL_H__ +#pragma once + +//------------------------------------------------------------------------------ +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include "imgui_node_editor.h" + +//------------------------------------------------------------------------------ +#include +#include + +#include +#include + +#include "crude_json.h" +#include "imgui_bezier_math.h" +#include "imgui_canvas.h" +#include "imgui_extra_math.h" + +//------------------------------------------------------------------------------ +namespace ax { +namespace NodeEditor { +namespace Detail { + +//------------------------------------------------------------------------------ +namespace ed = ax::NodeEditor::Detail; +namespace json = crude_json; + +//------------------------------------------------------------------------------ +using std::string; +using std::vector; + +//------------------------------------------------------------------------------ +void Log(const char* fmt, ...); + +//------------------------------------------------------------------------------ +// inline ImRect ToRect(const ax::rectf& rect); +// inline ImRect ToRect(const ax::rect& rect); +inline ImRect ImGui_GetItemRect(); +inline ImVec2 ImGui_GetMouseClickPos(ImGuiMouseButton buttonIndex); + +//------------------------------------------------------------------------------ +// https://stackoverflow.com/a/36079786 +#define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \ + \ + template \ + class __trait_name__ { \ + using check_type = ::std::remove_const_t<__boost_has_member_T__>; \ + struct no_type { \ + char x[2]; \ + }; \ + using yes_type = char; \ + \ + struct base { \ + void __member_name__() {} \ + }; \ + struct mixin : public base, public check_type {}; \ + \ + template \ + struct aux {}; \ + \ + template \ + static no_type test(aux<&U::__member_name__>*); \ + template \ + static yes_type test(...); \ + \ + public: \ + static constexpr bool value = \ + (sizeof(yes_type) == sizeof(test(0))); \ + } + +DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale); + +#undef DECLARE_HAS_MEMBER + +struct FringeScaleRef { + // Overload is present when ImDrawList does have _FringeScale member variable. + template + static float& Get( + typename std::enable_if::value, T>::type* drawList) { + return drawList->_FringeScale; + } + + // Overload is present when ImDrawList does not have _FringeScale member + // variable. + template + static float& Get( + typename std::enable_if::value, T>::type*) { + static float placeholder = 1.0f; + return placeholder; + } +}; + +static inline float& ImFringeScaleRef(ImDrawList* drawList) { + return FringeScaleRef::Get(drawList); +} + +struct FringeScaleScope { + FringeScaleScope(float scale) + : m_LastFringeScale(ImFringeScaleRef(ImGui::GetWindowDrawList())) { + ImFringeScaleRef(ImGui::GetWindowDrawList()) = scale; + } + + ~FringeScaleScope() { + ImFringeScaleRef(ImGui::GetWindowDrawList()) = m_LastFringeScale; + } + + private: + float m_LastFringeScale; +}; + +//------------------------------------------------------------------------------ +enum class ObjectType { None, Node, Link, Pin }; + +using ax::NodeEditor::PinKind; +using ax::NodeEditor::SaveReasonFlags; +using ax::NodeEditor::StyleColor; +using ax::NodeEditor::StyleVar; + +using ax::NodeEditor::LinkId; +using ax::NodeEditor::NodeId; +using ax::NodeEditor::PinId; + +struct ObjectId final : Details::SafePointerType { + using Super = Details::SafePointerType; + using Super::Super; + + ObjectId() : Super(Invalid), m_Type(ObjectType::None) {} + ObjectId(PinId pinId) : Super(pinId.AsPointer()), m_Type(ObjectType::Pin) {} + ObjectId(NodeId nodeId) + : Super(nodeId.AsPointer()), m_Type(ObjectType::Node) {} + ObjectId(LinkId linkId) + : Super(linkId.AsPointer()), m_Type(ObjectType::Link) {} + + explicit operator PinId() const { return AsPinId(); } + explicit operator NodeId() const { return AsNodeId(); } + explicit operator LinkId() const { return AsLinkId(); } + + PinId AsPinId() const { + IM_ASSERT(IsPinId()); + return PinId(AsPointer()); + } + NodeId AsNodeId() const { + IM_ASSERT(IsNodeId()); + return NodeId(AsPointer()); + } + LinkId AsLinkId() const { + IM_ASSERT(IsLinkId()); + return LinkId(AsPointer()); + } + + bool IsPinId() const { return m_Type == ObjectType::Pin; } + bool IsNodeId() const { return m_Type == ObjectType::Node; } + bool IsLinkId() const { return m_Type == ObjectType::Link; } + + ObjectType Type() const { return m_Type; } + + private: + ObjectType m_Type; +}; + +struct EditorContext; + +struct Node; +struct Pin; +struct Link; + +template +struct ObjectWrapper { + Id m_ID; + T* m_Object; + + T* operator->() { return m_Object; } + const T* operator->() const { return m_Object; } + + operator T*() { return m_Object; } + operator const T*() const { return m_Object; } + + bool operator<(const ObjectWrapper& rhs) const { + return m_ID.AsPointer() < rhs.m_ID.AsPointer(); + } +}; + +struct Object { + enum DrawFlags { + None = 0, + Hovered = 1, + Selected = 2, + Highlighted = 4, + }; + + inline friend DrawFlags operator|(DrawFlags lhs, DrawFlags rhs) { + return static_cast(static_cast(lhs) | + static_cast(rhs)); + } + inline friend DrawFlags operator&(DrawFlags lhs, DrawFlags rhs) { + return static_cast(static_cast(lhs) & + static_cast(rhs)); + } + inline friend DrawFlags& operator|=(DrawFlags& lhs, DrawFlags rhs) { + lhs = lhs | rhs; + return lhs; + } + inline friend DrawFlags& operator&=(DrawFlags& lhs, DrawFlags rhs) { + lhs = lhs & rhs; + return lhs; + } + + EditorContext* const Editor; + + bool m_IsLive; + bool m_IsSelected; + bool m_DeleteOnNewFrame; + + Object(EditorContext* editor) + : Editor(editor), + m_IsLive(true), + m_IsSelected(false), + m_DeleteOnNewFrame(false) {} + + virtual ~Object() = default; + + virtual ObjectId ID() = 0; + + bool IsVisible() const { + if (!m_IsLive) return false; + + const auto bounds = GetBounds(); + + return ImGui::IsRectVisible(bounds.Min, bounds.Max); + } + + virtual void Reset() { m_IsLive = false; } + + virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) = 0; + + virtual bool AcceptDrag() { return false; } + virtual void UpdateDrag(const ImVec2& offset) { IM_UNUSED(offset); } + virtual bool EndDrag() { return false; } + virtual ImVec2 DragStartLocation() { return GetBounds().Min; } + + virtual bool IsDraggable() { + bool result = AcceptDrag(); + EndDrag(); + return result; + } + virtual bool IsSelectable() { return false; } + + virtual bool TestHit(const ImVec2& point, float extraThickness = 0.0f) const { + if (!m_IsLive) return false; + + auto bounds = GetBounds(); + if (extraThickness > 0) bounds.Expand(extraThickness); + + return bounds.Contains(point); + } + + virtual bool TestHit(const ImRect& rect, bool allowIntersect = true) const { + if (!m_IsLive) return false; + + const auto bounds = GetBounds(); + + return !ImRect_IsEmpty(bounds) && + (allowIntersect ? bounds.Overlaps(rect) : rect.Contains(bounds)); + } + + virtual ImRect GetBounds() const = 0; + + virtual Node* AsNode() { return nullptr; } + virtual Pin* AsPin() { return nullptr; } + virtual Link* AsLink() { return nullptr; } +}; + +struct Pin final : Object { + using IdType = PinId; + + PinId m_ID; + PinKind m_Kind; + Node* m_Node; + ImRect m_Bounds; + ImRect m_Pivot; + Pin* m_PreviousPin; + ImU32 m_Color; + ImU32 m_BorderColor; + float m_BorderWidth; + float m_Rounding; + int m_Corners; + ImVec2 m_Dir; + float m_Strength; + float m_Radius; + float m_ArrowSize; + float m_ArrowWidth; + bool m_SnapLinkToDir; + bool m_HasConnection; + bool m_HadConnection; + + Pin(EditorContext* editor, PinId id, PinKind kind) + : Object(editor), + m_ID(id), + m_Kind(kind), + m_Node(nullptr), + m_Bounds(), + m_PreviousPin(nullptr), + m_Color(IM_COL32_WHITE), + m_BorderColor(IM_COL32_BLACK), + m_BorderWidth(0), + m_Rounding(0), + m_Corners(0), + m_Dir(0, 0), + m_Strength(0), + m_Radius(0), + m_ArrowSize(0), + m_ArrowWidth(0), + m_SnapLinkToDir(true), + m_HasConnection(false), + m_HadConnection(false) {} + + virtual ObjectId ID() override { return m_ID; } + + virtual void Reset() override final { + m_HadConnection = m_HasConnection && m_IsLive; + m_HasConnection = false; + + Object::Reset(); + } + + virtual void Draw(ImDrawList* drawList, + DrawFlags flags = None) override final; + + ImVec2 GetClosestPoint(const ImVec2& p) const; + ImLine GetClosestLine(const Pin* pin) const; + + virtual ImRect GetBounds() const override final { return m_Bounds; } + + virtual Pin* AsPin() override final { return this; } +}; + +enum class NodeType { Node, Group }; + +enum class NodeRegion : uint8_t { + None = 0x00, + Top = 0x01, + Bottom = 0x02, + Left = 0x04, + Right = 0x08, + Center = 0x10, + Header = 0x20, + TopLeft = Top | Left, + TopRight = Top | Right, + BottomLeft = Bottom | Left, + BottomRight = Bottom | Right, +}; + +inline NodeRegion operator|(NodeRegion lhs, NodeRegion rhs) { + return static_cast(static_cast(lhs) | + static_cast(rhs)); +} +inline NodeRegion operator&(NodeRegion lhs, NodeRegion rhs) { + return static_cast(static_cast(lhs) & + static_cast(rhs)); +} + +struct Node final : Object { + using IdType = NodeId; + + NodeId m_ID; + NodeType m_Type; + ImRect m_Bounds; + float m_ZPosition; + int m_Channel; + Pin* m_LastPin; + ImVec2 m_DragStart; + + ImU32 m_Color; + ImU32 m_BorderColor; + float m_BorderWidth; + float m_Rounding; + + ImU32 m_GroupColor; + ImU32 m_GroupBorderColor; + float m_GroupBorderWidth; + float m_GroupRounding; + ImRect m_GroupBounds; + + bool m_HighlightConnectedLinks; + + bool m_RestoreState; + bool m_CenterOnScreen; + + Node(EditorContext* editor, NodeId id) + : Object(editor), + m_ID(id), + m_Type(NodeType::Node), + m_Bounds(), + m_ZPosition(0.0f), + m_Channel(0), + m_LastPin(nullptr), + m_DragStart(), + m_Color(IM_COL32_WHITE), + m_BorderColor(IM_COL32_BLACK), + m_BorderWidth(0), + m_Rounding(0), + m_GroupBounds(), + m_HighlightConnectedLinks(false), + m_RestoreState(false), + m_CenterOnScreen(false) {} + + virtual ObjectId ID() override { return m_ID; } + + bool AcceptDrag() override; + void UpdateDrag(const ImVec2& offset) override; + bool EndDrag() override; // return true, when changed + ImVec2 DragStartLocation() override { return m_DragStart; } + + virtual bool IsSelectable() override { return true; } + + virtual void Draw(ImDrawList* drawList, + DrawFlags flags = None) override final; + void DrawBorder(ImDrawList* drawList, ImU32 color, float thickness = 1.0f, + float offset = 0.0f); + + void GetGroupedNodes(std::vector& result, bool append = false); + + void CenterOnScreenInNextFrame() { m_CenterOnScreen = true; } + + ImRect GetRegionBounds(NodeRegion region) const; + NodeRegion GetRegion(const ImVec2& point) const; + + virtual ImRect GetBounds() const override final { return m_Bounds; } + + virtual Node* AsNode() override final { return this; } +}; + +struct Link final : Object { + using IdType = LinkId; + + LinkId m_ID; + Pin* m_StartPin; + Pin* m_EndPin; + ImU32 m_Color; + ImU32 m_HighlightColor; + float m_Thickness; + ImVec2 m_Start; + ImVec2 m_End; + + Link(EditorContext* editor, LinkId id) + : Object(editor), + m_ID(id), + m_StartPin(nullptr), + m_EndPin(nullptr), + m_Color(IM_COL32_WHITE), + m_Thickness(1.0f) {} + + virtual ObjectId ID() override { return m_ID; } + + virtual bool IsSelectable() override { return true; } + + virtual void Draw(ImDrawList* drawList, + DrawFlags flags = None) override final; + void Draw(ImDrawList* drawList, ImU32 color, + float extraThickness = 0.0f) const; + + void UpdateEndpoints(); + + ImCubicBezierPoints GetCurve() const; + + virtual bool TestHit(const ImVec2& point, + float extraThickness = 0.0f) const override final; + virtual bool TestHit(const ImRect& rect, + bool allowIntersect = true) const override final; + + virtual ImRect GetBounds() const override final; + + virtual Link* AsLink() override final { return this; } +}; + +struct NodeSettings { + NodeId m_ID; + ImVec2 m_Location; + ImVec2 m_Size; + ImVec2 m_GroupSize; + bool m_WasUsed; + + bool m_Saved; + bool m_IsDirty; + SaveReasonFlags m_DirtyReason; + + NodeSettings(NodeId id) + : m_ID(id), + m_Location(0, 0), + m_Size(0, 0), + m_GroupSize(0, 0), + m_WasUsed(false), + m_Saved(false), + m_IsDirty(false), + m_DirtyReason(SaveReasonFlags::None) {} + + void ClearDirty(); + void MakeDirty(SaveReasonFlags reason); + + json::value Serialize(); + + static bool Parse(const std::string& string, NodeSettings& settings); + static bool Parse(const json::value& data, NodeSettings& result); +}; + +struct Settings { + bool m_IsDirty; + SaveReasonFlags m_DirtyReason; + + vector m_Nodes; + vector m_Selection; + ImVec2 m_ViewScroll; + float m_ViewZoom; + ImRect m_VisibleRect; + + Settings() + : m_IsDirty(false), + m_DirtyReason(SaveReasonFlags::None), + m_ViewScroll(0, 0), + m_ViewZoom(1.0f), + m_VisibleRect() {} + + NodeSettings* AddNode(NodeId id); + NodeSettings* FindNode(NodeId id); + void RemoveNode(NodeId id); + + void ClearDirty(Node* node = nullptr); + void MakeDirty(SaveReasonFlags reason, Node* node = nullptr); + + std::string Serialize(); + + static bool Parse(const std::string& string, Settings& settings); +}; + +struct Control { + Object* HotObject; + Object* ActiveObject; + Object* ClickedObject; + Object* DoubleClickedObject; + Node* HotNode; + Node* ActiveNode; + Node* ClickedNode; + Node* DoubleClickedNode; + Pin* HotPin; + Pin* ActivePin; + Pin* ClickedPin; + Pin* DoubleClickedPin; + Link* HotLink; + Link* ActiveLink; + Link* ClickedLink; + Link* DoubleClickedLink; + bool BackgroundHot; + bool BackgroundActive; + int BackgroundClickButtonIndex; + int BackgroundDoubleClickButtonIndex; + + Control() + : Control(nullptr, nullptr, nullptr, nullptr, false, false, -1, -1) {} + + Control(Object* hotObject, Object* activeObject, Object* clickedObject, + Object* doubleClickedObject, bool backgroundHot, + bool backgroundActive, int backgroundClickButtonIndex, + int backgroundDoubleClickButtonIndex) + : HotObject(hotObject), + ActiveObject(activeObject), + ClickedObject(clickedObject), + DoubleClickedObject(doubleClickedObject), + HotNode(nullptr), + ActiveNode(nullptr), + ClickedNode(nullptr), + DoubleClickedNode(nullptr), + HotPin(nullptr), + ActivePin(nullptr), + ClickedPin(nullptr), + DoubleClickedPin(nullptr), + HotLink(nullptr), + ActiveLink(nullptr), + ClickedLink(nullptr), + DoubleClickedLink(nullptr), + BackgroundHot(backgroundHot), + BackgroundActive(backgroundActive), + BackgroundClickButtonIndex(backgroundClickButtonIndex), + BackgroundDoubleClickButtonIndex(backgroundDoubleClickButtonIndex) { + if (hotObject) { + HotNode = hotObject->AsNode(); + HotPin = hotObject->AsPin(); + HotLink = hotObject->AsLink(); + + if (HotPin) HotNode = HotPin->m_Node; + } + + if (activeObject) { + ActiveNode = activeObject->AsNode(); + ActivePin = activeObject->AsPin(); + ActiveLink = activeObject->AsLink(); + } + + if (clickedObject) { + ClickedNode = clickedObject->AsNode(); + ClickedPin = clickedObject->AsPin(); + ClickedLink = clickedObject->AsLink(); + } + + if (doubleClickedObject) { + DoubleClickedNode = doubleClickedObject->AsNode(); + DoubleClickedPin = doubleClickedObject->AsPin(); + DoubleClickedLink = doubleClickedObject->AsLink(); + } + } +}; + +struct NavigateAction; +struct SizeAction; +struct DragAction; +struct SelectAction; +struct CreateItemAction; +struct DeleteItemsAction; +struct ContextMenuAction; +struct ShortcutAction; + +struct AnimationController; +struct FlowAnimationController; + +struct Animation { + enum State { Playing, Stopped }; + + EditorContext* Editor; + State m_State; + float m_Time; + float m_Duration; + + Animation(EditorContext* editor); + virtual ~Animation(); + + void Play(float duration); + void Stop(); + void Finish(); + void Update(); + + bool IsPlaying() const { return m_State == Playing; } + + float GetProgress() const { return m_Time / m_Duration; } + + protected: + virtual void OnPlay() {} + virtual void OnFinish() {} + virtual void OnStop() {} + + virtual void OnUpdate(float progress) { IM_UNUSED(progress); } +}; + +struct NavigateAnimation final : Animation { + NavigateAction& Action; + ImRect m_Start; + ImRect m_Target; + + NavigateAnimation(EditorContext* editor, NavigateAction& scrollAction); + + void NavigateTo(const ImRect& target, float duration); + + private: + void OnUpdate(float progress) override final; + void OnStop() override final; + void OnFinish() override final; +}; + +struct FlowAnimation final : Animation { + FlowAnimationController* Controller; + Link* m_Link; + float m_Speed; + float m_MarkerDistance; + float m_Offset; + + FlowAnimation(FlowAnimationController* controller); + + void Flow(Link* link, float markerDistance, float speed, float duration); + + void Draw(ImDrawList* drawList); + + private: + struct CurvePoint { + float Distance; + ImVec2 Point; + }; + + ImVec2 m_LastStart; + ImVec2 m_LastEnd; + float m_PathLength; + vector m_Path; + + bool IsLinkValid() const; + bool IsPathValid() const; + void UpdatePath(); + void ClearPath(); + + ImVec2 SamplePath(float distance) const; + + void OnUpdate(float progress) override final; + void OnStop() override final; +}; + +struct AnimationController { + EditorContext* Editor; + + AnimationController(EditorContext* editor) : Editor(editor) {} + + virtual ~AnimationController() {} + + virtual void Draw(ImDrawList* drawList) { IM_UNUSED(drawList); } +}; + +struct FlowAnimationController final : AnimationController { + FlowAnimationController(EditorContext* editor); + virtual ~FlowAnimationController(); + + void Flow(Link* link, FlowDirection direction = FlowDirection::Forward); + + virtual void Draw(ImDrawList* drawList) override final; + + void Release(FlowAnimation* animation); + + private: + FlowAnimation* GetOrCreate(Link* link); + + vector m_Animations; + vector m_FreePool; +}; + +struct EditorAction { + enum AcceptResult { False, True, Possible }; + + EditorAction(EditorContext* editor) : Editor(editor) {} + + virtual ~EditorAction() {} + + virtual const char* GetName() const = 0; + + virtual AcceptResult Accept(const Control& control) = 0; + virtual bool Process(const Control& control) = 0; + virtual void Reject() { + } // celled when Accept return 'Possible' and was rejected + + virtual ImGuiMouseCursor GetCursor() { return ImGuiMouseCursor_Arrow; } + + virtual bool IsDragging() { return false; } + + virtual void ShowMetrics() {} + + virtual NavigateAction* AsNavigate() { return nullptr; } + virtual SizeAction* AsSize() { return nullptr; } + virtual DragAction* AsDrag() { return nullptr; } + virtual SelectAction* AsSelect() { return nullptr; } + virtual CreateItemAction* AsCreateItem() { return nullptr; } + virtual DeleteItemsAction* AsDeleteItems() { return nullptr; } + virtual ContextMenuAction* AsContextMenu() { return nullptr; } + virtual ShortcutAction* AsCutCopyPaste() { return nullptr; } + + EditorContext* Editor; +}; + +struct NavigateAction final : EditorAction { + enum class ZoomMode { None, Exact, WithMargin }; + + enum class NavigationReason { + Unknown, + MouseZoom, + Selection, + Object, + Content, + Edge + }; + + bool m_IsActive; + float m_Zoom; + ImRect m_VisibleRect; + ImVec2 m_Scroll; + ImVec2 m_ScrollStart; + ImVec2 m_ScrollDelta; + + NavigateAction(EditorContext* editor, ImGuiEx::Canvas& canvas); + + virtual const char* GetName() const override final { return "Navigate"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual void ShowMetrics() override final; + + virtual NavigateAction* AsNavigate() override final { return this; } + + void NavigateTo(const ImRect& bounds, ZoomMode zoomMode, + float duration = -1.0f, + NavigationReason reason = NavigationReason::Unknown); + void StopNavigation(); + void FinishNavigation(); + + bool MoveOverEdge(const ImVec2& canvasSize); + void StopMoveOverEdge(); + bool IsMovingOverEdge() const { return m_MovingOverEdge; } + ImVec2 GetMoveScreenOffset() const { return m_MoveScreenOffset; } + + void SetWindow(ImVec2 position, ImVec2 size); + ImVec2 GetWindowScreenPos() const { return m_WindowScreenPos; }; + ImVec2 GetWindowScreenSize() const { return m_WindowScreenSize; }; + + ImGuiEx::CanvasView GetView() const; + ImVec2 GetViewOrigin() const; + float GetViewScale() const; + + void SetViewRect(const ImRect& rect); + ImRect GetViewRect() const; + + private: + ImGuiEx::Canvas& m_Canvas; + ImVec2 m_WindowScreenPos; + ImVec2 m_WindowScreenSize; + + NavigateAnimation m_Animation; + NavigationReason m_Reason; + uint64_t m_LastSelectionId; + Object* m_LastObject; + bool m_MovingOverEdge; + ImVec2 m_MoveScreenOffset; + + const float* m_ZoomLevels; + int m_ZoomLevelCount; + + bool HandleZoom(const Control& control); + + void NavigateTo(const ImRect& target, float duration = -1.0f, + NavigationReason reason = NavigationReason::Unknown); + + float MatchZoom(int steps, float fallbackZoom); + int MatchZoomIndex(int direction); + + static const float s_DefaultZoomLevels[]; + static const int s_DefaultZoomLevelCount; +}; + +struct SizeAction final : EditorAction { + bool m_IsActive; + bool m_Clean; + Node* m_SizedNode; + + SizeAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Size"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual ImGuiMouseCursor GetCursor() override final { return m_Cursor; } + + virtual void ShowMetrics() override final; + + virtual SizeAction* AsSize() override final { return this; } + + virtual bool IsDragging() override final { return m_IsActive; } + + const ImRect& GetStartGroupBounds() const { return m_StartGroupBounds; } + + private: + NodeRegion GetRegion(Node* node); + ImGuiMouseCursor ChooseCursor(NodeRegion region); + + ImRect m_StartBounds; + ImRect m_StartGroupBounds; + ImVec2 m_LastSize; + ImVec2 m_MinimumSize; + ImVec2 m_LastDragOffset; + ed::NodeRegion m_Pivot; + ImGuiMouseCursor m_Cursor; +}; + +struct DragAction final : EditorAction { + bool m_IsActive; + bool m_Clear; + Object* m_DraggedObject; + vector m_Objects; + + DragAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Drag"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual ImGuiMouseCursor GetCursor() override final { + return ImGuiMouseCursor_ResizeAll; + } + + virtual bool IsDragging() override final { return m_IsActive; } + + virtual void ShowMetrics() override final; + + virtual DragAction* AsDrag() override final { return this; } +}; + +struct SelectAction final : EditorAction { + bool m_IsActive; + + bool m_SelectGroups; + bool m_SelectLinkMode; + bool m_CommitSelection; + ImVec2 m_StartPoint; + ImVec2 m_EndPoint; + vector m_CandidateObjects; + vector m_SelectedObjectsAtStart; + + Animation m_Animation; + + SelectAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Select"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual void ShowMetrics() override final; + + virtual bool IsDragging() override final { return m_IsActive; } + + virtual SelectAction* AsSelect() override final { return this; } + + void Draw(ImDrawList* drawList); +}; + +struct ContextMenuAction final : EditorAction { + enum Menu { None, Node, Pin, Link, Background }; + + Menu m_CandidateMenu; + Menu m_CurrentMenu; + ObjectId m_ContextId; + + ContextMenuAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Context Menu"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + virtual void Reject() override final; + + virtual void ShowMetrics() override final; + + virtual ContextMenuAction* AsContextMenu() override final { return this; } + + bool ShowNodeContextMenu(NodeId* nodeId); + bool ShowPinContextMenu(PinId* pinId); + bool ShowLinkContextMenu(LinkId* linkId); + bool ShowBackgroundContextMenu(); +}; + +struct ShortcutAction final : EditorAction { + enum Action { None, Cut, Copy, Paste, Duplicate, CreateNode }; + + bool m_IsActive; + bool m_InAction; + Action m_CurrentAction; + vector m_Context; + + ShortcutAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Shortcut"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + virtual void Reject() override final; + + virtual void ShowMetrics() override final; + + virtual ShortcutAction* AsCutCopyPaste() override final { return this; } + + bool Begin(); + void End(); + + bool AcceptCut(); + bool AcceptCopy(); + bool AcceptPaste(); + bool AcceptDuplicate(); + bool AcceptCreateNode(); +}; + +struct CreateItemAction final : EditorAction { + enum Stage { None, Possible, Create }; + + enum Action { Unknown, UserReject, UserAccept }; + + enum Type { NoItem, Node, Link }; + + enum Result { True, False, Indeterminate }; + + bool m_InActive; + Stage m_NextStage; + + Stage m_CurrentStage; + Type m_ItemType; + Action m_UserAction; + ImU32 m_LinkColor; + float m_LinkThickness; + Pin* m_LinkStart; + Pin* m_LinkEnd; + + bool m_IsActive; + Pin* m_DraggedPin; + + int m_LastChannel = -1; + + CreateItemAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Create Item"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual ImGuiMouseCursor GetCursor() override final { + return ImGuiMouseCursor_Arrow; + } + + virtual void ShowMetrics() override final; + + virtual bool IsDragging() override final { return m_IsActive; } + + virtual CreateItemAction* AsCreateItem() override final { return this; } + + void SetStyle(ImU32 color, float thickness); + + bool Begin(); + void End(); + + Result RejectItem(); + Result AcceptItem(); + + Result QueryLink(PinId* startId, PinId* endId); + Result QueryNode(PinId* pinId); + + private: + bool m_IsInGlobalSpace; + + void DragStart(Pin* startPin); + void DragEnd(); + void DropPin(Pin* endPin); + void DropNode(); + void DropNothing(); +}; + +struct DeleteItemsAction final : EditorAction { + bool m_IsActive; + bool m_InInteraction; + + DeleteItemsAction(EditorContext* editor); + + virtual const char* GetName() const override final { return "Delete Items"; } + + virtual AcceptResult Accept(const Control& control) override final; + virtual bool Process(const Control& control) override final; + + virtual void ShowMetrics() override final; + + virtual DeleteItemsAction* AsDeleteItems() override final { return this; } + + bool Add(Object* object); + + bool Begin(); + void End(); + + bool QueryLink(LinkId* linkId, PinId* startId = nullptr, + PinId* endId = nullptr); + bool QueryNode(NodeId* nodeId); + + bool AcceptItem(bool deleteDependencies); + void RejectItem(); + + private: + enum IteratorType { Unknown, Link, Node }; + enum UserAction { Undetermined, Accepted, Rejected }; + + void DeleteDeadLinks(NodeId nodeId); + void DeleteDeadPins(NodeId nodeId); + + bool QueryItem(ObjectId* itemId, IteratorType itemType); + void RemoveItem(bool deleteDependencies); + Object* DropCurrentItem(); + + vector m_ManuallyDeletedObjects; + + IteratorType m_CurrentItemType; + UserAction m_UserAction; + vector m_CandidateObjects; + int m_CandidateItemIndex; +}; + +struct NodeBuilder { + EditorContext* const Editor; + + Node* m_CurrentNode; + Pin* m_CurrentPin; + + ImRect m_NodeRect; + + ImRect m_PivotRect; + ImVec2 m_PivotAlignment; + ImVec2 m_PivotSize; + ImVec2 m_PivotScale; + bool m_ResolvePinRect; + bool m_ResolvePivot; + + ImRect m_GroupBounds; + bool m_IsGroup; + + ImDrawListSplitter m_Splitter; + ImDrawListSplitter m_PinSplitter; + + NodeBuilder(EditorContext* editor); + ~NodeBuilder(); + + void Begin(NodeId nodeId); + void End(); + + void BeginPin(PinId pinId, PinKind kind); + void EndPin(); + + void PinRect(const ImVec2& a, const ImVec2& b); + void PinPivotRect(const ImVec2& a, const ImVec2& b); + void PinPivotSize(const ImVec2& size); + void PinPivotScale(const ImVec2& scale); + void PinPivotAlignment(const ImVec2& alignment); + + void Group(const ImVec2& size); + + ImDrawList* GetUserBackgroundDrawList() const; + ImDrawList* GetUserBackgroundDrawList(Node* node) const; +}; + +struct HintBuilder { + EditorContext* const Editor; + bool m_IsActive; + Node* m_CurrentNode; + float m_LastFringe = 1.0f; + int m_LastChannel = 0; + + HintBuilder(EditorContext* editor); + + bool Begin(NodeId nodeId); + void End(); + + ImVec2 GetGroupMin(); + ImVec2 GetGroupMax(); + + ImDrawList* GetForegroundDrawList(); + ImDrawList* GetBackgroundDrawList(); +}; + +struct Style : ax::NodeEditor::Style { + void PushColor(StyleColor colorIndex, const ImVec4& color); + void PopColor(int count = 1); + + void PushVar(StyleVar varIndex, float value); + void PushVar(StyleVar varIndex, const ImVec2& value); + void PushVar(StyleVar varIndex, const ImVec4& value); + void PopVar(int count = 1); + + const char* GetColorName(StyleColor colorIndex) const; + + private: + struct ColorModifier { + StyleColor Index; + ImVec4 Value; + }; + + struct VarModifier { + StyleVar Index; + ImVec4 Value; + }; + + float* GetVarFloatAddr(StyleVar idx); + ImVec2* GetVarVec2Addr(StyleVar idx); + ImVec4* GetVarVec4Addr(StyleVar idx); + + vector m_ColorStack; + vector m_VarStack; +}; + +struct Config : ax::NodeEditor::Config { + Config(const ax::NodeEditor::Config* config); + + std::string Load(); + std::string LoadNode(NodeId nodeId); + + void BeginSave(); + bool Save(const std::string& data, SaveReasonFlags flags); + bool SaveNode(NodeId nodeId, const std::string& data, SaveReasonFlags flags); + void EndSave(); +}; + +enum class SuspendFlags : uint8_t { None = 0, KeepSplitter = 1 }; + +inline SuspendFlags operator|(SuspendFlags lhs, SuspendFlags rhs) { + return static_cast(static_cast(lhs) | + static_cast(rhs)); +} +inline SuspendFlags operator&(SuspendFlags lhs, SuspendFlags rhs) { + return static_cast(static_cast(lhs) & + static_cast(rhs)); +} + +struct EditorContext { + EditorContext(const ax::NodeEditor::Config* config = nullptr); + ~EditorContext(); + + const Config& GetConfig() const { return m_Config; } + + Style& GetStyle() { return m_Style; } + + void Begin(const char* id, const ImVec2& size = ImVec2(0, 0)); + void End(); + + bool DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, + float thickness); + + NodeBuilder& GetNodeBuilder() { return m_NodeBuilder; } + HintBuilder& GetHintBuilder() { return m_HintBuilder; } + + EditorAction* GetCurrentAction() { return m_CurrentAction; } + + CreateItemAction& GetItemCreator() { return m_CreateItemAction; } + DeleteItemsAction& GetItemDeleter() { return m_DeleteItemsAction; } + ContextMenuAction& GetContextMenu() { return m_ContextMenuAction; } + ShortcutAction& GetShortcut() { return m_ShortcutAction; } + + const ImGuiEx::CanvasView& GetView() const { return m_Canvas.View(); } + const ImRect& GetViewRect() const { return m_Canvas.ViewRect(); } + const ImRect& GetRect() const { return m_Canvas.Rect(); } + + void SetNodePosition(NodeId nodeId, const ImVec2& screenPosition); + void SetGroupSize(NodeId nodeId, const ImVec2& size); + ImVec2 GetNodePosition(NodeId nodeId); + ImVec2 GetNodeSize(NodeId nodeId); + + void SetNodeZPosition(NodeId nodeId, float z); + float GetNodeZPosition(NodeId nodeId); + + void MarkNodeToRestoreState(Node* node); + void UpdateNodeState(Node* node); + + void RemoveSettings(Object* object); + + void ClearSelection(); + void SelectObject(Object* object); + void DeselectObject(Object* object); + void SetSelectedObject(Object* object); + void ToggleObjectSelection(Object* object); + bool IsSelected(Object* object); + const vector& GetSelectedObjects(); + bool IsAnyNodeSelected(); + bool IsAnyLinkSelected(); + bool HasSelectionChanged(); + uint64_t GetSelectionId() const { return m_SelectionId; } + + Node* FindNodeAt(const ImVec2& p); + void FindNodesInRect(const ImRect& r, vector& result, + bool append = false, bool includeIntersecting = true); + void FindLinksInRect(const ImRect& r, vector& result, + bool append = false); + + bool HasAnyLinks(NodeId nodeId) const; + bool HasAnyLinks(PinId pinId) const; + + int BreakLinks(NodeId nodeId); + int BreakLinks(PinId pinId); + + void FindLinksForNode(NodeId nodeId, vector& result, bool add = false); + + bool PinHadAnyLinks(PinId pinId); + + ImVec2 ToCanvas(const ImVec2& point) const { return m_Canvas.ToLocal(point); } + ImVec2 ToScreen(const ImVec2& point) const { + return m_Canvas.FromLocal(point); + } + + void NotifyLinkDeleted(Link* link); + + void Suspend(SuspendFlags flags = SuspendFlags::None); + void Resume(SuspendFlags flags = SuspendFlags::None); + bool IsSuspended(); + + bool IsFocused(); + bool IsHovered() const; + bool IsHoveredWithoutOverlapp() const; + bool CanAcceptUserInput() const; + + void MakeDirty(SaveReasonFlags reason); + void MakeDirty(SaveReasonFlags reason, Node* node); + + int CountLiveNodes() const; + int CountLivePins() const; + int CountLiveLinks() const; + + Pin* CreatePin(PinId id, PinKind kind); + Node* CreateNode(NodeId id); + Link* CreateLink(LinkId id); + + Node* FindNode(NodeId id); + Pin* FindPin(PinId id); + Link* FindLink(LinkId id); + Object* FindObject(ObjectId id); + + Node* GetNode(NodeId id); + Pin* GetPin(PinId id, PinKind kind); + Link* GetLink(LinkId id); + + Link* FindLinkAt(const ImVec2& p); + + template + ImRect GetBounds(const std::vector& objects) { + ImRect bounds(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + + for (auto object : objects) + if (object->m_IsLive) bounds.Add(object->GetBounds()); + + if (ImRect_IsEmpty(bounds)) bounds = ImRect(); + + return bounds; + } + + template + ImRect GetBounds(const std::vector>& objects) { + ImRect bounds(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + + for (auto object : objects) + if (object.m_Object->m_IsLive) bounds.Add(object.m_Object->GetBounds()); + + if (ImRect_IsEmpty(bounds)) bounds = ImRect(); + + return bounds; + } + + ImRect GetSelectionBounds() { return GetBounds(m_SelectedObjects); } + ImRect GetContentBounds() { return GetBounds(m_Nodes); } + + ImU32 GetColor(StyleColor colorIndex) const; + ImU32 GetColor(StyleColor colorIndex, float alpha) const; + + int GetNodeIds(NodeId* nodes, int size) const; + + void NavigateTo(const ImRect& bounds, bool zoomIn = false, + float duration = -1) { + auto zoomMode = zoomIn ? NavigateAction::ZoomMode::WithMargin + : NavigateAction::ZoomMode::None; + m_NavigateAction.NavigateTo(bounds, zoomMode, duration); + } + + void RegisterAnimation(Animation* animation); + void UnregisterAnimation(Animation* animation); + + void Flow(Link* link, FlowDirection direction); + + void SetUserContext(bool globalSpace = false); + + void EnableShortcuts(bool enable); + bool AreShortcutsEnabled(); + + NodeId GetHoveredNode() const { return m_HoveredNode; } + PinId GetHoveredPin() const { return m_HoveredPin; } + LinkId GetHoveredLink() const { return m_HoveredLink; } + NodeId GetDoubleClickedNode() const { return m_DoubleClickedNode; } + PinId GetDoubleClickedPin() const { return m_DoubleClickedPin; } + LinkId GetDoubleClickedLink() const { return m_DoubleClickedLink; } + bool IsBackgroundClicked() const { return m_BackgroundClickButtonIndex >= 0; } + bool IsBackgroundDoubleClicked() const { + return m_BackgroundDoubleClickButtonIndex >= 0; + } + ImGuiMouseButton GetBackgroundClickButtonIndex() const { + return m_BackgroundClickButtonIndex; + } + ImGuiMouseButton GetBackgroundDoubleClickButtonIndex() const { + return m_BackgroundDoubleClickButtonIndex; + } + + float AlignPointToGrid(float p) const { + if (!ImGui::GetIO().KeyAlt) + return p - ImFmod(p, 16.0f); + else + return p; + } + + ImVec2 AlignPointToGrid(const ImVec2& p) const { + return ImVec2(AlignPointToGrid(p.x), AlignPointToGrid(p.y)); + } + + ImDrawList* GetDrawList() { return m_DrawList; } + + private: + void LoadSettings(); + void SaveSettings(); + + Control BuildControl(bool allowOffscreen); + + void ShowMetrics(const Control& control); + + void UpdateAnimations(); + + Config m_Config; + + ImGuiID m_EditorActiveId; + bool m_IsFirstFrame; + bool m_IsFocused; + bool m_IsHovered; + bool m_IsHoveredWithoutOverlapp; + + bool m_ShortcutsEnabled; + + Style m_Style; + + vector> m_Nodes; + vector> m_Pins; + vector> m_Links; + + vector m_SelectedObjects; + + vector m_LastSelectedObjects; + uint64_t m_SelectionId; + + Link* m_LastActiveLink; + + vector m_LiveAnimations; + vector m_LastLiveAnimations; + + ImGuiEx::Canvas m_Canvas; + bool m_IsCanvasVisible; + + NodeBuilder m_NodeBuilder; + HintBuilder m_HintBuilder; + + EditorAction* m_CurrentAction; + NavigateAction m_NavigateAction; + SizeAction m_SizeAction; + DragAction m_DragAction; + SelectAction m_SelectAction; + ContextMenuAction m_ContextMenuAction; + ShortcutAction m_ShortcutAction; + CreateItemAction m_CreateItemAction; + DeleteItemsAction m_DeleteItemsAction; + + vector m_AnimationControllers; + FlowAnimationController m_FlowAnimationController; + + NodeId m_HoveredNode; + PinId m_HoveredPin; + LinkId m_HoveredLink; + NodeId m_DoubleClickedNode; + PinId m_DoubleClickedPin; + LinkId m_DoubleClickedLink; + int m_BackgroundClickButtonIndex; + int m_BackgroundDoubleClickButtonIndex; + + bool m_IsInitialized; + Settings m_Settings; + + ImDrawList* m_DrawList; + int m_ExternalChannel; + ImDrawListSplitter m_Splitter; +}; + +//------------------------------------------------------------------------------ +} // namespace Detail +} // namespace NodeEditor +} // namespace ax + +//------------------------------------------------------------------------------ +#include "imgui_node_editor_internal.inl" + +//------------------------------------------------------------------------------ +#endif // __IMGUI_NODE_EDITOR_INTERNAL_H__ diff --git a/symmetri/gui/imgui_node_editor_internal.inl b/symmetri/gui/imgui_node_editor_internal.inl new file mode 100644 index 0000000..df0dd25 --- /dev/null +++ b/symmetri/gui/imgui_node_editor_internal.inl @@ -0,0 +1,65 @@ +//------------------------------------------------------------------------------ +// VERSION 0.9.1 +// +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# ifndef __IMGUI_NODE_EDITOR_INTERNAL_INL__ +# define __IMGUI_NODE_EDITOR_INTERNAL_INL__ +# pragma once + + +//------------------------------------------------------------------------------ +# include "imgui_node_editor_internal.h" + + +//------------------------------------------------------------------------------ +namespace ax { +namespace NodeEditor { +namespace Detail { + + +//------------------------------------------------------------------------------ +//inline ImRect ToRect(const ax::rectf& rect) +//{ +// return ImRect( +// to_imvec(rect.top_left()), +// to_imvec(rect.bottom_right()) +// ); +//} +// +//inline ImRect ToRect(const ax::rect& rect) +//{ +// return ImRect( +// to_imvec(rect.top_left()), +// to_imvec(rect.bottom_right()) +// ); +//} + +inline ImRect ImGui_GetItemRect() +{ + return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); +} + +inline ImVec2 ImGui_GetMouseClickPos(ImGuiMouseButton buttonIndex) +{ + if (ImGui::IsMouseDown(buttonIndex)) + return ImGui::GetIO().MouseClickedPos[buttonIndex]; + else + return ImGui::GetMousePos(); +} + + +//------------------------------------------------------------------------------ +} // namespace Detail +} // namespace Editor +} // namespace ax + + +//------------------------------------------------------------------------------ +# endif // __IMGUI_NODE_EDITOR_INTERNAL_INL__ diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm new file mode 100644 index 0000000..8f8e0ad --- /dev/null +++ b/symmetri/gui/main.mm @@ -0,0 +1,145 @@ +#include +#include + +#include +#include +#include "drawable.h" +#include "imgui.h" +#include "imgui_impl_glfw.h" +#include "imgui_impl_metal.h" +#include "imgui_node_editor.h" +#include "menu_bar.hpp" +#include "redux.hpp" +#include "view.hpp" +#define GLFW_INCLUDE_NONE +#define GLFW_EXPOSE_NATIVE_COCOA +#include +#include +#import +#import + +namespace ed = ax::NodeEditor; + +static void glfw_error_callback(int error, const char *description) { + fprintf(stderr, "Glfw Error %d: %s\n", error, description); +} + +int main(int, char **) { + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); + (void)io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + + // Setup style + // ImGui::StyleColorsDark(); + ImGui::StyleColorsLight(); + + // Setup window + glfwSetErrorCallback(glfw_error_callback); + if (!glfwInit()) return 1; + + // Create window with graphics context + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + GLFWwindow *window = + glfwCreateWindow(1280, 720, "Dear ImGui GLFW+Metal example", nullptr, nullptr); + if (window == nullptr) return 1; + + id device = MTLCreateSystemDefaultDevice(); + id commandQueue = [device newCommandQueue]; + + // Setup Platform/Renderer backends + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplMetal_Init(device); + + NSWindow *nswin = glfwGetCocoaWindow(window); + CAMetalLayer *layer = [CAMetalLayer layer]; + layer.device = device; + layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + nswin.contentView.layer = layer; + nswin.contentView.wantsLayer = YES; + + MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor new]; + + float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; + + ed::Config config; + config.SettingsFile = "Simple.json"; + auto m_Context = ed::CreateEditor(&config); + // Main loop + while (!glfwWindowShouldClose(window)) { + while (auto v = MVC::dequeue()) { + MVC::update(v.value()); + } + auto model = MVC::getView(); + + @autoreleasepool { + glfwPollEvents(); + + int width, height; + glfwGetFramebufferSize(window, &width, &height); + layer.drawableSize = CGSizeMake(width, height); + id drawable = [layer nextDrawable]; + + id commandBuffer = [commandQueue commandBuffer]; + renderPassDescriptor.colorAttachments[0].clearColor = + MTLClearColorMake(clear_color[0] * clear_color[3], clear_color[1] * clear_color[3], + clear_color[2] * clear_color[3], clear_color[3]); + renderPassDescriptor.colorAttachments[0].texture = drawable.texture; + renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; + renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + id renderEncoder = + [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; + [renderEncoder pushDebugGroup:@"ImGui demo"]; + + // Start the Dear ImGui frame + ImGui_ImplMetal_NewFrame(renderPassDescriptor); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + draw(model); + + ed::SetCurrentEditor(m_Context); + ed::Begin("My Editor", ImVec2(0.0, 0.0f)); + int uniqueId = 1; + // Start drawing nodes. + ed::BeginNode(uniqueId++); + ImGui::Text("Node A"); + ed::BeginPin(uniqueId++, ed::PinKind::Input); + ImGui::Text("-> In"); + ed::EndPin(); + ImGui::SameLine(); + ed::BeginPin(uniqueId++, ed::PinKind::Output); + ImGui::Text("Out ->"); + ed::EndPin(); + ed::EndNode(); + ed::End(); + ed::SetCurrentEditor(nullptr); + // Rendering + ImGui::Render(); + ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), commandBuffer, renderEncoder); + + [renderEncoder popDebugGroup]; + [renderEncoder endEncoding]; + + [commandBuffer presentDrawable:drawable]; + [commandBuffer commit]; + if (ImGui::GetIO().Framerate > 61.f) { + std::this_thread::sleep_for( + std::chrono::milliseconds(200)); // drop to 5 FPS when invisible + } + } + } + + // Cleanup + ImGui_ImplMetal_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + glfwDestroyWindow(window); + glfwTerminate(); + + return 0; +} diff --git a/symmetri/gui/menu_bar.hpp b/symmetri/gui/menu_bar.hpp new file mode 100644 index 0000000..38ed146 --- /dev/null +++ b/symmetri/gui/menu_bar.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include "imfilebrowser.h" +#include "redux.hpp" +#include "symmetri/parsers.h" + +std::function updateActiveFile( + const std::filesystem::path &file) { + return [=](Model &&m) { + m.active_file = file; + Net net; + std::tie(net, m.marking) = symmetri::readPnml({file}); + m.drawables.push_back(net); + return m; + }; +} + +void menuBar(ImGui::FileBrowser &fileDialog) { + fileDialog.Display(); + if (fileDialog.HasSelected()) { + MVC::push(updateActiveFile(fileDialog.GetSelected())); + fileDialog.ClearSelected(); + } + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("New")) { + } + if (ImGui::MenuItem("Open", "Ctrl+O")) { + fileDialog.Open(); + } + // Exit... + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Edit")) { + //... + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Window")) { + //... + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Help")) { + //... + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); + } +} + +void draw(const ImGui::FileBrowser &file_dialog) { menuBar(file_dialog); } diff --git a/symmetri/gui/redux.hpp b/symmetri/gui/redux.hpp new file mode 100644 index 0000000..f5fe7a9 --- /dev/null +++ b/symmetri/gui/redux.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "blockingconcurrentqueue.h" +#include "drawable.h" +#include "view.hpp" + +namespace fs = std::filesystem; + +using namespace symmetri; + +struct Model { + std::filesystem::path working_dir; + std::optional active_file; + // Net net; + Marking marking; + std::vector drawables; +}; + +void draw(const Model &m) { + for (const auto &drawable : m.drawables) { + draw(drawable); + } +} + +inline Model initializeModel(Model &&m) { + m.working_dir = "/users/thomashorstink/Projects/symmetri/nets"; + m.active_file = "/users/thomashorstink/Projects/symmetri/nets/n1.pnml"; + Net net; + std::tie(net, m.marking) = symmetri::readPnml({m.active_file.value()}); + m.drawables.push_back(net); + + // create a file browser instance + ImGui::FileBrowser fileDialog; + + // (optional) set browser properties + fileDialog.SetTitle("title"); + fileDialog.SetTypeFilters({".pnml", ".grml"}); + fileDialog.SetPwd(m.working_dir); + m.drawables.push_back(fileDialog); + return m; +} + +using Reducer = std::function; +class MVC { + private: + inline static moodycamel::BlockingConcurrentQueue reducers{10}; + + public: + inline static Model model = initializeModel(Model()); + inline static Model getView() { return model; }; + inline static void update(Reducer f) { model = f(std::move(model)); } + inline static void push(Reducer &&f) { + reducers.enqueue(std::forward(f)); + } + inline static std::optional dequeue() { + static Reducer r; + return reducers.try_dequeue(r) ? std::optional(r) : std::nullopt; + } +}; diff --git a/symmetri/gui/view.hpp b/symmetri/gui/view.hpp new file mode 100644 index 0000000..d4c5267 --- /dev/null +++ b/symmetri/gui/view.hpp @@ -0,0 +1,42 @@ +#pragma once +#include + +#include +#include + +#include "imgui.h" + +using Mutation = + std::pair>, + std::vector>>; + +static void showMutation(const Mutation& mutation) { + std::stringstream s; + for (const auto& input : mutation.first) { + s << "(" << input.first << "," << symmetri::Color::toString(input.second) + << ")"; + } + s << " -> "; + for (const auto& output : mutation.second) { + s << "(" << output.first << "," << symmetri::Color::toString(output.second) + << ")"; + } + if (ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(s.str().c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +void draw(const symmetri::Net& net) { + std::cout << "drawable!" << std::endl; + if (ImGui::BeginListBox("##bla")) { + for (const auto& [t, p] : net) { + if (ImGui::Selectable(t.c_str(), false)) { + } + showMutation(p); + } + ImGui::EndListBox(); + } +} From e9494bb5c17e304e32337c481b31e2963573df3f Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Wed, 29 Nov 2023 19:45:00 +0100 Subject: [PATCH 002/142] wip gui --- CMakeLists.txt | 1 - symmetri/CMakeLists.txt | 3 +- symmetri/gui/CMakeLists.txt | 60 +- symmetri/gui/drawable.h | 23 +- symmetri/gui/emo.cpp | 9975 ----------------- symmetri/gui/{ => extensions}/crude_json.cpp | 0 symmetri/gui/{ => extensions}/crude_json.h | 0 symmetri/gui/{ => extensions}/imfilebrowser.h | 0 .../gui/{ => extensions}/imgui_bezier_math.h | 0 .../{ => extensions}/imgui_bezier_math.inl | 0 .../gui/{ => extensions}/imgui_canvas.cpp | 0 symmetri/gui/{ => extensions}/imgui_canvas.h | 0 .../gui/{ => extensions}/imgui_extra_math.h | 0 .../gui/{ => extensions}/imgui_extra_math.inl | 0 .../{ => extensions}/imgui_node_editor.cpp | 0 .../gui/{ => extensions}/imgui_node_editor.h | 0 .../imgui_node_editor_api.cpp | 0 .../imgui_node_editor_internal.h | 0 .../imgui_node_editor_internal.inl | 0 symmetri/gui/graph.hpp | 192 + symmetri/gui/initialize.hpp | 37 + symmetri/gui/main.mm | 45 +- symmetri/gui/marking.hpp | 21 + symmetri/gui/menu_bar.hpp | 23 +- symmetri/gui/redux.hpp | 63 +- symmetri/gui/symbol.cpp | 71 + symmetri/gui/symbol.h | 114 + symmetri/gui/view.hpp | 4 +- 28 files changed, 551 insertions(+), 10081 deletions(-) delete mode 100644 symmetri/gui/emo.cpp rename symmetri/gui/{ => extensions}/crude_json.cpp (100%) rename symmetri/gui/{ => extensions}/crude_json.h (100%) rename symmetri/gui/{ => extensions}/imfilebrowser.h (100%) rename symmetri/gui/{ => extensions}/imgui_bezier_math.h (100%) rename symmetri/gui/{ => extensions}/imgui_bezier_math.inl (100%) rename symmetri/gui/{ => extensions}/imgui_canvas.cpp (100%) rename symmetri/gui/{ => extensions}/imgui_canvas.h (100%) rename symmetri/gui/{ => extensions}/imgui_extra_math.h (100%) rename symmetri/gui/{ => extensions}/imgui_extra_math.inl (100%) rename symmetri/gui/{ => extensions}/imgui_node_editor.cpp (100%) rename symmetri/gui/{ => extensions}/imgui_node_editor.h (100%) rename symmetri/gui/{ => extensions}/imgui_node_editor_api.cpp (100%) rename symmetri/gui/{ => extensions}/imgui_node_editor_internal.h (100%) rename symmetri/gui/{ => extensions}/imgui_node_editor_internal.inl (100%) create mode 100644 symmetri/gui/graph.hpp create mode 100644 symmetri/gui/initialize.hpp create mode 100644 symmetri/gui/marking.hpp create mode 100644 symmetri/gui/symbol.cpp create mode 100644 symmetri/gui/symbol.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dbcc0f8..10b61fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -Wextra -pedantic") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") if(BUILD_TESTING) enable_testing() diff --git a/symmetri/CMakeLists.txt b/symmetri/CMakeLists.txt index 1aa8254..4e39d1f 100644 --- a/symmetri/CMakeLists.txt +++ b/symmetri/CMakeLists.txt @@ -74,5 +74,6 @@ install( if(BUILD_TESTING) add_subdirectory(tests) - add_subdirectory(gui) endif() + +add_subdirectory(gui) diff --git a/symmetri/gui/CMakeLists.txt b/symmetri/gui/CMakeLists.txt index 42ce746..4060224 100644 --- a/symmetri/gui/CMakeLists.txt +++ b/symmetri/gui/CMakeLists.txt @@ -4,11 +4,25 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) find_package(glfw3 REQUIRED) +find_package(ogdf REQUIRED) +if(ASAN_BUILD AND NOT TSAN_BUILD) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=address,undefined -fno-omit-frame-pointer -O0") +elseif(TSAN_BUILD AND NOT ASAN_BUILD) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=thread -fno-omit-frame-pointer -O0") +endif() -include_directories(imgui imgui/backends ../externals gui) +include_directories( + imgui + imgui/backends + ../externals + gui + extensions + /usr/local/include +) set(SOURCES main.mm + symbol.cpp ) set(IMGUI_SOURCES @@ -19,31 +33,31 @@ set(IMGUI_SOURCES imgui/imgui_tables.cpp imgui/backends/imgui_impl_glfw.cpp imgui/backends/imgui_impl_metal.mm - crude_json.cpp - imgui_node_editor_api.cpp - imgui_node_editor.cpp - imgui_canvas.cpp - ) + extensions/crude_json.cpp + extensions/imgui_node_editor_api.cpp + extensions/imgui_node_editor.cpp + extensions/imgui_canvas.cpp +) if(APPLE) - include_directories(/opt/homebrew/include) - find_library(COCOA_LIBRARY Cocoa) - find_library(OPENGL_LIBRARY OpenGL) - find_library(IOKIT_LIBRARY IOKit) - find_library(COREVIDEO_LIBRARY CoreVideo) - set(EXTRA_LIBS - ${COCOA_LIBRARY} - ${OPENGL_LIBRARY} - ${IOKIT_LIBRARY} - ${COREVIDEO_LIBRARY} - glfw - "-framework Metal" - "-framework MetalKit" - "-framework AppKit" - "-framework Foundation" - "-framework QuartzCore" + include_directories(/opt/homebrew/include) + find_library(COCOA_LIBRARY Cocoa) + find_library(OPENGL_LIBRARY OpenGL) + find_library(IOKIT_LIBRARY IOKit) + find_library(COREVIDEO_LIBRARY CoreVideo) + set(EXTRA_LIBS + ${COCOA_LIBRARY} + ${OPENGL_LIBRARY} + ${IOKIT_LIBRARY} + ${COREVIDEO_LIBRARY} + glfw + "-framework Metal" + "-framework MetalKit" + "-framework AppKit" + "-framework Foundation" + "-framework QuartzCore" ) endif(APPLE) add_executable(Farbart ${SOURCES} ${IMGUI_SOURCES}) -target_link_libraries(Farbart symmetri ${EXTRA_LIBS} ) +target_link_libraries(Farbart symmetri OGDF ${EXTRA_LIBS}) diff --git a/symmetri/gui/drawable.h b/symmetri/gui/drawable.h index 6b37aa1..11df582 100644 --- a/symmetri/gui/drawable.h +++ b/symmetri/gui/drawable.h @@ -5,24 +5,29 @@ #include #include -class Drawable { - public: - template - Drawable(drawable_t drawable) - : self_(std::make_shared>(std::move(drawable))) {} - friend void draw(const Drawable &drawable) { return drawable.self_->draw_(); } +template +void draw(T &) { + std::cout << "joe" << std::endl; +} +class Drawable { private: struct concept_t { virtual ~concept_t() = default; - virtual void draw_() const = 0; + virtual void draw_() = 0; }; template struct model final : concept_t { model(drawable_t &&x) : drawable_(std::move(x)) {} - void draw_() const override { return draw(drawable_); } + void draw_() override { return draw(drawable_); } drawable_t drawable_; }; - std::shared_ptr self_; + public: + template + Drawable(drawable_t drawable) + : self_(std::make_shared>(std::move(drawable))) {} + friend void draw(Drawable &drawable) { return drawable.self_->draw_(); } + + std::shared_ptr self_; }; diff --git a/symmetri/gui/emo.cpp b/symmetri/gui/emo.cpp deleted file mode 100644 index 829b1b3..0000000 --- a/symmetri/gui/emo.cpp +++ /dev/null @@ -1,9975 +0,0 @@ -// dear imgui, v1.90 WIP -// (demo code) - -// 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 imgui.cpp for more details, documentation and comments. -// Get the latest version at https://github.com/ocornut/imgui - -//--------------------------------------------------- -// 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 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 -// (which you won't delete) - -//-------------------------------------------- -// 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. - -//----------------------------------------- -// 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:: 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 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 IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in -// comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. -// - With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can -// also follow symbols in comments. - -/* - -Index of this file: - -// [SECTION] Forward Declarations -// [SECTION] Helpers -// [SECTION] Demo Window / ShowDemoWindow() -// - ShowDemoWindow() -// - sub section: ShowDemoWindowWidgets() -// - sub section: ShowDemoWindowLayout() -// - sub section: ShowDemoWindowPopups() -// - sub section: ShowDemoWindowTables() -// - sub section: ShowDemoWindowInputs() -// [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() -// [SECTION] Example App: Simple Layout / ShowExampleAppLayout() -// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() -// [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: Fullscreen window / ShowExampleAppFullscreen() -// [SECTION] Example App: Manipulating window titles / -ShowExampleAppWindowTitles() -// [SECTION] Example App: Custom Rendering using ImDrawList API / -ShowExampleAppCustomRendering() -// [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() - -*/ - -#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) -#define _CRT_SECURE_NO_WARNINGS -#endif - -#include "imgui.h" -#ifndef IMGUI_DISABLE - -// System includes -#include // toupper -#include // INT_MIN, INT_MAX -#include // sqrtf, powf, cosf, sinf, floorf, ceilf -#include // intptr_t -#include // vsnprintf, sscanf, printf -#include // NULL, malloc, free, atoi -#if !defined(_MSC_VER) || _MSC_VER >= 1800 -#include // PRId64/PRIu64, not avail in some MinGW headers. -#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 : 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 - -// 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-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 -#elif defined(__GNUC__) -#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after - // '#pragma GCC diagnostic' kind -#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 -#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. -#endif - -// Play it nice with Windows users (Update: May 2018, Notepad now supports -// Unix-style carriage returns!) -#ifdef _WIN32 -#define IM_NEWLINE "\r\n" -#else -#define IM_NEWLINE "\n" -#endif - -// 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, Helpers -//----------------------------------------------------------------------------- - -#if !defined(IMGUI_DISABLE_DEMO_WINDOWS) - -// Forward Declarations -static void ShowExampleAppMainMenuBar(); -static void ShowExampleAppConsole(bool* p_open); -static void ShowExampleAppCustomRendering(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 ShowExampleAppSimpleOverlay(bool* p_open); -static void ShowExampleAppAutoResize(bool* p_open); -static void ShowExampleAppConstrainedResize(bool* p_open); -static void ShowExampleAppFullscreen(bool* p_open); -static void ShowExampleAppLongText(bool* p_open); -static void ShowExampleAppWindowTitles(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 grow non-linearly) -static void ShowDemoWindowWidgets(); -static void ShowDemoWindowLayout(); -static void ShowDemoWindowPopups(); -static void ShowDemoWindowTables(); -static void ShowDemoWindowColumns(); -static void ShowDemoWindowInputs(); - -//----------------------------------------------------------------------------- -// [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 docs/FONTS.md) -static void HelpMarker(const char* desc) { - ImGui::TextDisabled("(?)"); - if (ImGui::BeginItemTooltip()) { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::TextUnformatted(desc); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } -} - -// 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() -//----------------------------------------------------------------------------- -// - ShowDemoWindow() -// - ShowDemoWindowWidgets() -// - ShowDemoWindowLayout() -// - ShowDemoWindowPopups() -// - ShowDemoWindowTables() -// - ShowDemoWindowColumns() -// - ShowDemoWindowInputs() -//----------------------------------------------------------------------------- - -// 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. -void ImGui::ShowDemoWindow(bool* p_open) { - // 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!"); - - // Examples Apps (accessible from the "Examples" menu) - static bool show_app_main_menu_bar = false; - static bool show_app_console = false; - static bool show_app_custom_rendering = false; - static bool show_app_documents = false; - static bool show_app_log = false; - static bool show_app_layout = false; - static bool show_app_property_editor = false; - static bool show_app_simple_overlay = false; - static bool show_app_auto_resize = false; - static bool show_app_constrained_resize = false; - static bool show_app_fullscreen = false; - static bool show_app_long_text = false; - static bool show_app_window_titles = false; - - if (show_app_main_menu_bar) ShowExampleAppMainMenuBar(); - if (show_app_documents) ShowExampleAppDocuments(&show_app_documents); - if (show_app_console) ShowExampleAppConsole(&show_app_console); - if (show_app_custom_rendering) - ShowExampleAppCustomRendering(&show_app_custom_rendering); - 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_simple_overlay) - ShowExampleAppSimpleOverlay(&show_app_simple_overlay); - if (show_app_auto_resize) ShowExampleAppAutoResize(&show_app_auto_resize); - if (show_app_constrained_resize) - ShowExampleAppConstrainedResize(&show_app_constrained_resize); - if (show_app_fullscreen) ShowExampleAppFullscreen(&show_app_fullscreen); - if (show_app_long_text) ShowExampleAppLongText(&show_app_long_text); - if (show_app_window_titles) - ShowExampleAppWindowTitles(&show_app_window_titles); - - // Dear ImGui Tools (accessible from the "Tools" menu) - static bool show_tool_metrics = false; - static bool show_tool_debug_log = false; - static bool show_tool_id_stack_tool = false; - static bool show_tool_style_editor = false; - static bool show_tool_about = false; - - if (show_tool_metrics) ImGui::ShowMetricsWindow(&show_tool_metrics); - if (show_tool_debug_log) ImGui::ShowDebugLogWindow(&show_tool_debug_log); - if (show_tool_id_stack_tool) - ImGui::ShowIDStackToolWindow(&show_tool_id_stack_tool); - if (show_tool_style_editor) { - ImGui::Begin("Dear ImGui Style Editor", &show_tool_style_editor); - ImGui::ShowStyleEditor(); - ImGui::End(); - } - if (show_tool_about) ImGui::ShowAboutWindow(&show_tool_about); - - // Demonstrate the various window flags. Typically you would just use the - // default! - static bool no_titlebar = false; - static bool no_scrollbar = false; - static bool no_menu = false; - static bool no_move = false; - static bool no_resize = false; - static bool no_collapse = false; - static bool no_close = false; - static bool no_nav = false; - static bool no_background = false; - static bool no_bring_to_front = false; - static bool unsaved_document = false; - - ImGuiWindowFlags window_flags = 0; - if (no_titlebar) window_flags |= ImGuiWindowFlags_NoTitleBar; - if (no_scrollbar) window_flags |= ImGuiWindowFlags_NoScrollbar; - if (!no_menu) window_flags |= ImGuiWindowFlags_MenuBar; - if (no_move) window_flags |= ImGuiWindowFlags_NoMove; - if (no_resize) window_flags |= ImGuiWindowFlags_NoResize; - if (no_collapse) window_flags |= ImGuiWindowFlags_NoCollapse; - if (no_nav) window_flags |= ImGuiWindowFlags_NoNav; - if (no_background) window_flags |= ImGuiWindowFlags_NoBackground; - if (no_bring_to_front) window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus; - 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. 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. - if (!ImGui::Begin("Dear ImGui Demo", p_open, window_flags)) { - // Early out if the window is collapsed, as an optimization. - ImGui::End(); - return; - } - - // Most "big" widgets share a common width settings by default. See - // 'Demo->Layout->Widgets Width' for details. e.g. Use 2/3 of the space for - // widgets and 1/3 for labels (right align) - // ImGui::PushItemWidth(-ImGui::GetWindowWidth() * 0.35f); - // e.g. Leave a fixed amount of width for labels (by passing a negative - // value), the rest goes to widgets. - ImGui::PushItemWidth(ImGui::GetFontSize() * -12); - - // Menu Bar - 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, &show_app_main_menu_bar); - - ImGui::SeparatorText("Mini apps"); - ImGui::MenuItem("Console", NULL, &show_app_console); - ImGui::MenuItem("Custom rendering", NULL, &show_app_custom_rendering); - ImGui::MenuItem("Documents", NULL, &show_app_documents); - ImGui::MenuItem("Log", NULL, &show_app_log); - ImGui::MenuItem("Property editor", NULL, &show_app_property_editor); - ImGui::MenuItem("Simple layout", NULL, &show_app_layout); - ImGui::MenuItem("Simple overlay", NULL, &show_app_simple_overlay); - - ImGui::SeparatorText("Concepts"); - ImGui::MenuItem("Auto-resizing window", NULL, &show_app_auto_resize); - ImGui::MenuItem("Constrained-resizing window", NULL, - &show_app_constrained_resize); - ImGui::MenuItem("Fullscreen window", NULL, &show_app_fullscreen); - ImGui::MenuItem("Long text display", NULL, &show_app_long_text); - ImGui::MenuItem("Manipulating window titles", NULL, - &show_app_window_titles); - - ImGui::EndMenu(); - } - // if (ImGui::MenuItem("MenuItem")) {} // You can also use MenuItem() inside - // a menu bar! - if (ImGui::BeginMenu("Tools")) { - IMGUI_DEMO_MARKER("Menu/Tools"); -#ifndef IMGUI_DISABLE_DEBUG_TOOLS - const bool has_debug_tools = true; -#else - const bool has_debug_tools = false; -#endif - ImGui::MenuItem("Metrics/Debugger", NULL, &show_tool_metrics, - has_debug_tools); - ImGui::MenuItem("Debug Log", NULL, &show_tool_debug_log, has_debug_tools); - ImGui::MenuItem("ID Stack Tool", NULL, &show_tool_id_stack_tool, - has_debug_tools); - ImGui::MenuItem("Style Editor", NULL, &show_tool_style_editor); - ImGui::MenuItem("About Dear ImGui", NULL, &show_tool_about); - ImGui::EndMenu(); - } - ImGui::EndMenuBar(); - } - - ImGui::Text("dear imgui says hello! (%s) (%d)", IMGUI_VERSION, - IMGUI_VERSION_NUM); - ImGui::Spacing(); - - IMGUI_DEMO_MARKER("Help"); - if (ImGui::CollapsingHeader("Help")) { - 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::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 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::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: NavEnableSetMousePos", - &io.ConfigFlags, - ImGuiConfigFlags_NavEnableSetMousePos); - ImGui::SameLine(); - HelpMarker( - "Instruct navigation to move the mouse cursor. See comment for " - "ImGuiConfigFlags_NavEnableSetMousePos."); - ImGui::CheckboxFlags("io.ConfigFlags: NoMouse", &io.ConfigFlags, - ImGuiConfigFlags_NoMouse); - if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) { - // The "NoMouse" option can get us stuck with a disabled mouse! Let's - // provide an alternative way to fix it: - if (fmodf((float)ImGui::GetTime(), 0.40f) < 0.20f) { - ImGui::SameLine(); - ImGui::Text("<>"); - } - if (ImGui::IsKeyPressed(ImGuiKey_Space)) - io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; - } - ImGui::CheckboxFlags("io.ConfigFlags: NoMouseCursorChange", - &io.ConfigFlags, - ImGuiConfigFlags_NoMouseCursorChange); - ImGui::SameLine(); - HelpMarker( - "Instruct backend to not alter mouse cursor shape and visibility."); - 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 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("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.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::Checkbox("io.ConfigMacOSXBehaviors", &io.ConfigMacOSXBehaviors); - ImGui::Text("Also see Style->Rendering for rendering options."); - - ImGui::SeparatorText("Debug"); - 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::Spacing(); - } - - IMGUI_DEMO_MARKER("Configuration/Backend Flags"); - if (ImGui::TreeNode("Backend Flags")) { - 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."); - - // 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: RendererHasVtxOffset", - &io.BackendFlags, - ImGuiBackendFlags_RendererHasVtxOffset); - ImGui::EndDisabled(); - ImGui::TreePop(); - ImGui::Spacing(); - } - - IMGUI_DEMO_MARKER("Configuration/Style"); - if (ImGui::TreeNode("Style")) { - HelpMarker( - "The same contents can be accessed in 'Tools->Style Editor' or by " - "calling the ShowStyleEditor() function."); - ImGui::ShowStyleEditor(); - ImGui::TreePop(); - ImGui::Spacing(); - } - - IMGUI_DEMO_MARKER("Configuration/Capture, Logging"); - if (ImGui::TreeNode("Capture/Logging")) { - 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(); - - 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(); - ImGui::LogText("Hello, world!"); - ImGui::LogFinish(); - } - ImGui::TreePop(); - } - } - - IMGUI_DEMO_MARKER("Window options"); - if (ImGui::CollapsingHeader("Window options")) { - 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("Unsaved document", &unsaved_document); - ImGui::EndTable(); - } - } - - // All demo contents - ShowDemoWindowWidgets(); - ShowDemoWindowLayout(); - ShowDemoWindowPopups(); - ShowDemoWindowTables(); - ShowDemoWindowInputs(); - - // End of ShowDemoWindow() - ImGui::PopItemWidth(); - ImGui::End(); -} - -static void ShowDemoWindowWidgets() { - IMGUI_DEMO_MARKER("Widgets"); - if (!ImGui::CollapsingHeader("Widgets")) return; - - static bool disable_all = false; // The Checkbox for that is inside the - // "Disabled" section at the bottom - if (disable_all) ImGui::BeginDisabled(); - - 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++; - if (clicked & 1) { - ImGui::SameLine(); - 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::Button("Click"); - ImGui::PopStyleColor(3); - ImGui::PopID(); - } - - // 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); - if (ImGui::ArrowButton("##left", ImGuiDir_Left)) { - counter--; - } - ImGui::SameLine(0.0f, spacing); - if (ImGui::ArrowButton("##right", ImGuiDir_Right)) { - counter++; - } - ImGui::PopButtonRepeat(); - ImGui::SameLine(); - ImGui::Text("%d", counter); - - ImGui::Button("Tooltip"); - ImGui::SetItemTooltip("I am a tooltip"); - - ImGui::LabelText("label", "Value"); - - 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:\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 clipboard.\n" - "CTRL+Z,CTRL+Y undo/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); - - static float f0 = 0.001f; - ImGui::InputFloat("input float", &f0, 0.01f, 1.0f, "%.3f"); - - static double d0 = 999999.00000001; - ImGui::InputDouble("input double", &d0, 0.01f, 1.0f, "%.8f"); - - 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\"."); - - static float vec4a[4] = {0.10f, 0.20f, 0.30f, 0.44f}; - ImGui::InputFloat3("input float3", vec4a); - } - - ImGui::SeparatorText("Drags"); - - { - IMGUI_DEMO_MARKER("Widgets/Basic/DragInt, DragFloat"); - static int i1 = 50, i2 = 42; - ImGui::DragInt("drag int", &i1, 1); - 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); - - 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::SeparatorText("Sliders"); - - { - 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; - ImGui::SliderFloat("slider float", &f1, 0.0f, 1.0f, "ratio = %.3f"); - 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"); - - { - 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 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); - } - - { - // 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.\nRefer 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.\nRefer to the " - "\"List boxes\" section below for an explanation of how to use the " - "more flexible and general BeginListBox/EndListBox API."); - } - - ImGui::TreePop(); - } - - 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 gamepad/keyboard 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. - // As a result, Set - ImGui::BeginDisabled(); - ImGui::Button("Disabled item", sz); - ImGui::EndDisabled(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) - ImGui::SetTooltip("I am a a tooltip for a disabled item."); - - 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."); - - 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); - - if (ImGui::TreeNode((void*)(intptr_t)i, "Child %d", i)) { - ImGui::Text("blah blah"); - ImGui::SameLine(); - if (ImGui::SmallButton("button")) { - } - ImGui::TreePop(); - } - } - 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_SpanAllColumns", &base_flags, - ImGuiTreeNodeFlags_SpanAllColumns); - ImGui::SameLine(); - HelpMarker("For use in Tables only."); - 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 (node_open) { - ImGui::BulletText("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 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(); - } - - 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", ImGuiTreeNodeFlags_None)) { - ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); - for (int i = 0; i < 5; i++) ImGui::Text("Some content %d", i); - } - if (ImGui::CollapsingHeader("Header with a close button", - &closable_group)) { - ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); - 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(); - } - - IMGUI_DEMO_MARKER("Widgets/Bullets"); - if (ImGui::TreeNode("Bullets")) { - ImGui::BulletText("Bullet point 1"); - ImGui::BulletText("Bullet point 2\nOn multiple lines"); - if (ImGui::TreeNode("Tree node")) { - ImGui::BulletText("Another bullet point"); - ImGui::TreePop(); - } - ImGui::Bullet(); - ImGui::Text("Bullet point 3 (two calls)"); - ImGui::Bullet(); - ImGui::SmallButton("Button"); - ImGui::TreePop(); - } - - 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)"); // 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(); - } - - 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; - { - static bool use_text_color_for_tint = false; - ImGui::Checkbox("Use Text Color for Tint", &use_text_color_for_tint); - 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 - ImVec4 tint_col = use_text_color_for_tint - ? ImGui::GetStyleColorVec4(ImGuiCol_Text) - : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // No tint - ImVec4 border_col = ImGui::GetStyleColorVec4(ImGuiCol_Border); - ImGui::Image(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, - tint_col, border_col); - 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::Image(my_tex_id, ImVec2(region_sz * zoom, region_sz * zoom), uv0, - uv1, tint_col, border_col); - ImGui::EndTooltip(); - } - } - - 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(); - } - - IMGUI_DEMO_MARKER("Widgets/Combo"); - if (ImGui::TreeNode("Combo")) { - // 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 the other flag, as we - // cannot combine both - if (ImGui::CheckboxFlags("ImGuiComboFlags_NoPreview", &flags, - ImGuiComboFlags_NoPreview)) - flags &= ~(ImGuiComboFlags_NoArrowButton | - ImGuiComboFlags_WidthFitPreview); // Clear the other flag, as - // we cannot combine both - 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_current_idx = - 0; // Here we store our selection data as an index. - const char* combo_preview_value = - items[item_current_idx]; // Pass in the preview value visible before - // opening the combo (it could be anything) - if (ImGui::BeginCombo("combo 1", combo_preview_value, flags)) { - for (int n = 0; n < IM_ARRAYSIZE(items); n++) { - const bool is_selected = (item_current_idx == n); - if (ImGui::Selectable(items[n], is_selected)) item_current_idx = n; - - // Set the initial focus when opening the combo (scrolling + keyboard - // navigation focus) - if (is_selected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - - ImGui::Spacing(); - ImGui::SeparatorText("One-liner variants"); - HelpMarker("Flags 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 2 (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 3 (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 4 (function)", &item_current_4, - [](void* data, int n) { return ((const char**)data)[n]; }, items, - IM_ARRAYSIZE(items)); - - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Widgets/List Boxes"); - if (ImGui::TreeNode("List boxes")) { - // 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_current_idx = - 0; // Here we store our selection data as an index. - if (ImGui::BeginListBox("listbox 1")) { - for (int n = 0; n < IM_ARRAYSIZE(items); n++) { - const bool is_selected = (item_current_idx == n); - if (ImGui::Selectable(items[n], is_selected)) item_current_idx = n; - - // Set the initial focus when opening the combo (scrolling + keyboard - // navigation focus) - if (is_selected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndListBox(); - } - - // 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++) { - const bool is_selected = (item_current_idx == n); - if (ImGui::Selectable(items[n], is_selected)) item_current_idx = n; - - // Set the initial focus when opening the combo (scrolling + keyboard - // navigation focus) - if (is_selected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndListBox(); - } - - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Widgets/Selectables"); - if (ImGui::TreeNode("Selectables")) { - // 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")) { - 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_DEMO_MARKER("Widgets/Selectables/Single Selection"); - 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(); - } - IMGUI_DEMO_MARKER("Widgets/Selectables/Multiple Selection"); - 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(); - } - IMGUI_DEMO_MARKER( - "Widgets/Selectables/Rendering more items on the same line"); - if (ImGui::TreeNode("Rendering more items on the same line")) { - // (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 columns"); - if (ImGui::TreeNode("In columns")) { - static bool selected[10] = {}; - - if (ImGui::BeginTable("split1", 3, - ImGuiTableFlags_Resizable | - ImGuiTableFlags_NoSavedSettings | - ImGuiTableFlags_Borders)) { - for (int i = 0; i < 10; i++) { - char label[32]; - sprintf(label, "Item %d", i); - ImGui::TableNextColumn(); - ImGui::Selectable(label, - &selected[i]); // FIXME-TABLE: Selection overlap - } - ImGui::EndTable(); - } - 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(); - } - - // 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::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/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(); - } - - // Tabs - 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; - if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) { - if (ImGui::BeginTabItem("Avocado")) { - ImGui::Text("This is the Avocado tab!\nblah blah blah blah blah"); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Broccoli")) { - ImGui::Text("This is the Broccoli tab!\nblah blah blah blah blah"); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Cucumber")) { - ImGui::Text("This is the Cucumber tab!\nblah blah blah blah blah"); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - ImGui::Separator(); - 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", &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); - if ((tab_bar_flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) - tab_bar_flags |= ImGuiTabBarFlags_FittingPolicyDefault_; - 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); - - // Tab Bar - 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::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. - 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], - ImGuiTabItemFlags_None)) { - ImGui::Text("This is the %s tab!", names[n]); - if (n & 1) ImGui::Text("I am an odd tab."); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - 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 "+") Note that we - // submit it before the regular tabs, but because of 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(); - } - - // 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")) { - static bool animate = true; - ImGui::Checkbox("Animate", &animate); - - // Plot as lines and plot as histogram - IMGUI_DEMO_MARKER("Widgets/Plotting/PlotLines, PlotHistogram"); - 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)); - - // 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", 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 - IMGUI_DEMO_MARKER("Widgets/Plotting/ProgressBar"); - 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) 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); - ImGui::TreePop(); - } - - 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 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::SeparatorText("Options"); - 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."); - ImGuiColorEditFlags 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_DEMO_MARKER("Widgets/Color/ColorEdit"); - ImGui::SeparatorText("Inline color editor"); - ImGui::Text("Color widget:"); - 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, misc_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_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_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.\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 | misc_flags); - - IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (with custom Picker popup)"); - ImGui::Text("Color button with Custom Picker Popup:"); - - // Generate a default palette. The palette will persist and can be edited. - static bool saved_palette_init = true; - 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); - 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(0, ImGui::GetStyle().ItemInnerSpacing.x); - open_popup |= ImGui::Button("Palette"); - if (open_popup) { - ImGui::OpenPopup("mypicker"); - backup_color = color; - } - if (ImGui::BeginPopup("mypicker")) { - ImGui::Text("MY CUSTOM COLOR PICKER WITH AN AMAZING PALETTE!"); - ImGui::Separator(); - ImGui::ColorPicker4("##picker", (float*)&color, - misc_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::Text("Previous"); - if (ImGui::ColorButton("##previous", backup_color, - ImGuiColorEditFlags_NoPicker | - ImGuiColorEditFlags_AlphaPreviewHalf, - ImVec2(60, 40))) - color = backup_color; - ImGui::Separator(); - ImGui::Text("Palette"); - for (int n = 0; n < IM_ARRAYSIZE(saved_palette); n++) { - ImGui::PushID(n); - if ((n % 8) != 0) - ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); - - 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 specifying - // the ImGuiColorEditFlags_NoDragDrop flag. - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* payload = - ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) - memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 3); - if (const ImGuiPayload* payload = - ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) - memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 4); - ImGui::EndDragDropTarget(); - } - - ImGui::PopID(); - } - ImGui::EndGroup(); - ImGui::EndPopup(); - } - - IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (simple)"); - ImGui::Text("Color button only:"); - static bool no_border = false; - ImGui::Checkbox("ImGuiColorEditFlags_NoBorder", &no_border); - ImGui::ColorButton( - "MyColor##3c", *(ImVec4*)&color, - misc_flags | (no_border ? ImGuiColorEditFlags_NoBorder : 0), - ImVec2(80, 80)); - - IMGUI_DEMO_MARKER("Widgets/Color/ColorPicker"); - ImGui::SeparatorText("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 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) { - 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::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 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()."); - ImGui::SameLine(); - HelpMarker( - "When not specified explicitly (Auto/Current mode), 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; - 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 - if (display_mode == 2) - flags |= ImGuiColorEditFlags_DisplayRGB; // Override display mode - if (display_mode == 3) flags |= ImGuiColorEditFlags_DisplayHSV; - if (display_mode == 4) flags |= ImGuiColorEditFlags_DisplayHex; - ImGui::ColorPicker4("MyColor##4", (float*)&color, flags, - ref_color ? &ref_color_v.x : NULL); - - 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 both a small version of both types of pickers (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); - - // HSV encoded support (to avoid RGB<>HSV round trips and singularities when - // S==0 or V==0) - 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::Text("Color widget with InputHSV:"); - 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(); - } - - IMGUI_DEMO_MARKER("Widgets/Drag and Slider Flags"); - if (ImGui::TreeNode("Drag/Slider Flags")) { - // 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::SameLine(); - HelpMarker( - "Always clamp value to min/max bounds (if any) when input manually " - "with CTRL+Click."); - 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."); - - // 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::DragInt("DragInt (0 -> 100)", &drag_i, 0.5f, 0, 100, "%d", flags); - - // Sliders - static float slider_f = 0.5f; - static int slider_i = 50; - ImGui::Text("Underlying float value: %f", slider_f); - ImGui::SliderFloat("SliderFloat (0 -> 1)", &slider_f, 0.0f, 1.0f, "%.3f", - flags); - ImGui::SliderInt("SliderInt (0 -> 100)", &slider_i, 0, 100, "%d", flags); - - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Widgets/Range Widgets"); - if (ImGui::TreeNode("Range Widgets")) { - 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(); - } - - IMGUI_DEMO_MARKER("Widgets/Data Types"); - if (ImGui::TreeNode("Data Types")) { -// 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); -// } - -// 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; - ImU64 ULLONG_MAX = (2ULL * 9223372036854775807LL + 1); -#endif - const char s8_zero = 0, s8_one = 1, s8_fifty = 50, s8_min = -128, - s8_max = 127; - const ImU8 u8_zero = 0, u8_one = 1, u8_fifty = 50, u8_min = 0, u8_max = 255; - const short s16_zero = 0, s16_one = 1, s16_fifty = 50, s16_min = -32768, - s16_max = 32767; - const ImU16 u16_zero = 0, u16_one = 1, u16_fifty = 50, u16_min = 0, - u16_max = 65535; - const ImS32 s32_zero = 0, s32_one = 1, s32_fifty = 50, - s32_min = INT_MIN / 2, s32_max = INT_MAX / 2, - s32_hi_a = INT_MAX / 2 - 100, s32_hi_b = INT_MAX / 2; - const ImU32 u32_zero = 0, u32_one = 1, u32_fifty = 50, u32_min = 0, - u32_max = UINT_MAX / 2, u32_hi_a = UINT_MAX / 2 - 100, - u32_hi_b = UINT_MAX / 2; - const ImS64 s64_zero = 0, s64_one = 1, s64_fifty = 50, - s64_min = LLONG_MIN / 2, s64_max = LLONG_MAX / 2, - s64_hi_a = LLONG_MAX / 2 - 100, s64_hi_b = LLONG_MAX / 2; - const ImU64 u64_zero = 0, u64_one = 1, u64_fifty = 50, u64_min = 0, - u64_max = ULLONG_MAX / 2, u64_hi_a = ULLONG_MAX / 2 - 100, - u64_hi_b = ULLONG_MAX / 2; - const float f32_zero = 0.f, f32_one = 1.f, f32_lo_a = -10000000000.0f, - f32_hi_a = +10000000000.0f; - const double f64_zero = 0., f64_one = 1., f64_lo_a = -1000000000000000.0, - f64_hi_a = +1000000000000000.0; - - // State - static char s8_v = 127; - static ImU8 u8_v = 255; - static short s16_v = 32767; - static ImU16 u16_v = 65535; - static ImS32 s32_v = -1; - static ImU32 u32_v = (ImU32)-1; - static ImS64 s64_v = -1; - static ImU64 u64_v = (ImU64)-1; - static float f32_v = 0.123f; - static double f64_v = 90000.01234567890123456789; - - const float drag_speed = 0.2f; - static bool drag_clamp = false; - 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"); - 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_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::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, "%04X"); - 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"); - 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::TreePop(); - } - - 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::TreePop(); - } - - 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(); - } - - 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 - // '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::TreePop(); - } - - IMGUI_DEMO_MARKER("Widgets/Drag and drop/Copy-swap items"); - if (ImGui::TreeNode("Drag and drop to copy/swap items")) { - enum Mode { Mode_Copy, Mode_Move, Mode_Swap }; - static int mode = 0; - 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"}; - 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)); - - // Our buttons are both drag sources and drag targets here! - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { - // 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(); - } - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* payload = - ImGui::AcceptDragDropPayload("DND_DEMO_CELL")) { - IM_ASSERT(payload->DataSize == sizeof(int)); - int payload_n = *(const int*)payload->Data; - if (mode == Mode_Copy) { - names[n] = names[payload_n]; - } - if (mode == Mode_Move) { - names[n] = names[payload_n]; - names[payload_n] = ""; - } - if (mode == Mode_Swap) { - const char* tmp = names[n]; - names[n] = names[payload_n]; - names[payload_n] = tmp; - } - } - ImGui::EndDragDropTarget(); - } - ImGui::PopID(); - } - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Widgets/Drag and Drop/Drag to reorder items (simple)"); - if (ImGui::TreeNode("Drag to reorder items (simple)")) { - // 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::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::BeginTooltip(); - ImGui::Text("Cannot drop here!"); - ImGui::EndTooltip(); - } - 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(); - } - - 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] = {}; - 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) { - ImGui::PushButtonRepeat(true); - ret = ImGui::Button("ITEM: Button"); - ImGui::PopButtonRepeat(); - } // 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(_AllowWhenOverlappedByItem) = %d\n" - "IsItemHovered(_AllowWhenOverlappedByWindow) = %d\n" - "IsItemHovered(_AllowWhenDisabled) = %d\n" - "IsItemHovered(_RectOnly) = %d\n" - "IsItemActive() = %d\n" - "IsItemEdited() = %d\n" - "IsItemActivated() = %d\n" - "IsItemDeactivated() = %d\n" - "IsItemDeactivatedAfterEdit() = %d\n" - "IsItemVisible() = %d\n" - "IsItemClicked() = %d\n" - "IsItemToggledOpen() = %d\n" - "GetItemRectMin() = (%.1f, %.1f)\n" - "GetItemRectMax() = (%.1f, %.1f)\n" - "GetItemRectSize() = (%.1f, %.1f)", - ret, ImGui::IsItemFocused(), ImGui::IsItemHovered(), - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByItem), - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow), - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled), - ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly), ImGui::IsItemActive(), - ImGui::IsItemEdited(), ImGui::IsItemActivated(), - ImGui::IsItemDeactivated(), 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 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.0f), - true); - - // Testing IsWindowFocused() function with its various flags. - ImGui::BulletText( - "IsWindowFocused() = %d\n" - "IsWindowFocused(_ChildWindows) = %d\n" - "IsWindowFocused(_ChildWindows|_NoPopupHierarchy) = %d\n" - "IsWindowFocused(_ChildWindows|_RootWindow) = %d\n" - "IsWindowFocused(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" - "IsWindowFocused(_RootWindow) = %d\n" - "IsWindowFocused(_RootWindow|_NoPopupHierarchy) = %d\n" - "IsWindowFocused(_AnyWindow) = %d\n", - ImGui::IsWindowFocused(), - ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows), - ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | - ImGuiFocusedFlags_NoPopupHierarchy), - ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | - ImGuiFocusedFlags_RootWindow), - ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | - ImGuiFocusedFlags_RootWindow | - ImGuiFocusedFlags_NoPopupHierarchy), - ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow), - ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow | - ImGuiFocusedFlags_NoPopupHierarchy), - ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)); - - // 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|_RootWindow) = %d\n" - "IsWindowHovered(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" - "IsWindowHovered(_RootWindow) = %d\n" - "IsWindowHovered(_RootWindow|_NoPopupHierarchy) = %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_RootWindow), - ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | - ImGuiHoveredFlags_RootWindow | - ImGuiHoveredFlags_NoPopupHierarchy), - ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow), - ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow | - ImGuiHoveredFlags_NoPopupHierarchy), - ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | - ImGuiHoveredFlags_AllowWhenBlockedByPopup), - ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow), - ImGui::IsWindowHovered(ImGuiHoveredFlags_Stationary)); - - ImGui::BeginChild("child", ImVec2(0, 50), true); - ImGui::Text( - "This is another child window for testing the _ChildWindows flag."); - ImGui::EndChild(); - if (embed_all_inside_a_child_window) ImGui::EndChild(); - - // 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 associated to the title bar of a window. - static bool test_window = false; - ImGui::Checkbox("Hovered/Active tests after Begin() for title bar testing", - &test_window); - if (test_window) { - ImGui::Begin("Title bar Hovered/Active tests", &test_window); - if (ImGui::BeginPopupContextItem()) // <-- This is using IsItemHovered() - { - if (ImGui::MenuItem("Close")) { - test_window = false; - } - ImGui::EndPopup(); - } - ImGui::Text( - "IsItemHovered() after begin = %d (== is title bar hovered)\n" - "IsItemActive() after begin = %d (== is window being " - "clicked/moved)\n", - ImGui::IsItemHovered(), ImGui::IsItemActive()); - ImGui::End(); - } - - ImGui::TreePop(); - } - - // Demonstrate BeginDisabled/EndDisabled using a checkbox located at the - // bottom of the section (which is a bit odd: logically we'd have this - // checkbox at the top of the section, but we don't want this feature to steal - // that space) - if (disable_all) ImGui::EndDisabled(); - - IMGUI_DEMO_MARKER("Widgets/Disable Block"); - if (ImGui::TreeNode("Disable block")) { - ImGui::Checkbox("Disable entire section above", &disable_all); - ImGui::SameLine(); - HelpMarker( - "Demonstrate using BeginDisabled()/EndDisabled() across this section."); - ImGui::TreePop(); - } - - 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(); - } -} - -static void ShowDemoWindowLayout() { - 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), - false, 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), true, 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(); - } - - 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; - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); - ImGui::DragInt("Offset X", &offset_x, 1.0f, -1000, 1000); - - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (float)offset_x); - ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(255, 0, 0, 100)); - ImGui::BeginChild("Red", ImVec2(200, 100), true, ImGuiWindowFlags_None); - 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::PopStyleColor(); - 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::GetWindowPos().x + ImGui::GetWindowContentRegionMax().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( - "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(); - ImGui::Button("AAA"); - ImGui::SameLine(); - ImGui::Button("BBB"); - ImGui::SameLine(); - ImGui::BeginGroup(); - ImGui::Button("CCC"); - ImGui::Button("DDD"); - ImGui::EndGroup(); - ImGui::SameLine(); - ImGui::Button("EEE"); - ImGui::EndGroup(); - 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::SameLine(); - 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::BeginListBox("List", size)) { - ImGui::Selectable("Selected", true); - ImGui::Selectable("Not Selected", false); - ImGui::EndListBox(); - } - - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Layout/Text Baseline Alignment"); - if (ImGui::TreeNode("Text Baseline Alignment")) { - { - 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("KO Blahblah"); - ImGui::SameLine(); - ImGui::Button("Some framed item"); - ImGui::SameLine(); - HelpMarker("Baseline of button will look misaligned with text.."); - - // 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"); - ImGui::SameLine(); - HelpMarker( - "We call AlignTextToFramePadding() to vertically align the text " - "baseline by +FramePadding.y"); - - // SmallButton() uses the same vertical padding as Text - ImGui::Button("TEST##1"); - ImGui::SameLine(); - ImGui::Text("TEST"); - ImGui::SameLine(); - ImGui::SmallButton("TEST##2"); - - // 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::Unindent(); - } - - ImGui::Spacing(); - - { - ImGui::BulletText("Multi-line text:"); - ImGui::Indent(); - ImGui::Text("One\nTwo\nThree"); - ImGui::SameLine(); - ImGui::Text("Hello\nWorld"); - ImGui::SameLine(); - ImGui::Text("Banana"); - - ImGui::Text("Banana"); - ImGui::SameLine(); - ImGui::Text("Hello\nWorld"); - ImGui::SameLine(); - ImGui::Text("One\nTwo\nThree"); - - ImGui::Button("HOP##1"); - ImGui::SameLine(); - ImGui::Text("Banana"); - ImGui::SameLine(); - ImGui::Text("Hello\nWorld"); - ImGui::SameLine(); - ImGui::Text("Banana"); - - 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")) { - // Vertical scroll functions - IMGUI_DEMO_MARKER("Layout/Scrolling/Vertical"); - HelpMarker( - "Use SetScrollHereY() or SetScrollFromPosY() to scroll to a given " - "vertical position."); - - 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("Decoration", &enable_extra_decorations); - - ImGui::Checkbox("Track", &enable_track); - ImGui::PushItemWidth(100); - 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", &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", &scroll_to_pos_px, 1.00f, -10, - FLT_MAX, "X/Y = %.0f px"); - ImGui::PopItemWidth(); - - if (scroll_to_off || scroll_to_pos) 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(); - const char* names[] = {"Top", "25%", "Center", "75%", "Bottom"}; - ImGui::TextUnformatted(names[i]); - - 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), true, 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); - if (child_is_visible) // Avoid calling SetScrollHereY when running with - // culled items - { - for (int item = 0; item < 100; item++) { - 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(); - float scroll_max_y = ImGui::GetScrollMaxY(); - ImGui::EndChild(); - ImGui::Text("%.0f/%.0f", scroll_y, scroll_max_y); - ImGui::EndGroup(); - } - ImGui::PopID(); - - // 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), true, 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)); - ImVec2 scrolling_child_size = - ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 7 + 30); - ImGui::BeginChild("scrolling", scrolling_child_size, true, - 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. - int num_buttons = 10 + ((line & 1) ? line * 9 : line * 3); - for (int n = 0; n < num_buttons; n++) { - if (n > 0) ImGui::SameLine(); - 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; - 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)); - ImGui::Button(label, - ImVec2(40.0f + sinf((float)(line + n)) * 20.0f, 0.0f)); - ImGui::PopStyleColor(3); - ImGui::PopID(); - } - } - float scroll_x = ImGui::GetScrollX(); - float scroll_max_x = ImGui::GetScrollMaxX(); - 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::Text("Scroll from code"); - 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) { - // 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(); - } - ImGui::Spacing(); - - static bool show_horizontal_contents_size_demo_window = false; - ImGui::Checkbox("Show Horizontal contents size demo window", - &show_horizontal_contents_size_demo_window); - - if (show_horizontal_contents_size_demo_window) { - static bool show_h_scrollbar = true; - static bool show_button = true; - static bool show_tree_nodes = true; - static bool show_text_wrapped = false; - static bool show_columns = true; - static bool show_tab_bar = true; - static bool show_child = false; - static bool explicit_content_size = false; - static float contents_size_x = 300.0f; - 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."); - 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 - ImGui::Checkbox("Text wrapped", - &show_text_wrapped); // Will grow and use contents size - ImGui::Checkbox("Columns", &show_columns); // Will use contents size - ImGui::Checkbox("Tab bar", &show_tab_bar); // Will use contents size - ImGui::Checkbox("Child", &show_child); // Will grow and use contents size - ImGui::Checkbox("Explicit content size", &explicit_content_size); - ImGui::Text("Scroll %.1f/%.1f %.1f/%.1f", ImGui::GetScrollX(), - ImGui::GetScrollMaxX(), ImGui::GetScrollY(), - ImGui::GetScrollMaxY()); - if (explicit_content_size) { - ImGui::SameLine(); - ImGui::SetNextItemWidth(100); - ImGui::DragFloat("##csx", &contents_size_x); - 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)); - } - ImGui::PopStyleVar(2); - ImGui::Separator(); - if (show_button) { - ImGui::Button("this is a 300-wide button", ImVec2(300, 0)); - } - if (show_tree_nodes) { - bool open = true; - if (ImGui::TreeNode("this is a tree node")) { - if (ImGui::TreeNode("another one of those tree node...")) { - ImGui::Text("Some tree contents"); - ImGui::TreePop(); - } - ImGui::TreePop(); - } - ImGui::CollapsingHeader("CollapsingHeader", &open); - } - if (show_text_wrapped) { - ImGui::TextWrapped( - "This text should automatically wrap on the edge of the work " - "rectangle."); - } - 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++) { - ImGui::Text("Width %.2f", ImGui::GetColumnWidth()); - ImGui::NextColumn(); - } - ImGui::Columns(1); - } - if (show_tab_bar && ImGui::BeginTabBar("Hello")) { - if (ImGui::BeginTabItem("OneOneOne")) { - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("TwoTwoTwo")) { - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("ThreeThreeThree")) { - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("FourFourFour")) { - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - if (show_child) { - ImGui::BeginChild("child", ImVec2(0, 0), true); - ImGui::EndChild(); - } - ImGui::End(); - } - - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Layout/Clipping"); - if (ImGui::TreeNode("Clipping")) { - 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 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() { - 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. - - // 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(); - // Typical use for popups: - // if (ImGui::Button("Open")) ImGui::OpenPopup("MyPopup"); if - // (ImGui::BeginPopup("MyPopup") { [...] EndPopup(); } - - // 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."); - - 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) - 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::SeparatorText("Aquarium"); - for (int i = 0; i < IM_ARRAYSIZE(names); i++) - if (ImGui::Selectable(names[i])) selected_fish = i; - ImGui::EndPopup(); - } - - // Showing a menu with toggles - if (ImGui::Button("Toggle..")) ImGui::OpenPopup("my_toggle_popup"); - if (ImGui::BeginPopup("my_toggle_popup")) { - for (int i = 0; i < IM_ARRAYSIZE(names); i++) - ImGui::MenuItem(names[i], "", &toggles[i]); - if (ImGui::BeginMenu("Sub-menu")) { - ImGui::MenuItem("Click me"); - ImGui::EndMenu(); - } - - ImGui::Separator(); - ImGui::Text("Tooltip here"); - ImGui::SetItemTooltip("I am a tooltip over a popup"); - - if (ImGui::Button("Stacked Popup")) ImGui::OpenPopup("another popup"); - if (ImGui::BeginPopup("another popup")) { - for (int i = 0; i < IM_ARRAYSIZE(names); i++) - ImGui::MenuItem(names[i], "", &toggles[i]); - if (ImGui::BeginMenu("Sub-menu")) { - ImGui::MenuItem("Click me"); - if (ImGui::Button("Stacked Popup")) ImGui::OpenPopup("another popup"); - if (ImGui::BeginPopup("another popup")) { - ImGui::Text("I am the last one here."); - ImGui::EndPopup(); - } - ImGui::EndMenu(); - } - ImGui::EndPopup(); - } - ImGui::EndPopup(); - } - - // Call the more complete ShowExampleMenuFile which we use in various places - // of this demo - if (ImGui::Button("With a menu..")) ImGui::OpenPopup("my_file_popup"); - if (ImGui::BeginPopup("my_file_popup", ImGuiWindowFlags_MenuBar)) { - 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 (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. - { - 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"); - } - } - - // 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. - { - 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::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."); - - 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!"); - ImGui::Separator(); - - // 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)); - ImGui::Checkbox("Don't ask me next time", &dont_ask_me_next_time); - ImGui::PopStyleVar(); - - if (ImGui::Button("OK", ImVec2(120, 0))) { - ImGui::CloseCurrentPopup(); - } - ImGui::SetItemDefaultFocus(); - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - - if (ImGui::Button("Stacked modals..")) ImGui::OpenPopup("Stacked 1"); - if (ImGui::BeginPopupModal("Stacked 1", NULL, ImGuiWindowFlags_MenuBar)) { - if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Some menu item")) { - } - ImGui::EndMenu(); - } - ImGui::EndMenuBar(); - } - ImGui::Text( - "Hello from Stacked The First\nUsing " - "style.Colors[ImGuiCol_ModalWindowDimBg] behind it."); - - // 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}; - ImGui::Combo("Combo", &item, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0"); - 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 unused_open = true; - if (ImGui::BeginPopupModal("Stacked 2", &unused_open)) { - ImGui::Text("Hello from Stacked The Second!"); - if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - - if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - - 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(); - - ImGui::MenuItem("Menu item", "CTRL+M"); - if (ImGui::BeginMenu("Menu inside a regular window")) { - ShowExampleMenuFile(); - ImGui::EndMenu(); - } - ImGui::Separator(); - ImGui::TreePop(); - } -} - -// 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 { -// 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; -} // namespace - -// Make the UI compact because there are so many fields -static void PushStyleCompact() { - ImGuiStyle& style = ImGui::GetStyle(); - ImGui::PushStyleVar( - ImGuiStyleVar_FramePadding, - ImVec2(style.FramePadding.x, (float)(int)(style.FramePadding.y * 0.60f))); - ImGui::PushStyleVar( - ImGuiStyleVar_ItemSpacing, - ImVec2(style.ItemSpacing.x, (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); -} - -static void ShowDemoWindowTables() { - // ImGui::SetNextItemOpen(true, ImGuiCond_Once); - IMGUI_DEMO_MARKER("Tables"); - if (!ImGui::CollapsingHeader("Tables & Columns")) return; - - // 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 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_BordersInnerV\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 too much. 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\nWe 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 + ScrollX together 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++) { - ImGui::Indent( - indent_step); // Add some indentation to demonstrate usage of - // per-column IndentEnable/IndentDisable flags. - 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 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\nWe 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\nPlease 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::PushStyleVar(ImGuiStyleVar_CellPadding, - ImVec2(style.CellPadding.x, 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 flags = - ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | - ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | - ImGuiTableFlags_NoBordersInBody; - - static ImGuiTreeNodeFlags tree_node_flags = - ImGuiTreeNodeFlags_SpanAllColumns; - ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &tree_node_flags, - ImGuiTreeNodeFlags_SpanFullWidth); - ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &tree_node_flags, - ImGuiTreeNodeFlags_SpanAllColumns); - - if (ImGui::BeginTable("3ways", 3, 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); - if (is_folder) { - bool open = ImGui::TreeNodeEx(node->Name, tree_node_flags); - 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, - tree_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", "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 - 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(); - } - - 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 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("_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); - - 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], - ImGuiTableColumnFlags_AngledHeader | - ImGuiTableColumnFlags_WidthFixed); - 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.\nUsing " - "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-colum 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_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? - ImGui::PushButtonRepeat(true); -#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 a more advanced system in your - // code 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(); - } - } - ImGui::PopButtonRepeat(); - - // 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(); - - ShowDemoWindowColumns(); - - 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 ShowDemoWindowColumns() { - 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:"); - ImGui::Columns(3, "mycolumns3", false); // 3-ways, no border - ImGui::Separator(); - for (int n = 0; n < 14; n++) { - char label[32]; - sprintf(label, "Item %d", n); - if (ImGui::Selectable(label)) { - } - // if (ImGui::Button(label, ImVec2(-FLT_MIN,0.0f))) {} - ImGui::NextColumn(); - } - ImGui::Columns(1); - ImGui::Separator(); - - ImGui::Text("With border:"); - ImGui::Columns(4, "mycolumns"); // 4-ways, with border - ImGui::Separator(); - ImGui::Text("ID"); - ImGui::NextColumn(); - ImGui::Text("Name"); - ImGui::NextColumn(); - ImGui::Text("Path"); - ImGui::NextColumn(); - ImGui::Text("Hovered"); - ImGui::NextColumn(); - ImGui::Separator(); - const char* names[3] = {"One", "Two", "Three"}; - const char* paths[3] = {"/path/one", "/path/two", "/path/three"}; - static int selected = -1; - for (int i = 0; i < 3; i++) { - char label[32]; - sprintf(label, "%04d", i); - if (ImGui::Selectable(label, selected == i, - ImGuiSelectableFlags_SpanAllColumns)) - selected = i; - bool hovered = ImGui::IsItemHovered(); - ImGui::NextColumn(); - ImGui::Text(names[i]); - ImGui::NextColumn(); - ImGui::Text(paths[i]); - ImGui::NextColumn(); - ImGui::Text("%d", hovered); - ImGui::NextColumn(); - } - ImGui::Columns(1); - ImGui::Separator(); - 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(columns_count, NULL, v_borders); - for (int i = 0; i < columns_count * lines_count; i++) { - if (h_borders && ImGui::GetColumnIndex() == 0) ImGui::Separator(); - 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(-FLT_MIN, 0.0f)); - ImGui::NextColumn(); - } - ImGui::Columns(1); - if (h_borders) ImGui::Separator(); - ImGui::TreePop(); - } - - // 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"); - ImGui::Separator(); - - ImGui::Text("Hello"); - ImGui::Button("Banana"); - ImGui::NextColumn(); - - ImGui::Text("ImGui"); - ImGui::Button("Apple"); - static float foo = 1.0f; - ImGui::InputFloat("red", &foo, 0.05f, 0, "%.3f"); - ImGui::Text("An extra line here."); - ImGui::NextColumn(); - - ImGui::Text("Sailor"); - ImGui::Button("Corniflower"); - static float bar = 1.0f; - ImGui::InputFloat("blue", &bar, 0.05f, 0, "%.3f"); - ImGui::NextColumn(); - - if (ImGui::CollapsingHeader("Category A")) { - ImGui::Text("Blah blah blah"); - } - ImGui::NextColumn(); - if (ImGui::CollapsingHeader("Category B")) { - ImGui::Text("Blah blah blah"); - } - ImGui::NextColumn(); - if (ImGui::CollapsingHeader("Category C")) { - ImGui::Text("Blah blah blah"); - } - ImGui::NextColumn(); - ImGui::Columns(1); - ImGui::Separator(); - ImGui::TreePop(); - } - - // Word wrapping - IMGUI_DEMO_MARKER("Columns (legacy API)/Word-wrapping"); - if (ImGui::TreeNode("Word-wrapping")) { - ImGui::Columns(2, "word-wrapping"); - ImGui::Separator(); - ImGui::TextWrapped("The quick brown fox jumps over the lazy dog."); - ImGui::TextWrapped("Hello Left"); - ImGui::NextColumn(); - ImGui::TextWrapped("The quick brown fox jumps over the lazy dog."); - ImGui::TextWrapped("Hello Right"); - ImGui::Columns(1); - ImGui::Separator(); - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Columns (legacy API)/Horizontal Scrolling"); - if (ImGui::TreeNode("Horizontal Scrolling")) { - ImGui::SetNextWindowContentSize(ImVec2(1500.0f, 0.0f)); - ImVec2 child_size = ImVec2(0, ImGui::GetFontSize() * 20.0f); - ImGui::BeginChild("##ScrollingRegion", child_size, false, - ImGuiWindowFlags_HorizontalScrollbar); - ImGui::Columns(10); - - // Also demonstrate using clipper for large vertical lists - int ITEMS_COUNT = 2000; - ImGuiListClipper clipper; - clipper.Begin(ITEMS_COUNT); - while (clipper.Step()) { - for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - for (int j = 0; j < 10; j++) { - ImGui::Text("Line %d Column %d...", i, j); - ImGui::NextColumn(); - } - } - ImGui::Columns(1); - ImGui::EndChild(); - ImGui::TreePop(); - } - - IMGUI_DEMO_MARKER("Columns (legacy API)/Tree"); - if (ImGui::TreeNode("Tree")) { - ImGui::Columns(2, "tree", true); - for (int x = 0; x < 3; x++) { - bool open1 = ImGui::TreeNode((void*)(intptr_t)x, "Node%d", x); - ImGui::NextColumn(); - ImGui::Text("Node contents"); - ImGui::NextColumn(); - if (open1) { - for (int y = 0; y < 3; y++) { - bool open2 = ImGui::TreeNode((void*)(intptr_t)y, "Node%d.%d", x, y); - ImGui::NextColumn(); - ImGui::Text("Node contents"); - if (open2) { - ImGui::Text("Even more contents"); - if (ImGui::TreeNode("Tree in column")) { - ImGui::Text("The quick brown fox jumps over the lazy dog"); - ImGui::TreePop(); - } - } - ImGui::NextColumn(); - if (open2) ImGui::TreePop(); - } - ImGui::TreePop(); - } - } - ImGui::Columns(1); - ImGui::TreePop(); - } - - ImGui::TreePop(); -} - -static void ShowDemoWindowInputs() { - IMGUI_DEMO_MARKER("Inputs & Focus"); - if (ImGui::CollapsingHeader("Inputs & Focus")) { - ImGuiIO& io = ImGui::GetIO(); - - // Display inputs submitted to ImGuiIO - IMGUI_DEMO_MARKER("Inputs & Focus/Inputs"); - ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::TreeNode("Inputs")) { - HelpMarker( - "This is a simplified view. See more detailed input state:\n" - "- in 'Tools->Metrics/Debugger->Inputs'.\n" - "- in 'Tools->Debug Log->IO'."); - 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 (ImGui::IsMouseDown(i)) { - ImGui::SameLine(); - ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); - } - ImGui::Text("Mouse wheel: %.1f", io.MouseWheel); - - // We iterate both legacy native range and named ImGuiKey ranges, which is - // a little 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. -#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO - struct funcs { - static bool IsLegacyNativeDupe(ImGuiKey) { return false; } - }; - ImGuiKey start_key = ImGuiKey_NamedKey_BEGIN; -#else - struct funcs { - static bool IsLegacyNativeDupe(ImGuiKey key) { - return key < 512 && ImGui::GetIO().KeyMap[key] != -1; - } - }; // Hide Native<>ImGuiKey duplicates when both exists in the array - ImGuiKey start_key = (ImGuiKey)0; -#endif - 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::TreePop(); - } - - // Display ImGuiIO output flags - IMGUI_DEMO_MARKER("Inputs & Focus/Outputs"); - ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::TreeNode("Outputs")) { - 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)."); - 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(); - } - - // 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", "NotAllowed"}; - IM_ASSERT(IM_ARRAYSIZE(mouse_cursors_names) == ImGuiMouseCursor_COUNT); - - ImGuiMouseCursor current = ImGui::GetMouseCursor(); - ImGui::Text("Current mouse cursor = %d: %s", current, - mouse_cursors_names[current]); - 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] = "hello"; - ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); - ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); - ImGui::InputText("3", buf, IM_ARRAYSIZE(buf)); - ImGui::PushTabStop(false); - ImGui::InputText("4 (tab skip)", buf, IM_ARRAYSIZE(buf)); - ImGui::SameLine(); - HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); - ImGui::PopTabStop(); - 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(); - bool focus_2 = ImGui::Button("Focus on 2"); - ImGui::SameLine(); - bool focus_3 = ImGui::Button("Focus on 3"); - int has_focus = 0; - static char buf[128] = "click on a button to set focus"; - - if (focus_1) ImGui::SetKeyboardFocusHere(); - ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); - if (ImGui::IsItemActive()) has_focus = 1; - - if (focus_2) ImGui::SetKeyboardFocusHere(); - ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); - if (ImGui::IsItemActive()) has_focus = 2; - - ImGui::PushTabStop(false); - if (focus_3) ImGui::SetKeyboardFocusHere(); - ImGui::InputText("3 (tab skip)", buf, IM_ARRAYSIZE(buf)); - if (ImGui::IsItemActive()) has_focus = 3; - ImGui::SameLine(); - HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); - ImGui::PopTabStop(); - - if (has_focus) - ImGui::Text("Item with focus: %d", has_focus); - else - ImGui::Text("Item with focus: "); - - // Use >= 0 parameter to SetKeyboardFocusHere() to focus an upcoming item - static float f3[3] = {0.0f, 0.0f, 0.0f}; - int focus_ahead = -1; - if (ImGui::Button("Focus on X")) { - focus_ahead = 0; - } - ImGui::SameLine(); - if (ImGui::Button("Focus on Y")) { - focus_ahead = 1; - } - ImGui::SameLine(); - if (ImGui::Button("Focus on Z")) { - focus_ahead = 2; - } - if (focus_ahead != -1) ImGui::SetKeyboardFocusHere(focus_ahead); - ImGui::SliderFloat3("Float3", &f3[0], 0.0f, 1.0f); - - ImGui::TextWrapped( - "NB: Cursor & selection are preserved when refocusing last used item " - "in code."); - 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):", 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(). - 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):"); - 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(); - } - } -} - -//----------------------------------------------------------------------------- -// [SECTION] About Window / ShowAboutWindow() -// Access from Dear ImGui Demo -> Tools -> About -//----------------------------------------------------------------------------- - -void ImGui::ShowAboutWindow(bool* p_open) { - if (!ImGui::Begin("About Dear ImGui", p_open, - ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::End(); - return; - } - IMGUI_DEMO_MARKER("Tools/About Dear ImGui"); - ImGui::Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); - ImGui::Separator(); - ImGui::Text("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 sponsoring the project!"); - - static bool show_config_info = false; - ImGui::Checkbox("Config/Build Information", &show_config_info); - if (show_config_info) { - ImGuiIO& io = ImGui::GetIO(); - ImGuiStyle& style = ImGui::GetStyle(); - - bool copy_to_clipboard = ImGui::Button("Copy to clipboard"); - ImVec2 child_size = ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 18); - ImGui::BeginChildFrame(ImGui::GetID("cfg_infos"), child_size, - ImGuiWindowFlags_NoMove); - 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(); - ImGui::Text( - "sizeof(size_t): %d, sizeof(ImDrawIdx): %d, sizeof(ImDrawVert): %d", - (int)sizeof(size_t), (int)sizeof(ImDrawIdx), (int)sizeof(ImDrawVert)); - ImGui::Text("define: __cplusplus=%d", (int)__cplusplus); -#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - ImGui::Text("define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS"); -#endif -#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO - ImGui::Text("define: IMGUI_DISABLE_OBSOLETE_KEYIO"); -#endif -#ifdef IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS - ImGui::Text("define: IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS"); -#endif -#ifdef IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS - ImGui::Text("define: IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS"); -#endif -#ifdef IMGUI_DISABLE_WIN32_FUNCTIONS - ImGui::Text("define: IMGUI_DISABLE_WIN32_FUNCTIONS"); -#endif -#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"); -#endif -#ifdef IMGUI_USE_BGRA_PACKED_COLOR - ImGui::Text("define: IMGUI_USE_BGRA_PACKED_COLOR"); -#endif -#ifdef _WIN32 - ImGui::Text("define: _WIN32"); -#endif -#ifdef _WIN64 - ImGui::Text("define: _WIN64"); -#endif -#ifdef __linux__ - ImGui::Text("define: __linux__"); -#endif -#ifdef __APPLE__ - ImGui::Text("define: __APPLE__"); -#endif -#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 -#ifdef __MINGW64__ - ImGui::Text("define: __MINGW64__"); -#endif -#ifdef __GNUC__ - ImGui::Text("define: __GNUC__=%d", (int)__GNUC__); -#endif -#ifdef __clang_version__ - ImGui::Text("define: __clang_version__=%s", __clang_version__); -#endif -#ifdef __EMSCRIPTEN__ - ImGui::Text("define: __EMSCRIPTEN__"); -#endif - ImGui::Separator(); - ImGui::Text("io.BackendPlatformName: %s", - io.BackendPlatformName ? io.BackendPlatformName : "NULL"); - ImGui::Text("io.BackendRendererName: %s", - io.BackendRendererName ? io.BackendRendererName : "NULL"); - 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.MouseDrawCursor) ImGui::Text("io.MouseDrawCursor"); - if (io.ConfigMacOSXBehaviors) ImGui::Text("io.ConfigMacOSXBehaviors"); - 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_RendererHasVtxOffset) - ImGui::Text(" RendererHasVtxOffset"); - 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); - ImGui::Text("io.DisplayFramebufferScale: %.2f,%.2f", - io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y); - ImGui::Separator(); - ImGui::Text("style.WindowPadding: %.2f,%.2f", style.WindowPadding.x, - style.WindowPadding.y); - ImGui::Text("style.WindowBorderSize: %.2f", style.WindowBorderSize); - ImGui::Text("style.FramePadding: %.2f,%.2f", style.FramePadding.x, - style.FramePadding.y); - ImGui::Text("style.FrameRounding: %.2f", style.FrameRounding); - ImGui::Text("style.FrameBorderSize: %.2f", style.FrameBorderSize); - ImGui::Text("style.ItemSpacing: %.2f,%.2f", style.ItemSpacing.x, - style.ItemSpacing.y); - 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::End(); -} - -//----------------------------------------------------------------------------- -// [SECTION] Style Editor / ShowStyleEditor() -//----------------------------------------------------------------------------- -// - ShowFontSelector() -// - ShowStyleSelector() -// - ShowStyleEditor() -//----------------------------------------------------------------------------- - -// 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 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 (ImFont* font : io.Fonts->Fonts) { - ImGui::PushID((void*)font); - if (ImGui::Selectable(font->GetDebugName(), font == font_current)) - io.FontDefault = font; - ImGui::PopID(); - } - ImGui::EndCombo(); - } - ImGui::SameLine(); - 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 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) { - 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; - - // Default to using internal storage as reference - static bool init = true; - if (init && ref == NULL) ref_saved_style = style; - init = false; - if (ref == NULL) ref = &ref_saved_style; - - ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.50f); - - if (ImGui::ShowStyleSelector("Colors##Selector")) ref_saved_style = style; - ImGui::ShowFontSelector("Fonts##Selector"); - - // 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 border = (style.WindowBorderSize > 0.0f); - if (ImGui::Checkbox("WindowBorder", &border)) { - style.WindowBorderSize = border ? 1.0f : 0.0f; - } - } - ImGui::SameLine(); - { - bool border = (style.FrameBorderSize > 0.0f); - if (ImGui::Checkbox("FrameBorder", &border)) { - style.FrameBorderSize = border ? 1.0f : 0.0f; - } - } - ImGui::SameLine(); - { - 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")) *ref = ref_saved_style = style; - ImGui::SameLine(); - 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\" below to save them somewhere."); - - ImGui::Separator(); - - if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { - if (ImGui::BeginTabItem("Sizes")) { - 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"); - ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, - 0.0f, 20.0f, "%.0f"); - ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, - 0.0f, 10.0f, "%.0f"); - 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::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::SliderFloat("TabBarBorderSize", &style.TabBarBorderSize, 0.0f, - 2.0f, "%.0f"); - - 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::SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, - "%.0f"); - - ImGui::SeparatorText("Tables"); - ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, - 20.0f, "%.0f"); - ImGui::SliderAngle("TableAngledHeadersAngle", - &style.TableAngledHeadersAngle, -50.0f, +50.0f); - - ImGui::SeparatorText("Widgets"); - ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, - 0.0f, 1.0f, "%.2f"); - int window_menu_button_position = style.WindowMenuButtonPosition + 1; - if (ImGui::Combo("WindowMenuButtonPosition", - (int*)&window_menu_button_position, - "None\0Left\0Right\0")) - style.WindowMenuButtonPosition = window_menu_button_position - 1; - 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::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("DisplaySafeAreaPadding", - (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, - "%.0f"); - 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::EndTabItem(); - } - - if (ImGui::BeginTabItem("Colors")) { - static int output_dest = 0; - static bool output_only_modified = true; - if (ImGui::Button("Export")) { - if (output_dest == 0) - ImGui::LogToClipboard(); - else - ImGui::LogToTTY(); - ImGui::LogText("ImVec4* colors = ImGui::GetStyle().Colors;" IM_NEWLINE); - for (int i = 0; i < ImGuiCol_COUNT; i++) { - 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::LogFinish(); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth(120); - ImGui::Combo("##output_type", &output_dest, "To Clipboard\0To TTY\0"); - ImGui::SameLine(); - ImGui::Checkbox("Only Modified Colors", &output_only_modified); - - static ImGuiTextFilter filter; - filter.Draw("Filter colors", ImGui::GetFontSize() * 16); - - static ImGuiColorEditFlags alpha_flags = 0; - if (ImGui::RadioButton("Opaque", - alpha_flags == ImGuiColorEditFlags_None)) { - alpha_flags = ImGuiColorEditFlags_None; - } - ImGui::SameLine(); - if (ImGui::RadioButton("Alpha", - alpha_flags == ImGuiColorEditFlags_AlphaPreview)) { - alpha_flags = ImGuiColorEditFlags_AlphaPreview; - } - 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(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); - 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 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); - ImGui::PopID(); - } - ImGui::PopItemWidth(); - ImGui::EndChild(); - - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Fonts")) { - ImGuiIO& io = ImGui::GetIO(); - ImFontAtlas* atlas = io.Fonts; - 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; - 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, MIN_SCALE, - MAX_SCALE, "%.2f", - ImGuiSliderFlags_AlwaysClamp); // Scale everything - ImGui::PopItemWidth(); - - ImGui::EndTabItem(); - } - - 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 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(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 = number of segments)"); - ImGui::Spacing(); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - const float min_widget_width = ImGui::CalcTextSize("N: MMM\nR: 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(); - - 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\" " - "tesselation 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(); - } - - ImGui::EndTabBar(); - } - - 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 inputing 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,CTRL+Y to undo/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 (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() { - if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("File")) { - ShowExampleMenuFile(); - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Edit")) { - if (ImGui::MenuItem("Undo", "CTRL+Z")) { - } - if (ImGui::MenuItem("Redo", "CTRL+Y", false, false)) { - } // Disabled item - ImGui::Separator(); - if (ImGui::MenuItem("Cut", "CTRL+X")) { - } - if (ImGui::MenuItem("Copy", "CTRL+C")) { - } - if (ImGui::MenuItem("Paste", "CTRL+V")) { - } - ImGui::EndMenu(); - } - ImGui::EndMainMenuBar(); - } -} - -// 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_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")) { - ImGui::MenuItem("fish_hat.c"); - ImGui::MenuItem("fish_hat.inl"); - ImGui::MenuItem("fish_hat.h"); - if (ImGui::BeginMenu("More..")) { - ImGui::MenuItem("Hello"); - ImGui::MenuItem("Sailor"); - if (ImGui::BeginMenu("Recurse..")) { - ShowExampleMenuFile(); - ImGui::EndMenu(); - } - ImGui::EndMenu(); - } - ImGui::EndMenu(); - } - 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); - for (int i = 0; i < 10; i++) ImGui::Text("Scrolling Text %d", i); - ImGui::EndChild(); - static float f = 0.5f; - static int n = 0; - ImGui::SliderFloat("Value", &f, 0.0f, 1.0f); - ImGui::InputFloat("Input", &f, 0.1f); - ImGui::Combo("Combo", &n, "Yes\0No\0Maybe\0\0"); - ImGui::EndMenu(); - } - - IMGUI_DEMO_MARKER("Examples/Menu/Colors"); - if (ImGui::BeginMenu("Colors")) { - float sz = ImGui::GetTextLineHeight(); - for (int i = 0; i < ImGuiCol_COUNT; i++) { - 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::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")) { - } -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Debug Console / ShowExampleAppConsole() -//----------------------------------------------------------------------------- - -// Demonstrate creating a simple console window, with scrolling, filtering, -// completion and history. 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]; - ImVector Items; - ImVector Commands; - ImVector History; - int HistoryPos; // -1: new line, 0..History.Size-1 browsing history. - ImGuiTextFilter Filter; - bool AutoScroll; - bool ScrollToBottom; - - 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"); - AutoScroll = true; - ScrollToBottom = false; - AddLog("Welcome to Dear ImGui!"); - } - ~ExampleAppConsole() { - ClearLog(); - for (int i = 0; i < History.Size; i++) free(History[i]); - } - - // Portable helpers - 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 = malloc(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]); - Items.clear(); - } - - void AddLog(const char* fmt, ...) IM_FMTARGS(2) { - // FIXME-OPT - char buf[1024]; - va_list args; - va_start(args, fmt); - vsnprintf(buf, IM_ARRAYSIZE(buf), fmt, args); - buf[IM_ARRAYSIZE(buf) - 1] = 0; - va_end(args); - Items.push_back(Strdup(buf)); - } - - void Draw(const char* title, bool* p_open) { - 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. Here we create a context menu - // only available from the title bar. - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Close Console")) *p_open = false; - ImGui::EndPopup(); - } - - 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 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(); - - // Options menu - if (ImGui::BeginPopup("Options")) { - ImGui::Checkbox("Auto-scroll", &AutoScroll); - ImGui::EndPopup(); - } - - // Options, Filter - if (ImGui::Button("Options")) ImGui::OpenPopup("Options"); - ImGui::SameLine(); - Filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180); - ImGui::Separator(); - - // 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), false, - ImGuiWindowFlags_HorizontalScrollbar)) { - 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. 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 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(); - } - ImGui::EndChild(); - ImGui::Separator(); - - // Command-line - bool reclaim_focus = false; - 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); - if (s[0]) ExecCommand(s); - strcpy(s, ""); - reclaim_focus = true; - } - - // Auto-focus on window apparition - ImGui::SetItemDefaultFocus(); - if (reclaim_focus) - ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget - - ImGui::End(); - } - - void ExecCommand(const char* command_line) { - 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. - HistoryPos = -1; - for (int i = History.Size - 1; i >= 0; i--) - if (Stricmp(History[i], command_line) == 0) { - free(History[i]); - History.erase(History.begin() + i); - break; - } - History.push_back(Strdup(command_line)); - - // Process command - if (Stricmp(command_line, "CLEAR") == 0) { - ClearLog(); - } else if (Stricmp(command_line, "HELP") == 0) { - AddLog("Commands:"); - for (int i = 0; i < Commands.Size; i++) AddLog("- %s", Commands[i]); - } else if (Stricmp(command_line, "HISTORY") == 0) { - int first = History.Size - 10; - for (int i = first > 0 ? first : 0; i < History.Size; i++) - AddLog("%3d: %s\n", i, History[i]); - } else { - AddLog("Unknown command: '%s'\n", command_line); - } - - // On command input, we scroll to bottom even if AutoScroll==false - ScrollToBottom = true; - } - - // 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); - } - - int TextEditCallback(ImGuiInputTextCallbackData* data) { - // AddLog("cursor: %d, selection: %d-%d", data->CursorPos, - // data->SelectionStart, data->SelectionEnd); - switch (data->EventFlag) { - case ImGuiInputTextFlags_CallbackCompletion: { - // Example of TEXT COMPLETION - - // Locate beginning of current word - const char* word_end = data->Buf + data->CursorPos; - const char* word_start = word_end; - while (word_start > data->Buf) { - const char c = word_start[-1]; - if (c == ' ' || c == '\t' || c == ',' || c == ';') break; - word_start--; - } - - // 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) - candidates.push_back(Commands[i]); - - if (candidates.Size == 0) { - // No match - 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)); - data->InsertChars(data->CursorPos, candidates[0]); - data->InsertChars(data->CursorPos, " "); - } else { - // Multiple matches. Complete as much as we can.. - // So inputing "C"+Tab will complete to "CL" then display "CLEAR" and - // "CLASSIFY" as matches. - int match_len = (int)(word_end - word_start); - for (;;) { - int c = 0; - bool all_candidates_matches = true; - for (int i = 0; i < candidates.Size && all_candidates_matches; i++) - if (i == 0) - c = toupper(candidates[i][match_len]); - else if (c == 0 || c != toupper(candidates[i][match_len])) - all_candidates_matches = false; - if (!all_candidates_matches) break; - match_len++; - } - - if (match_len > 0) { - data->DeleteChars((int)(word_start - data->Buf), - (int)(word_end - word_start)); - data->InsertChars(data->CursorPos, candidates[0], - candidates[0] + match_len); - } - - // List matches - AddLog("Possible matches:\n"); - for (int i = 0; i < candidates.Size; i++) - AddLog("- %s\n", candidates[i]); - } - - break; - } - case ImGuiInputTextFlags_CallbackHistory: { - // Example of HISTORY - const int prev_history_pos = HistoryPos; - if (data->EventKey == ImGuiKey_UpArrow) { - if (HistoryPos == -1) - HistoryPos = History.Size - 1; - else if (HistoryPos > 0) - HistoryPos--; - } else if (data->EventKey == ImGuiKey_DownArrow) { - if (HistoryPos != -1) - if (++HistoryPos >= History.Size) HistoryPos = -1; - } - - // A better implementation would preserve the data on the current input - // line along with cursor position. - if (prev_history_pos != HistoryPos) { - const char* history_str = - (HistoryPos >= 0) ? History[HistoryPos] : ""; - data->DeleteChars(0, data->BufTextLen); - data->InsertChars(0, history_str); - } - } - } - return 0; - } -}; - -static void ShowExampleAppConsole(bool* p_open) { - static ExampleAppConsole console; - console.Draw("Example: Console", p_open); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Debug Log / ShowExampleAppLog() -//----------------------------------------------------------------------------- - -// Usage: -// static ExampleAppLog my_log; -// my_log.AddLog("Hello %d world\n", 123); -// my_log.Draw("title"); -struct ExampleAppLog { - ImGuiTextBuffer Buf; - ImGuiTextFilter Filter; - ImVector LineOffsets; // Index to lines offset. We maintain this with - // AddLog() calls. - bool AutoScroll; // Keep scrolling if already at the bottom. - - ExampleAppLog() { - AutoScroll = true; - Clear(); - } - - void Clear() { - Buf.clear(); - LineOffsets.clear(); - LineOffsets.push_back(0); - } - - void AddLog(const char* fmt, ...) IM_FMTARGS(2) { - int old_size = Buf.size(); - va_list args; - va_start(args, fmt); - Buf.appendfv(fmt, args); - va_end(args); - for (int new_size = Buf.size(); old_size < new_size; old_size++) - if (Buf[old_size] == '\n') LineOffsets.push_back(old_size + 1); - } - - void Draw(const char* title, bool* p_open = NULL) { - if (!ImGui::Begin(title, p_open)) { - ImGui::End(); - return; - } - - // Options menu - if (ImGui::BeginPopup("Options")) { - ImGui::Checkbox("Auto-scroll", &AutoScroll); - ImGui::EndPopup(); - } - - // Main window - if (ImGui::Button("Options")) ImGui::OpenPopup("Options"); - ImGui::SameLine(); - bool clear = ImGui::Button("Clear"); - ImGui::SameLine(); - bool copy = ImGui::Button("Copy"); - ImGui::SameLine(); - Filter.Draw("Filter", -100.0f); - - ImGui::Separator(); - - if (ImGui::BeginChild("scrolling", ImVec2(0, 0), false, - 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 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; - 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 - // - 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(); - - // 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(); - } -}; - -// Demonstrate creating a simple log window with basic filtering. -static void ShowExampleAppLog(bool* p_open) { - static ExampleAppLog log; - - // For the demo: add a debug button _BEFORE_ the normal log window contents - // We take advantage of a rarely used feature: multiple calls to Begin()/End() - // are appending to the _same_ window. 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* 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(), category, ImGui::GetTime(), word); - counter++; - } - } - ImGui::End(); - - // Actually call in the regular Log helper (which will Begin() into the same - // window as we just did) - log.Draw("Example: Log", p_open); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Simple Layout / ShowExampleAppLayout() -//----------------------------------------------------------------------------- - -// Demonstrate create a window with multiple child windows. -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", "Ctrl+W")) { - *p_open = false; - } - ImGui::EndMenu(); - } - ImGui::EndMenuBar(); - } - - // Left - static int selected = 0; - { - ImGui::BeginChild("left pane", ImVec2(150, 0), true); - 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::SameLine(); - - // 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)) { - 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::EndChild(); - if (ImGui::Button("Revert")) { - } - ImGui::SameLine(); - if (ImGui::Button("Save")) { - } - ImGui::EndGroup(); - } - } - ImGui::End(); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() -//----------------------------------------------------------------------------- - -static void ShowPlaceholderObject(const char* prefix, int uid) { - // Use object uid as identifier. Most commonly you could also use the object - // pointer as a base ID. - ImGui::PushID(uid); - - // Text and Tree nodes are less high than framed widgets, using - // AlignTextToFramePadding() we add vertical spacing to make the tree lines - // equal high. - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::AlignTextToFramePadding(); - bool node_open = ImGui::TreeNode("Object", "%s_%u", prefix, uid); - ImGui::TableSetColumnIndex(1); - ImGui::Text("my sailor is rich"); - - if (node_open) { - static float placeholder_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) { - ShowPlaceholderObject("Child", 424242); - } else { - // Here we use a TreeNode to highlight on hover (we could use e.g. - // Selectable as well) - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::AlignTextToFramePadding(); - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | - ImGuiTreeNodeFlags_NoTreePushOnOpen | - ImGuiTreeNodeFlags_Bullet; - ImGui::TreeNodeEx("Field", flags, "Field_%d", i); - - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-FLT_MIN); - if (i >= 5) - ImGui::InputFloat("##value", &placeholder_members[i], 1.0f); - else - ImGui::DragFloat("##value", &placeholder_members[i], 0.01f); - ImGui::NextColumn(); - } - ImGui::PopID(); - } - ImGui::TreePop(); - } - ImGui::PopID(); -} - -// Demonstrate create a simple property editor. -// This demo is a bit lackluster nowadays, would be nice to improve. -static void ShowExampleAppPropertyEditor(bool* p_open) { - ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Example: Property editor", p_open)) { - ImGui::End(); - return; - } - - IMGUI_DEMO_MARKER("Examples/Property Editor"); - HelpMarker( - "This example shows how you may implement a property editor using two " - "columns.\n" - "All objects/fields data are dummies here.\n"); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); - if (ImGui::BeginTable("##split", 2, - ImGuiTableFlags_BordersOuter | - ImGuiTableFlags_Resizable | - ImGuiTableFlags_ScrollY)) { - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Object"); - ImGui::TableSetupColumn("Contents"); - ImGui::TableHeadersRow(); - - // Iterate placeholder objects (all the same data) - for (int obj_i = 0; obj_i < 4; obj_i++) - ShowPlaceholderObject("Object", obj_i); - - ImGui::EndTable(); - } - ImGui::PopStyleVar(); - ImGui::End(); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Long Text / ShowExampleAppLongText() -//----------------------------------------------------------------------------- - -// 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); - 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()\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); - lines += 1000; - } - ImGui::BeginChild("Log"); - switch (test_type) { - case 0: - // Single call to TextUnformatted() with a big buffer - ImGui::TextUnformatted(log.begin(), log.end()); - break; - 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; - 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); - ImGui::PopStyleVar(); - break; - } - case 2: - // Multiple calls to Text(), not clipped (slow) - 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(); - break; - } - ImGui::EndChild(); - ImGui::End(); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Auto Resize / ShowExampleAppAutoResize() -//----------------------------------------------------------------------------- - -// Demonstrate creating a window which gets auto-resized according to its -// content. -static void ShowExampleAppAutoResize(bool* p_open) { - if (!ImGui::Begin("Example: Auto-resizing window", p_open, - ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::End(); - return; - } - IMGUI_DEMO_MARKER("Examples/Auto-resizing window"); - - static int lines = 10; - 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 - ImGui::End(); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Constrained Resize / ShowExampleAppConstrainedResize() -//----------------------------------------------------------------------------- - -// 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 - // FIXME: This doesn't take account of decoration size (e.g. title bar), - // library should make this easier. - static void AspectRatio(ImGuiSizeCallbackData* data) { - float aspect_ratio = *(float*)data->UserData; - data->DesiredSize.x = IM_MAX(data->CurrentSize.x, data->CurrentSize.y); - data->DesiredSize.y = (float)(int)(data->DesiredSize.x / aspect_ratio); - } - static void Square(ImGuiSizeCallbackData* data) { - data->DesiredSize.x = data->DesiredSize.y = - IM_MAX(data->CurrentSize.x, data->CurrentSize.y); - } - static void Step(ImGuiSizeCallbackData* data) { - float step = *(float*)data->UserData; - data->DesiredSize = - ImVec2((int)(data->CurrentSize.x / step + 0.5f) * step, - (int)(data->CurrentSize.y / step + 0.5f) * step); - } - }; - - const char* test_desc[] = { - "Between 100x100 and 500x500", "At least 100x100", - "Resize vertical only", "Resize horizontal only", - "Width Between 400 and 500", "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 = 5; // 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)); // Vertical only - if (type == 3) - ImGui::SetNextWindowSizeConstraints( - ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Horizontal only - if (type == 4) - ImGui::SetNextWindowSizeConstraints( - ImVec2(400, -1), ImVec2(500, -1)); // Width Between and 400 and 500 - if (type == 5) - ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), - CustomConstraints::AspectRatio, - (void*)&aspect_ratio); // Aspect ratio - if (type == 6) - ImGui::SetNextWindowSizeConstraints( - ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), - CustomConstraints::Square); // Always Square - if (type == 7) - 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) { - // 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::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() -//----------------------------------------------------------------------------- - -// 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) { - static int location = 0; - ImGuiIO& io = ImGui::GetIO(); - ImGuiWindowFlags window_flags = - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoNav; - if (location >= 0) { - 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); - window_flags |= ImGuiWindowFlags_NoMove; - } else if (location == -2) { - // 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); - else - ImGui::Text("Mouse Position: "); - if (ImGui::BeginPopupContextWindow()) { - 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(); - } - } - 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 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(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(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(); - - // 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(base_pos.x + 100, base_pos.y + 300), - ImGuiCond_FirstUseEver); - ImGui::Begin(buf); - ImGui::Text("This window has a changing title."); - ImGui::End(); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Custom Rendering using ImDrawList API / -// ShowExampleAppCustomRendering() -//----------------------------------------------------------------------------- - -// Demonstrate using the low-level ImDrawList to draw custom shapes. -static void ShowExampleAppCustomRendering(bool* p_open) { - 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. 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")) { - 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, 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); - 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; - 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->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), 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 - 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 - - // Quadratic Bezier Curve (3 control points) - ImVec2 cp3[3] = {ImVec2(x, y + sz * 0.6f), - ImVec2(x + sz * 0.5f, y - sz * 0.4f), - ImVec2(x + sz, y + sz)}; - draw_list->AddBezierQuadratic(cp3[0], cp3[1], cp3[2], col, th, - curve_segments); - x += sz + spacing; - - // Cubic Bezier Curve (4 control points) - ImVec2 cp4[4] = {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)}; - draw_list->AddBezierCubic(cp4[0], cp4[1], cp4[2], cp4[3], col, th, - curve_segments); - - x = p.x + 4; - y += sz + spacing; - } - 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), - 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 - 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) - 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) * 11.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; - - 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."); - - // 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), true, - // 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) { - points.back() = mouse_pos_in_canvas; - if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) adding_line = false; - } - - // 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)) { - scrolling.x += io.MouseDelta.x; - scrolling.y += io.MouseDelta.y; - } - - // 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(); - - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("BG/FG draw lists")) { - 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), 0, - 10 + 4); - if (draw_fg) - 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(); - } - - ImGui::EndTabBar(); - } - - ImGui::End(); -} - -//----------------------------------------------------------------------------- -// [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() -//----------------------------------------------------------------------------- - -// Simplified structure to mimic a Document model -struct MyDocument { - const char* Name; // Document 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 - bool WantClose; // Set when the document - 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)) { - Name = 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 placeholder contents for the Document - static void DisplayContents(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::SameLine(); - if (ImGui::Button("Save", ImVec2(100, 0))) doc->DoSave(); - 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) { - if (!ImGui::BeginPopupContextItem()) return; - - char buf[256]; - sprintf(buf, "Save %s", doc->Name); - if (ImGui::MenuItem(buf, "CTRL+S", false, doc->Open)) doc->DoSave(); - if (ImGui::MenuItem("Close", "CTRL+W", false, doc->Open)) - doc->DoQueueClose(); - ImGui::EndPopup(); - } -}; - -struct ExampleAppDocuments { - ImVector Documents; - - ExampleAppDocuments() { - 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)); - } -}; - -// [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 (MyDocument& doc : app.Documents) { - if (!doc.Open && doc.OpenPrev) ImGui::SetTabItemClosed(doc.Name); - doc.OpenPrev = doc.Open; - } -} - -void ShowExampleAppDocuments(bool* p_open) { - static ExampleAppDocuments app; - - // Options - static bool opt_reorderable = true; - static ImGuiTabBarFlags opt_fitting_flags = - ImGuiTabBarFlags_FittingPolicyDefault_; - - bool window_contents_visible = - ImGui::Begin("Example: Documents", p_open, ImGuiWindowFlags_MenuBar); - if (!window_contents_visible) { - ImGui::End(); - return; - } - - // Menu - if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("File")) { - int open_count = 0; - for (MyDocument& doc : app.Documents) open_count += doc.Open ? 1 : 0; - - if (ImGui::BeginMenu("Open", open_count < app.Documents.Size)) { - 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 (MyDocument& doc : app.Documents) doc.DoQueueClose(); - if (ImGui::MenuItem("Exit", "Ctrl+F4") && p_open) *p_open = false; - ImGui::EndMenu(); - } - ImGui::EndMenuBar(); - } - - // [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]; - if (doc_n > 0) ImGui::SameLine(); - ImGui::PushID(&doc); - if (ImGui::Checkbox(doc.Name, &doc.Open)) - if (!doc.Open) doc.DoForceClose(); - ImGui::PopID(); - } - - ImGui::Separator(); - - // 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. - - // Submit Tab Bar and Tabs - { - ImGuiTabBarFlags tab_bar_flags = - (opt_fitting_flags) | - (opt_reorderable ? ImGuiTabBarFlags_Reorderable : 0); - if (ImGui::BeginTabBar("##tabs", tab_bar_flags)) { - if (opt_reorderable) NotifyOfDocumentsClosedElsewhere(app); - - // [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 (MyDocument& doc : app.Documents) { - if (!doc.Open) continue; - - ImGuiTabItemFlags tab_flags = - (doc.Dirty ? ImGuiTabItemFlags_UnsavedDocument : 0); - bool visible = ImGui::BeginTabItem(doc.Name, &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) { - doc.Open = true; - doc.DoQueueClose(); - } - - MyDocument::DisplayContextMenu(&doc); - if (visible) { - MyDocument::DisplayContents(&doc); - ImGui::EndTabItem(); - } - } - - ImGui::EndTabBar(); - } - } - - // Update closing queue - static ImVector close_queue; - if (close_queue.empty()) { - // Close queue is locked once we started a popup - for (MyDocument& doc : app.Documents) - if (doc.WantClose) { - doc.WantClose = false; - close_queue.push_back(&doc); - } - } - - // Display closing confirmation UI - if (!close_queue.empty()) { - int close_queue_unsaved_documents = 0; - for (int n = 0; n < close_queue.Size; n++) - if (close_queue[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(); - } else { - if (!ImGui::IsPopupOpen("Save?")) ImGui::OpenPopup("Save?"); - if (ImGui::BeginPopupModal("Save?", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Save change to the following items?"); - float item_height = ImGui::GetTextLineHeightWithSpacing(); - if (ImGui::BeginChildFrame(ImGui::GetID("frame"), - ImVec2(-FLT_MIN, 6.25f * item_height))) { - for (int n = 0; n < close_queue.Size; n++) - if (close_queue[n]->Dirty) ImGui::Text("%s", close_queue[n]->Name); - } - ImGui::EndChildFrame(); - - ImVec2 button_size(ImGui::GetFontSize() * 7.0f, 0.0f); - if (ImGui::Button("Yes", button_size)) { - for (int n = 0; n < close_queue.Size; n++) { - if (close_queue[n]->Dirty) close_queue[n]->DoSave(); - close_queue[n]->DoForceClose(); - } - close_queue.clear(); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("No", button_size)) { - for (int n = 0; n < close_queue.Size; n++) - close_queue[n]->DoForceClose(); - close_queue.clear(); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", button_size)) { - close_queue.clear(); - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - } - } - - ImGui::End(); -} - -// End of Demo code -#else - -void ImGui::ShowAboutWindow(bool*) {} -void ImGui::ShowDemoWindow(bool*) {} -void ImGui::ShowUserGuide() {} -void ImGui::ShowStyleEditor(ImGuiStyle*) {} - -#endif - -#endif // #ifndef IMGUI_DISABLE diff --git a/symmetri/gui/crude_json.cpp b/symmetri/gui/extensions/crude_json.cpp similarity index 100% rename from symmetri/gui/crude_json.cpp rename to symmetri/gui/extensions/crude_json.cpp diff --git a/symmetri/gui/crude_json.h b/symmetri/gui/extensions/crude_json.h similarity index 100% rename from symmetri/gui/crude_json.h rename to symmetri/gui/extensions/crude_json.h diff --git a/symmetri/gui/imfilebrowser.h b/symmetri/gui/extensions/imfilebrowser.h similarity index 100% rename from symmetri/gui/imfilebrowser.h rename to symmetri/gui/extensions/imfilebrowser.h diff --git a/symmetri/gui/imgui_bezier_math.h b/symmetri/gui/extensions/imgui_bezier_math.h similarity index 100% rename from symmetri/gui/imgui_bezier_math.h rename to symmetri/gui/extensions/imgui_bezier_math.h diff --git a/symmetri/gui/imgui_bezier_math.inl b/symmetri/gui/extensions/imgui_bezier_math.inl similarity index 100% rename from symmetri/gui/imgui_bezier_math.inl rename to symmetri/gui/extensions/imgui_bezier_math.inl diff --git a/symmetri/gui/imgui_canvas.cpp b/symmetri/gui/extensions/imgui_canvas.cpp similarity index 100% rename from symmetri/gui/imgui_canvas.cpp rename to symmetri/gui/extensions/imgui_canvas.cpp diff --git a/symmetri/gui/imgui_canvas.h b/symmetri/gui/extensions/imgui_canvas.h similarity index 100% rename from symmetri/gui/imgui_canvas.h rename to symmetri/gui/extensions/imgui_canvas.h diff --git a/symmetri/gui/imgui_extra_math.h b/symmetri/gui/extensions/imgui_extra_math.h similarity index 100% rename from symmetri/gui/imgui_extra_math.h rename to symmetri/gui/extensions/imgui_extra_math.h diff --git a/symmetri/gui/imgui_extra_math.inl b/symmetri/gui/extensions/imgui_extra_math.inl similarity index 100% rename from symmetri/gui/imgui_extra_math.inl rename to symmetri/gui/extensions/imgui_extra_math.inl diff --git a/symmetri/gui/imgui_node_editor.cpp b/symmetri/gui/extensions/imgui_node_editor.cpp similarity index 100% rename from symmetri/gui/imgui_node_editor.cpp rename to symmetri/gui/extensions/imgui_node_editor.cpp diff --git a/symmetri/gui/imgui_node_editor.h b/symmetri/gui/extensions/imgui_node_editor.h similarity index 100% rename from symmetri/gui/imgui_node_editor.h rename to symmetri/gui/extensions/imgui_node_editor.h diff --git a/symmetri/gui/imgui_node_editor_api.cpp b/symmetri/gui/extensions/imgui_node_editor_api.cpp similarity index 100% rename from symmetri/gui/imgui_node_editor_api.cpp rename to symmetri/gui/extensions/imgui_node_editor_api.cpp diff --git a/symmetri/gui/imgui_node_editor_internal.h b/symmetri/gui/extensions/imgui_node_editor_internal.h similarity index 100% rename from symmetri/gui/imgui_node_editor_internal.h rename to symmetri/gui/extensions/imgui_node_editor_internal.h diff --git a/symmetri/gui/imgui_node_editor_internal.inl b/symmetri/gui/extensions/imgui_node_editor_internal.inl similarity index 100% rename from symmetri/gui/imgui_node_editor_internal.inl rename to symmetri/gui/extensions/imgui_node_editor_internal.inl diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp new file mode 100644 index 0000000..5f9b4bb --- /dev/null +++ b/symmetri/gui/graph.hpp @@ -0,0 +1,192 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "imgui_node_editor.h" +#include "redux.hpp" +#include "symbol.h" + +struct Graph { + struct Node { + std::string name; + ogdf::node node; + Symbol id; + Symbol input; + Symbol output; + double x, y; + }; + struct Arc { + ogdf::edge edge; + Symbol from, to; + }; + ogdf::Graph G; + ax::NodeEditor::Config config; + ax::NodeEditor::EditorContext* m_Context; + + explicit Graph(const symmetri::Net net, const symmetri::Marking&) { + config.SettingsFile = "Simple.json"; + m_Context = ax::NodeEditor::CreateEditor(&config); + ogdf::GraphAttributes GA(G, ogdf::GraphAttributes::nodeGraphics | + ogdf::GraphAttributes::nodeLabel | + ogdf::GraphAttributes::edgeGraphics); + for (const auto& [t, io] : net) { + const auto transition_key = Symbol('t', transitions.size() + 1); + const auto transition_input_key = Symbol('a', transitions.size() + 1); + const auto transition_output_key = Symbol('b', transitions.size() + 1); + + for (const auto& s : io.first) { + const auto place_key = Symbol('p', places.size() + 1); + const auto place_input_key = Symbol('c', places.size() + 1); + const auto place_output_key = Symbol('d', places.size() + 1); + + if (std::find_if(places.begin(), places.end(), [&](const auto& n) { + return s.first == n.name; + }) == std::end(places)) { + places.push_back({s.first, G.newNode(), place_key, place_input_key, + place_output_key}); + auto& current_node = places.back(); + GA.label(current_node.node) = current_node.name; + GA.shape(current_node.node) = ogdf::Shape::Ellipse; + } + } + + for (const auto& s : io.second) { + const auto place_key = Symbol('p', places.size() + 1); + const auto place_input_key = Symbol('c', places.size() + 1); + const auto place_output_key = Symbol('d', places.size() + 1); + + if (std::find_if(places.begin(), places.end(), [&](const auto& n) { + return s.first == n.name; + }) == std::end(places)) { + places.push_back({s.first, G.newNode(), place_key, place_input_key, + place_output_key}); + auto& current_node = places.back(); + GA.label(current_node.node) = current_node.name; + GA.shape(current_node.node) = ogdf::Shape::Ellipse; + } + } + transitions.push_back(Node{t, G.newNode(), transition_key, + transition_input_key, transition_output_key}); + GA.label(transitions.back().node) = transitions.back().name; + } + + // make the graph + for (const auto& x : net) { + const auto [t, io] = x; + const auto transition = + std::find_if(transitions.begin(), transitions.end(), + [&](const auto& n) { return x.first == n.name; }); + + for (const auto& s : io.first) { + const auto place = + std::find_if(places.begin(), places.end(), + [&](const auto& n) { return s.first == n.name; }); + arcs.push_back({G.newEdge(place->node, transition->node), place->output, + transition->input}); + GA.bends(arcs.back().edge); + } + + for (const auto& s : io.second) { + const auto place = + std::find_if(places.begin(), places.end(), + [&](const auto& n) { return s.first == n.name; }); + arcs.push_back({G.newEdge(transition->node, place->node), + transition->output, place->input}); + GA.bends(arcs.back().edge); + } + } + + { + using namespace ogdf; + for (node v : G.nodes) { + GA.width(v) /= 2; + GA.height(v) /= 2; + } + + GraphIO::write(GA, "output-ERDiagram.svg", GraphIO::drawSVG); + SugiyamaLayout SL; + SL.setRanking(new OptimalRanking); + SL.setCrossMin(new MedianHeuristic); + + OptimalHierarchyLayout* ohl = new OptimalHierarchyLayout; + ohl->layerDistance(30.0); + ohl->nodeDistance(15.0); + ohl->weightBalancing(0.2); + SL.setLayout(ohl); + + SL.call(GA); + GA.rotateLeft90(); + GraphIO::write(GA, "output-unix-history-hierarchical.svg", + GraphIO::drawSVG); + } + + double x_factor = 2; + double y_factor = 3; + for (auto& t : transitions) { + t.x = x_factor * GA.x(t.node); + t.y = y_factor * GA.y(t.node); + } + for (auto& p : places) { + p.x = x_factor * GA.x(p.node); + p.y = y_factor * GA.y(p.node); + } + } + std::vector places; + std::vector transitions; + std::vector arcs; +}; + +template <> +void draw(Graph& g) { + namespace ed = ax::NodeEditor; + ed::SetCurrentEditor(g.m_Context); + ed::Begin("My Editor", ImVec2(0.0, 0.0f)); + for (const auto& [name, node, id, i, o, x, y] : g.transitions) { + ed::BeginNode(id.key()); + ed::BeginPin(i.key(), ed::PinKind::Input); + ed::EndPin(); + ImGui::SameLine(); + // ImGui::Text("%s", name.c_str()); + // ImGui::SameLine(); + ed::BeginPin(o.key(), ed::PinKind::Output); + ed::EndPin(); + ed::EndNode(); + ed::SetNodePosition(id.key(), ImVec2(x, y)); + } + + for (const auto& [name, node, id, i, o, x, y] : g.places) { + ed::BeginNode(id.key()); + ed::BeginPin(i.key(), ed::PinKind::Input); + ed::EndPin(); + ImGui::SameLine(); + // ImGui::Text("%s", name.c_str()); + // ImGui::SameLine(); + ed::BeginPin(o.key(), ed::PinKind::Output); + ed::EndPin(); + ed::EndNode(); + ed::SetNodePosition(id.key(), ImVec2(x, y)); + } + + int uniqueId = 1; + for (const auto& [edge, i, o] : g.arcs) { + ax::NodeEditor::Link(uniqueId++, i.key(), o.key()); + } + ed::End(); + ed::SetCurrentEditor(nullptr); +} diff --git a/symmetri/gui/initialize.hpp b/symmetri/gui/initialize.hpp new file mode 100644 index 0000000..a759b7f --- /dev/null +++ b/symmetri/gui/initialize.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include "graph.hpp" +#include "marking.hpp" +#include "menu_bar.hpp" +#include "view.hpp" +inline Model initializeModel(Model &&m) { + m.working_dir = "/users/thomashorstink/Projects/symmetri/nets"; + m.active_file = + "/users/thomashorstink/Projects/symmetri/nets/InspectionSeqPNv21.pnml"; + symmetri::Net net; + symmetri::Marking marking; + symmetri::PriorityTable pt; + const std::filesystem::path pn_file = m.active_file.value(); + if (pn_file.extension() == std::string(".pnml")) { + std::tie(net, marking) = symmetri::readPnml({pn_file}); + + } else { + std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); + } + + current(m.documents).push_back(net); + current(m.documents).push_back(marking); + current(m.documents).push_back(Graph{net, marking}); + + // create a file browser instance + ImGui::FileBrowser fileDialog; + + // (optional) set browser properties + fileDialog.SetTitle("title"); + fileDialog.SetTypeFilters({".pnml", ".grml"}); + fileDialog.SetPwd(m.working_dir); + m.statics.push_back(fileDialog); + return m; +} diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 8f8e0ad..797f7cc 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -1,16 +1,9 @@ -#include #include -#include -#include -#include "drawable.h" #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_metal.h" -#include "imgui_node_editor.h" -#include "menu_bar.hpp" -#include "redux.hpp" -#include "view.hpp" +#include "initialize.hpp" #define GLFW_INCLUDE_NONE #define GLFW_EXPOSE_NATIVE_COCOA #include @@ -18,14 +11,14 @@ #import #import -namespace ed = ax::NodeEditor; - static void glfw_error_callback(int error, const char *description) { fprintf(stderr, "Glfw Error %d: %s\n", error, description); } int main(int, char **) { // Setup Dear ImGui context + MVC::push(&initializeModel); + IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO &io = ImGui::GetIO(); @@ -65,15 +58,14 @@ int main(int, char **) { float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; - ed::Config config; - config.SettingsFile = "Simple.json"; - auto m_Context = ed::CreateEditor(&config); // Main loop while (!glfwWindowShouldClose(window)) { while (auto v = MVC::dequeue()) { - MVC::update(v.value()); + if (v.has_value()) { + MVC::update(v.value()); + } + // should happen on a different thread? } - auto model = MVC::getView(); @autoreleasepool { glfwPollEvents(); @@ -84,9 +76,11 @@ int main(int, char **) { id drawable = [layer nextDrawable]; id commandBuffer = [commandQueue commandBuffer]; + // colors renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clear_color[0] * clear_color[3], clear_color[1] * clear_color[3], clear_color[2] * clear_color[3], clear_color[3]); + renderPassDescriptor.colorAttachments[0].texture = drawable.texture; renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; @@ -99,26 +93,11 @@ int main(int, char **) { ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); - draw(model); - - ed::SetCurrentEditor(m_Context); - ed::Begin("My Editor", ImVec2(0.0, 0.0f)); - int uniqueId = 1; - // Start drawing nodes. - ed::BeginNode(uniqueId++); - ImGui::Text("Node A"); - ed::BeginPin(uniqueId++, ed::PinKind::Input); - ImGui::Text("-> In"); - ed::EndPin(); - ImGui::SameLine(); - ed::BeginPin(uniqueId++, ed::PinKind::Output); - ImGui::Text("Out ->"); - ed::EndPin(); - ed::EndNode(); - ed::End(); - ed::SetCurrentEditor(nullptr); + MVC::render(); + // Rendering ImGui::Render(); + ImGui::EndFrame(); // <-- Added ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), commandBuffer, renderEncoder); [renderEncoder popDebugGroup]; diff --git a/symmetri/gui/marking.hpp b/symmetri/gui/marking.hpp new file mode 100644 index 0000000..4c5a7e2 --- /dev/null +++ b/symmetri/gui/marking.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +#include +#include + +#include "imgui.h" +#include "redux.hpp" + +template <> +void draw(symmetri::Marking& marking) { + if (ImGui::BeginListBox("##marking")) { + for (const auto& [place, color] : marking) { + std::stringstream s; + s << "(" << place << "," << symmetri::Color::toString(color) << ")"; + if (ImGui::Selectable(s.str().c_str(), false)) { + } + } + ImGui::EndListBox(); + } +} diff --git a/symmetri/gui/menu_bar.hpp b/symmetri/gui/menu_bar.hpp index 38ed146..f805d21 100644 --- a/symmetri/gui/menu_bar.hpp +++ b/symmetri/gui/menu_bar.hpp @@ -2,32 +2,39 @@ #include +#include "graph.hpp" #include "imfilebrowser.h" #include "redux.hpp" #include "symmetri/parsers.h" -std::function updateActiveFile( - const std::filesystem::path &file) { +Reducer updateActiveFile(const std::filesystem::path &file) { return [=](Model &&m) { m.active_file = file; - Net net; - std::tie(net, m.marking) = symmetri::readPnml({file}); - m.drawables.push_back(net); + auto [net, marking] = symmetri::readPnml({file}); + m.documents.push_back({net, marking, Graph{net, marking}}); + commit(m.documents); return m; }; } -void menuBar(ImGui::FileBrowser &fileDialog) { +template <> +void draw(ImGui::FileBrowser &fileDialog) { fileDialog.Display(); if (fileDialog.HasSelected()) { MVC::push(updateActiveFile(fileDialog.GetSelected())); fileDialog.ClearSelected(); } if (ImGui::BeginMainMenuBar()) { + // silly but ok. + MVC::push([menu_height = ImGui::GetWindowSize().y](Model &&m) { + m.menu_height = menu_height; + return m; + }); + if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("New")) { } - if (ImGui::MenuItem("Open", "Ctrl+O")) { + if (ImGui::MenuItem("Open")) { fileDialog.Open(); } // Exit... @@ -52,5 +59,3 @@ void menuBar(ImGui::FileBrowser &fileDialog) { ImGui::EndMainMenuBar(); } } - -void draw(const ImGui::FileBrowser &file_dialog) { menuBar(file_dialog); } diff --git a/symmetri/gui/redux.hpp b/symmetri/gui/redux.hpp index f5fe7a9..39ccdc8 100644 --- a/symmetri/gui/redux.hpp +++ b/symmetri/gui/redux.hpp @@ -4,60 +4,67 @@ #include #include +#include #include #include #include +#include #include "blockingconcurrentqueue.h" #include "drawable.h" -#include "view.hpp" -namespace fs = std::filesystem; +using View = std::vector; +using History = std::vector; -using namespace symmetri; +void commit(History& x) { + assert(!x.empty()); + x.push_back(x.back()); +} +void undo(History& x) { + assert(!x.empty()); + x.pop_back(); +} +View& current(History& x) { + assert(!x.empty()); + return x.back(); +} struct Model { std::filesystem::path working_dir; std::optional active_file; - // Net net; - Marking marking; - std::vector drawables; + int menu_height = 0; + View statics; + History documents = History{1}; }; -void draw(const Model &m) { - for (const auto &drawable : m.drawables) { +void draw(Model& m) { + for (auto& drawable : m.statics) { draw(drawable); } -} -inline Model initializeModel(Model &&m) { - m.working_dir = "/users/thomashorstink/Projects/symmetri/nets"; - m.active_file = "/users/thomashorstink/Projects/symmetri/nets/n1.pnml"; - Net net; - std::tie(net, m.marking) = symmetri::readPnml({m.active_file.value()}); - m.drawables.push_back(net); + ImGui::SetNextWindowSize( + ImVec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y)); - // create a file browser instance - ImGui::FileBrowser fileDialog; + ImGui::SetNextWindowPos(ImVec2(0, m.menu_height)); - // (optional) set browser properties - fileDialog.SetTitle("title"); - fileDialog.SetTypeFilters({".pnml", ".grml"}); - fileDialog.SetPwd(m.working_dir); - m.drawables.push_back(fileDialog); - return m; + ImGui::Begin("test", NULL, ImGuiWindowFlags_NoTitleBar); + for (auto& drawable : current(m.documents)) { + draw(drawable); + } + ImGui::End(); } -using Reducer = std::function; +using Reducer = std::function; + class MVC { private: inline static moodycamel::BlockingConcurrentQueue reducers{10}; public: - inline static Model model = initializeModel(Model()); - inline static Model getView() { return model; }; - inline static void update(Reducer f) { model = f(std::move(model)); } - inline static void push(Reducer &&f) { + inline static Model model; + inline static void render() { draw(model); }; + inline static void update(const Reducer& f) { model = f(model); } + inline static void push(Reducer&& f) { reducers.enqueue(std::forward(f)); } inline static std::optional dequeue() { diff --git a/symmetri/gui/symbol.cpp b/symmetri/gui/symbol.cpp new file mode 100644 index 0000000..3d803cf --- /dev/null +++ b/symmetri/gui/symbol.cpp @@ -0,0 +1,71 @@ +/* ---------------------------------------------------------------------------- + + * GTSAM Copyright 2010, Georgia Tech Research Corporation, + * Atlanta, Georgia 30332-0415 + * All Rights Reserved + * Authors: Frank Dellaert, et al. (see THANKS for the full author list) + + * See LICENSE for the license information + + * -------------------------------------------------------------------------- */ + +/** + * @file Symbol.cpp + * @date June 9, 2012 + * @author: Frank Dellaert + * @author: Richard Roberts + */ + +#include "symbol.h" + +#include + +#include +#include +#include +#include + +static const size_t keyBits = sizeof(Key) * 8; +static const size_t chrBits = sizeof(unsigned char) * 8; +static const size_t indexBits = keyBits - chrBits; +static const Key chrMask = + Key(UCHAR_MAX) + << indexBits; // For some reason, std::numeric_limits::max() + // fails +static const Key indexMask = ~chrMask; + +Symbol::Symbol(Key key) + : c_((unsigned char)((key & chrMask) >> indexBits)), j_(key & indexMask) {} + +Key Symbol::key() const { + if (j_ > indexMask) { + std::stringstream msg; + msg << "Symbol index is too large, j=" << j_ << ", indexMask=" << indexMask; + throw std::invalid_argument(msg.str()); + } + Key key = (Key(c_) << indexBits) | j_; + return key; +} + +void Symbol::print(const std::string& s) const { + std::cout << s << (std::string)(*this) << std::endl; +} + +bool Symbol::equals(const Symbol& expected, double tol) const { + return (*this) == expected; +} + +Symbol::operator std::string() const { + char buffer[10]; + snprintf(buffer, 10, "%c%llu", c_, static_cast(j_)); + return std::string(buffer); +} + +static Symbol make(Key key) { return Symbol(key); } + +std::function Symbol::ChrTest(unsigned char c) { + auto equals = [](unsigned char s, unsigned char c) { return s == c; }; + return std::bind( + equals, std::bind(&Symbol::chr, std::bind(make, std::placeholders::_1)), + c); +} diff --git a/symmetri/gui/symbol.h b/symmetri/gui/symbol.h new file mode 100644 index 0000000..94a28f3 --- /dev/null +++ b/symmetri/gui/symbol.h @@ -0,0 +1,114 @@ +/* ---------------------------------------------------------------------------- + + * GTSAM Copyright 2010, Georgia Tech Research Corporation, + * Atlanta, Georgia 30332-0415 + * All Rights Reserved + * Authors: Frank Dellaert, et al. (see THANKS for the full author list) + + * See LICENSE for the license information + + * -------------------------------------------------------------------------- */ + +#pragma once + +#include +#include +#include + +using Key = std::uint64_t; +class Symbol { + protected: + unsigned char c_; + std::uint64_t j_; + + public: + Symbol() : c_(0), j_(0) {} + + Symbol(const Symbol& key) : c_(key.c_), j_(key.j_) {} + + Symbol(unsigned char c, std::uint64_t j) : c_(c), j_(j) {} + + Symbol(Key key); + + Key key() const; + + operator Key() const { return key(); } + + void print(const std::string& s = "") const; + + bool equals(const Symbol& expected, double tol = 0.0) const; + + unsigned char chr() const { return c_; } + + std::uint64_t index() const { return j_; } + + operator std::string() const; + + std::string string() const { return std::string(*this); } + + bool operator<(const Symbol& comp) const { + return c_ < comp.c_ || (comp.c_ == c_ && j_ < comp.j_); + } + + bool operator==(const Symbol& comp) const { + return comp.c_ == c_ && comp.j_ == j_; + } + + bool operator==(Key comp) const { return comp == (Key)(*this); } + + bool operator!=(const Symbol& comp) const { + return comp.c_ != c_ || comp.j_ != j_; + } + + bool operator!=(Key comp) const { return comp != (Key)(*this); } + + static std::function ChrTest(unsigned char c); + + friend std::ostream& operator<<(std::ostream&, const Symbol&); +}; + +inline Key symbol(unsigned char c, std::uint64_t j) { + return (Key)Symbol(c, j); +} + +inline unsigned char symbolChr(Key key) { return Symbol(key).chr(); } + +inline std::uint64_t symbolIndex(Key key) { return Symbol(key).index(); } + +namespace symbol_shorthand { +inline Key A(std::uint64_t j) { return Symbol('a', j); } +inline Key B(std::uint64_t j) { return Symbol('b', j); } +inline Key C(std::uint64_t j) { return Symbol('c', j); } +inline Key D(std::uint64_t j) { return Symbol('d', j); } +inline Key E(std::uint64_t j) { return Symbol('e', j); } +inline Key F(std::uint64_t j) { return Symbol('f', j); } +inline Key G(std::uint64_t j) { return Symbol('g', j); } +inline Key H(std::uint64_t j) { return Symbol('h', j); } +inline Key I(std::uint64_t j) { return Symbol('i', j); } +inline Key J(std::uint64_t j) { return Symbol('j', j); } +inline Key K(std::uint64_t j) { return Symbol('k', j); } +inline Key L(std::uint64_t j) { return Symbol('l', j); } +inline Key M(std::uint64_t j) { return Symbol('m', j); } +inline Key N(std::uint64_t j) { return Symbol('n', j); } +inline Key O(std::uint64_t j) { return Symbol('o', j); } +inline Key P(std::uint64_t j) { return Symbol('p', j); } +inline Key Q(std::uint64_t j) { return Symbol('q', j); } +inline Key R(std::uint64_t j) { return Symbol('r', j); } +inline Key S(std::uint64_t j) { return Symbol('s', j); } +inline Key T(std::uint64_t j) { return Symbol('t', j); } +inline Key U(std::uint64_t j) { return Symbol('u', j); } +inline Key V(std::uint64_t j) { return Symbol('v', j); } +inline Key W(std::uint64_t j) { return Symbol('w', j); } +inline Key X(std::uint64_t j) { return Symbol('x', j); } +inline Key Y(std::uint64_t j) { return Symbol('y', j); } +inline Key Z(std::uint64_t j) { return Symbol('z', j); } +} // namespace symbol_shorthand + +class SymbolGenerator { + const unsigned char c_; + + public: + constexpr SymbolGenerator(const unsigned char c) : c_(c) {} + Symbol operator()(const std::uint64_t j) const { return Symbol(c_, j); } + constexpr unsigned char chr() const { return c_; } +}; diff --git a/symmetri/gui/view.hpp b/symmetri/gui/view.hpp index d4c5267..907ddce 100644 --- a/symmetri/gui/view.hpp +++ b/symmetri/gui/view.hpp @@ -29,8 +29,8 @@ static void showMutation(const Mutation& mutation) { } } -void draw(const symmetri::Net& net) { - std::cout << "drawable!" << std::endl; +template <> +void draw(symmetri::Net& net) { if (ImGui::BeginListBox("##bla")) { for (const auto& [t, p] : net) { if (ImGui::Selectable(t.c_str(), false)) { From 70e849f3a04c31000d2884d634531175e5d6debe Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Fri, 8 Dec 2023 21:09:13 +0100 Subject: [PATCH 003/142] new gui attempt --- symmetri/gui/CMakeLists.txt | 15 +- symmetri/gui/draw_graph.cpp | 268 ++++++++++++++++++++++++++++++++++++ symmetri/gui/graph.hpp | 165 +++++++++------------- symmetri/gui/initialize.hpp | 8 +- symmetri/gui/main.mm | 1 - symmetri/gui/menu_bar.hpp | 2 +- symmetri/gui/redux.hpp | 9 +- 7 files changed, 354 insertions(+), 114 deletions(-) create mode 100644 symmetri/gui/draw_graph.cpp diff --git a/symmetri/gui/CMakeLists.txt b/symmetri/gui/CMakeLists.txt index 4060224..12de331 100644 --- a/symmetri/gui/CMakeLists.txt +++ b/symmetri/gui/CMakeLists.txt @@ -5,10 +5,11 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) find_package(glfw3 REQUIRED) find_package(ogdf REQUIRED) + if(ASAN_BUILD AND NOT TSAN_BUILD) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=address,undefined -fno-omit-frame-pointer -O0") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=address,undefined -fno-omit-frame-pointer -O0") elseif(TSAN_BUILD AND NOT ASAN_BUILD) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=thread -fno-omit-frame-pointer -O0") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=thread -fno-omit-frame-pointer -O0") endif() include_directories( @@ -23,6 +24,7 @@ include_directories( set(SOURCES main.mm symbol.cpp + draw_graph.cpp ) set(IMGUI_SOURCES @@ -33,10 +35,11 @@ set(IMGUI_SOURCES imgui/imgui_tables.cpp imgui/backends/imgui_impl_glfw.cpp imgui/backends/imgui_impl_metal.mm - extensions/crude_json.cpp - extensions/imgui_node_editor_api.cpp - extensions/imgui_node_editor.cpp - extensions/imgui_canvas.cpp + + # extensions/crude_json.cpp + # extensions/imgui_node_editor_api.cpp + # extensions/imgui_node_editor.cpp + # extensions/imgui_canvas.cpp ) if(APPLE) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp new file mode 100644 index 0000000..0a0a435 --- /dev/null +++ b/symmetri/gui/draw_graph.cpp @@ -0,0 +1,268 @@ +#include "drawable.h" +#include "graph.hpp" +#include "imgui.h" +#include "redux.hpp" +// Creating a node graph editor for Dear ImGui +// Quick sample, not production code! +// This is quick demo I crafted in a few hours in 2015 showcasing how to use +// Dear ImGui to create custom stuff, which ended up feeding a thread full of +// better experiments. See https://github.com/ocornut/imgui/issues/306 for +// details + +// Fast forward to 2023, see e.g. +// https://github.com/ocornut/imgui/wiki/Useful-Extensions#node-editors + +// Changelog +// - v0.05 (2023-03): fixed for renamed api: AddBezierCurve()->AddBezierCubic(). +// - v0.04 (2020-03): minor tweaks +// - v0.03 (2018-03): fixed grid offset issue, inverted sign of 'scrolling' +// - v0.01 (2015-08): initial version + +#include // fmodf + +#include "symmetri/colors.hpp" + +inline ImU32 getColor(symmetri::Token token) { + using namespace symmetri; + switch (token) { + case Color::Scheduled: + case Color::Started: + case Color::Deadlocked: + case Color::Canceled: + case Color::Paused: + case Color::Failed: + return IM_COL32(255, 0, 0, 255); + break; + case Color::Success: + return IM_COL32(0, 255, 0, 255); + break; + default: { + // create new color and lookup if it already exists. + return IM_COL32(255, 200, 0, 255); + } + } +}; + +// NB: You can use math functions/operators on ImVec2 if you #define +// IMGUI_DEFINE_MATH_OPERATORS and #include "imgui_internal.h" Here we only +// declare simple +/- operators so others don't leak into the demo code. +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); +} + +// Dummy data structure provided for the example. +// Note that we storing links as indices (not ID) to make example code shorter. +template <> +void draw(Graph& g) { + // State + static ImVec2 scrolling = ImVec2(0.0f, 0.0f); + static bool show_grid = true; + static Symbol node_selected = -1; + + // Initialization + ImGuiIO& io = ImGui::GetIO(); + // Draw a list of nodes on the left side + bool open_context_menu = false; + Symbol node_hovered_in_list = -1; + int node_hovered_in_scene = -1; + ImGui::BeginChild("node_list", ImVec2(150, 0)); + ImGui::Text("Places"); + ImGui::Separator(); + for (const auto& node : g.nodes) { + if (node.id.chr() == 'P') { + ImGui::PushID(node.id); + if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { + node_selected = node.id; + } + if (ImGui::IsItemHovered()) { + node_hovered_in_list = node.id; + // node_hovered_in_list = node.id; + // open_context_menu |= ImGui::IsMouseClicked(1); + } + ImGui::PopID(); + } + } + + ImGui::Dummy(ImVec2(0.0f, 20.0f)); + ImGui::Text("Transitions"); + ImGui::Separator(); + for (const auto& node : g.nodes) { + if (node.id.chr() == 'T') { + ImGui::PushID(node.id); + if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { + node_selected = node.id; + } + if (ImGui::IsItemHovered()) { + node_hovered_in_list = node.id; + // node_hovered_in_list = node.id; + // open_context_menu |= ImGui::IsMouseClicked(1); + } + ImGui::PopID(); + } + } + ImGui::EndChild(); + + ImGui::SameLine(); + ImGui::BeginGroup(); + + const float NODE_SLOT_RADIUS = 4.0f; + const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); + + // Create our child canvas + ImGui::Text("Hold middle mouse button to scroll (%.2f,%.2f)", scrolling.x, + scrolling.y); + ImGui::SameLine(ImGui::GetWindowWidth() - 100); + ImGui::Checkbox("Show grid", &show_grid); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(60, 60, 70, 200)); + ImGui::BeginChild("scrolling_region", ImVec2(0, 0), true, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove); + ImGui::PopStyleVar(); // WindowPadding + ImGui::PushItemWidth(120.0f); + + const ImVec2 offset = ImGui::GetCursorScreenPos() + scrolling; + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Display grid + if (show_grid) { + ImU32 GRID_COLOR = IM_COL32(200, 200, 200, 40); + float GRID_SZ = 64.0f; + ImVec2 win_pos = ImGui::GetCursorScreenPos(); + ImVec2 canvas_sz = ImGui::GetWindowSize(); + for (float x = fmodf(scrolling.x, GRID_SZ); x < canvas_sz.x; x += GRID_SZ) + draw_list->AddLine(ImVec2(x, 0.0f) + win_pos, + ImVec2(x, canvas_sz.y) + win_pos, GRID_COLOR); + for (float y = fmodf(scrolling.y, GRID_SZ); y < canvas_sz.y; y += GRID_SZ) + draw_list->AddLine(ImVec2(0.0f, y) + win_pos, + ImVec2(canvas_sz.x, y) + win_pos, GRID_COLOR); + } + + // Display links + draw_list->ChannelsSplit(2); + draw_list->ChannelsSetCurrent(0); // Background + for (const auto& [edge, color, from_to] : g.arcs) { + ImVec2 p1 = offset + from_to[0]->GetCenterPos(); + ImVec2 p2 = offset + from_to[1]->GetCenterPos(); + ImVec2 d = p1 - p2; + d.x *= 0.33; + d.y *= 0.33; + ImU32 imcolor = getColor(color); + draw_list->AddLine(p1, p2, imcolor, 2.0f); + } + + // Display nodes + for (auto& node : g.nodes) { + ImGui::PushID(node.id); + ImVec2 node_rect_min = offset + node.Pos; + + // Display node contents first + draw_list->ChannelsSetCurrent(1); // Foreground + bool old_any_active = ImGui::IsAnyItemActive(); + auto textWidth = ImGui::CalcTextSize(node.name.c_str()).x; + ImGui::SetCursorScreenPos(node_rect_min + NODE_WINDOW_PADDING + + ImVec2(8.0f - textWidth * 0.5f, -20.0f)); + ImGui::BeginGroup(); // Lock horizontal position + ImGui::Text("%s", node.name.c_str()); + ImGui::EndGroup(); + + // Save the size of what we have emitted and whether any of the widgets are + // being used + bool node_widgets_active = (!old_any_active && ImGui::IsAnyItemActive()); + node.Size = + ImGui::GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING; + node.Size.x = node.Size.y; + ImVec2 node_rect_max = node_rect_min + node.Size; + + // Display node box + draw_list->ChannelsSetCurrent(0); // Background + ImGui::SetCursorScreenPos(node_rect_min); + ImGui::InvisibleButton("node", node.Size); + if (ImGui::IsItemHovered()) { + node_hovered_in_scene = node.id; + open_context_menu |= ImGui::IsMouseClicked(1); + } + bool node_moving_active = ImGui::IsItemActive(); + if (node_widgets_active || node_moving_active) node_selected = node.id; + if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + node.Pos = node.Pos + io.MouseDelta; + + const int opacity = 255; + auto select_color = node.id == node_hovered_in_list + ? IM_COL32(255, 255, 0, opacity) + : IM_COL32(100, 100, 100, opacity); + if (node.id.chr() == 'P') { + draw_list->AddCircleFilled(offset + node.GetCenterPos(), + 0.5f * node.Size.x, + IM_COL32(135, 135, 135, opacity), -5); + draw_list->AddCircle(offset + node.GetCenterPos(), 0.5f * node.Size.x, + select_color, -5, 3.0f); + + } else { + draw_list->AddRectFilled(node_rect_min, node_rect_max, + IM_COL32(200, 200, 200, opacity), 4.0f); + draw_list->AddRect(node_rect_min, node_rect_max, select_color, 4.0f, 0, + 3.0f); + } + + ImGui::PopID(); + } + draw_list->ChannelsMerge(); + + // Open context menu + if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) + if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) || + !ImGui::IsAnyItemHovered()) { + node_selected = node_hovered_in_list = node_hovered_in_scene = -1; + open_context_menu = true; + } + if (open_context_menu) { + ImGui::OpenPopup("context_menu"); + // if (node_hovered_in_list != -1) node_selected = node_hovered_in_list; + // if (node_hovered_in_scene != -1) node_selected = node_hovered_in_scene; + } + + // Draw context menu + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); + if (ImGui::BeginPopup("context_menu")) { + // Node* node = node_selected != Symbol('x') ? &nodes[node_selected] : NULL; + ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; + if (false) { + // if (node) { + // ImGui::Text("Node '%s'", node->Name); + ImGui::Separator(); + if (ImGui::MenuItem("Rename..", NULL, false, false)) { + } + if (ImGui::MenuItem("Delete", NULL, false, false)) { + } + if (ImGui::MenuItem("Copy", NULL, false, false)) { + } + } else { + if (ImGui::MenuItem("Add")) { + MVC::push([=](Model&& m) { + std::cout << "add node" << std::endl; + return m; + }); + // nodes.push_back(Node(nodes.Size, "New node", scene_pos, 2, 2, 1, 1)); + } + if (ImGui::MenuItem("Paste", NULL, false, false)) { + } + } + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + + // Scrolling + if (ImGui::IsWindowHovered() && !ImGui::IsAnyItemActive() && + ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 0.0f)) + scrolling = scrolling + io.MouseDelta; + + ImGui::PopItemWidth(); + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::EndGroup(); +} diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index 5f9b4bb..a3e2fd1 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -1,5 +1,6 @@ #pragma once +#include // fmodf #include #include #include @@ -18,175 +19,141 @@ #include #include -#include "imgui_node_editor.h" -#include "redux.hpp" +#include "imgui.h" #include "symbol.h" - struct Graph { struct Node { std::string name; ogdf::node node; Symbol id; - Symbol input; - Symbol output; - double x, y; + ImVec2 Pos, Size; + + ImVec2 GetCenterPos() const { + return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); + } }; + struct Arc { ogdf::edge edge; - Symbol from, to; + symmetri::Token color; + std::array from_to; }; + ogdf::Graph G; - ax::NodeEditor::Config config; - ax::NodeEditor::EditorContext* m_Context; explicit Graph(const symmetri::Net net, const symmetri::Marking&) { - config.SettingsFile = "Simple.json"; - m_Context = ax::NodeEditor::CreateEditor(&config); ogdf::GraphAttributes GA(G, ogdf::GraphAttributes::nodeGraphics | ogdf::GraphAttributes::nodeLabel | ogdf::GraphAttributes::edgeGraphics); for (const auto& [t, io] : net) { - const auto transition_key = Symbol('t', transitions.size() + 1); - const auto transition_input_key = Symbol('a', transitions.size() + 1); - const auto transition_output_key = Symbol('b', transitions.size() + 1); - for (const auto& s : io.first) { - const auto place_key = Symbol('p', places.size() + 1); - const auto place_input_key = Symbol('c', places.size() + 1); - const auto place_output_key = Symbol('d', places.size() + 1); + const auto place_key = Symbol('P', nodes.size() + 1); - if (std::find_if(places.begin(), places.end(), [&](const auto& n) { + if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; - }) == std::end(places)) { - places.push_back({s.first, G.newNode(), place_key, place_input_key, - place_output_key}); - auto& current_node = places.back(); + }) == std::end(nodes)) { + nodes.push_back({s.first, G.newNode(), place_key}); + auto& current_node = nodes.back(); GA.label(current_node.node) = current_node.name; GA.shape(current_node.node) = ogdf::Shape::Ellipse; } } for (const auto& s : io.second) { - const auto place_key = Symbol('p', places.size() + 1); - const auto place_input_key = Symbol('c', places.size() + 1); - const auto place_output_key = Symbol('d', places.size() + 1); - - if (std::find_if(places.begin(), places.end(), [&](const auto& n) { + const auto place_key = Symbol('P', nodes.size() + 1); + if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; - }) == std::end(places)) { - places.push_back({s.first, G.newNode(), place_key, place_input_key, - place_output_key}); - auto& current_node = places.back(); + }) == std::end(nodes)) { + nodes.push_back({s.first, G.newNode(), place_key}); + auto& current_node = nodes.back(); GA.label(current_node.node) = current_node.name; GA.shape(current_node.node) = ogdf::Shape::Ellipse; } } - transitions.push_back(Node{t, G.newNode(), transition_key, - transition_input_key, transition_output_key}); - GA.label(transitions.back().node) = transitions.back().name; + + const auto transition_key = Symbol('T', nodes.size() + 1); + nodes.push_back(Node{t, G.newNode(), transition_key}); + GA.label(nodes.back().node) = nodes.back().name; } // make the graph for (const auto& x : net) { const auto [t, io] = x; const auto transition = - std::find_if(transitions.begin(), transitions.end(), + std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return x.first == n.name; }); for (const auto& s : io.first) { const auto place = - std::find_if(places.begin(), places.end(), + std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; }); - arcs.push_back({G.newEdge(place->node, transition->node), place->output, - transition->input}); - GA.bends(arcs.back().edge); + arcs.push_back({G.newEdge(place->node, transition->node), + s.second, + {&(*place), &(*transition)}}); + // GA.bends(arcs.back().edge); } for (const auto& s : io.second) { const auto place = - std::find_if(places.begin(), places.end(), + std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; }); arcs.push_back({G.newEdge(transition->node, place->node), - transition->output, place->input}); - GA.bends(arcs.back().edge); + s.second, + {&(*transition), &(*place)}}); + // GA.bends(arcs.back().edge); } } { using namespace ogdf; for (node v : G.nodes) { - GA.width(v) /= 2; - GA.height(v) /= 2; + GA.width(v) = 150.0; + GA.height(v) = 25.0; } - GraphIO::write(GA, "output-ERDiagram.svg", GraphIO::drawSVG); + // PlanarizationLayout pl; + + // SubgraphPlanarizer* crossMin = new SubgraphPlanarizer; + // PlanarSubgraphFast* ps = new PlanarSubgraphFast; + // ps->runs(100); + // VariableEmbeddingInserter* ves = new VariableEmbeddingInserter; + // ves->removeReinsert(RemoveReinsertType::All); + + // crossMin->setSubgraph(ps); + // crossMin->setInserter(ves); + // pl.setCrossMin(crossMin); + + // EmbedderMinDepthMaxFaceLayers* emb = new EmbedderMinDepthMaxFaceLayers; + // pl.setEmbedder(emb); + + // OrthoLayout* ol = new OrthoLayout; + // ol->separation(40.0); + // ol->cOverhang(0.4); + // pl.setPlanarLayouter(ol); + + // pl.call(GA); SugiyamaLayout SL; SL.setRanking(new OptimalRanking); SL.setCrossMin(new MedianHeuristic); OptimalHierarchyLayout* ohl = new OptimalHierarchyLayout; - ohl->layerDistance(30.0); - ohl->nodeDistance(15.0); - ohl->weightBalancing(0.2); + // ohl->layerDistance(30.0); + // ohl->nodeDistance(25.0); + // ohl->weightBalancing(0.8); SL.setLayout(ohl); SL.call(GA); GA.rotateLeft90(); + GA.translateToNonNeg(); GraphIO::write(GA, "output-unix-history-hierarchical.svg", GraphIO::drawSVG); } - double x_factor = 2; - double y_factor = 3; - for (auto& t : transitions) { - t.x = x_factor * GA.x(t.node); - t.y = y_factor * GA.y(t.node); - } - for (auto& p : places) { - p.x = x_factor * GA.x(p.node); - p.y = y_factor * GA.y(p.node); + for (auto& n : nodes) { + n.Pos = ImVec2(GA.x(n.node), GA.y(n.node)); } } - std::vector places; - std::vector transitions; + + std::vector nodes; std::vector arcs; }; - -template <> -void draw(Graph& g) { - namespace ed = ax::NodeEditor; - ed::SetCurrentEditor(g.m_Context); - ed::Begin("My Editor", ImVec2(0.0, 0.0f)); - for (const auto& [name, node, id, i, o, x, y] : g.transitions) { - ed::BeginNode(id.key()); - ed::BeginPin(i.key(), ed::PinKind::Input); - ed::EndPin(); - ImGui::SameLine(); - // ImGui::Text("%s", name.c_str()); - // ImGui::SameLine(); - ed::BeginPin(o.key(), ed::PinKind::Output); - ed::EndPin(); - ed::EndNode(); - ed::SetNodePosition(id.key(), ImVec2(x, y)); - } - - for (const auto& [name, node, id, i, o, x, y] : g.places) { - ed::BeginNode(id.key()); - ed::BeginPin(i.key(), ed::PinKind::Input); - ed::EndPin(); - ImGui::SameLine(); - // ImGui::Text("%s", name.c_str()); - // ImGui::SameLine(); - ed::BeginPin(o.key(), ed::PinKind::Output); - ed::EndPin(); - ed::EndNode(); - ed::SetNodePosition(id.key(), ImVec2(x, y)); - } - - int uniqueId = 1; - for (const auto& [edge, i, o] : g.arcs) { - ax::NodeEditor::Link(uniqueId++, i.key(), o.key()); - } - ed::End(); - ed::SetCurrentEditor(nullptr); -} diff --git a/symmetri/gui/initialize.hpp b/symmetri/gui/initialize.hpp index a759b7f..d38c5a5 100644 --- a/symmetri/gui/initialize.hpp +++ b/symmetri/gui/initialize.hpp @@ -8,8 +8,10 @@ #include "view.hpp" inline Model initializeModel(Model &&m) { m.working_dir = "/users/thomashorstink/Projects/symmetri/nets"; + // m.active_file = "/users/thomashorstink/Projects/symmetri/nets/n1.pnml"; m.active_file = - "/users/thomashorstink/Projects/symmetri/nets/InspectionSeqPNv21.pnml"; + "/Users/thomashorstink/Projects/Symmetri/examples/combinations/" + "DualProcessWorker.pnml"; symmetri::Net net; symmetri::Marking marking; symmetri::PriorityTable pt; @@ -21,8 +23,8 @@ inline Model initializeModel(Model &&m) { std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); } - current(m.documents).push_back(net); - current(m.documents).push_back(marking); + // current(m.documents).push_back(net); + // current(m.documents).push_back(marking); current(m.documents).push_back(Graph{net, marking}); // create a file browser instance diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 797f7cc..358e4a3 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -10,7 +10,6 @@ #include #import #import - static void glfw_error_callback(int error, const char *description) { fprintf(stderr, "Glfw Error %d: %s\n", error, description); } diff --git a/symmetri/gui/menu_bar.hpp b/symmetri/gui/menu_bar.hpp index f805d21..a303fbc 100644 --- a/symmetri/gui/menu_bar.hpp +++ b/symmetri/gui/menu_bar.hpp @@ -11,7 +11,7 @@ Reducer updateActiveFile(const std::filesystem::path &file) { return [=](Model &&m) { m.active_file = file; auto [net, marking] = symmetri::readPnml({file}); - m.documents.push_back({net, marking, Graph{net, marking}}); + m.documents.push_back({Graph{net, marking}}); commit(m.documents); return m; }; diff --git a/symmetri/gui/redux.hpp b/symmetri/gui/redux.hpp index 39ccdc8..9b89774 100644 --- a/symmetri/gui/redux.hpp +++ b/symmetri/gui/redux.hpp @@ -12,19 +12,20 @@ #include "blockingconcurrentqueue.h" #include "drawable.h" +#include "imgui.h" using View = std::vector; using History = std::vector; -void commit(History& x) { +inline void commit(History& x) { assert(!x.empty()); x.push_back(x.back()); } -void undo(History& x) { +inline void undo(History& x) { assert(!x.empty()); x.pop_back(); } -View& current(History& x) { +inline View& current(History& x) { assert(!x.empty()); return x.back(); } @@ -37,7 +38,7 @@ struct Model { History documents = History{1}; }; -void draw(Model& m) { +inline void draw(Model& m) { for (auto& drawable : m.statics) { draw(drawable); } From 89633a3aded3536e13f0bacfb107565d6f18ffdb Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Mon, 18 Dec 2023 20:24:20 +0100 Subject: [PATCH 004/142] wip2 --- symmetri/gui/draw_graph.cpp | 98 ++++++++++++----------- symmetri/gui/graph.hpp | 152 ++++++++++++++++-------------------- symmetri/gui/symbol.cpp | 4 +- 3 files changed, 121 insertions(+), 133 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 0a0a435..b4e1e08 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -1,7 +1,10 @@ +#include + #include "drawable.h" #include "graph.hpp" #include "imgui.h" #include "redux.hpp" + // Creating a node graph editor for Dear ImGui // Quick sample, not production code! // This is quick demo I crafted in a few hours in 2015 showcasing how to use @@ -61,6 +64,7 @@ void draw(Graph& g) { static ImVec2 scrolling = ImVec2(0.0f, 0.0f); static bool show_grid = true; static Symbol node_selected = -1; + static ImVec2 size; // Initialization ImGuiIO& io = ImGui::GetIO(); @@ -71,37 +75,33 @@ void draw(Graph& g) { ImGui::BeginChild("node_list", ImVec2(150, 0)); ImGui::Text("Places"); ImGui::Separator(); - for (const auto& node : g.nodes) { - if (node.id.chr() == 'P') { - ImGui::PushID(node.id); - if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { - node_selected = node.id; - } - if (ImGui::IsItemHovered()) { - node_hovered_in_list = node.id; - // node_hovered_in_list = node.id; - // open_context_menu |= ImGui::IsMouseClicked(1); - } - ImGui::PopID(); + for (const auto& node : g.places) { + ImGui::PushID(node.id.key()); + if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { + node_selected = node.id; } + if (ImGui::IsItemHovered()) { + node_hovered_in_list = node.id; + // node_hovered_in_list = node.id; + // open_context_menu |= ImGui::IsMouseClicked(1); + } + ImGui::PopID(); } ImGui::Dummy(ImVec2(0.0f, 20.0f)); ImGui::Text("Transitions"); ImGui::Separator(); - for (const auto& node : g.nodes) { - if (node.id.chr() == 'T') { - ImGui::PushID(node.id); - if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { - node_selected = node.id; - } - if (ImGui::IsItemHovered()) { - node_hovered_in_list = node.id; - // node_hovered_in_list = node.id; - // open_context_menu |= ImGui::IsMouseClicked(1); - } - ImGui::PopID(); + for (const auto& node : g.transitions) { + ImGui::PushID(node.id.key()); + if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { + node_selected = node.id; + } + if (ImGui::IsItemHovered()) { + node_hovered_in_list = node.id; + // node_hovered_in_list = node.id; + // open_context_menu |= ImGui::IsMouseClicked(1); } + ImGui::PopID(); } ImGui::EndChild(); @@ -144,9 +144,9 @@ void draw(Graph& g) { // Display links draw_list->ChannelsSplit(2); draw_list->ChannelsSetCurrent(0); // Background - for (const auto& [edge, color, from_to] : g.arcs) { - ImVec2 p1 = offset + from_to[0]->GetCenterPos(); - ImVec2 p2 = offset + from_to[1]->GetCenterPos(); + for (const auto& [color, from_to] : g.arcs) { + ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); + ImVec2 p2 = offset + Node::GetCenterPos(*from_to[1], size); ImVec2 d = p1 - p2; d.x *= 0.33; d.y *= 0.33; @@ -155,51 +155,57 @@ void draw(Graph& g) { } // Display nodes - for (auto& node : g.nodes) { - ImGui::PushID(node.id); - ImVec2 node_rect_min = offset + node.Pos; + std::vector nodes; + for (auto& n : g.transitions) { + nodes.push_back(&n); + } + for (auto& n : g.places) { + nodes.push_back(&n); + } + for (auto* node : nodes) { + ImGui::PushID(node->id.key()); + ImVec2 node_rect_min = offset + node->Pos; // Display node contents first draw_list->ChannelsSetCurrent(1); // Foreground bool old_any_active = ImGui::IsAnyItemActive(); - auto textWidth = ImGui::CalcTextSize(node.name.c_str()).x; + auto textWidth = ImGui::CalcTextSize(node->name.c_str()).x; ImGui::SetCursorScreenPos(node_rect_min + NODE_WINDOW_PADDING + ImVec2(8.0f - textWidth * 0.5f, -20.0f)); ImGui::BeginGroup(); // Lock horizontal position - ImGui::Text("%s", node.name.c_str()); + ImGui::Text("%s", node->name.c_str()); ImGui::EndGroup(); // Save the size of what we have emitted and whether any of the widgets are // being used bool node_widgets_active = (!old_any_active && ImGui::IsAnyItemActive()); - node.Size = - ImGui::GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING; - node.Size.x = node.Size.y; - ImVec2 node_rect_max = node_rect_min + node.Size; + size = ImGui::GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING; + size.x = size.y; + ImVec2 node_rect_max = node_rect_min + size; // Display node box draw_list->ChannelsSetCurrent(0); // Background ImGui::SetCursorScreenPos(node_rect_min); - ImGui::InvisibleButton("node", node.Size); + ImGui::InvisibleButton("node", size); if (ImGui::IsItemHovered()) { - node_hovered_in_scene = node.id; + node_hovered_in_scene = node->id; open_context_menu |= ImGui::IsMouseClicked(1); } bool node_moving_active = ImGui::IsItemActive(); - if (node_widgets_active || node_moving_active) node_selected = node.id; + if (node_widgets_active || node_moving_active) node_selected = node->id; if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) - node.Pos = node.Pos + io.MouseDelta; + node->Pos = node->Pos + io.MouseDelta; const int opacity = 255; - auto select_color = node.id == node_hovered_in_list + auto select_color = node->id == node_hovered_in_list ? IM_COL32(255, 255, 0, opacity) : IM_COL32(100, 100, 100, opacity); - if (node.id.chr() == 'P') { - draw_list->AddCircleFilled(offset + node.GetCenterPos(), - 0.5f * node.Size.x, + if (node->id.chr() == 'P') { + draw_list->AddCircleFilled(offset + Node::GetCenterPos(node->Pos, size), + 0.5f * size.x, IM_COL32(135, 135, 135, opacity), -5); - draw_list->AddCircle(offset + node.GetCenterPos(), 0.5f * node.Size.x, - select_color, -5, 3.0f); + draw_list->AddCircle(offset + Node::GetCenterPos(node->Pos, size), + 0.5f * size.x, select_color, -5, 3.0f); } else { draw_list->AddRectFilled(node_rect_min, node_rect_max, diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index a3e2fd1..d01dabc 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -21,125 +21,102 @@ #include "imgui.h" #include "symbol.h" -struct Graph { - struct Node { - std::string name; - ogdf::node node; - Symbol id; - ImVec2 Pos, Size; - - ImVec2 GetCenterPos() const { - return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); - } - }; - struct Arc { - ogdf::edge edge; - symmetri::Token color; - std::array from_to; - }; +template +size_t toIndex(const std::vector& m, + const std::function& s) { + auto ptr = std::find_if(m.begin(), m.end(), s); + return std::distance(m.begin(), ptr); +} + +struct Node { + std::string name; + Symbol id; + ImVec2 Pos; + static ImVec2 GetCenterPos(const ImVec2& pos, const ImVec2& size) { + return ImVec2(pos.x + size.x * 0.5f, pos.y + size.y * 0.5f); + } +}; - ogdf::Graph G; +struct Arc { + symmetri::Token color; + std::array from_to_pos; +}; +struct Graph { explicit Graph(const symmetri::Net net, const symmetri::Marking&) { + ogdf::Graph G; ogdf::GraphAttributes GA(G, ogdf::GraphAttributes::nodeGraphics | ogdf::GraphAttributes::nodeLabel | ogdf::GraphAttributes::edgeGraphics); + std::vector ogdf_places, ogdf_transitions; + for (const auto& [t, io] : net) { for (const auto& s : io.first) { - const auto place_key = Symbol('P', nodes.size() + 1); - - if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { + if (std::find_if(places.begin(), places.end(), [&](const auto& n) { return s.first == n.name; - }) == std::end(nodes)) { - nodes.push_back({s.first, G.newNode(), place_key}); - auto& current_node = nodes.back(); - GA.label(current_node.node) = current_node.name; - GA.shape(current_node.node) = ogdf::Shape::Ellipse; + }) == std::end(places)) { + const auto place_key = Symbol('P', places.size() + 1); + ogdf_places.push_back(G.newNode()); + auto current_node = ogdf_places.back(); + GA.label(current_node) = s.first; + GA.shape(current_node) = ogdf::Shape::Ellipse; + places.push_back({s.first, place_key}); } } for (const auto& s : io.second) { - const auto place_key = Symbol('P', nodes.size() + 1); - if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { + if (std::find_if(places.begin(), places.end(), [&](const auto& n) { return s.first == n.name; - }) == std::end(nodes)) { - nodes.push_back({s.first, G.newNode(), place_key}); - auto& current_node = nodes.back(); - GA.label(current_node.node) = current_node.name; - GA.shape(current_node.node) = ogdf::Shape::Ellipse; + }) == std::end(places)) { + const auto place_key = Symbol('P', places.size() + 1); + ogdf_places.push_back(G.newNode()); + auto current_node = ogdf_places.back(); + GA.label(current_node) = s.first; + GA.shape(current_node) = ogdf::Shape::Ellipse; + places.push_back({s.first, place_key}); } } - const auto transition_key = Symbol('T', nodes.size() + 1); - nodes.push_back(Node{t, G.newNode(), transition_key}); - GA.label(nodes.back().node) = nodes.back().name; + const auto transition_key = Symbol('T', transitions.size() + 1); + ogdf_transitions.push_back(G.newNode()); + auto current_node = ogdf_transitions.back(); + GA.label(current_node) = t; + transitions.push_back({t, transition_key}); } // make the graph for (const auto& x : net) { const auto [t, io] = x; - const auto transition = - std::find_if(nodes.begin(), nodes.end(), - [&](const auto& n) { return x.first == n.name; }); - + const auto transition_idx = toIndex( + transitions, [=](const Node& n) { return x.first == n.name; }); for (const auto& s : io.first) { - const auto place = - std::find_if(nodes.begin(), nodes.end(), - [&](const auto& n) { return s.first == n.name; }); - arcs.push_back({G.newEdge(place->node, transition->node), - s.second, - {&(*place), &(*transition)}}); - // GA.bends(arcs.back().edge); + const auto place_idx = toIndex( + places, [=](const Node& n) { return s.first == n.name; }); + // ogdf_arcs.push_back(); + G.newEdge(ogdf_places[place_idx], ogdf_transitions[transition_idx]); + arcs.push_back( + {s.second, + {&(places[place_idx].Pos), &(transitions[transition_idx].Pos)}}); } for (const auto& s : io.second) { - const auto place = - std::find_if(nodes.begin(), nodes.end(), - [&](const auto& n) { return s.first == n.name; }); - arcs.push_back({G.newEdge(transition->node, place->node), - s.second, - {&(*transition), &(*place)}}); - // GA.bends(arcs.back().edge); + const auto place_idx = toIndex( + places, [=](const Node& n) { return s.first == n.name; }); + G.newEdge(ogdf_transitions[transition_idx], ogdf_places[place_idx]); + arcs.push_back( + {s.second, + {&(transitions[transition_idx].Pos), &(places[place_idx].Pos)}}); } } { using namespace ogdf; - for (node v : G.nodes) { - GA.width(v) = 150.0; - GA.height(v) = 25.0; - } - - // PlanarizationLayout pl; - - // SubgraphPlanarizer* crossMin = new SubgraphPlanarizer; - // PlanarSubgraphFast* ps = new PlanarSubgraphFast; - // ps->runs(100); - // VariableEmbeddingInserter* ves = new VariableEmbeddingInserter; - // ves->removeReinsert(RemoveReinsertType::All); - - // crossMin->setSubgraph(ps); - // crossMin->setInserter(ves); - // pl.setCrossMin(crossMin); - - // EmbedderMinDepthMaxFaceLayers* emb = new EmbedderMinDepthMaxFaceLayers; - // pl.setEmbedder(emb); - - // OrthoLayout* ol = new OrthoLayout; - // ol->separation(40.0); - // ol->cOverhang(0.4); - // pl.setPlanarLayouter(ol); - - // pl.call(GA); SugiyamaLayout SL; SL.setRanking(new OptimalRanking); SL.setCrossMin(new MedianHeuristic); OptimalHierarchyLayout* ohl = new OptimalHierarchyLayout; - // ohl->layerDistance(30.0); - // ohl->nodeDistance(25.0); - // ohl->weightBalancing(0.8); SL.setLayout(ohl); SL.call(GA); @@ -149,11 +126,16 @@ struct Graph { GraphIO::drawSVG); } - for (auto& n : nodes) { - n.Pos = ImVec2(GA.x(n.node), GA.y(n.node)); + for (size_t i = 0; i < ogdf_places.size(); i++) { + places[i].Pos = ImVec2(GA.x(ogdf_places[i]), 2 * GA.y(ogdf_places[i])); + } + for (size_t i = 0; i < ogdf_transitions.size(); i++) { + transitions[i].Pos = + ImVec2(GA.x(ogdf_transitions[i]), 2 * GA.y(ogdf_transitions[i])); } } - std::vector nodes; + std::vector places; + std::vector transitions; std::vector arcs; }; diff --git a/symmetri/gui/symbol.cpp b/symmetri/gui/symbol.cpp index 3d803cf..f7641b2 100644 --- a/symmetri/gui/symbol.cpp +++ b/symmetri/gui/symbol.cpp @@ -25,8 +25,8 @@ #include #include -static const size_t keyBits = sizeof(Key) * 8; -static const size_t chrBits = sizeof(unsigned char) * 8; +static const size_t keyBits = sizeof(Key) * 4; +static const size_t chrBits = sizeof(unsigned char) * 4; static const size_t indexBits = keyBits - chrBits; static const Key chrMask = Key(UCHAR_MAX) From de332ae9057bf3de1609632b1f2a4578ffdb7f09 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Fri, 22 Dec 2023 15:09:25 +0100 Subject: [PATCH 005/142] o o xwip2 --- symmetri/gui/draw_graph.cpp | 247 ++++++++++++++++++------------------ symmetri/gui/graph.hpp | 160 +++++++++++------------ symmetri/gui/initialize.hpp | 2 +- symmetri/gui/menu_bar.hpp | 2 +- 4 files changed, 204 insertions(+), 207 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index b4e1e08..7943f58 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -46,6 +46,19 @@ inline ImU32 getColor(symmetri::Token token) { } }; +// State +static ImVec2 scrolling = ImVec2(0.0f, 0.0f); +static bool show_grid = true; +static Symbol node_selected = -1; +static ImVec2 size; +static ImVec2 offset; +static ImDrawList* draw_list; +static bool open_context_menu = false; +static Symbol node_hovered_in_list = -1; +static int node_hovered_in_scene = -1; +static const float NODE_SLOT_RADIUS = 4.0f; +static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); + // NB: You can use math functions/operators on ImVec2 if you #define // IMGUI_DEFINE_MATH_OPERATORS and #include "imgui_internal.h" Here we only // declare simple +/- operators so others don't leak into the demo code. @@ -56,61 +69,116 @@ static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); } +void draw_arc(Arc& arc) { + const auto& [color, from_to] = arc; + ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); + ImVec2 p2 = offset + Node::GetCenterPos(*from_to[1], size); + ImVec2 d = p1 - p2; + d.x *= 0.33; + d.y *= 0.33; + ImU32 imcolor = getColor(color); + draw_list->AddLine(p1, p2, imcolor, 2.0f); +}; + +void draw_nodes(Node& node) { + open_context_menu = false; + ImGui::PushID(node.id.key()); + ImVec2 node_rect_min = offset + node.Pos; + + // Display node contents first + bool old_any_active = ImGui::IsAnyItemActive(); + auto textWidth = ImGui::CalcTextSize(node.name.c_str()).x; + ImGui::SetCursorScreenPos(node_rect_min + NODE_WINDOW_PADDING + + ImVec2(8.0f - textWidth * 0.5f, -20.0f)); + ImGui::BeginGroup(); // Lock horizontal position + ImGui::Text("%s", node.name.c_str()); + ImGui::EndGroup(); + + // Save the size of what we have emitted and whether any of the widgets are + // being used + bool node_widgets_active = (!old_any_active && ImGui::IsAnyItemActive()); + size = ImGui::GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING; + size.x = size.y; + ImVec2 node_rect_max = node_rect_min + size; + + // Display node box + ImGui::SetCursorScreenPos(node_rect_min); + ImGui::InvisibleButton("node", size); + if (ImGui::IsItemHovered()) { + node_hovered_in_scene = node.id; + open_context_menu |= ImGui::IsMouseClicked(1); + } + bool node_moving_active = ImGui::IsItemActive(); + if (node_widgets_active || node_moving_active) node_selected = node.id; + if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + node.Pos = node.Pos + ImGui::GetIO().MouseDelta; + + const int opacity = 255; + auto select_color = node.id == node_hovered_in_list + ? IM_COL32(255, 255, 0, opacity) + : IM_COL32(100, 100, 100, opacity); + if (node.id.chr() == 'P') { + draw_list->AddCircleFilled(offset + Node::GetCenterPos(node.Pos, size), + 0.5f * size.x, IM_COL32(135, 135, 135, opacity), + -5); + draw_list->AddCircle(offset + Node::GetCenterPos(node.Pos, size), + 0.5f * size.x, select_color, -5, 3.0f); + + } else { + draw_list->AddRectFilled(node_rect_min, node_rect_max, + IM_COL32(200, 200, 200, opacity), 4.0f); + draw_list->AddRect(node_rect_min, node_rect_max, select_color, 4.0f, 0, + 3.0f); + } + + ImGui::PopID(); +}; + // Dummy data structure provided for the example. // Note that we storing links as indices (not ID) to make example code shorter. template <> void draw(Graph& g) { - // State - static ImVec2 scrolling = ImVec2(0.0f, 0.0f); - static bool show_grid = true; - static Symbol node_selected = -1; - static ImVec2 size; - - // Initialization - ImGuiIO& io = ImGui::GetIO(); // Draw a list of nodes on the left side - bool open_context_menu = false; - Symbol node_hovered_in_list = -1; - int node_hovered_in_scene = -1; ImGui::BeginChild("node_list", ImVec2(150, 0)); ImGui::Text("Places"); ImGui::Separator(); - for (const auto& node : g.places) { - ImGui::PushID(node.id.key()); - if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { - node_selected = node.id; - } - if (ImGui::IsItemHovered()) { - node_hovered_in_list = node.id; - // node_hovered_in_list = node.id; - // open_context_menu |= ImGui::IsMouseClicked(1); + for (const auto& node : g.nodes) { + if (node.id.chr() == 'P') { + ImGui::PushID(node.id.key()); + if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { + node_selected = node.id; + } + if (ImGui::IsItemHovered()) { + node_hovered_in_list = node.id; + // node_hovered_in_list = node.id; + open_context_menu |= ImGui::IsMouseClicked(1); + } + ImGui::PopID(); } - ImGui::PopID(); } ImGui::Dummy(ImVec2(0.0f, 20.0f)); ImGui::Text("Transitions"); ImGui::Separator(); - for (const auto& node : g.transitions) { - ImGui::PushID(node.id.key()); - if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { - node_selected = node.id; - } - if (ImGui::IsItemHovered()) { - node_hovered_in_list = node.id; - // node_hovered_in_list = node.id; - // open_context_menu |= ImGui::IsMouseClicked(1); + for (const auto& node : g.nodes) { + if (node.id.chr() == 'T') { + ImGui::PushID(node.id.key()); + if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { + node_selected = node.id; + } + if (ImGui::IsItemHovered()) { + node_hovered_in_list = node.id; + // node_hovered_in_list = node.id; + open_context_menu |= ImGui::IsMouseClicked(1); + } + ImGui::PopID(); } - ImGui::PopID(); } ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginGroup(); - const float NODE_SLOT_RADIUS = 4.0f; - const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); - // Create our child canvas ImGui::Text("Hold middle mouse button to scroll (%.2f,%.2f)", scrolling.x, scrolling.y); @@ -124,8 +192,8 @@ void draw(Graph& g) { ImGui::PopStyleVar(); // WindowPadding ImGui::PushItemWidth(120.0f); - const ImVec2 offset = ImGui::GetCursorScreenPos() + scrolling; - ImDrawList* draw_list = ImGui::GetWindowDrawList(); + offset = ImGui::GetCursorScreenPos() + scrolling; + draw_list = ImGui::GetWindowDrawList(); // Display grid if (show_grid) { @@ -141,82 +209,8 @@ void draw(Graph& g) { ImVec2(canvas_sz.x, y) + win_pos, GRID_COLOR); } - // Display links - draw_list->ChannelsSplit(2); - draw_list->ChannelsSetCurrent(0); // Background - for (const auto& [color, from_to] : g.arcs) { - ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); - ImVec2 p2 = offset + Node::GetCenterPos(*from_to[1], size); - ImVec2 d = p1 - p2; - d.x *= 0.33; - d.y *= 0.33; - ImU32 imcolor = getColor(color); - draw_list->AddLine(p1, p2, imcolor, 2.0f); - } - - // Display nodes - std::vector nodes; - for (auto& n : g.transitions) { - nodes.push_back(&n); - } - for (auto& n : g.places) { - nodes.push_back(&n); - } - for (auto* node : nodes) { - ImGui::PushID(node->id.key()); - ImVec2 node_rect_min = offset + node->Pos; - - // Display node contents first - draw_list->ChannelsSetCurrent(1); // Foreground - bool old_any_active = ImGui::IsAnyItemActive(); - auto textWidth = ImGui::CalcTextSize(node->name.c_str()).x; - ImGui::SetCursorScreenPos(node_rect_min + NODE_WINDOW_PADDING + - ImVec2(8.0f - textWidth * 0.5f, -20.0f)); - ImGui::BeginGroup(); // Lock horizontal position - ImGui::Text("%s", node->name.c_str()); - ImGui::EndGroup(); - - // Save the size of what we have emitted and whether any of the widgets are - // being used - bool node_widgets_active = (!old_any_active && ImGui::IsAnyItemActive()); - size = ImGui::GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING; - size.x = size.y; - ImVec2 node_rect_max = node_rect_min + size; - - // Display node box - draw_list->ChannelsSetCurrent(0); // Background - ImGui::SetCursorScreenPos(node_rect_min); - ImGui::InvisibleButton("node", size); - if (ImGui::IsItemHovered()) { - node_hovered_in_scene = node->id; - open_context_menu |= ImGui::IsMouseClicked(1); - } - bool node_moving_active = ImGui::IsItemActive(); - if (node_widgets_active || node_moving_active) node_selected = node->id; - if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) - node->Pos = node->Pos + io.MouseDelta; - - const int opacity = 255; - auto select_color = node->id == node_hovered_in_list - ? IM_COL32(255, 255, 0, opacity) - : IM_COL32(100, 100, 100, opacity); - if (node->id.chr() == 'P') { - draw_list->AddCircleFilled(offset + Node::GetCenterPos(node->Pos, size), - 0.5f * size.x, - IM_COL32(135, 135, 135, opacity), -5); - draw_list->AddCircle(offset + Node::GetCenterPos(node->Pos, size), - 0.5f * size.x, select_color, -5, 3.0f); - - } else { - draw_list->AddRectFilled(node_rect_min, node_rect_max, - IM_COL32(200, 200, 200, opacity), 4.0f); - draw_list->AddRect(node_rect_min, node_rect_max, select_color, 4.0f, 0, - 3.0f); - } - - ImGui::PopID(); - } - draw_list->ChannelsMerge(); + std::for_each(g.arcs.begin(), g.arcs.end(), &draw_arc); + std::for_each(g.nodes.begin(), g.nodes.end(), &draw_nodes); // Open context menu if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) @@ -234,11 +228,12 @@ void draw(Graph& g) { // Draw context menu ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); if (ImGui::BeginPopup("context_menu")) { - // Node* node = node_selected != Symbol('x') ? &nodes[node_selected] : NULL; + auto node = + std::find_if(g.nodes.begin(), g.nodes.end(), + [=](const auto& n) { return n.id == node_selected; }); ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; - if (false) { - // if (node) { - // ImGui::Text("Node '%s'", node->Name); + if (node != std::end(g.nodes)) { + ImGui::Text("Node '%s'", node->name.c_str()); ImGui::Separator(); if (ImGui::MenuItem("Rename..", NULL, false, false)) { } @@ -247,12 +242,19 @@ void draw(Graph& g) { if (ImGui::MenuItem("Copy", NULL, false, false)) { } } else { - if (ImGui::MenuItem("Add")) { - MVC::push([=](Model&& m) { - std::cout << "add node" << std::endl; + if (ImGui::MenuItem("Add place")) { + MVC::push([&, scene_pos](Model&& m) { + g.nodes.push_back( + Node{"New node", Symbol('P', g.nodes.size()), scene_pos}); + return m; + }); + } + if (ImGui::MenuItem("Add transition")) { + MVC::push([&, scene_pos](Model&& m) { + g.nodes.push_back( + Node{"New node", Symbol('T', g.nodes.size()), scene_pos}); return m; }); - // nodes.push_back(Node(nodes.Size, "New node", scene_pos, 2, 2, 1, 1)); } if (ImGui::MenuItem("Paste", NULL, false, false)) { } @@ -263,8 +265,9 @@ void draw(Graph& g) { // Scrolling if (ImGui::IsWindowHovered() && !ImGui::IsAnyItemActive() && - ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 0.0f)) - scrolling = scrolling + io.MouseDelta; + ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 0.0f)) { + scrolling = scrolling + ImGui::GetIO().MouseDelta; + } ImGui::PopItemWidth(); ImGui::EndChild(); diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index d01dabc..b3a54b8 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -26,7 +26,7 @@ template size_t toIndex(const std::vector& m, const std::function& s) { auto ptr = std::find_if(m.begin(), m.end(), s); - return std::distance(m.begin(), ptr); + return ptr->id.index(); } struct Node { @@ -44,98 +44,92 @@ struct Arc { }; struct Graph { - explicit Graph(const symmetri::Net net, const symmetri::Marking&) { - ogdf::Graph G; - ogdf::GraphAttributes GA(G, ogdf::GraphAttributes::nodeGraphics | - ogdf::GraphAttributes::nodeLabel | - ogdf::GraphAttributes::edgeGraphics); - std::vector ogdf_places, ogdf_transitions; - - for (const auto& [t, io] : net) { - for (const auto& s : io.first) { - if (std::find_if(places.begin(), places.end(), [&](const auto& n) { - return s.first == n.name; - }) == std::end(places)) { - const auto place_key = Symbol('P', places.size() + 1); - ogdf_places.push_back(G.newNode()); - auto current_node = ogdf_places.back(); - GA.label(current_node) = s.first; - GA.shape(current_node) = ogdf::Shape::Ellipse; - places.push_back({s.first, place_key}); - } - } + std::vector arcs; + std::vector nodes; +}; - for (const auto& s : io.second) { - if (std::find_if(places.begin(), places.end(), [&](const auto& n) { - return s.first == n.name; - }) == std::end(places)) { - const auto place_key = Symbol('P', places.size() + 1); - ogdf_places.push_back(G.newNode()); - auto current_node = ogdf_places.back(); - GA.label(current_node) = s.first; - GA.shape(current_node) = ogdf::Shape::Ellipse; - places.push_back({s.first, place_key}); - } +inline Graph createGraph(const symmetri::Net net) { + std::vector nodes; + std::vector arcs; + ogdf::Graph G; + ogdf::GraphAttributes GA(G, ogdf::GraphAttributes::nodeGraphics | + ogdf::GraphAttributes::nodeLabel | + ogdf::GraphAttributes::edgeGraphics); + std::vector ogdf_places, ogdf_transitions, ogdf_nodes; + + for (const auto& [t, io] : net) { + for (const auto& s : io.first) { + if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { + return s.first == n.name; + }) == std::end(nodes)) { + const auto place_key = Symbol('P', nodes.size()); + ogdf_nodes.push_back(G.newNode()); + auto current_node = ogdf_nodes.back(); + GA.label(current_node) = s.first; + GA.shape(current_node) = ogdf::Shape::Ellipse; + nodes.push_back({s.first, place_key}); } - - const auto transition_key = Symbol('T', transitions.size() + 1); - ogdf_transitions.push_back(G.newNode()); - auto current_node = ogdf_transitions.back(); - GA.label(current_node) = t; - transitions.push_back({t, transition_key}); } - // make the graph - for (const auto& x : net) { - const auto [t, io] = x; - const auto transition_idx = toIndex( - transitions, [=](const Node& n) { return x.first == n.name; }); - for (const auto& s : io.first) { - const auto place_idx = toIndex( - places, [=](const Node& n) { return s.first == n.name; }); - // ogdf_arcs.push_back(); - G.newEdge(ogdf_places[place_idx], ogdf_transitions[transition_idx]); - arcs.push_back( - {s.second, - {&(places[place_idx].Pos), &(transitions[transition_idx].Pos)}}); - } - - for (const auto& s : io.second) { - const auto place_idx = toIndex( - places, [=](const Node& n) { return s.first == n.name; }); - G.newEdge(ogdf_transitions[transition_idx], ogdf_places[place_idx]); - arcs.push_back( - {s.second, - {&(transitions[transition_idx].Pos), &(places[place_idx].Pos)}}); + for (const auto& s : io.second) { + if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { + return s.first == n.name; + }) == std::end(nodes)) { + const auto place_key = Symbol('P', nodes.size()); + ogdf_nodes.push_back(G.newNode()); + auto current_node = ogdf_nodes.back(); + GA.label(current_node) = s.first; + GA.shape(current_node) = ogdf::Shape::Ellipse; + nodes.push_back({s.first, place_key}); } } - { - using namespace ogdf; - SugiyamaLayout SL; - SL.setRanking(new OptimalRanking); - SL.setCrossMin(new MedianHeuristic); - - OptimalHierarchyLayout* ohl = new OptimalHierarchyLayout; - SL.setLayout(ohl); + const auto transition_key = Symbol('T', nodes.size()); + ogdf_nodes.push_back(G.newNode()); + auto current_node = ogdf_nodes.back(); + GA.label(current_node) = t; + nodes.push_back({t, transition_key}); + } - SL.call(GA); - GA.rotateLeft90(); - GA.translateToNonNeg(); - GraphIO::write(GA, "output-unix-history-hierarchical.svg", - GraphIO::drawSVG); + // make the graph + for (const auto& x : net) { + const auto [t, io] = x; + const auto transition_idx = + toIndex(nodes, [=](const Node& n) { return x.first == n.name; }); + for (const auto& s : io.first) { + const auto place_idx = toIndex( + nodes, [=](const Node& n) { return s.first == n.name; }); + G.newEdge(ogdf_nodes[place_idx], ogdf_nodes[transition_idx]); + arcs.push_back( + {s.second, {&(nodes[place_idx].Pos), &(nodes[transition_idx].Pos)}}); } - for (size_t i = 0; i < ogdf_places.size(); i++) { - places[i].Pos = ImVec2(GA.x(ogdf_places[i]), 2 * GA.y(ogdf_places[i])); - } - for (size_t i = 0; i < ogdf_transitions.size(); i++) { - transitions[i].Pos = - ImVec2(GA.x(ogdf_transitions[i]), 2 * GA.y(ogdf_transitions[i])); + for (const auto& s : io.second) { + const auto place_idx = toIndex( + nodes, [=](const Node& n) { return s.first == n.name; }); + G.newEdge(ogdf_nodes[transition_idx], ogdf_nodes[place_idx]); + arcs.push_back( + {s.second, {&(nodes[transition_idx].Pos), &(nodes[place_idx].Pos)}}); } } - std::vector places; - std::vector transitions; - std::vector arcs; -}; + { + using namespace ogdf; + SugiyamaLayout SL; + SL.setRanking(new OptimalRanking); + SL.setCrossMin(new MedianHeuristic); + + OptimalHierarchyLayout* ohl = new OptimalHierarchyLayout; + SL.setLayout(ohl); + + SL.call(GA); + GA.rotateLeft90(); + GA.translateToNonNeg(); + } + + for (size_t i = 0; i < ogdf_nodes.size(); i++) { + nodes[i].Pos = ImVec2(GA.x(ogdf_nodes[i]), 2 * GA.y(ogdf_nodes[i])); + } + + return {std::move(arcs), std::move(nodes)}; +} diff --git a/symmetri/gui/initialize.hpp b/symmetri/gui/initialize.hpp index d38c5a5..6b41686 100644 --- a/symmetri/gui/initialize.hpp +++ b/symmetri/gui/initialize.hpp @@ -25,7 +25,7 @@ inline Model initializeModel(Model &&m) { // current(m.documents).push_back(net); // current(m.documents).push_back(marking); - current(m.documents).push_back(Graph{net, marking}); + current(m.documents).push_back({createGraph(net)}); // create a file browser instance ImGui::FileBrowser fileDialog; diff --git a/symmetri/gui/menu_bar.hpp b/symmetri/gui/menu_bar.hpp index a303fbc..60ebf40 100644 --- a/symmetri/gui/menu_bar.hpp +++ b/symmetri/gui/menu_bar.hpp @@ -11,7 +11,7 @@ Reducer updateActiveFile(const std::filesystem::path &file) { return [=](Model &&m) { m.active_file = file; auto [net, marking] = symmetri::readPnml({file}); - m.documents.push_back({Graph{net, marking}}); + m.documents.push_back({createGraph(net)}); commit(m.documents); return m; }; From 149ac36d42edd9e194f630940db94915613a2380 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Tue, 26 Dec 2023 12:07:05 +0100 Subject: [PATCH 006/142] adding stuff --- symmetri/gui/draw_graph.cpp | 17 ++++++++++++++++- symmetri/gui/graph.hpp | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 7943f58..a9eb6d7 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -4,7 +4,6 @@ #include "graph.hpp" #include "imgui.h" #include "redux.hpp" - // Creating a node graph editor for Dear ImGui // Quick sample, not production code! // This is quick demo I crafted in a few hours in 2015 showcasing how to use @@ -256,6 +255,22 @@ void draw(Graph& g) { return m; }); } + if (ImGui::BeginMenu("Add arc")) { + for (const auto& from : g.nodes) { + if (ImGui::BeginMenu(from.name.c_str())) { + for (const auto& to : g.nodes) { + if (from.id.chr() != to.id.chr() && + ImGui::MenuItem(to.name.c_str())) { + g.arcs.push_back( + Arc{symmetri::Color::Success, &from.Pos, &to.Pos}); + } + } + ImGui::EndMenu(); + } + } + ImGui::EndMenu(); + } + if (ImGui::MenuItem("Paste", NULL, false, false)) { } } diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index b3a54b8..5d80aac 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -40,7 +40,7 @@ struct Node { struct Arc { symmetri::Token color; - std::array from_to_pos; + std::array from_to_pos; }; struct Graph { From 5c7fb56d837bc4f1240867cc129e94c65c9f18c4 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Wed, 27 Dec 2023 15:43:03 +0100 Subject: [PATCH 007/142] naming stuff --- symmetri/gui/draw_graph.cpp | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index a9eb6d7..bb90ae4 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -167,12 +167,36 @@ void draw(Graph& g) { } if (ImGui::IsItemHovered()) { node_hovered_in_list = node.id; - // node_hovered_in_list = node.id; open_context_menu |= ImGui::IsMouseClicked(1); } ImGui::PopID(); } } + ImGui::Dummy(ImVec2(0.0f, 20.0f)); + ImGui::Text("Selected"); + ImGui::Separator(); + if (node_hovered_in_list != Symbol(-1)) { + auto node = std::find_if( + g.nodes.begin(), g.nodes.end(), + [=](const auto& n) { return n.id == node_hovered_in_list; }); + static char buf[32] = "hello"; + ImGui::Text("Name"); + ImGui::SameLine(); + const auto id = std::to_string(node->id.key()); + ImGui::InputText(id.c_str(), node->name.data(), 30); + static int priority = 1; + static int multiplicity = 1; + if (node->id.chr() == 'T') { + ImGui::Text("Priority"); + ImGui::SameLine(); + ImGui::InputInt("##", &priority); + ImGui::Text("Weight"); + ImGui::SameLine(); + ImGui::InputInt("##", &multiplicity); + } else { + } + } + ImGui::EndChild(); ImGui::SameLine(); @@ -220,8 +244,14 @@ void draw(Graph& g) { } if (open_context_menu) { ImGui::OpenPopup("context_menu"); - // if (node_hovered_in_list != -1) node_selected = node_hovered_in_list; - // if (node_hovered_in_scene != -1) node_selected = node_hovered_in_scene; + if (node_hovered_in_list != Symbol(-1)) { + node_selected = node_hovered_in_list; + node_hovered_in_scene = node_selected; + } + if (node_hovered_in_scene != Symbol(-1)) { + node_selected = node_hovered_in_scene; + node_hovered_in_list = node_selected; + } } // Draw context menu From 773327d4df4e7af315220e8407d5cd2e702ae265 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Wed, 27 Dec 2023 15:47:56 +0100 Subject: [PATCH 008/142] naming stuff --- symmetri/gui/draw_graph.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index bb90ae4..a400f58 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -108,7 +108,8 @@ void draw_nodes(Node& node) { open_context_menu |= ImGui::IsMouseClicked(1); } bool node_moving_active = ImGui::IsItemActive(); - if (node_widgets_active || node_moving_active) node_selected = node.id; + if (node_widgets_active || node_moving_active) + node_selected = node_hovered_in_list = node.id; if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) node.Pos = node.Pos + ImGui::GetIO().MouseDelta; @@ -175,10 +176,10 @@ void draw(Graph& g) { ImGui::Dummy(ImVec2(0.0f, 20.0f)); ImGui::Text("Selected"); ImGui::Separator(); - if (node_hovered_in_list != Symbol(-1)) { - auto node = std::find_if( - g.nodes.begin(), g.nodes.end(), - [=](const auto& n) { return n.id == node_hovered_in_list; }); + if (node_selected != Symbol(-1)) { + auto node = + std::find_if(g.nodes.begin(), g.nodes.end(), + [=](const auto& n) { return n.id == node_selected; }); static char buf[32] = "hello"; ImGui::Text("Name"); ImGui::SameLine(); @@ -245,12 +246,12 @@ void draw(Graph& g) { if (open_context_menu) { ImGui::OpenPopup("context_menu"); if (node_hovered_in_list != Symbol(-1)) { - node_selected = node_hovered_in_list; - node_hovered_in_scene = node_selected; + // node_selected = node_hovered_in_list; + node_hovered_in_scene = node_hovered_in_list; } if (node_hovered_in_scene != Symbol(-1)) { - node_selected = node_hovered_in_scene; - node_hovered_in_list = node_selected; + // node_selected = node_hovered_in_scene; + node_hovered_in_list = node_hovered_in_scene; } } From 0108dd29a29882a7565b2707fe1e69d5776ab567 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Thu, 28 Dec 2023 17:52:58 +0100 Subject: [PATCH 009/142] olayout + directed arrows --- symmetri/gui/draw_graph.cpp | 173 ++++++++++++++++++++++++------------ symmetri/gui/symbol.cpp | 4 +- 2 files changed, 118 insertions(+), 59 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index a400f58..f65715d 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -1,9 +1,15 @@ #include +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include #include "drawable.h" #include "graph.hpp" #include "imgui.h" +#include "imgui_internal.h" #include "redux.hpp" + // Creating a node graph editor for Dear ImGui // Quick sample, not production code! // This is quick demo I crafted in a few hours in 2015 showcasing how to use @@ -33,14 +39,14 @@ inline ImU32 getColor(symmetri::Token token) { case Color::Canceled: case Color::Paused: case Color::Failed: - return IM_COL32(255, 0, 0, 255); + return IM_COL32(255, 0, 0, 128); break; case Color::Success: - return IM_COL32(0, 255, 0, 255); + return IM_COL32(0, 255, 0, 128); break; default: { // create new color and lookup if it already exists. - return IM_COL32(255, 200, 0, 255); + return IM_COL32(255, 200, 0, 128); } } }; @@ -48,35 +54,53 @@ inline ImU32 getColor(symmetri::Token token) { // State static ImVec2 scrolling = ImVec2(0.0f, 0.0f); static bool show_grid = true; -static Symbol node_selected = -1; +static Symbol node_selected = Symbol(-1); static ImVec2 size; static ImVec2 offset; static ImDrawList* draw_list; static bool open_context_menu = false; -static Symbol node_hovered_in_list = -1; -static int node_hovered_in_scene = -1; +static Symbol node_hovered_in_list = Symbol(-1); +static Symbol node_hovered_in_scene = Symbol(-1); +static Arc* active_arc = nullptr; static const float NODE_SLOT_RADIUS = 4.0f; static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); -// NB: You can use math functions/operators on ImVec2 if you #define -// IMGUI_DEFINE_MATH_OPERATORS and #include "imgui_internal.h" Here we only -// declare simple +/- operators so others don't leak into the demo code. -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); -} - void draw_arc(Arc& arc) { const auto& [color, from_to] = arc; ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); ImVec2 p2 = offset + Node::GetCenterPos(*from_to[1], size); - ImVec2 d = p1 - p2; - d.x *= 0.33; - d.y *= 0.33; + // use with: + using namespace ImGui; ImU32 imcolor = getColor(color); - draw_list->AddLine(p1, p2, imcolor, 2.0f); + const float max_distance = 2.f; + const auto mouse_pos = ImGui::GetIO().MousePos; + ImVec2 mouse_pos_projected_on_segment = ImLineClosestPoint(p1, p2, mouse_pos); + ImVec2 mouse_pos_delta_to_segment = + mouse_pos_projected_on_segment - mouse_pos; + bool is_segment_hovered = + active_arc == &arc || (node_hovered_in_scene == Symbol(-1) && + (ImLengthSqr(mouse_pos_delta_to_segment) <= + max_distance * max_distance)); + + imcolor |= ((ImU32)IM_F32_TO_INT8_SAT(is_segment_hovered ? 1.0f : 0.65f)) + << IM_COL32_A_SHIFT; + + if (is_segment_hovered && ImGui::IsMouseClicked(0)) { + active_arc = &arc; + } + + const auto d = p2 - p1; + const auto theta = std::atan2(d.y, d.x) - M_PI_2; + const float h = draw_list->_Data->FontSize * 1.00f; + const float r = h * 0.40f * 1; + const ImVec2 center = p1 + d / 2.f; + const auto a_sin = std::sin(theta); + const auto a_cos = std::cos(theta); + const auto a = ImRotate(ImVec2(+0.000f, +1.f) * r, a_cos, a_sin); + const auto b = ImRotate(ImVec2(-1.f, -1.f) * r, a_cos, a_sin); + const auto c = ImRotate(ImVec2(+1.f, -1.f) * r, a_cos, a_sin); + draw_list->AddTriangleFilled(center + a, center + b, center + c, imcolor); + draw_list->AddLine(p1, p2, imcolor, is_segment_hovered ? 3.0f : 2.0f); }; void draw_nodes(Node& node) { @@ -103,19 +127,20 @@ void draw_nodes(Node& node) { // Display node box ImGui::SetCursorScreenPos(node_rect_min); ImGui::InvisibleButton("node", size); - if (ImGui::IsItemHovered()) { - node_hovered_in_scene = node.id; - open_context_menu |= ImGui::IsMouseClicked(1); - } + node_hovered_in_scene = ImGui::IsItemHovered() ? node.id + : ImGui::IsAnyItemHovered() ? node_hovered_in_scene + : Symbol(-1); bool node_moving_active = ImGui::IsItemActive(); - if (node_widgets_active || node_moving_active) + if (node_widgets_active || node_moving_active) { node_selected = node_hovered_in_list = node.id; - if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + } + if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { node.Pos = node.Pos + ImGui::GetIO().MouseDelta; - + } const int opacity = 255; - auto select_color = node.id == node_hovered_in_list - ? IM_COL32(255, 255, 0, opacity) + auto select_color = node.id == node_selected ? IM_COL32(255, 255, 0, opacity) + : node.id == node_hovered_in_list + ? IM_COL32(255, 255, 50, 100) : IM_COL32(100, 100, 100, opacity); if (node.id.chr() == 'P') { draw_list->AddCircleFilled(offset + Node::GetCenterPos(node.Pos, size), @@ -138,10 +163,64 @@ void draw_nodes(Node& node) { // Note that we storing links as indices (not ID) to make example code shorter. template <> void draw(Graph& g) { + ImVec2 WindowSize = ImGui::GetWindowSize(); + WindowSize.y -= 140.0f; // Draw a list of nodes on the left side - ImGui::BeginChild("node_list", ImVec2(150, 0)); + ImGui::BeginChild("some", ImVec2(200, 0)); + ImGui::Text("Selected"); + ImGui::Separator(); + ImGui::BeginChild("selected_node", ImVec2(200, 0.1 * WindowSize.y)); + if (node_selected != Symbol(-1)) { + auto node = + std::find_if(g.nodes.begin(), g.nodes.end(), + [=](const auto& n) { return n.id == node_selected; }); + ImGui::Text("Name"); + ImGui::SameLine(); + const auto id = std::string("##") + std::to_string(node->id.key()); + ImGui::PushItemWidth(-1); + ImGui::InputText(id.c_str(), node->name.data(), 30); + ImGui::PopItemWidth(); + static int priority = 1; + static int multiplicity = 1; + if (node->id.chr() == 'T') { + ImGui::Text("Priority"); + ImGui::SameLine(); + ImGui::InputInt("##", &priority); + ImGui::Text("Weight"); + ImGui::SameLine(); + ImGui::InputInt("##", &multiplicity); + } + } + ImGui::Separator(); + if (active_arc != nullptr) { + constexpr std::size_t offset = offsetof(Node, Pos); + const Node* from = reinterpret_cast( + reinterpret_cast(active_arc->from_to_pos[0]) - offset); + const Node* to = reinterpret_cast( + reinterpret_cast(active_arc->from_to_pos[1]) - offset); + + ImGui::Text("From:"); + ImGui::SameLine(); + ImGui::PushItemWidth(-1); + ImGui::Text("%s", from->name.data()); + ImGui::PopItemWidth(); + ImGui::Text("To:"); + ImGui::SameLine(); + ImGui::PushItemWidth(-1); + ImGui::Text("%s", to->name.data()); + ImGui::PopItemWidth(); + ImGui::Text("Color"); + ImGui::SameLine(); + ImGui::Text("%s", symmetri::Color::toString(active_arc->color).c_str()); + } + ImGui::EndChild(); + + ImGui::Dummy(ImVec2(0.0f, 20.0f)); + ImGui::Text("Places"); ImGui::Separator(); + constexpr float height_fraction = 0.8 / 2.0; + ImGui::BeginChild("place_list", ImVec2(200, height_fraction * WindowSize.y)); for (const auto& node : g.nodes) { if (node.id.chr() == 'P') { ImGui::PushID(node.id.key()); @@ -156,10 +235,13 @@ void draw(Graph& g) { ImGui::PopID(); } } + ImGui::EndChild(); ImGui::Dummy(ImVec2(0.0f, 20.0f)); ImGui::Text("Transitions"); ImGui::Separator(); + ImGui::BeginChild("transition_list", + ImVec2(200, height_fraction * WindowSize.y)); for (const auto& node : g.nodes) { if (node.id.chr() == 'T') { ImGui::PushID(node.id.key()); @@ -168,35 +250,12 @@ void draw(Graph& g) { } if (ImGui::IsItemHovered()) { node_hovered_in_list = node.id; - open_context_menu |= ImGui::IsMouseClicked(1); + // open_context_menu |= ImGui::IsMouseClicked(1); } ImGui::PopID(); } } - ImGui::Dummy(ImVec2(0.0f, 20.0f)); - ImGui::Text("Selected"); - ImGui::Separator(); - if (node_selected != Symbol(-1)) { - auto node = - std::find_if(g.nodes.begin(), g.nodes.end(), - [=](const auto& n) { return n.id == node_selected; }); - static char buf[32] = "hello"; - ImGui::Text("Name"); - ImGui::SameLine(); - const auto id = std::to_string(node->id.key()); - ImGui::InputText(id.c_str(), node->name.data(), 30); - static int priority = 1; - static int multiplicity = 1; - if (node->id.chr() == 'T') { - ImGui::Text("Priority"); - ImGui::SameLine(); - ImGui::InputInt("##", &priority); - ImGui::Text("Weight"); - ImGui::SameLine(); - ImGui::InputInt("##", &multiplicity); - } else { - } - } + ImGui::EndChild(); ImGui::EndChild(); @@ -240,14 +299,14 @@ void draw(Graph& g) { if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) || !ImGui::IsAnyItemHovered()) { - node_selected = node_hovered_in_list = node_hovered_in_scene = -1; + node_selected = node_hovered_in_list = node_hovered_in_scene = Symbol(-1); open_context_menu = true; } if (open_context_menu) { ImGui::OpenPopup("context_menu"); if (node_hovered_in_list != Symbol(-1)) { // node_selected = node_hovered_in_list; - node_hovered_in_scene = node_hovered_in_list; + // node_hovered_in_scene = node_hovered_in_list; } if (node_hovered_in_scene != Symbol(-1)) { // node_selected = node_hovered_in_scene; diff --git a/symmetri/gui/symbol.cpp b/symmetri/gui/symbol.cpp index f7641b2..3d803cf 100644 --- a/symmetri/gui/symbol.cpp +++ b/symmetri/gui/symbol.cpp @@ -25,8 +25,8 @@ #include #include -static const size_t keyBits = sizeof(Key) * 4; -static const size_t chrBits = sizeof(unsigned char) * 4; +static const size_t keyBits = sizeof(Key) * 8; +static const size_t chrBits = sizeof(unsigned char) * 8; static const size_t indexBits = keyBits - chrBits; static const Key chrMask = Key(UCHAR_MAX) From 93c65112b8301470286e223b6458596dccc4b781 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sat, 6 Jan 2024 16:01:34 +0100 Subject: [PATCH 010/142] highlight --- symmetri/gui/draw_graph.cpp | 67 +++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index f65715d..a85f08a 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -59,8 +59,10 @@ static ImVec2 size; static ImVec2 offset; static ImDrawList* draw_list; static bool open_context_menu = false; -static Symbol node_hovered_in_list = Symbol(-1); -static Symbol node_hovered_in_scene = Symbol(-1); +// static Symbol node_hovered_in_list = Symbol(-1); +// static Symbol node_hovered_in_scene = Symbol(-1); +static Node* node_hovered_in_list = nullptr; +static Node* node_hovered_in_scene = nullptr; static Arc* active_arc = nullptr; static const float NODE_SLOT_RADIUS = 4.0f; static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); @@ -78,9 +80,8 @@ void draw_arc(Arc& arc) { ImVec2 mouse_pos_delta_to_segment = mouse_pos_projected_on_segment - mouse_pos; bool is_segment_hovered = - active_arc == &arc || (node_hovered_in_scene == Symbol(-1) && - (ImLengthSqr(mouse_pos_delta_to_segment) <= - max_distance * max_distance)); + active_arc == &arc || + (ImLengthSqr(mouse_pos_delta_to_segment) <= max_distance * max_distance); imcolor |= ((ImU32)IM_F32_TO_INT8_SAT(is_segment_hovered ? 1.0f : 0.65f)) << IM_COL32_A_SHIFT; @@ -127,21 +128,22 @@ void draw_nodes(Node& node) { // Display node box ImGui::SetCursorScreenPos(node_rect_min); ImGui::InvisibleButton("node", size); - node_hovered_in_scene = ImGui::IsItemHovered() ? node.id + node_hovered_in_scene = ImGui::IsItemHovered() ? &node : ImGui::IsAnyItemHovered() ? node_hovered_in_scene - : Symbol(-1); + : nullptr; bool node_moving_active = ImGui::IsItemActive(); if (node_widgets_active || node_moving_active) { - node_selected = node_hovered_in_list = node.id; + node_selected = node.id; } if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { node.Pos = node.Pos + ImGui::GetIO().MouseDelta; } const int opacity = 255; - auto select_color = node.id == node_selected ? IM_COL32(255, 255, 0, opacity) - : node.id == node_hovered_in_list - ? IM_COL32(255, 255, 50, 100) - : IM_COL32(100, 100, 100, opacity); + auto select_color = + node.id == node_selected ? IM_COL32(255, 255, 0, opacity) + : &node == node_hovered_in_list || &node == node_hovered_in_scene + ? IM_COL32(255, 255, 50, 100) + : IM_COL32(100, 100, 100, opacity); if (node.id.chr() == 'P') { draw_list->AddCircleFilled(offset + Node::GetCenterPos(node.Pos, size), 0.5f * size.x, IM_COL32(135, 135, 135, opacity), @@ -221,14 +223,18 @@ void draw(Graph& g) { ImGui::Separator(); constexpr float height_fraction = 0.8 / 2.0; ImGui::BeginChild("place_list", ImVec2(200, height_fraction * WindowSize.y)); - for (const auto& node : g.nodes) { + for (auto& node : g.nodes) { if (node.id.chr() == 'P') { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { node_selected = node.id; } - if (ImGui::IsItemHovered()) { - node_hovered_in_list = node.id; + node_hovered_in_list = ImGui::IsItemHovered() ? &node + : ImGui::IsAnyItemHovered() ? node_hovered_in_list + : nullptr; + + if (node_hovered_in_list) { + // node_hovered_in_list = node.id; // node_hovered_in_list = node.id; open_context_menu |= ImGui::IsMouseClicked(1); } @@ -242,16 +248,19 @@ void draw(Graph& g) { ImGui::Separator(); ImGui::BeginChild("transition_list", ImVec2(200, height_fraction * WindowSize.y)); - for (const auto& node : g.nodes) { + for (auto& node : g.nodes) { if (node.id.chr() == 'T') { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { node_selected = node.id; } - if (ImGui::IsItemHovered()) { - node_hovered_in_list = node.id; - // open_context_menu |= ImGui::IsMouseClicked(1); - } + node_hovered_in_list = ImGui::IsItemHovered() ? &node + : ImGui::IsAnyItemHovered() ? node_hovered_in_list + : nullptr; + // if (ImGui::IsItemHovered()) { + // node_hovered_in_list = node.id; + // open_context_menu |= ImGui::IsMouseClicked(1); + // } ImGui::PopID(); } } @@ -299,19 +308,19 @@ void draw(Graph& g) { if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) || !ImGui::IsAnyItemHovered()) { - node_selected = node_hovered_in_list = node_hovered_in_scene = Symbol(-1); + node_selected = Symbol(-1); open_context_menu = true; } if (open_context_menu) { ImGui::OpenPopup("context_menu"); - if (node_hovered_in_list != Symbol(-1)) { - // node_selected = node_hovered_in_list; - // node_hovered_in_scene = node_hovered_in_list; - } - if (node_hovered_in_scene != Symbol(-1)) { - // node_selected = node_hovered_in_scene; - node_hovered_in_list = node_hovered_in_scene; - } + // if (node_hovered_in_list != Symbol(-1)) { + // node_selected = node_hovered_in_list; + // node_hovered_in_scene = node_hovered_in_list; + // } + // if (node_hovered_in_scene != Symbol(-1)) { + // // node_selected = node_hovered_in_scene; + // node_hovered_in_list = node_hovered_in_scene; + // } } // Draw context menu From 920575e7b5eabe099ddc62bbfd1cc280986ff4c2 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sat, 6 Jan 2024 16:08:34 +0100 Subject: [PATCH 011/142] highlight --- symmetri/gui/draw_graph.cpp | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index a85f08a..b60a5f1 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -54,13 +54,11 @@ inline ImU32 getColor(symmetri::Token token) { // State static ImVec2 scrolling = ImVec2(0.0f, 0.0f); static bool show_grid = true; -static Symbol node_selected = Symbol(-1); static ImVec2 size; static ImVec2 offset; static ImDrawList* draw_list; static bool open_context_menu = false; -// static Symbol node_hovered_in_list = Symbol(-1); -// static Symbol node_hovered_in_scene = Symbol(-1); +static Node* node_selected = nullptr; static Node* node_hovered_in_list = nullptr; static Node* node_hovered_in_scene = nullptr; static Arc* active_arc = nullptr; @@ -81,7 +79,8 @@ void draw_arc(Arc& arc) { mouse_pos_projected_on_segment - mouse_pos; bool is_segment_hovered = active_arc == &arc || - (ImLengthSqr(mouse_pos_delta_to_segment) <= max_distance * max_distance); + (ImLengthSqr(mouse_pos_delta_to_segment) <= max_distance * max_distance && + node_hovered_in_scene == nullptr); imcolor |= ((ImU32)IM_F32_TO_INT8_SAT(is_segment_hovered ? 1.0f : 0.65f)) << IM_COL32_A_SHIFT; @@ -133,14 +132,14 @@ void draw_nodes(Node& node) { : nullptr; bool node_moving_active = ImGui::IsItemActive(); if (node_widgets_active || node_moving_active) { - node_selected = node.id; + node_selected = &node; } if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { node.Pos = node.Pos + ImGui::GetIO().MouseDelta; } const int opacity = 255; auto select_color = - node.id == node_selected ? IM_COL32(255, 255, 0, opacity) + &node == node_selected ? IM_COL32(255, 255, 0, opacity) : &node == node_hovered_in_list || &node == node_hovered_in_scene ? IM_COL32(255, 255, 50, 100) : IM_COL32(100, 100, 100, opacity); @@ -172,10 +171,10 @@ void draw(Graph& g) { ImGui::Text("Selected"); ImGui::Separator(); ImGui::BeginChild("selected_node", ImVec2(200, 0.1 * WindowSize.y)); - if (node_selected != Symbol(-1)) { + if (node_selected) { auto node = std::find_if(g.nodes.begin(), g.nodes.end(), - [=](const auto& n) { return n.id == node_selected; }); + [=](const auto& n) { return n.id == node_selected->id; }); ImGui::Text("Name"); ImGui::SameLine(); const auto id = std::string("##") + std::to_string(node->id.key()); @@ -226,16 +225,14 @@ void draw(Graph& g) { for (auto& node : g.nodes) { if (node.id.chr() == 'P') { ImGui::PushID(node.id.key()); - if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { - node_selected = node.id; + if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { + node_selected = &node; } node_hovered_in_list = ImGui::IsItemHovered() ? &node : ImGui::IsAnyItemHovered() ? node_hovered_in_list : nullptr; if (node_hovered_in_list) { - // node_hovered_in_list = node.id; - // node_hovered_in_list = node.id; open_context_menu |= ImGui::IsMouseClicked(1); } ImGui::PopID(); @@ -251,8 +248,8 @@ void draw(Graph& g) { for (auto& node : g.nodes) { if (node.id.chr() == 'T') { ImGui::PushID(node.id.key()); - if (ImGui::Selectable(node.name.c_str(), node.id == node_selected)) { - node_selected = node.id; + if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { + node_selected = &node; } node_hovered_in_list = ImGui::IsItemHovered() ? &node : ImGui::IsAnyItemHovered() ? node_hovered_in_list @@ -308,7 +305,7 @@ void draw(Graph& g) { if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) || !ImGui::IsAnyItemHovered()) { - node_selected = Symbol(-1); + node_selected = nullptr; open_context_menu = true; } if (open_context_menu) { @@ -328,7 +325,7 @@ void draw(Graph& g) { if (ImGui::BeginPopup("context_menu")) { auto node = std::find_if(g.nodes.begin(), g.nodes.end(), - [=](const auto& n) { return n.id == node_selected; }); + [=](const auto& n) { return n.id == node_selected->id; }); ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; if (node != std::end(g.nodes)) { ImGui::Text("Node '%s'", node->name.c_str()); From d993aff07f503e00ace9e804f98b6d0bb91d668d Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sat, 6 Jan 2024 16:28:47 +0100 Subject: [PATCH 012/142] some const --- symmetri/gui/draw_graph.cpp | 44 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index b60a5f1..0e2dd4b 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -30,6 +30,16 @@ #include "symmetri/colors.hpp" +template +bool isSelected(const T& n, const T* selected) { + return const_cast(&n) == selected; +} + +template +const T* assignSelected(const T& n) { + return const_cast(&n); +} + inline ImU32 getColor(symmetri::Token token) { using namespace symmetri; switch (token) { @@ -58,14 +68,15 @@ static ImVec2 size; static ImVec2 offset; static ImDrawList* draw_list; static bool open_context_menu = false; -static Node* node_selected = nullptr; -static Node* node_hovered_in_list = nullptr; -static Node* node_hovered_in_scene = nullptr; -static Arc* active_arc = nullptr; +static const Node* node_selected = nullptr; +static const Node* node_hovered_in_list = nullptr; +static const Node* node_hovered_in_scene = nullptr; +static const Arc* active_arc = nullptr; + static const float NODE_SLOT_RADIUS = 4.0f; static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); -void draw_arc(Arc& arc) { +void draw_arc(const Arc& arc) { const auto& [color, from_to] = arc; ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); ImVec2 p2 = offset + Node::GetCenterPos(*from_to[1], size); @@ -78,7 +89,7 @@ void draw_arc(Arc& arc) { ImVec2 mouse_pos_delta_to_segment = mouse_pos_projected_on_segment - mouse_pos; bool is_segment_hovered = - active_arc == &arc || + active_arc == assignSelected(arc) || (ImLengthSqr(mouse_pos_delta_to_segment) <= max_distance * max_distance && node_hovered_in_scene == nullptr); @@ -86,7 +97,7 @@ void draw_arc(Arc& arc) { << IM_COL32_A_SHIFT; if (is_segment_hovered && ImGui::IsMouseClicked(0)) { - active_arc = &arc; + active_arc = assignSelected(arc); } const auto d = p2 - p1; @@ -222,13 +233,13 @@ void draw(Graph& g) { ImGui::Separator(); constexpr float height_fraction = 0.8 / 2.0; ImGui::BeginChild("place_list", ImVec2(200, height_fraction * WindowSize.y)); - for (auto& node : g.nodes) { + for (const auto& node : g.nodes) { if (node.id.chr() == 'P') { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { - node_selected = &node; + node_selected = assignSelected(node); } - node_hovered_in_list = ImGui::IsItemHovered() ? &node + node_hovered_in_list = ImGui::IsItemHovered() ? assignSelected(node) : ImGui::IsAnyItemHovered() ? node_hovered_in_list : nullptr; @@ -245,13 +256,13 @@ void draw(Graph& g) { ImGui::Separator(); ImGui::BeginChild("transition_list", ImVec2(200, height_fraction * WindowSize.y)); - for (auto& node : g.nodes) { + for (const auto& node : g.nodes) { if (node.id.chr() == 'T') { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { - node_selected = &node; + node_selected = assignSelected(node); } - node_hovered_in_list = ImGui::IsItemHovered() ? &node + node_hovered_in_list = ImGui::IsItemHovered() ? assignSelected(node) : ImGui::IsAnyItemHovered() ? node_hovered_in_list : nullptr; // if (ImGui::IsItemHovered()) { @@ -324,8 +335,11 @@ void draw(Graph& g) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); if (ImGui::BeginPopup("context_menu")) { auto node = - std::find_if(g.nodes.begin(), g.nodes.end(), - [=](const auto& n) { return n.id == node_selected->id; }); + std::find_if(g.nodes.begin(), g.nodes.end(), [=](const auto& n) { + return isSelected(n, node_selected); + // return const_cast(&n) == node_selected; + // return node_selected && n.id == node_selected->id; + }); ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; if (node != std::end(g.nodes)) { ImGui::Text("Node '%s'", node->name.c_str()); From b7b1299ea56bfad2d21df393354b947bfed03aa4 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sat, 6 Jan 2024 16:33:22 +0100 Subject: [PATCH 013/142] some const --- symmetri/gui/draw_graph.cpp | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 0e2dd4b..0661d1e 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -265,10 +265,9 @@ void draw(Graph& g) { node_hovered_in_list = ImGui::IsItemHovered() ? assignSelected(node) : ImGui::IsAnyItemHovered() ? node_hovered_in_list : nullptr; - // if (ImGui::IsItemHovered()) { - // node_hovered_in_list = node.id; - // open_context_menu |= ImGui::IsMouseClicked(1); - // } + if (node_hovered_in_list) { + open_context_menu |= ImGui::IsMouseClicked(1); + } ImGui::PopID(); } } @@ -317,29 +316,20 @@ void draw(Graph& g) { if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) || !ImGui::IsAnyItemHovered()) { node_selected = nullptr; + active_arc = nullptr; open_context_menu = true; } + if (open_context_menu) { ImGui::OpenPopup("context_menu"); - // if (node_hovered_in_list != Symbol(-1)) { - // node_selected = node_hovered_in_list; - // node_hovered_in_scene = node_hovered_in_list; - // } - // if (node_hovered_in_scene != Symbol(-1)) { - // // node_selected = node_hovered_in_scene; - // node_hovered_in_list = node_hovered_in_scene; - // } } // Draw context menu ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); if (ImGui::BeginPopup("context_menu")) { - auto node = - std::find_if(g.nodes.begin(), g.nodes.end(), [=](const auto& n) { - return isSelected(n, node_selected); - // return const_cast(&n) == node_selected; - // return node_selected && n.id == node_selected->id; - }); + auto node = std::find_if( + g.nodes.begin(), g.nodes.end(), + [=](const auto& n) { return isSelected(n, node_selected); }); ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; if (node != std::end(g.nodes)) { ImGui::Text("Node '%s'", node->name.c_str()); From 32ff3b43cf4e85d61adbefc71b48a4eeeb013a00 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sat, 6 Jan 2024 17:37:38 +0100 Subject: [PATCH 014/142] some view --- symmetri/gui/draw_graph.cpp | 130 +++++++++++++++--------------------- symmetri/gui/graph.hpp | 6 +- symmetri/gui/redux.hpp | 2 + 3 files changed, 60 insertions(+), 78 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 0661d1e..dd592d2 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -1,44 +1,32 @@ -#include #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif +#include // fmodf + #include +#include #include "drawable.h" #include "graph.hpp" #include "imgui.h" #include "imgui_internal.h" #include "redux.hpp" - -// Creating a node graph editor for Dear ImGui -// Quick sample, not production code! -// This is quick demo I crafted in a few hours in 2015 showcasing how to use -// Dear ImGui to create custom stuff, which ended up feeding a thread full of -// better experiments. See https://github.com/ocornut/imgui/issues/306 for -// details - -// Fast forward to 2023, see e.g. -// https://github.com/ocornut/imgui/wiki/Useful-Extensions#node-editors - -// Changelog -// - v0.05 (2023-03): fixed for renamed api: AddBezierCurve()->AddBezierCubic(). -// - v0.04 (2020-03): minor tweaks -// - v0.03 (2018-03): fixed grid offset issue, inverted sign of 'scrolling' -// - v0.01 (2015-08): initial version - -#include // fmodf - #include "symmetri/colors.hpp" -template -bool isSelected(const T& n, const T* selected) { - return const_cast(&n) == selected; -} +// State +static ImVec2 scrolling = ImVec2(0.0f, 0.0f); +static bool show_grid = true; +static ImVec2 size; +static ImVec2 offset; +static ImDrawList* draw_list; +static bool open_context_menu = false; +static const Node* node_selected = nullptr; +static const Node* node_hovered_in_list = nullptr; +static const Node* node_hovered_in_scene = nullptr; +static const Arc* active_arc = nullptr; -template -const T* assignSelected(const T& n) { - return const_cast(&n); -} +static const float NODE_SLOT_RADIUS = 4.0f; +static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); inline ImU32 getColor(symmetri::Token token) { using namespace symmetri; @@ -61,21 +49,6 @@ inline ImU32 getColor(symmetri::Token token) { } }; -// State -static ImVec2 scrolling = ImVec2(0.0f, 0.0f); -static bool show_grid = true; -static ImVec2 size; -static ImVec2 offset; -static ImDrawList* draw_list; -static bool open_context_menu = false; -static const Node* node_selected = nullptr; -static const Node* node_hovered_in_list = nullptr; -static const Node* node_hovered_in_scene = nullptr; -static const Arc* active_arc = nullptr; - -static const float NODE_SLOT_RADIUS = 4.0f; -static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); - void draw_arc(const Arc& arc) { const auto& [color, from_to] = arc; ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); @@ -89,7 +62,7 @@ void draw_arc(const Arc& arc) { ImVec2 mouse_pos_delta_to_segment = mouse_pos_projected_on_segment - mouse_pos; bool is_segment_hovered = - active_arc == assignSelected(arc) || + active_arc == &arc || (ImLengthSqr(mouse_pos_delta_to_segment) <= max_distance * max_distance && node_hovered_in_scene == nullptr); @@ -97,7 +70,7 @@ void draw_arc(const Arc& arc) { << IM_COL32_A_SHIFT; if (is_segment_hovered && ImGui::IsMouseClicked(0)) { - active_arc = assignSelected(arc); + active_arc = &arc; } const auto d = p2 - p1; @@ -233,19 +206,17 @@ void draw(Graph& g) { ImGui::Separator(); constexpr float height_fraction = 0.8 / 2.0; ImGui::BeginChild("place_list", ImVec2(200, height_fraction * WindowSize.y)); - for (const auto& node : g.nodes) { + for (auto idx : g.n_idx) { + const auto& node = g.nodes[idx]; if (node.id.chr() == 'P') { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { - node_selected = assignSelected(node); + node_selected = &node; } - node_hovered_in_list = ImGui::IsItemHovered() ? assignSelected(node) + node_hovered_in_list = ImGui::IsItemHovered() ? &node : ImGui::IsAnyItemHovered() ? node_hovered_in_list : nullptr; - if (node_hovered_in_list) { - open_context_menu |= ImGui::IsMouseClicked(1); - } ImGui::PopID(); } } @@ -256,18 +227,16 @@ void draw(Graph& g) { ImGui::Separator(); ImGui::BeginChild("transition_list", ImVec2(200, height_fraction * WindowSize.y)); - for (const auto& node : g.nodes) { + for (auto idx : g.n_idx) { + const auto& node = g.nodes[idx]; if (node.id.chr() == 'T') { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { - node_selected = assignSelected(node); + node_selected = &node; } - node_hovered_in_list = ImGui::IsItemHovered() ? assignSelected(node) + node_hovered_in_list = ImGui::IsItemHovered() ? &node : ImGui::IsAnyItemHovered() ? node_hovered_in_list : nullptr; - if (node_hovered_in_list) { - open_context_menu |= ImGui::IsMouseClicked(1); - } ImGui::PopID(); } } @@ -308,17 +277,28 @@ void draw(Graph& g) { ImVec2(canvas_sz.x, y) + win_pos, GRID_COLOR); } - std::for_each(g.arcs.begin(), g.arcs.end(), &draw_arc); - std::for_each(g.nodes.begin(), g.nodes.end(), &draw_nodes); + for (auto idx : g.a_idx) { + draw_arc(g.arcs[idx]); + } + + for (auto idx : g.n_idx) { + draw_nodes(g.nodes[idx]); + } + // std::for_each(g.arcs.begin(), g.arcs.end(), &draw_arc); + // std::for_each(g.nodes.begin(), g.nodes.end(), &draw_nodes); // Open context menu - if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) + if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) || !ImGui::IsAnyItemHovered()) { - node_selected = nullptr; + node_selected = node_hovered_in_scene; active_arc = nullptr; open_context_menu = true; + } else { + node_selected = nullptr; + active_arc = nullptr; } + } if (open_context_menu) { ImGui::OpenPopup("context_menu"); @@ -327,22 +307,16 @@ void draw(Graph& g) { // Draw context menu ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); if (ImGui::BeginPopup("context_menu")) { - auto node = std::find_if( - g.nodes.begin(), g.nodes.end(), - [=](const auto& n) { return isSelected(n, node_selected); }); - ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; - if (node != std::end(g.nodes)) { - ImGui::Text("Node '%s'", node->name.c_str()); + if (node_selected) { + ImGui::Text("Node '%s'", node_selected->name.c_str()); ImGui::Separator(); - if (ImGui::MenuItem("Rename..", NULL, false, false)) { - } if (ImGui::MenuItem("Delete", NULL, false, false)) { } - if (ImGui::MenuItem("Copy", NULL, false, false)) { - } } else { + ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; if (ImGui::MenuItem("Add place")) { MVC::push([&, scene_pos](Model&& m) { + g.n_idx.push_back(g.nodes.size()); g.nodes.push_back( Node{"New node", Symbol('P', g.nodes.size()), scene_pos}); return m; @@ -350,6 +324,7 @@ void draw(Graph& g) { } if (ImGui::MenuItem("Add transition")) { MVC::push([&, scene_pos](Model&& m) { + g.n_idx.push_back(g.nodes.size()); g.nodes.push_back( Node{"New node", Symbol('T', g.nodes.size()), scene_pos}); return m; @@ -361,8 +336,12 @@ void draw(Graph& g) { for (const auto& to : g.nodes) { if (from.id.chr() != to.id.chr() && ImGui::MenuItem(to.name.c_str())) { - g.arcs.push_back( - Arc{symmetri::Color::Success, &from.Pos, &to.Pos}); + MVC::push([&](Model&& m) { + g.a_idx.push_back(g.arcs.size()); + g.arcs.push_back( + Arc{symmetri::Color::Success, &from.Pos, &to.Pos}); + return m; + }); } } ImGui::EndMenu(); @@ -370,9 +349,6 @@ void draw(Graph& g) { } ImGui::EndMenu(); } - - if (ImGui::MenuItem("Paste", NULL, false, false)) { - } } ImGui::EndPopup(); } @@ -380,7 +356,7 @@ void draw(Graph& g) { // Scrolling if (ImGui::IsWindowHovered() && !ImGui::IsAnyItemActive() && - ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 0.0f)) { + ImGui::IsMouseDragging(ImGuiMouseButton_Right, 0.0f)) { scrolling = scrolling + ImGui::GetIO().MouseDelta; } diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index 5d80aac..6e0b048 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -46,6 +46,7 @@ struct Arc { struct Graph { std::vector arcs; std::vector nodes; + std::vector a_idx, n_idx; }; inline Graph createGraph(const symmetri::Net net) { @@ -131,5 +132,8 @@ inline Graph createGraph(const symmetri::Net net) { nodes[i].Pos = ImVec2(GA.x(ogdf_nodes[i]), 2 * GA.y(ogdf_nodes[i])); } - return {std::move(arcs), std::move(nodes)}; + std::vector v(arcs.size()), w(nodes.size()); + std::iota(v.begin(), v.end(), 0); + std::iota(w.begin(), w.end(), 0); + return {std::move(arcs), std::move(nodes), std::move(v), std::move(w)}; } diff --git a/symmetri/gui/redux.hpp b/symmetri/gui/redux.hpp index 9b89774..cbc2bff 100644 --- a/symmetri/gui/redux.hpp +++ b/symmetri/gui/redux.hpp @@ -30,6 +30,8 @@ inline View& current(History& x) { return x.back(); } +struct ViewModel {}; + struct Model { std::filesystem::path working_dir; std::optional active_file; From 489c3884e7c928b5a783d71ec7a24f091f7923bb Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sun, 7 Jan 2024 17:27:36 +0100 Subject: [PATCH 015/142] remove stuff --- symmetri/gui/draw_graph.cpp | 32 +- symmetri/gui/extensions/crude_json.cpp | 811 --- symmetri/gui/extensions/crude_json.h | 458 -- symmetri/gui/extensions/imgui_bezier_math.h | 194 - symmetri/gui/extensions/imgui_bezier_math.inl | 675 --- symmetri/gui/extensions/imgui_canvas.cpp | 575 -- symmetri/gui/extensions/imgui_canvas.h | 272 - symmetri/gui/extensions/imgui_extra_math.h | 67 - symmetri/gui/extensions/imgui_extra_math.inl | 191 - symmetri/gui/extensions/imgui_node_editor.cpp | 5368 ----------------- symmetri/gui/extensions/imgui_node_editor.h | 524 -- .../gui/extensions/imgui_node_editor_api.cpp | 596 -- .../extensions/imgui_node_editor_internal.h | 1488 ----- .../extensions/imgui_node_editor_internal.inl | 65 - 14 files changed, 17 insertions(+), 11299 deletions(-) delete mode 100644 symmetri/gui/extensions/crude_json.cpp delete mode 100644 symmetri/gui/extensions/crude_json.h delete mode 100644 symmetri/gui/extensions/imgui_bezier_math.h delete mode 100644 symmetri/gui/extensions/imgui_bezier_math.inl delete mode 100644 symmetri/gui/extensions/imgui_canvas.cpp delete mode 100644 symmetri/gui/extensions/imgui_canvas.h delete mode 100644 symmetri/gui/extensions/imgui_extra_math.h delete mode 100644 symmetri/gui/extensions/imgui_extra_math.inl delete mode 100644 symmetri/gui/extensions/imgui_node_editor.cpp delete mode 100644 symmetri/gui/extensions/imgui_node_editor.h delete mode 100644 symmetri/gui/extensions/imgui_node_editor_api.cpp delete mode 100644 symmetri/gui/extensions/imgui_node_editor_internal.h delete mode 100644 symmetri/gui/extensions/imgui_node_editor_internal.inl diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index dd592d2..a5f3f8c 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -49,6 +49,19 @@ inline ImU32 getColor(symmetri::Token token) { } }; +void draw_grid() { + ImU32 GRID_COLOR = IM_COL32(200, 200, 200, 40); + float GRID_SZ = 64.0f; + ImVec2 win_pos = ImGui::GetCursorScreenPos(); + ImVec2 canvas_sz = ImGui::GetWindowSize(); + for (float x = fmodf(scrolling.x, GRID_SZ); x < canvas_sz.x; x += GRID_SZ) + draw_list->AddLine(ImVec2(x, 0.0f) + win_pos, + ImVec2(x, canvas_sz.y) + win_pos, GRID_COLOR); + for (float y = fmodf(scrolling.y, GRID_SZ); y < canvas_sz.y; y += GRID_SZ) + draw_list->AddLine(ImVec2(0.0f, y) + win_pos, + ImVec2(canvas_sz.x, y) + win_pos, GRID_COLOR); +}; + void draw_arc(const Arc& arc) { const auto& [color, from_to] = arc; ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); @@ -241,16 +254,14 @@ void draw(Graph& g) { } } ImGui::EndChild(); - ImGui::EndChild(); - ImGui::SameLine(); ImGui::BeginGroup(); // Create our child canvas ImGui::Text("Hold middle mouse button to scroll (%.2f,%.2f)", scrolling.x, scrolling.y); - ImGui::SameLine(ImGui::GetWindowWidth() - 100); + ImGui::SameLine(ImGui::GetWindowWidth() - 440); ImGui::Checkbox("Show grid", &show_grid); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); @@ -265,27 +276,18 @@ void draw(Graph& g) { // Display grid if (show_grid) { - ImU32 GRID_COLOR = IM_COL32(200, 200, 200, 40); - float GRID_SZ = 64.0f; - ImVec2 win_pos = ImGui::GetCursorScreenPos(); - ImVec2 canvas_sz = ImGui::GetWindowSize(); - for (float x = fmodf(scrolling.x, GRID_SZ); x < canvas_sz.x; x += GRID_SZ) - draw_list->AddLine(ImVec2(x, 0.0f) + win_pos, - ImVec2(x, canvas_sz.y) + win_pos, GRID_COLOR); - for (float y = fmodf(scrolling.y, GRID_SZ); y < canvas_sz.y; y += GRID_SZ) - draw_list->AddLine(ImVec2(0.0f, y) + win_pos, - ImVec2(canvas_sz.x, y) + win_pos, GRID_COLOR); + draw_grid(); } + // draw arcs for (auto idx : g.a_idx) { draw_arc(g.arcs[idx]); } + // draw places & transitions for (auto idx : g.n_idx) { draw_nodes(g.nodes[idx]); } - // std::for_each(g.arcs.begin(), g.arcs.end(), &draw_arc); - // std::for_each(g.nodes.begin(), g.nodes.end(), &draw_nodes); // Open context menu if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { diff --git a/symmetri/gui/extensions/crude_json.cpp b/symmetri/gui/extensions/crude_json.cpp deleted file mode 100644 index ab43354..0000000 --- a/symmetri/gui/extensions/crude_json.cpp +++ /dev/null @@ -1,811 +0,0 @@ -// Crude implementation of JSON value object and parser. -// -// VERSION 0.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -#include "crude_json.h" - -#include -#include -#include -#include -#include -#include -#if CRUDE_JSON_IO -#include - -#include -#endif - -namespace crude_json { - -value::value(value&& other) : m_Type(other.m_Type) { - switch (m_Type) { - case type_t::object: - construct(m_Storage, std::move(*object_ptr(other.m_Storage))); - break; - case type_t::array: - construct(m_Storage, std::move(*array_ptr(other.m_Storage))); - break; - case type_t::string: - construct(m_Storage, std::move(*string_ptr(other.m_Storage))); - break; - case type_t::boolean: - construct(m_Storage, std::move(*boolean_ptr(other.m_Storage))); - break; - case type_t::number: - construct(m_Storage, std::move(*number_ptr(other.m_Storage))); - break; - default: - break; - } - destruct(other.m_Storage, other.m_Type); - other.m_Type = type_t::null; -} - -value::value(const value& other) : m_Type(other.m_Type) { - switch (m_Type) { - case type_t::object: - construct(m_Storage, *object_ptr(other.m_Storage)); - break; - case type_t::array: - construct(m_Storage, *array_ptr(other.m_Storage)); - break; - case type_t::string: - construct(m_Storage, *string_ptr(other.m_Storage)); - break; - case type_t::boolean: - construct(m_Storage, *boolean_ptr(other.m_Storage)); - break; - case type_t::number: - construct(m_Storage, *number_ptr(other.m_Storage)); - break; - default: - break; - } -} - -value& value::operator[](size_t index) { - if (is_null()) m_Type = construct(m_Storage, type_t::array); - - if (is_array()) { - auto& v = *array_ptr(m_Storage); - if (index >= v.size()) v.insert(v.end(), index - v.size() + 1, value()); - - return v[index]; - } - - CRUDE_ASSERT(false && "operator[] on unsupported type"); - std::terminate(); -} - -const value& value::operator[](size_t index) const { - if (is_array()) return (*array_ptr(m_Storage))[index]; - - CRUDE_ASSERT(false && "operator[] on unsupported type"); - std::terminate(); -} - -value& value::operator[](const string& key) { - if (is_null()) m_Type = construct(m_Storage, type_t::object); - - if (is_object()) return (*object_ptr(m_Storage))[key]; - - CRUDE_ASSERT(false && "operator[] on unsupported type"); - std::terminate(); -} - -const value& value::operator[](const string& key) const { - if (is_object()) { - auto& o = *object_ptr(m_Storage); - auto it = o.find(key); - CRUDE_ASSERT(it != o.end()); - return it->second; - } - - CRUDE_ASSERT(false && "operator[] on unsupported type"); - std::terminate(); -} - -bool value::contains(const string& key) const { - if (is_object()) { - auto& o = *object_ptr(m_Storage); - auto it = o.find(key); - return it != o.end(); - } - - return false; -} - -void value::push_back(const value& value) { - if (is_null()) m_Type = construct(m_Storage, type_t::array); - - if (is_array()) { - auto& v = *array_ptr(m_Storage); - v.push_back(value); - } else { - CRUDE_ASSERT(false && "operator[] on unsupported type"); - std::terminate(); - } -} - -void value::push_back(value&& value) { - if (is_null()) m_Type = construct(m_Storage, type_t::array); - - if (is_array()) { - auto& v = *array_ptr(m_Storage); - v.push_back(std::move(value)); - } else { - CRUDE_ASSERT(false && "operator[] on unsupported type"); - std::terminate(); - } -} - -size_t value::erase(const string& key) { - if (!is_object()) return 0; - - auto& o = *object_ptr(m_Storage); - auto it = o.find(key); - - if (it == o.end()) return 0; - - o.erase(it); - - return 1; -} - -void value::swap(value& other) { - using std::swap; - - if (m_Type == other.m_Type) { - switch (m_Type) { - case type_t::object: - swap(*object_ptr(m_Storage), *object_ptr(other.m_Storage)); - break; - case type_t::array: - swap(*array_ptr(m_Storage), *array_ptr(other.m_Storage)); - break; - case type_t::string: - swap(*string_ptr(m_Storage), *string_ptr(other.m_Storage)); - break; - case type_t::boolean: - swap(*boolean_ptr(m_Storage), *boolean_ptr(other.m_Storage)); - break; - case type_t::number: - swap(*number_ptr(m_Storage), *number_ptr(other.m_Storage)); - break; - default: - break; - } - } else { - value tmp(std::move(other)); - other.~value(); - new (&other) value(std::move(*this)); - this->~value(); - new (this) value(std::move(tmp)); - } -} - -string value::dump(const int indent, const char indent_char) const { - dump_context_t context(indent, indent_char); - - context.out.precision(std::numeric_limits::max_digits10 + 1); - context.out << std::defaultfloat; - - dump(context, 0); - return context.out.str(); -} - -void value::dump_context_t::write_indent(int level) { - if (indent <= 0 || level == 0) return; - - out.fill(indent_char); - out.width(indent * level); - out << indent_char; - out.width(0); -} - -void value::dump_context_t::write_separator() { - if (indent < 0) return; - - out.put(' '); -} - -void value::dump_context_t::write_newline() { - if (indent < 0) return; - - out.put('\n'); -} - -void value::dump(dump_context_t& context, int level) const { - context.write_indent(level); - - switch (m_Type) { - case type_t::null: - context.out << "null"; - break; - - case type_t::object: - context.out << '{'; - { - context.write_newline(); - bool first = true; - for (auto& entry : *object_ptr(m_Storage)) { - if (!first) { - context.out << ','; - context.write_newline(); - } else - first = false; - context.write_indent(level + 1); - context.out << '\"' << entry.first << "\":"; - if (!entry.second.is_structured()) { - context.write_separator(); - entry.second.dump(context, 0); - } else { - context.write_newline(); - entry.second.dump(context, level + 1); - } - } - if (!first) context.write_newline(); - } - context.write_indent(level); - context.out << '}'; - break; - - case type_t::array: - context.out << '['; - { - context.write_newline(); - bool first = true; - for (auto& entry : *array_ptr(m_Storage)) { - if (!first) { - context.out << ','; - context.write_newline(); - } else - first = false; - if (!entry.is_structured()) { - context.write_indent(level + 1); - entry.dump(context, 0); - } else { - entry.dump(context, level + 1); - } - } - if (!first) context.write_newline(); - } - context.write_indent(level); - context.out << ']'; - break; - - case type_t::string: - context.out << '\"'; - - if (string_ptr(m_Storage)->find_first_of("\"\\/\b\f\n\r") != - string::npos || - string_ptr(m_Storage)->find('\0') != string::npos) { - for (auto c : *string_ptr(m_Storage)) { - if (c == '\"') - context.out << "\\\""; - else if (c == '\\') - context.out << "\\\\"; - else if (c == '/') - context.out << "\\/"; - else if (c == '\b') - context.out << "\\b"; - else if (c == '\f') - context.out << "\\f"; - else if (c == '\n') - context.out << "\\n"; - else if (c == '\r') - context.out << "\\r"; - else if (c == '\t') - context.out << "\\t"; - else if (c == 0) - context.out << "\\u0000"; - else - context.out << c; - } - } else - context.out << *string_ptr(m_Storage); - context.out << '\"'; - break; - - case type_t::boolean: - if (*boolean_ptr(m_Storage)) - context.out << "true"; - else - context.out << "false"; - break; - - case type_t::number: - context.out << *number_ptr(m_Storage); - break; - - default: - break; - } -} - -struct value::parser { - parser(const char* begin, const char* end) : m_Cursor(begin), m_End(end) {} - - value parse() { - value v; - - // Switch to C locale to make strtod and strtol work as expected - auto previous_locale = std::setlocale(LC_NUMERIC, "C"); - - // Accept single value only when end of the stream is reached. - if (!accept_element(v) || !eof()) v = value(type_t::discarded); - - if (previous_locale && strcmp(previous_locale, "C") != 0) - std::setlocale(LC_NUMERIC, previous_locale); - - return v; - } - - private: - struct cursor_state { - cursor_state(parser* p) : m_Owner(p), m_LastCursor(p->m_Cursor) {} - - void reset() { m_Owner->m_Cursor = m_LastCursor; } - - bool operator()(bool accept) { - if (!accept) - reset(); - else - m_LastCursor = m_Owner->m_Cursor; - return accept; - } - - private: - parser* m_Owner; - const char* m_LastCursor; - }; - - cursor_state state() { return cursor_state(this); } - - bool accept_value(value& result) { - return accept_object(result) || accept_array(result) || - accept_string(result) || accept_number(result) || - accept_boolean(result) || accept_null(result); - } - - bool accept_object(value& result) { - auto s = state(); - - object o; - if (s(accept('{') && accept_ws() && accept('}'))) { - result = o; - return true; - } else if (s(accept('{') && accept_members(o) && accept('}'))) { - result = std::move(o); - return true; - } - - return false; - } - - bool accept_members(object& o) { - if (!accept_member(o)) return false; - - while (true) { - auto s = state(); - if (!s(accept(',') && accept_member(o))) break; - } - - return true; - } - - bool accept_member(object& o) { - auto s = state(); - - value key; - value v; - if (s(accept_ws() && accept_string(key) && accept_ws() && accept(':') && - accept_element(v))) { - o.emplace(std::move(key.get()), std::move(v)); - return true; - } - - return false; - } - - bool accept_array(value& result) { - auto s = state(); - - if (s(accept('[') && accept_ws() && accept(']'))) { - result = array(); - return true; - } - - array a; - if (s(accept('[') && accept_elements(a) && accept(']'))) { - result = std::move(a); - return true; - } - - return false; - } - - bool accept_elements(array& a) { - value v; - if (!accept_element(v)) return false; - - a.emplace_back(std::move(v)); - while (true) { - auto s = state(); - v = nullptr; - if (!s(accept(',') && accept_element(v))) break; - a.emplace_back(std::move(v)); - } - - return true; - } - - bool accept_element(value& result) { - auto s = state(); - return s(accept_ws() && accept_value(result) && accept_ws()); - } - - bool accept_string(value& result) { - auto s = state(); - - string v; - if (s(accept('\"') && accept_characters(v) && accept('\"'))) { - result = std::move(v); - return true; - } else - return false; - } - - bool accept_characters(string& result) { - int c; - while (accept_character(c)) { - CRUDE_ASSERT(c < 128); // #todo: convert characters > 127 to UTF-8 - result.push_back(static_cast(c)); - } - - return true; - } - - bool accept_character(int& c) { - auto s = state(); - - if (accept('\\')) { - return accept_escape(c); - } else if (expect('\"')) - return false; - - // #todo: Handle UTF-8 sequences. - return s((c = peek()) >= 0) && advance(); - } - - bool accept_escape(int& c) { - if (accept('\"')) { - c = '\"'; - return true; - } - if (accept('\\')) { - c = '\\'; - return true; - } - if (accept('/')) { - c = '/'; - return true; - } - if (accept('b')) { - c = '\b'; - return true; - } - if (accept('f')) { - c = '\f'; - return true; - } - if (accept('n')) { - c = '\n'; - return true; - } - if (accept('r')) { - c = '\r'; - return true; - } - if (accept('t')) { - c = '\t'; - return true; - } - - auto s = state(); - - string hex; - hex.reserve(4); - if (s(accept('u') && accept_hex(hex) && accept_hex(hex) && - accept_hex(hex) && accept_hex(hex))) { - char* end = nullptr; - auto v = std::strtol(hex.c_str(), &end, 16); - if (end != hex.c_str() + hex.size()) return false; - - c = static_cast(v); - return true; - } - - return false; - } - - bool accept_hex(string& result) { - if (accept_digit(result)) return true; - - auto c = peek(); - if ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) { - advance(); - result.push_back(static_cast(c)); - return true; - } - - return false; - } - - bool accept_number(value& result) { - auto s = state(); - - string n; - if (s(accept_int(n) && accept_frac(n) && accept_exp(n))) { - char* end = nullptr; - auto v = std::strtod(n.c_str(), &end); - if (end != n.c_str() + n.size()) return false; - - if (v != 0 && !std::isnormal(v)) return false; - - result = v; - return true; - } - - return false; - } - - bool accept_int(string& result) { - auto s = state(); - - string part; - if (s(accept_onenine(part) && accept_digits(part))) { - result += std::move(part); - return true; - } - - part.resize(0); - if (accept_digit(part)) { - result += std::move(part); - return true; - } - - part.resize(0); - if (s(accept('-') && accept_onenine(part) && accept_digits(part))) { - result += '-'; - result += std::move(part); - return true; - } - - part.resize(0); - if (s(accept('-') && accept_digit(part))) { - result += '-'; - result += std::move(part); - return true; - } - - return false; - } - - bool accept_digits(string& result) { - string part; - if (!accept_digit(part)) return false; - - while (accept_digit(part)) - ; - - result += std::move(part); - - return true; - } - - bool accept_digit(string& result) { - if (accept('0')) { - result.push_back('0'); - return true; - } else if (accept_onenine(result)) - return true; - - return false; - } - - bool accept_onenine(string& result) { - auto c = peek(); - if (c >= '1' && c <= '9') { - result.push_back(static_cast(c)); - return advance(); - } - - return false; - } - - bool accept_frac(string& result) { - auto s = state(); - - string part; - if (s(accept('.') && accept_digits(part))) { - result += '.'; - result += std::move(part); - } - - return true; - } - - bool accept_exp(string& result) { - auto s = state(); - - string part; - if (s(accept('e') && accept_sign(part) && accept_digits(part))) { - result += 'e'; - result += std::move(part); - return true; - } - part.resize(0); - if (s(accept('E') && accept_sign(part) && accept_digits(part))) { - result += 'E'; - result += std::move(part); - } - - return true; - } - - bool accept_sign(string& result) { - if (accept('+')) - result.push_back('+'); - else if (accept('-')) - result.push_back('-'); - - return true; - } - - bool accept_ws() { - while (expect('\x09') || expect('\x0A') || expect('\x0D') || expect('\x20')) - advance(); - return true; - } - - bool accept_boolean(value& result) { - if (accept("true")) { - result = true; - return true; - } else if (accept("false")) { - result = false; - return true; - } - - return false; - } - - bool accept_null(value& result) { - if (accept("null")) { - result = nullptr; - return true; - } - - return false; - } - - bool accept(char c) { - if (expect(c)) - return advance(); - else - return false; - } - - bool accept(const char* str) { - auto last = m_Cursor; - - while (*str) { - if (eof() || *str != *m_Cursor) { - m_Cursor = last; - return false; - } - - advance(); - ++str; - } - - return true; - } - - int peek() const { - if (!eof()) - return *m_Cursor; - else - return -1; - } - - bool expect(char c) { return peek() == c; } - - bool advance(int count = 1) { - if (m_Cursor + count > m_End) { - m_Cursor = m_End; - return false; - } - - m_Cursor += count; - - return true; - } - - bool eof() const { return m_Cursor == m_End; } - - const char* m_Cursor; - const char* m_End; -}; - -value value::parse(const string& data) { - auto p = parser(data.c_str(), data.c_str() + data.size()); - - auto v = p.parse(); - - return v; -} - -#if CRUDE_JSON_IO -std::pair value::load(const string& path) { - // Modern C++, so beautiful... - std::unique_ptr file{nullptr, [](FILE* file) { - if (file) fclose(file); - }}; -#if defined(_MSC_VER) || (defined(__STDC_LIB_EXT1__) && __STDC_WANT_LIB_EXT1__) - FILE* handle = nullptr; - if (fopen_s(&handle, path.c_str(), "rb") != 0) return {value{}, false}; - file.reset(handle); -#else - file.reset(fopen(path.c_str(), "rb")); -#endif - - if (!file) return {value{}, false}; - - fseek(file.get(), 0, SEEK_END); - auto size = static_cast(ftell(file.get())); - fseek(file.get(), 0, SEEK_SET); - - string data; - data.resize(size); - if (fread(const_cast(data.data()), size, 1, file.get()) != 1) - return {value{}, false}; - - return {parse(data), true}; -} - -bool value::save(const string& path, const int indent, - const char indent_char) const { - // Modern C++, so beautiful... - std::unique_ptr file{nullptr, [](FILE* file) { - if (file) fclose(file); - }}; -#if defined(_MSC_VER) || (defined(__STDC_LIB_EXT1__) && __STDC_WANT_LIB_EXT1__) - FILE* handle = nullptr; - if (fopen_s(&handle, path.c_str(), "wb") != 0) return false; - file.reset(handle); -#else - file.reset(fopen(path.c_str(), "wb")); -#endif - - if (!file) return false; - - auto data = dump(indent, indent_char); - - if (fwrite(data.data(), data.size(), 1, file.get()) != 1) return false; - - return true; -} - -#endif - -} // namespace crude_json diff --git a/symmetri/gui/extensions/crude_json.h b/symmetri/gui/extensions/crude_json.h deleted file mode 100644 index 1ef945b..0000000 --- a/symmetri/gui/extensions/crude_json.h +++ /dev/null @@ -1,458 +0,0 @@ -// Crude implementation of JSON value object and parser. -// -// VERSION 0.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -#ifndef __CRUDE_JSON_H__ -#define __CRUDE_JSON_H__ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#ifndef CRUDE_ASSERT -#include -#define CRUDE_ASSERT(expr) assert(expr) -#endif - -#ifndef CRUDE_JSON_IO -#define CRUDE_JSON_IO 1 -#endif - -namespace crude_json { - -struct value; - -using string = std::string; -using object = std::map; -using array = std::vector; -using number = double; -using boolean = bool; -using null = std::nullptr_t; - -enum class type_t { null, object, array, string, boolean, number, discarded }; - -struct value { - value(type_t type = type_t::null) : m_Type(construct(m_Storage, type)) {} - value(value&& other); - value(const value& other); - - value(null) : m_Type(construct(m_Storage, null())) {} - value(object&& v) : m_Type(construct(m_Storage, std::move(v))) {} - value(const object& v) : m_Type(construct(m_Storage, v)) {} - value(array&& v) : m_Type(construct(m_Storage, std::move(v))) {} - value(const array& v) : m_Type(construct(m_Storage, v)) {} - value(string&& v) : m_Type(construct(m_Storage, std::move(v))) {} - value(const string& v) : m_Type(construct(m_Storage, v)) {} - value(const char* v) : m_Type(construct(m_Storage, v)) {} - value(boolean v) : m_Type(construct(m_Storage, v)) {} - value(number v) : m_Type(construct(m_Storage, v)) {} - ~value() { destruct(m_Storage, m_Type); } - - value& operator=(value&& other) { - if (this != &other) { - value(std::move(other)).swap(*this); - } - return *this; - } - value& operator=(const value& other) { - if (this != &other) { - value(other).swap(*this); - } - return *this; - } - - value& operator=(null) { - auto other = value(); - swap(other); - return *this; - } - value& operator=(object&& v) { - auto other = value(std::move(v)); - swap(other); - return *this; - } - value& operator=(const object& v) { - auto other = value(v); - swap(other); - return *this; - } - value& operator=(array&& v) { - auto other = value(std::move(v)); - swap(other); - return *this; - } - value& operator=(const array& v) { - auto other = value(v); - swap(other); - return *this; - } - value& operator=(string&& v) { - auto other = value(std::move(v)); - swap(other); - return *this; - } - value& operator=(const string& v) { - auto other = value(v); - swap(other); - return *this; - } - value& operator=(const char* v) { - auto other = value(v); - swap(other); - return *this; - } - value& operator=(boolean v) { - auto other = value(v); - swap(other); - return *this; - } - value& operator=(number v) { - auto other = value(v); - swap(other); - return *this; - } - - type_t type() const { return m_Type; } - - operator type_t() const { return m_Type; } - - value& operator[](size_t index); - const value& operator[](size_t index) const; - value& operator[](const string& key); - const value& operator[](const string& key) const; - - bool contains(const string& key) const; - - void push_back(const value& value); - void push_back(value&& value); - - size_t erase(const string& key); - - bool is_primitive() const { - return is_string() || is_number() || is_boolean() || is_null(); - } - bool is_structured() const { return is_object() || is_array(); } - bool is_null() const { return m_Type == type_t::null; } - bool is_object() const { return m_Type == type_t::object; } - bool is_array() const { return m_Type == type_t::array; } - bool is_string() const { return m_Type == type_t::string; } - bool is_boolean() const { return m_Type == type_t::boolean; } - bool is_number() const { return m_Type == type_t::number; } - bool is_discarded() const { return m_Type == type_t::discarded; } - - template - const T& get() const; - template - T& get(); - - template - const T* get_ptr() const; - template - T* get_ptr(); - - string dump(const int indent = -1, const char indent_char = ' ') const; - - void swap(value& other); - - inline friend void swap(value& lhs, value& rhs) { lhs.swap(rhs); } - - // Returns discarded value for invalid inputs. - static value parse(const string& data); - -#if CRUDE_JSON_IO - static std::pair load(const string& path); - bool save(const string& path, const int indent = -1, - const char indent_char = ' ') const; -#endif - - private: - struct parser; - - // VS2015: std::max() is not constexpr yet. -#define CRUDE_MAX2(a, b) ((a) < (b) ? (b) : (a)) -#define CRUDE_MAX3(a, b, c) CRUDE_MAX2(CRUDE_MAX2(a, b), c) -#define CRUDE_MAX4(a, b, c, d) CRUDE_MAX2(CRUDE_MAX3(a, b, c), d) -#define CRUDE_MAX5(a, b, c, d, e) CRUDE_MAX2(CRUDE_MAX4(a, b, c, d), e) - enum { - max_size = CRUDE_MAX5(sizeof(string), sizeof(object), sizeof(array), - sizeof(number), sizeof(boolean)), - max_align = CRUDE_MAX5(alignof(string), alignof(object), alignof(array), - alignof(number), alignof(boolean)) - }; -#undef CRUDE_MAX5 -#undef CRUDE_MAX4 -#undef CRUDE_MAX3 -#undef CRUDE_MAX2 - using storage_t = std::aligned_storage::type; - - static object* object_ptr(storage_t& storage) { - return reinterpret_cast(&storage); - } - static const object* object_ptr(const storage_t& storage) { - return reinterpret_cast(&storage); - } - static array* array_ptr(storage_t& storage) { - return reinterpret_cast(&storage); - } - static const array* array_ptr(const storage_t& storage) { - return reinterpret_cast(&storage); - } - static string* string_ptr(storage_t& storage) { - return reinterpret_cast(&storage); - } - static const string* string_ptr(const storage_t& storage) { - return reinterpret_cast(&storage); - } - static boolean* boolean_ptr(storage_t& storage) { - return reinterpret_cast(&storage); - } - static const boolean* boolean_ptr(const storage_t& storage) { - return reinterpret_cast(&storage); - } - static number* number_ptr(storage_t& storage) { - return reinterpret_cast(&storage); - } - static const number* number_ptr(const storage_t& storage) { - return reinterpret_cast(&storage); - } - - static type_t construct(storage_t& storage, type_t type) { - switch (type) { - case type_t::object: - new (&storage) object(); - break; - case type_t::array: - new (&storage) array(); - break; - case type_t::string: - new (&storage) string(); - break; - case type_t::boolean: - new (&storage) boolean(); - break; - case type_t::number: - new (&storage) number(); - break; - default: - break; - } - - return type; - } - - static type_t construct(storage_t& storage, null) { - (void)storage; - return type_t::null; - } - static type_t construct(storage_t& storage, object&& value) { - new (&storage) object(std::forward(value)); - return type_t::object; - } - static type_t construct(storage_t& storage, const object& value) { - new (&storage) object(value); - return type_t::object; - } - static type_t construct(storage_t& storage, array&& value) { - new (&storage) array(std::forward(value)); - return type_t::array; - } - static type_t construct(storage_t& storage, const array& value) { - new (&storage) array(value); - return type_t::array; - } - static type_t construct(storage_t& storage, string&& value) { - new (&storage) string(std::forward(value)); - return type_t::string; - } - static type_t construct(storage_t& storage, const string& value) { - new (&storage) string(value); - return type_t::string; - } - static type_t construct(storage_t& storage, const char* value) { - new (&storage) string(value); - return type_t::string; - } - static type_t construct(storage_t& storage, boolean value) { - new (&storage) boolean(value); - return type_t::boolean; - } - static type_t construct(storage_t& storage, number value) { - new (&storage) number(value); - return type_t::number; - } - - static void destruct(storage_t& storage, type_t type) { - switch (type) { - case type_t::object: - object_ptr(storage)->~object(); - break; - case type_t::array: - array_ptr(storage)->~array(); - break; - case type_t::string: - string_ptr(storage)->~string(); - break; - default: - break; - } - } - - struct dump_context_t { - std::ostringstream out; - const int indent = -1; - const char indent_char = ' '; - - // VS2015: Aggregate initialization isn't a thing yet. - dump_context_t(const int indent, const char indent_char) - : indent(indent), indent_char(indent_char) {} - - void write_indent(int level); - void write_separator(); - void write_newline(); - }; - - void dump(dump_context_t& context, int level) const; - - storage_t m_Storage; - type_t m_Type; -}; - -template <> -inline const object& value::get() const { - CRUDE_ASSERT(m_Type == type_t::object); - return *object_ptr(m_Storage); -} -template <> -inline const array& value::get() const { - CRUDE_ASSERT(m_Type == type_t::array); - return *array_ptr(m_Storage); -} -template <> -inline const string& value::get() const { - CRUDE_ASSERT(m_Type == type_t::string); - return *string_ptr(m_Storage); -} -template <> -inline const boolean& value::get() const { - CRUDE_ASSERT(m_Type == type_t::boolean); - return *boolean_ptr(m_Storage); -} -template <> -inline const number& value::get() const { - CRUDE_ASSERT(m_Type == type_t::number); - return *number_ptr(m_Storage); -} - -template <> -inline object& value::get() { - CRUDE_ASSERT(m_Type == type_t::object); - return *object_ptr(m_Storage); -} -template <> -inline array& value::get() { - CRUDE_ASSERT(m_Type == type_t::array); - return *array_ptr(m_Storage); -} -template <> -inline string& value::get() { - CRUDE_ASSERT(m_Type == type_t::string); - return *string_ptr(m_Storage); -} -template <> -inline boolean& value::get() { - CRUDE_ASSERT(m_Type == type_t::boolean); - return *boolean_ptr(m_Storage); -} -template <> -inline number& value::get() { - CRUDE_ASSERT(m_Type == type_t::number); - return *number_ptr(m_Storage); -} - -template <> -inline const object* value::get_ptr() const { - if (m_Type == type_t::object) - return object_ptr(m_Storage); - else - return nullptr; -} -template <> -inline const array* value::get_ptr() const { - if (m_Type == type_t::array) - return array_ptr(m_Storage); - else - return nullptr; -} -template <> -inline const string* value::get_ptr() const { - if (m_Type == type_t::string) - return string_ptr(m_Storage); - else - return nullptr; -} -template <> -inline const boolean* value::get_ptr() const { - if (m_Type == type_t::boolean) - return boolean_ptr(m_Storage); - else - return nullptr; -} -template <> -inline const number* value::get_ptr() const { - if (m_Type == type_t::number) - return number_ptr(m_Storage); - else - return nullptr; -} - -template <> -inline object* value::get_ptr() { - if (m_Type == type_t::object) - return object_ptr(m_Storage); - else - return nullptr; -} -template <> -inline array* value::get_ptr() { - if (m_Type == type_t::array) - return array_ptr(m_Storage); - else - return nullptr; -} -template <> -inline string* value::get_ptr() { - if (m_Type == type_t::string) - return string_ptr(m_Storage); - else - return nullptr; -} -template <> -inline boolean* value::get_ptr() { - if (m_Type == type_t::boolean) - return boolean_ptr(m_Storage); - else - return nullptr; -} -template <> -inline number* value::get_ptr() { - if (m_Type == type_t::number) - return number_ptr(m_Storage); - else - return nullptr; -} - -} // namespace crude_json - -#endif // __CRUDE_JSON_H__ diff --git a/symmetri/gui/extensions/imgui_bezier_math.h b/symmetri/gui/extensions/imgui_bezier_math.h deleted file mode 100644 index eaeb372..0000000 --- a/symmetri/gui/extensions/imgui_bezier_math.h +++ /dev/null @@ -1,194 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -#ifndef __IMGUI_BEZIER_MATH_H__ -#define __IMGUI_BEZIER_MATH_H__ -#pragma once - -//------------------------------------------------------------------------------ -#include "imgui_extra_math.h" - -//------------------------------------------------------------------------------ -template -struct ImCubicBezierPointsT { - T P0; - T P1; - T P2; - T P3; -}; -using ImCubicBezierPoints = ImCubicBezierPointsT; - -//------------------------------------------------------------------------------ -// Low-level Bezier curve sampling. -template -inline T ImLinearBezier(const T& p0, const T& p1, float t); -template -inline T ImLinearBezierDt(const T& p0, const T& p1, float t); -template -inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t); -template -inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t); -template -inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, - float t); -template -inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, - float t); - -// High-level Bezier sampling, automatically collapse to lower level Bezier -// curves if control points overlap. -template -inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, - float t); -template -inline T ImCubicBezierSample(const ImCubicBezierPointsT& curve, float t); -template -inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, - const T& p3, float t); -template -inline T ImCubicBezierTangent(const ImCubicBezierPointsT& curve, float t); - -// Calculate approximate length of Cubic Bezier curve. -template -inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, - const T& p3); -template -inline float ImCubicBezierLength(const ImCubicBezierPointsT& curve); - -// Splits Cubic Bezier curve into two curves. -template -struct ImCubicBezierSplitResultT { - ImCubicBezierPointsT Left; - ImCubicBezierPointsT Right; -}; -using ImCubicBezierSplitResult = ImCubicBezierSplitResultT; - -template -inline ImCubicBezierSplitResultT ImCubicBezierSplit(const T& p0, const T& p1, - const T& p2, const T& p3, - float t); -template -inline ImCubicBezierSplitResultT ImCubicBezierSplit( - const ImCubicBezierPointsT& curve, float t); - -// Returns bounding rectangle of Cubic Bezier curve. -inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, - const ImVec2& p2, const ImVec2& p3); -inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve); - -// Project point on Cubic Bezier curve. -struct ImProjectResult { - ImVec2 Point; // Point on curve - float Time; // [0 - 1] - float Distance; // Distance to curve -}; - -inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImVec2& p0, - const ImVec2& p1, - const ImVec2& p2, - const ImVec2& p3, - const int subdivisions = 100); -inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, - const ImCubicBezierPoints& curve, - const int subdivisions = 100); - -// Calculate intersection between line and a Cubic Bezier curve. -struct ImCubicBezierIntersectResult { - int Count; - ImVec2 Points[3]; -}; - -inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect( - const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, - const ImVec2& a0, const ImVec2& a1); -inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect( - const ImCubicBezierPoints& curve, const ImLine& line); - -// Adaptive Cubic Bezier subdivision. -enum ImCubicBezierSubdivideFlags { - ImCubicBezierSubdivide_None = 0, - ImCubicBezierSubdivide_SkipFirst = 1 -}; - -struct ImCubicBezierSubdivideSample { - ImVec2 Point; - ImVec2 Tangent; -}; - -using ImCubicBezierSubdivideCallback = - void (*)(const ImCubicBezierSubdivideSample& p, void* user_pointer); - -inline void ImCubicBezierSubdivide( - ImCubicBezierSubdivideCallback callback, void* user_pointer, - const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, - float tess_tol = -1.0f, - ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); -inline void ImCubicBezierSubdivide( - ImCubicBezierSubdivideCallback callback, void* user_pointer, - const ImCubicBezierPoints& curve, float tess_tol = -1.0f, - ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); - -// F has signature void(const ImCubicBezierSubdivideSample& p) -template -inline void ImCubicBezierSubdivide( - F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, - const ImVec2& p3, float tess_tol = -1.0f, - ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); -template -inline void ImCubicBezierSubdivide( - F& callback, const ImCubicBezierPoints& curve, float tess_tol = -1.0f, - ImCubicBezierSubdivideFlags flags = ImCubicBezierSubdivide_None); - -// Fixed step Cubic Bezier subdivision. -struct ImCubicBezierFixedStepSample { - float T; - float Length; - ImVec2 Point; - bool BreakSearch; -}; - -using ImCubicBezierFixedStepCallback = - void (*)(ImCubicBezierFixedStepSample& sample, void* user_pointer); - -inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, - void* user_pointer, const ImVec2& p0, - const ImVec2& p1, const ImVec2& p2, - const ImVec2& p3, float step, - bool overshoot = false, - float max_value_error = 1e-3f, - float max_t_error = 1e-5f); -inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, - void* user_pointer, - const ImCubicBezierPoints& curve, float step, - bool overshoot = false, - float max_value_error = 1e-3f, - float max_t_error = 1e-5f); - -// F has signature void(const ImCubicBezierFixedStepSample& p) -template -inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, - const ImVec2& p1, const ImVec2& p2, - const ImVec2& p3, float step, - bool overshoot = false, - float max_value_error = 1e-3f, - float max_t_error = 1e-5f); -template -inline void ImCubicBezierFixedStep(F& callback, - const ImCubicBezierPoints& curve, float step, - bool overshoot = false, - float max_value_error = 1e-3f, - float max_t_error = 1e-5f); - -//------------------------------------------------------------------------------ -#include "imgui_bezier_math.inl" - -//------------------------------------------------------------------------------ -#endif // __IMGUI_BEZIER_MATH_H__ diff --git a/symmetri/gui/extensions/imgui_bezier_math.inl b/symmetri/gui/extensions/imgui_bezier_math.inl deleted file mode 100644 index 3020bdb..0000000 --- a/symmetri/gui/extensions/imgui_bezier_math.inl +++ /dev/null @@ -1,675 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -# ifndef __IMGUI_BEZIER_MATH_INL__ -# define __IMGUI_BEZIER_MATH_INL__ -# pragma once - - -//------------------------------------------------------------------------------ -# include "imgui_bezier_math.h" -# include // used in ImCubicBezierFixedStep - - -//------------------------------------------------------------------------------ -template -inline T ImLinearBezier(const T& p0, const T& p1, float t) -{ - return p0 + t * (p1 - p0); -} - -template -inline T ImLinearBezierDt(const T& p0, const T& p1, float t) -{ - IM_UNUSED(t); - - return p1 - p0; -} - -template -inline T ImQuadraticBezier(const T& p0, const T& p1, const T& p2, float t) -{ - const auto a = 1 - t; - - return a * a * p0 + 2 * t * a * p1 + t * t * p2; -} - -template -inline T ImQuadraticBezierDt(const T& p0, const T& p1, const T& p2, float t) -{ - return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1); -} - -template -inline T ImCubicBezier(const T& p0, const T& p1, const T& p2, const T& p3, float t) -{ - const auto a = 1 - t; - const auto b = a * a * a; - const auto c = t * t * t; - - return b * p0 + 3 * t * a * a * p1 + 3 * t * t * a * p2 + c * p3; -} - -template -inline T ImCubicBezierDt(const T& p0, const T& p1, const T& p2, const T& p3, float t) -{ - const auto a = 1 - t; - const auto b = a * a; - const auto c = t * t; - const auto d = 2 * t * a; - - return -3 * p0 * b + 3 * p1 * (b - d) + 3 * p2 * (d - c) + 3 * p3 * c; -} - -template -inline T ImCubicBezierSample(const T& p0, const T& p1, const T& p2, const T& p3, float t) -{ - const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f; - const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f; - - if (cp0_zero && cp1_zero) - return ImLinearBezier(p0, p3, t); - else if (cp0_zero) - return ImQuadraticBezier(p0, p2, p3, t); - else if (cp1_zero) - return ImQuadraticBezier(p0, p1, p3, t); - else - return ImCubicBezier(p0, p1, p2, p3, t); -} - -template -inline T ImCubicBezierSample(const ImCubicBezierPointsT& curve, float t) -{ - return ImCubicBezierSample(curve.P0, curve.P1, curve.P2, curve.P3, t); -} - -template -inline T ImCubicBezierTangent(const T& p0, const T& p1, const T& p2, const T& p3, float t) -{ - const auto cp0_zero = ImLengthSqr(p1 - p0) < 1e-5f; - const auto cp1_zero = ImLengthSqr(p3 - p2) < 1e-5f; - - if (cp0_zero && cp1_zero) - return ImLinearBezierDt(p0, p3, t); - else if (cp0_zero) - return ImQuadraticBezierDt(p0, p2, p3, t); - else if (cp1_zero) - return ImQuadraticBezierDt(p0, p1, p3, t); - else - return ImCubicBezierDt(p0, p1, p2, p3, t); -} - -template -inline T ImCubicBezierTangent(const ImCubicBezierPointsT& curve, float t) -{ - return ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, t); -} - -template -inline float ImCubicBezierLength(const T& p0, const T& p1, const T& p2, const T& p3) -{ - // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) - static const float t_values[] = - { - -0.0640568928626056260850430826247450385909f, - 0.0640568928626056260850430826247450385909f, - -0.1911188674736163091586398207570696318404f, - 0.1911188674736163091586398207570696318404f, - -0.3150426796961633743867932913198102407864f, - 0.3150426796961633743867932913198102407864f, - -0.4337935076260451384870842319133497124524f, - 0.4337935076260451384870842319133497124524f, - -0.5454214713888395356583756172183723700107f, - 0.5454214713888395356583756172183723700107f, - -0.6480936519369755692524957869107476266696f, - 0.6480936519369755692524957869107476266696f, - -0.7401241915785543642438281030999784255232f, - 0.7401241915785543642438281030999784255232f, - -0.8200019859739029219539498726697452080761f, - 0.8200019859739029219539498726697452080761f, - -0.8864155270044010342131543419821967550873f, - 0.8864155270044010342131543419821967550873f, - -0.9382745520027327585236490017087214496548f, - 0.9382745520027327585236490017087214496548f, - -0.9747285559713094981983919930081690617411f, - 0.9747285559713094981983919930081690617411f, - -0.9951872199970213601799974097007368118745f, - 0.9951872199970213601799974097007368118745f - }; - - // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article) - static const float c_values[] = - { - 0.1279381953467521569740561652246953718517f, - 0.1279381953467521569740561652246953718517f, - 0.1258374563468282961213753825111836887264f, - 0.1258374563468282961213753825111836887264f, - 0.1216704729278033912044631534762624256070f, - 0.1216704729278033912044631534762624256070f, - 0.1155056680537256013533444839067835598622f, - 0.1155056680537256013533444839067835598622f, - 0.1074442701159656347825773424466062227946f, - 0.1074442701159656347825773424466062227946f, - 0.0976186521041138882698806644642471544279f, - 0.0976186521041138882698806644642471544279f, - 0.0861901615319532759171852029837426671850f, - 0.0861901615319532759171852029837426671850f, - 0.0733464814110803057340336152531165181193f, - 0.0733464814110803057340336152531165181193f, - 0.0592985849154367807463677585001085845412f, - 0.0592985849154367807463677585001085845412f, - 0.0442774388174198061686027482113382288593f, - 0.0442774388174198061686027482113382288593f, - 0.0285313886289336631813078159518782864491f, - 0.0285313886289336631813078159518782864491f, - 0.0123412297999871995468056670700372915759f, - 0.0123412297999871995468056670700372915759f - }; - - static_assert(sizeof(t_values) / sizeof(*t_values) == sizeof(c_values) / sizeof(*c_values), ""); - - auto arc = [p0, p1, p2, p3](float t) - { - const auto p = ImCubicBezierDt(p0, p1, p2, p3, t); - const auto l = ImLength(p); - return l; - }; - - const auto z = 0.5f; - const auto n = sizeof(t_values) / sizeof(*t_values); - - auto accumulator = 0.0f; - for (size_t i = 0; i < n; ++i) - { - const auto t = z * t_values[i] + z; - accumulator += c_values[i] * arc(t); - } - - return z * accumulator; -} - -template -inline float ImCubicBezierLength(const ImCubicBezierPointsT& curve) -{ - return ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3); -} - -template -inline ImCubicBezierSplitResultT ImCubicBezierSplit(const T& p0, const T& p1, const T& p2, const T& p3, float t) -{ - const auto z1 = t; - const auto z2 = z1 * z1; - const auto z3 = z1 * z1 * z1; - const auto s1 = z1 - 1; - const auto s2 = s1 * s1; - const auto s3 = s1 * s1 * s1; - - return ImCubicBezierSplitResultT - { - ImCubicBezierPointsT - { - p0, - z1 * p1 - s1 * p0, - z2 * p2 - 2 * z1 * s1 * p1 + s2 * p0, - z3 * p3 - 3 * z2 * s1 * p2 + 3 * z1 * s2 * p1 - s3 * p0 - }, - ImCubicBezierPointsT - { - z3 * p0 - 3 * z2 * s1 * p1 + 3 * z1 * s2 * p2 - s3 * p3, - z2 * p1 - 2 * z1 * s1 * p2 + s2 * p3, - z1 * p2 - s1 * p3, - p3, - } - }; -} - -template -inline ImCubicBezierSplitResultT ImCubicBezierSplit(const ImCubicBezierPointsT& curve, float t) -{ - return ImCubicBezierSplit(curve.P0, curve.P1, curve.P2, curve.P3, t); -} - -inline ImRect ImCubicBezierBoundingRect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3) -{ - auto a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0; - auto b = 6 * p0 - 12 * p1 + 6 * p2; - auto c = 3 * p1 - 3 * p0; - auto delta_squared = ImMul(b, b) - 4 * ImMul(a, c); - - auto tl = ImMin(p0, p3); - auto rb = ImMax(p0, p3); - -# define IM_VEC2_INDEX(v, i) *(&v.x + i) - - for (int i = 0; i < 2; ++i) - { - if (IM_VEC2_INDEX(a, i) == 0.0f) - continue; - - if (IM_VEC2_INDEX(delta_squared, i) >= 0) - { - auto delta = ImSqrt(IM_VEC2_INDEX(delta_squared, i)); - - auto t0 = (-IM_VEC2_INDEX(b, i) + delta) / (2 * IM_VEC2_INDEX(a, i)); - if (t0 > 0 && t0 < 1) - { - auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t0); - IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p); - IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p); - } - - auto t1 = (-IM_VEC2_INDEX(b, i) - delta) / (2 * IM_VEC2_INDEX(a, i)); - if (t1 > 0 && t1 < 1) - { - auto p = ImCubicBezier(IM_VEC2_INDEX(p0, i), IM_VEC2_INDEX(p1, i), IM_VEC2_INDEX(p2, i), IM_VEC2_INDEX(p3, i), t1); - IM_VEC2_INDEX(tl, i) = ImMin(IM_VEC2_INDEX(tl, i), p); - IM_VEC2_INDEX(rb, i) = ImMax(IM_VEC2_INDEX(rb, i), p); - } - } - } - -# undef IM_VEC2_INDEX - - return ImRect(tl, rb); -} - -inline ImRect ImCubicBezierBoundingRect(const ImCubicBezierPoints& curve) -{ - return ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3); -} - -inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& point, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const int subdivisions) -{ - // http://pomax.github.io/bezierinfo/#projections - - const float epsilon = 1e-5f; - const float fixed_step = 1.0f / static_cast(subdivisions - 1); - - ImProjectResult result; - result.Point = point; - result.Time = 0.0f; - result.Distance = FLT_MAX; - - // Step 1: Coarse check - for (int i = 0; i < subdivisions; ++i) - { - auto t = i * fixed_step; - auto p = ImCubicBezier(p0, p1, p2, p3, t); - auto s = point - p; - auto d = ImDot(s, s); - - if (d < result.Distance) - { - result.Point = p; - result.Time = t; - result.Distance = d; - } - } - - if (result.Time == 0.0f || ImFabs(result.Time - 1.0f) <= epsilon) - { - result.Distance = ImSqrt(result.Distance); - return result; - } - - // Step 2: Fine check - auto left = result.Time - fixed_step; - auto right = result.Time + fixed_step; - auto step = fixed_step * 0.1f; - - for (auto t = left; t < right + step; t += step) - { - auto p = ImCubicBezier(p0, p1, p2, p3, t); - auto s = point - p; - auto d = ImDot(s, s); - - if (d < result.Distance) - { - result.Point = p; - result.Time = t; - result.Distance = d; - } - } - - result.Distance = ImSqrt(result.Distance); - - return result; -} - -inline ImProjectResult ImProjectOnCubicBezier(const ImVec2& p, const ImCubicBezierPoints& curve, const int subdivisions) -{ - return ImProjectOnCubicBezier(p, curve.P0, curve.P1, curve.P2, curve.P3, subdivisions); -} - -inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& a0, const ImVec2& a1) -{ - auto cubic_roots = [](float a, float b, float c, float d, float* roots) -> int - { - int count = 0; - - auto sign = [](float x) -> float { return x < 0 ? -1.0f : 1.0f; }; - - auto A = b / a; - auto B = c / a; - auto C = d / a; - - auto Q = (3 * B - ImPow(A, 2)) / 9; - auto R = (9 * A * B - 27 * C - 2 * ImPow(A, 3)) / 54; - auto D = ImPow(Q, 3) + ImPow(R, 2); // polynomial discriminant - - if (D >= 0) // complex or duplicate roots - { - auto S = sign(R + ImSqrt(D)) * ImPow(ImFabs(R + ImSqrt(D)), (1.0f / 3.0f)); - auto T = sign(R - ImSqrt(D)) * ImPow(ImFabs(R - ImSqrt(D)), (1.0f / 3.0f)); - - roots[0] = -A / 3 + (S + T); // real root - roots[1] = -A / 3 - (S + T) / 2; // real part of complex root - roots[2] = -A / 3 - (S + T) / 2; // real part of complex root - auto Im = ImFabs(ImSqrt(3) * (S - T) / 2); // complex part of root pair - - // discard complex roots - if (Im != 0) - count = 1; - else - count = 3; - } - else // distinct real roots - { - auto th = ImAcos(R / ImSqrt(-ImPow(Q, 3))); - - roots[0] = 2 * ImSqrt(-Q) * ImCos(th / 3) - A / 3; - roots[1] = 2 * ImSqrt(-Q) * ImCos((th + 2 * IM_PI) / 3) - A / 3; - roots[2] = 2 * ImSqrt(-Q) * ImCos((th + 4 * IM_PI) / 3) - A / 3; - - count = 3; - } - - return count; - }; - - // https://github.com/kaishiqi/Geometric-Bezier/blob/master/GeometricBezier/src/kaishiqi/geometric/intersection/Intersection.as - // - // Start with Bezier using Bernstein polynomials for weighting functions: - // (1-t^3)P0 + 3t(1-t)^2P1 + 3t^2(1-t)P2 + t^3P3 - // - // Expand and collect terms to form linear combinations of original Bezier - // controls. This ends up with a vector cubic in t: - // (-P0+3P1-3P2+P3)t^3 + (3P0-6P1+3P2)t^2 + (-3P0+3P1)t + P0 - // /\ /\ /\ /\ - // || || || || - // c3 c2 c1 c0 - - // Calculate the coefficients - auto c3 = -p0 + 3 * p1 - 3 * p2 + p3; - auto c2 = 3 * p0 - 6 * p1 + 3 * p2; - auto c1 = -3 * p0 + 3 * p1; - auto c0 = p0; - - // Convert line to normal form: ax + by + c = 0 - auto a = a1.y - a0.y; - auto b = a0.x - a1.x; - auto c = a0.x * (a0.y - a1.y) + a0.y * (a1.x - a0.x); - - // Rotate each cubic coefficient using line for new coordinate system? - // Find roots of rotated cubic - float roots[3]; - auto rootCount = cubic_roots( - a * c3.x + b * c3.y, - a * c2.x + b * c2.y, - a * c1.x + b * c1.y, - a * c0.x + b * c0.y + c, - roots); - - // Any roots in closed interval [0,1] are intersections on Bezier, but - // might not be on the line segment. - // Find intersections and calculate point coordinates - - auto min = ImMin(a0, a1); - auto max = ImMax(a0, a1); - - ImCubicBezierIntersectResult result; - auto points = result.Points; - - for (int i = 0; i < rootCount; ++i) - { - auto root = roots[i]; - - if (0 <= root && root <= 1) - { - // We're within the Bezier curve - // Find point on Bezier - auto p = ImCubicBezier(p0, p1, p2, p3, root); - - // See if point is on line segment - // Had to make special cases for vertical and horizontal lines due - // to slight errors in calculation of p00 - if (a0.x == a1.x) - { - if (min.y <= p.y && p.y <= max.y) - *points++ = p; - } - else if (a0.y == a1.y) - { - if (min.x <= p.x && p.x <= max.x) - *points++ = p; - } - else if (p.x >= min.x && p.y >= min.y && p.x <= max.x && p.y <= max.y) - { - *points++ = p; - } - } - } - - result.Count = static_cast(points - result.Points); - - return result; -} - -inline ImCubicBezierIntersectResult ImCubicBezierLineIntersect(const ImCubicBezierPoints& curve, const ImLine& line) -{ - return ImCubicBezierLineIntersect(curve.P0, curve.P1, curve.P2, curve.P3, line.A, line.B); -} - -inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags) -{ - return ImCubicBezierSubdivide(callback, user_pointer, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags); -} - -inline void ImCubicBezierSubdivide(ImCubicBezierSubdivideCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags) -{ - struct Tesselator - { - ImCubicBezierSubdivideCallback Callback; - void* UserPointer; - float TesselationTollerance; - ImCubicBezierSubdivideFlags Flags; - - void Commit(const ImVec2& p, const ImVec2& t) - { - ImCubicBezierSubdivideSample sample; - sample.Point = p; - sample.Tangent = t; - Callback(sample, UserPointer); - } - - void Subdivide(const ImCubicBezierPoints& curve, int level = 0) - { - float dx = curve.P3.x - curve.P0.x; - float dy = curve.P3.y - curve.P0.y; - float d2 = ((curve.P1.x - curve.P3.x) * dy - (curve.P1.y - curve.P3.y) * dx); - float d3 = ((curve.P2.x - curve.P3.x) * dy - (curve.P2.y - curve.P3.y) * dx); - d2 = (d2 >= 0) ? d2 : -d2; - d3 = (d3 >= 0) ? d3 : -d3; - if ((d2 + d3) * (d2 + d3) < TesselationTollerance * (dx * dx + dy * dy)) - { - Commit(curve.P3, ImCubicBezierTangent(curve, 1.0f)); - } - else if (level < 10) - { - const auto p12 = (curve.P0 + curve.P1) * 0.5f; - const auto p23 = (curve.P1 + curve.P2) * 0.5f; - const auto p34 = (curve.P2 + curve.P3) * 0.5f; - const auto p123 = (p12 + p23) * 0.5f; - const auto p234 = (p23 + p34) * 0.5f; - const auto p1234 = (p123 + p234) * 0.5f; - - Subdivide(ImCubicBezierPoints { curve.P0, p12, p123, p1234 }, level + 1); - Subdivide(ImCubicBezierPoints { p1234, p234, p34, curve.P3 }, level + 1); - } - } - }; - - if (tess_tol < 0) - tess_tol = 1.118f; // sqrtf(1.25f) - - Tesselator tesselator; - tesselator.Callback = callback; - tesselator.UserPointer = user_pointer; - tesselator.TesselationTollerance = tess_tol * tess_tol; - tesselator.Flags = flags; - - if (!(tesselator.Flags & ImCubicBezierSubdivide_SkipFirst)) - tesselator.Commit(curve.P0, ImCubicBezierTangent(curve, 0.0f)); - - tesselator.Subdivide(curve, 0); -} - -template inline void ImCubicBezierSubdivide(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float tess_tol, ImCubicBezierSubdivideFlags flags) -{ - auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer) - { - auto& callback = *reinterpret_cast(user_pointer); - callback(p); - }; - - ImCubicBezierSubdivide(handler, &callback, ImCubicBezierPoints{ p0, p1, p2, p3 }, tess_tol, flags); -} - -template inline void ImCubicBezierSubdivide(F& callback, const ImCubicBezierPoints& curve, float tess_tol, ImCubicBezierSubdivideFlags flags) -{ - auto handler = [](const ImCubicBezierSubdivideSample& p, void* user_pointer) - { - auto& callback = *reinterpret_cast(user_pointer); - callback(p); - }; - - ImCubicBezierSubdivide(handler, &callback, curve, tess_tol, flags); -} - -inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error) -{ - if (step <= 0.0f || !callback || max_value_error <= 0 || max_t_error <= 0) - return; - - ImCubicBezierFixedStepSample sample; - sample.T = 0.0f; - sample.Length = 0.0f; - sample.Point = p0; - sample.BreakSearch = false; - - callback(sample, user_pointer); - if (sample.BreakSearch) - return; - - const auto total_length = ImCubicBezierLength(p0, p1, p2, p3); - const auto point_count = static_cast(total_length / step) + (overshoot ? 2 : 1); - const auto t_min = 0.0f; - const auto t_max = step * point_count / total_length; - const auto t_0 = (t_min + t_max) * 0.5f; - - // #todo: replace map with ImVector + binary search - std::map cache; - for (int point_index = 1; point_index < point_count; ++point_index) - { - const auto targetLength = point_index * step; - - float t_start = t_min; - float t_end = t_max; - float t = t_0; - - float t_best = t; - float error_best = total_length; - - while (true) - { - auto cacheIt = cache.find(t); - if (cacheIt == cache.end()) - { - const auto front = ImCubicBezierSplit(p0, p1, p2, p3, t).Left; - const auto split_length = ImCubicBezierLength(front); - - cacheIt = cache.emplace(t, split_length).first; - } - - const auto length = cacheIt->second; - const auto error = targetLength - length; - - if (error < error_best) - { - error_best = error; - t_best = t; - } - - if (ImFabs(error) <= max_value_error || ImFabs(t_start - t_end) <= max_t_error) - { - sample.T = t; - sample.Length = length; - sample.Point = ImCubicBezier(p0, p1, p2, p3, t); - - callback(sample, user_pointer); - if (sample.BreakSearch) - return; - - break; - } - else if (error < 0.0f) - t_end = t; - else // if (error > 0.0f) - t_start = t; - - t = (t_start + t_end) * 0.5f; - } - } -} - -inline void ImCubicBezierFixedStep(ImCubicBezierFixedStepCallback callback, void* user_pointer, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error) -{ - ImCubicBezierFixedStep(callback, user_pointer, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error); -} - -// F has signature void(const ImCubicBezierFixedStepSample& p) -template -inline void ImCubicBezierFixedStep(F& callback, const ImVec2& p0, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float step, bool overshoot, float max_value_error, float max_t_error) -{ - auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer) - { - auto& callback = *reinterpret_cast(user_pointer); - callback(sample); - }; - - ImCubicBezierFixedStep(handler, &callback, p0, p1, p2, p3, step, overshoot, max_value_error, max_t_error); -} - -template -inline void ImCubicBezierFixedStep(F& callback, const ImCubicBezierPoints& curve, float step, bool overshoot, float max_value_error, float max_t_error) -{ - auto handler = [](ImCubicBezierFixedStepSample& sample, void* user_pointer) - { - auto& callback = *reinterpret_cast(user_pointer); - callback(sample); - }; - - ImCubicBezierFixedStep(handler, &callback, curve.P0, curve.P1, curve.P2, curve.P3, step, overshoot, max_value_error, max_t_error); -} - - -//------------------------------------------------------------------------------ -# endif // __IMGUI_BEZIER_MATH_INL__ diff --git a/symmetri/gui/extensions/imgui_canvas.cpp b/symmetri/gui/extensions/imgui_canvas.cpp deleted file mode 100644 index bfa5957..0000000 --- a/symmetri/gui/extensions/imgui_canvas.cpp +++ /dev/null @@ -1,575 +0,0 @@ -#ifndef IMGUI_DEFINE_MATH_OPERATORS -#define IMGUI_DEFINE_MATH_OPERATORS -#endif -#include "imgui_canvas.h" - -#include - -// https://stackoverflow.com/a/36079786 -#define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \ - \ - template \ - class __trait_name__ { \ - using check_type = ::std::remove_const_t<__boost_has_member_T__>; \ - struct no_type { \ - char x[2]; \ - }; \ - using yes_type = char; \ - \ - struct base { \ - void __member_name__() {} \ - }; \ - struct mixin : public base, public check_type {}; \ - \ - template \ - struct aux {}; \ - \ - template \ - static no_type test(aux<&U::__member_name__>*); \ - template \ - static yes_type test(...); \ - \ - public: \ - static constexpr bool value = \ - (sizeof(yes_type) == sizeof(test(0))); \ - } - -// Special sentinel value. This needs to be unique, so allow it to be overridden -// in the user's ImGui config -#ifndef ImDrawCallback_ImCanvas -#define ImDrawCallback_ImCanvas (ImDrawCallback)(-2) -#endif - -namespace ImCanvasDetails { - -DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale); - -struct FringeScaleRef { - // Overload is present when ImDrawList does have _FringeScale member variable. - template - static float& Get( - typename std::enable_if::value, T>::type* drawList) { - return drawList->_FringeScale; - } - - // Overload is present when ImDrawList does not have _FringeScale member - // variable. - template - static float& Get( - typename std::enable_if::value, T>::type*) { - static float placeholder = 1.0f; - return placeholder; - } -}; - -DECLARE_HAS_MEMBER(HasVtxCurrentOffset, _VtxCurrentOffset); - -struct VtxCurrentOffsetRef { - // Overload is present when ImDrawList does have _FringeScale member variable. - template - static unsigned int& Get( - typename std::enable_if::value, T>::type* - drawList) { - return drawList->_VtxCurrentOffset; - } - - // Overload is present when ImDrawList does not have _FringeScale member - // variable. - template - static unsigned int& Get( - typename std::enable_if::value, T>::type* - drawList) { - return drawList->_CmdHeader.VtxOffset; - } -}; - -} // namespace ImCanvasDetails - -// Returns a reference to _FringeScale extension to ImDrawList -// -// If ImDrawList does not have _FringeScale a placeholder is returned. -static inline float& ImFringeScaleRef(ImDrawList* drawList) { - using namespace ImCanvasDetails; - return FringeScaleRef::Get(drawList); -} - -static inline unsigned int& ImVtxOffsetRef(ImDrawList* drawList) { - using namespace ImCanvasDetails; - return VtxCurrentOffsetRef::Get(drawList); -} - -static inline ImVec2 ImSelectPositive(const ImVec2& lhs, const ImVec2& rhs) { - return ImVec2(lhs.x > 0.0f ? lhs.x : rhs.x, lhs.y > 0.0f ? lhs.y : rhs.y); -} - -bool ImGuiEx::Canvas::Begin(const char* id, const ImVec2& size) { - return Begin(ImGui::GetID(id), size); -} - -bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size) { - IM_ASSERT(m_InBeginEnd == false); - - m_WidgetPosition = ImGui::GetCursorScreenPos(); - m_WidgetSize = ImSelectPositive(size, ImGui::GetContentRegionAvail()); - m_WidgetRect = ImRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize); - m_DrawList = ImGui::GetWindowDrawList(); - - UpdateViewTransformPosition(); - -#if IMGUI_VERSION_NUM > 18415 - if (ImGui::IsClippedEx(m_WidgetRect, id)) return false; -#else - if (ImGui::IsClippedEx(m_WidgetRect, id, false)) return false; -#endif - - // Save current channel, so we can assert when user - // call canvas API with different one. - m_ExpectedChannel = m_DrawList->_Splitter._Current; - - // #debug: Canvas content. - // m_DrawList->AddRectFilled(m_StartPos, m_StartPos + m_CurrentSize, - // IM_COL32(0, 0, 0, 64)); m_DrawList->AddRect(m_WidgetRect.Min, - // m_WidgetRect.Max, IM_COL32(255, 0, 255, 64)); - - ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f)); - -#if IMGUI_EX_CANVAS_DEFERED() - m_Ranges.resize(0); -#endif - - SaveInputState(); - SaveViewportState(); - - // Record cursor max to prevent scrollbars from appearing. - m_WindowCursorMaxBackup = ImGui::GetCurrentWindow()->DC.CursorMaxPos; - - EnterLocalSpace(); - -#if IMGUI_VERSION_NUM >= 18967 - ImGui::SetNextItemAllowOverlap(); -#endif - - // Emit dummy widget matching bounds of the canvas. - ImGui::SetCursorScreenPos(m_ViewRect.Min); - ImGui::Dummy(m_ViewRect.GetSize()); - - ImGui::SetCursorScreenPos(ImVec2(0.0f, 0.0f)); - - m_InBeginEnd = true; - - return true; -} - -void ImGuiEx::Canvas::End() { - // If you're here your call to Begin() returned false, - // or Begin() wasn't called at all. - IM_ASSERT(m_InBeginEnd == true); - - // If you're here, please make sure you do not interleave - // channel splitter with canvas. - // Always call canvas function with using same channel. - IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); - - // auto& io = ImGui::GetIO(); - - // Check: Unmatched calls to Suspend() / Resume(). Please check your code. - IM_ASSERT(m_SuspendCounter == 0); - - LeaveLocalSpace(); - - ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup; - -#if IMGUI_VERSION_NUM < 18967 - ImGui::SetItemAllowOverlap(); -#endif - - // Emit dummy widget matching bounds of the canvas. - ImGui::SetCursorScreenPos(m_WidgetPosition); - ImGui::Dummy(m_WidgetSize); - - // #debug: Rect around canvas. Content should be inside these bounds. - // m_DrawList->AddRect(m_WidgetPosition - ImVec2(1.0f, 1.0f), m_WidgetPosition - // + m_WidgetSize + ImVec2(1.0f, 1.0f), IM_COL32(196, 0, 0, 255)); - - m_InBeginEnd = false; -} - -void ImGuiEx::Canvas::SetView(const ImVec2& origin, float scale) { - SetView(CanvasView(origin, scale)); -} - -void ImGuiEx::Canvas::SetView(const CanvasView& view) { - if (m_InBeginEnd) LeaveLocalSpace(); - - if (m_View.Origin.x != view.Origin.x || m_View.Origin.y != view.Origin.y) { - m_View.Origin = view.Origin; - - UpdateViewTransformPosition(); - } - - if (m_View.Scale != view.Scale) { - m_View.Scale = view.Scale; - m_View.InvScale = view.InvScale; - } - - if (m_InBeginEnd) EnterLocalSpace(); -} - -void ImGuiEx::Canvas::CenterView(const ImVec2& canvasPoint) { - auto view = CalcCenterView(canvasPoint); - SetView(view); -} - -ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView( - const ImVec2& canvasPoint) const { - auto localCenter = ToLocal(m_WidgetPosition + m_WidgetSize * 0.5f); - auto localOffset = canvasPoint - localCenter; - auto offset = FromLocalV(localOffset); - - return CanvasView{m_View.Origin - offset, m_View.Scale}; -} - -void ImGuiEx::Canvas::CenterView(const ImRect& canvasRect) { - auto view = CalcCenterView(canvasRect); - - SetView(view); -} - -ImGuiEx::CanvasView ImGuiEx::Canvas::CalcCenterView( - const ImRect& canvasRect) const { - auto canvasRectSize = canvasRect.GetSize(); - - if (canvasRectSize.x <= 0.0f || canvasRectSize.y <= 0.0f) return View(); - - auto widgetAspectRatio = - m_WidgetSize.y > 0.0f ? m_WidgetSize.x / m_WidgetSize.y : 0.0f; - auto canvasRectAspectRatio = - canvasRectSize.y > 0.0f ? canvasRectSize.x / canvasRectSize.y : 0.0f; - - if (widgetAspectRatio <= 0.0f || canvasRectAspectRatio <= 0.0f) return View(); - - auto newOrigin = m_View.Origin; - auto newScale = m_View.Scale; - if (canvasRectAspectRatio > widgetAspectRatio) { - // width span across view - newScale = m_WidgetSize.x / canvasRectSize.x; - newOrigin = canvasRect.Min * -newScale; - newOrigin.y += (m_WidgetSize.y - canvasRectSize.y * newScale) * 0.5f; - } else { - // height span across view - newScale = m_WidgetSize.y / canvasRectSize.y; - newOrigin = canvasRect.Min * -newScale; - newOrigin.x += (m_WidgetSize.x - canvasRectSize.x * newScale) * 0.5f; - } - - return CanvasView{newOrigin, newScale}; -} - -void ImGuiEx::Canvas::Suspend() { - // If you're here, please make sure you do not interleave - // channel splitter with canvas. - // Always call canvas function with using same channel. - IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); - - if (m_SuspendCounter == 0) LeaveLocalSpace(); - - ++m_SuspendCounter; -} - -void ImGuiEx::Canvas::Resume() { - // If you're here, please make sure you do not interleave - // channel splitter with canvas. - // Always call canvas function with using same channel. - IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); - - // Check: Number of calls to Resume() do not match calls to Suspend(). Please - // check your code. - IM_ASSERT(m_SuspendCounter > 0); - if (--m_SuspendCounter == 0) EnterLocalSpace(); -} - -ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point) const { - return point * m_View.Scale + m_ViewTransformPosition; -} - -ImVec2 ImGuiEx::Canvas::FromLocal(const ImVec2& point, - const CanvasView& view) const { - return point * view.Scale + view.Origin + m_WidgetPosition; -} - -ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector) const { - return vector * m_View.Scale; -} - -ImVec2 ImGuiEx::Canvas::FromLocalV(const ImVec2& vector, - const CanvasView& view) const { - return vector * view.Scale; -} - -ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point) const { - return (point - m_ViewTransformPosition) * m_View.InvScale; -} - -ImVec2 ImGuiEx::Canvas::ToLocal(const ImVec2& point, - const CanvasView& view) const { - return (point - view.Origin - m_WidgetPosition) * view.InvScale; -} - -ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector) const { - return vector * m_View.InvScale; -} - -ImVec2 ImGuiEx::Canvas::ToLocalV(const ImVec2& vector, - const CanvasView& view) const { - return vector * view.InvScale; -} - -ImRect ImGuiEx::Canvas::CalcViewRect(const CanvasView& view) const { - ImRect result; - result.Min = ImVec2(-view.Origin.x, -view.Origin.y) * view.InvScale; - result.Max = (m_WidgetSize - view.Origin) * view.InvScale; - return result; -} - -void ImGuiEx::Canvas::UpdateViewTransformPosition() { - m_ViewTransformPosition = m_View.Origin + m_WidgetPosition; -} - -void ImGuiEx::Canvas::SaveInputState() { - auto& io = ImGui::GetIO(); - m_MousePosBackup = io.MousePos; - m_MousePosPrevBackup = io.MousePosPrev; - for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) - m_MouseClickedPosBackup[i] = io.MouseClickedPos[i]; -} - -void ImGuiEx::Canvas::RestoreInputState() { - auto& io = ImGui::GetIO(); - io.MousePos = m_MousePosBackup; - io.MousePosPrev = m_MousePosPrevBackup; - for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) - io.MouseClickedPos[i] = m_MouseClickedPosBackup[i]; -} - -void ImGuiEx::Canvas::SaveViewportState() { -#if defined(IMGUI_HAS_VIEWPORT) - auto window = ImGui::GetCurrentWindow(); - auto viewport = ImGui::GetWindowViewport(); - - m_WindowPosBackup = window->Pos; - m_ViewportPosBackup = viewport->Pos; - m_ViewportSizeBackup = viewport->Size; -#if IMGUI_VERSION_NUM > 18002 - m_ViewportWorkPosBackup = viewport->WorkPos; - m_ViewportWorkSizeBackup = viewport->WorkSize; -#else - m_ViewportWorkOffsetMinBackup = viewport->WorkOffsetMin; - m_ViewportWorkOffsetMaxBackup = viewport->WorkOffsetMax; -#endif -#endif -} - -void ImGuiEx::Canvas::RestoreViewportState() { -#if defined(IMGUI_HAS_VIEWPORT) - auto window = ImGui::GetCurrentWindow(); - auto viewport = ImGui::GetWindowViewport(); - - window->Pos = m_WindowPosBackup; - viewport->Pos = m_ViewportPosBackup; - viewport->Size = m_ViewportSizeBackup; -#if IMGUI_VERSION_NUM > 18002 - viewport->WorkPos = m_ViewportWorkPosBackup; - viewport->WorkSize = m_ViewportWorkSizeBackup; -#else - viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup; - viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup; -#endif -#endif -} - -void ImGuiEx::Canvas::EnterLocalSpace() { - // Prepare ImDrawList for drawing in local coordinate system: - // - determine visible part of the canvas - // - start unique draw command - // - add clip rect matching canvas size - // - record current command index - // - record current vertex write index - - // Determine visible part of the canvas. Make it before - // adding new command, to avoid round rip where command - // is removed in PopClipRect() and added again next PushClipRect(). - ImGui::PushClipRect(m_WidgetPosition, m_WidgetPosition + m_WidgetSize, true); - auto clipped_clip_rect = m_DrawList->_ClipRectStack.back(); - ImGui::PopClipRect(); - -#if IMGUI_EX_CANVAS_DEFERED() - m_Ranges.resize(m_Ranges.Size + 1); - m_CurrentRange = &m_Ranges.back(); - m_CurrentRange->BeginComandIndex = ImMax(m_DrawList->CmdBuffer.Size, 0); - m_CurrentRange->BeginVertexIndex = - m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList); -#endif - m_DrawListCommadBufferSize = ImMax(m_DrawList->CmdBuffer.Size, 0); - m_DrawListStartVertexIndex = - m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList); - - // Make sure we do not share draw command with anyone. We don't want to mess - // with someones clip rectangle. - - // #FIXME: - // This condition is not enough to avoid when user choose - // to use channel splitter. - // - // To deal with Suspend()/Resume() calls empty draw command - // is always added then splitter is active. Otherwise - // channel merger will collapse our draw command one with - // different clip rectangle. - // - // More investigation is needed. To get to the bottom of this. - if ((!m_DrawList->CmdBuffer.empty() && - m_DrawList->CmdBuffer.back().ElemCount > 0) || - m_DrawList->_Splitter._Count > 1) - m_DrawList->AddCallback(ImDrawCallback_ImCanvas, nullptr); - -#if defined(IMGUI_HAS_VIEWPORT) - auto window = ImGui::GetCurrentWindow(); - window->Pos = ImVec2(0.0f, 0.0f); - - auto viewport_min = m_ViewportPosBackup; - auto viewport_max = m_ViewportPosBackup + m_ViewportSizeBackup; - - viewport_min.x = - (viewport_min.x - m_ViewTransformPosition.x) * m_View.InvScale; - viewport_min.y = - (viewport_min.y - m_ViewTransformPosition.y) * m_View.InvScale; - viewport_max.x = - (viewport_max.x - m_ViewTransformPosition.x) * m_View.InvScale; - viewport_max.y = - (viewport_max.y - m_ViewTransformPosition.y) * m_View.InvScale; - - auto viewport = ImGui::GetWindowViewport(); - viewport->Pos = viewport_min; - viewport->Size = viewport_max - viewport_min; - -#if IMGUI_VERSION_NUM > 18002 - viewport->WorkPos = m_ViewportWorkPosBackup * m_View.InvScale; - viewport->WorkSize = m_ViewportWorkSizeBackup * m_View.InvScale; -#else - viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup * m_View.InvScale; - viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup * m_View.InvScale; -#endif -#endif - - // Clip rectangle in parent canvas space and move it to local space. - clipped_clip_rect.x = - (clipped_clip_rect.x - m_ViewTransformPosition.x) * m_View.InvScale; - clipped_clip_rect.y = - (clipped_clip_rect.y - m_ViewTransformPosition.y) * m_View.InvScale; - clipped_clip_rect.z = - (clipped_clip_rect.z - m_ViewTransformPosition.x) * m_View.InvScale; - clipped_clip_rect.w = - (clipped_clip_rect.w - m_ViewTransformPosition.y) * m_View.InvScale; - ImGui::PushClipRect(ImVec2(clipped_clip_rect.x, clipped_clip_rect.y), - ImVec2(clipped_clip_rect.z, clipped_clip_rect.w), false); - - // Transform mouse position to local space. - auto& io = ImGui::GetIO(); - io.MousePos = (m_MousePosBackup - m_ViewTransformPosition) * m_View.InvScale; - io.MousePosPrev = - (m_MousePosPrevBackup - m_ViewTransformPosition) * m_View.InvScale; - for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) - io.MouseClickedPos[i] = - (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * - m_View.InvScale; - - m_ViewRect = CalcViewRect(m_View); - ; - - auto& fringeScale = ImFringeScaleRef(m_DrawList); - m_LastFringeScale = fringeScale; - fringeScale *= m_View.InvScale; -} - -void ImGuiEx::Canvas::LeaveLocalSpace() { - IM_ASSERT(m_DrawList->_Splitter._Current == m_ExpectedChannel); - -#if IMGUI_EX_CANVAS_DEFERED() - IM_ASSERT(m_CurrentRange != nullptr); - - m_CurrentRange->EndVertexIndex = - m_DrawList->_VtxCurrentIdx + ImVtxOffsetRef(m_DrawList); - m_CurrentRange->EndCommandIndex = m_DrawList->CmdBuffer.size(); - if (m_CurrentRange->BeginVertexIndex == m_CurrentRange->EndVertexIndex) { - // Drop empty range - m_Ranges.resize(m_Ranges.Size - 1); - } - m_CurrentRange = nullptr; -#endif - - // Move vertices to screen space. - auto vertex = m_DrawList->VtxBuffer.Data + m_DrawListStartVertexIndex; - auto vertexEnd = m_DrawList->VtxBuffer.Data + m_DrawList->_VtxCurrentIdx + - ImVtxOffsetRef(m_DrawList); - - // If canvas view is not scaled take a faster path. - if (m_View.Scale != 1.0f) { - while (vertex < vertexEnd) { - vertex->pos.x = vertex->pos.x * m_View.Scale + m_ViewTransformPosition.x; - vertex->pos.y = vertex->pos.y * m_View.Scale + m_ViewTransformPosition.y; - ++vertex; - } - - // Move clip rectangles to screen space. - for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); - ++i) { - auto& command = m_DrawList->CmdBuffer[i]; - command.ClipRect.x = - command.ClipRect.x * m_View.Scale + m_ViewTransformPosition.x; - command.ClipRect.y = - command.ClipRect.y * m_View.Scale + m_ViewTransformPosition.y; - command.ClipRect.z = - command.ClipRect.z * m_View.Scale + m_ViewTransformPosition.x; - command.ClipRect.w = - command.ClipRect.w * m_View.Scale + m_ViewTransformPosition.y; - } - } else { - while (vertex < vertexEnd) { - vertex->pos.x = vertex->pos.x + m_ViewTransformPosition.x; - vertex->pos.y = vertex->pos.y + m_ViewTransformPosition.y; - ++vertex; - } - - // Move clip rectangles to screen space. - for (int i = m_DrawListCommadBufferSize; i < m_DrawList->CmdBuffer.size(); - ++i) { - auto& command = m_DrawList->CmdBuffer[i]; - command.ClipRect.x = command.ClipRect.x + m_ViewTransformPosition.x; - command.ClipRect.y = command.ClipRect.y + m_ViewTransformPosition.y; - command.ClipRect.z = command.ClipRect.z + m_ViewTransformPosition.x; - command.ClipRect.w = command.ClipRect.w + m_ViewTransformPosition.y; - } - } - - // Remove sentinel draw command if present - if (m_DrawListCommadBufferSize > 0) { - if (m_DrawList->CmdBuffer.size() > m_DrawListCommadBufferSize && - m_DrawList->CmdBuffer[m_DrawListCommadBufferSize].UserCallback == - ImDrawCallback_ImCanvas) - m_DrawList->CmdBuffer.erase(m_DrawList->CmdBuffer.Data + - m_DrawListCommadBufferSize); - else if (m_DrawList->CmdBuffer.size() >= m_DrawListCommadBufferSize && - m_DrawList->CmdBuffer[m_DrawListCommadBufferSize - 1] - .UserCallback == ImDrawCallback_ImCanvas) - m_DrawList->CmdBuffer.erase(m_DrawList->CmdBuffer.Data + - m_DrawListCommadBufferSize - 1); - } - - auto& fringeScale = ImFringeScaleRef(m_DrawList); - fringeScale = m_LastFringeScale; - - // And pop \o/ - ImGui::PopClipRect(); - - RestoreInputState(); - RestoreViewportState(); -} diff --git a/symmetri/gui/extensions/imgui_canvas.h b/symmetri/gui/extensions/imgui_canvas.h deleted file mode 100644 index 0ef5cf5..0000000 --- a/symmetri/gui/extensions/imgui_canvas.h +++ /dev/null @@ -1,272 +0,0 @@ -// Canvas widget - view over infinite virtual space. -// -// Canvas allows you to draw your widgets anywhere over infinite space and -// provide view over it with support for panning and scaling. -// -// When you enter a canvas ImGui is moved to virtual space which mean: -// - ImGui::GetCursorScreenPos() return (0, 0) and which correspond to top -// left corner -// of the canvas on the screen (this can be changed using CanvasView()). -// - Mouse input is brought to canvas space, so widgets works as usual. -// - Everything you draw with ImDrawList will be in virtual space. -// -// By default origin point is on top left corner of canvas widget. It can be -// changed with call to CanvasView() where you can specify what part of space -// should be viewed by setting viewport origin point and scale. Current state -// can be queried with CanvasViewOrigin() and CanvasViewScale(). -// -// Viewport size is controlled by 'size' parameter in BeginCanvas(). You can -// query it using CanvasContentMin/Max/Size functions. They are useful if you to -// not specify canvas size in which case all free space is used. -// -// Bounds of visible region of infinite space can be queried using -// CanvasViewMin/Max/Size functions. Everything that is drawn outside of this -// region will be clipped as usual in ImGui. -// -// While drawing inside canvas you can translate position from world (usual -// ImGui space) to virtual space and back using -// CanvasFromWorld()/CanvasToWorld(). -// -// Canvas can be nested in each other (they are regular widgets after all). -// There is a way to transform position between current and parent canvas with -// CanvasFromParent()/CanvasToParent(). -// -// Sometimes in more elaborate scenarios you want to move out canvas virtual -// space, do something and came back. You can do that with SuspendCanvas() and -// ResumeCanvas(). -// -// Note: -// It is not valid to call canvas API outside of BeginCanvas() / EndCanvas() -// scope. -// -// VERSION 0.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -#ifndef __IMGUI_EX_CANVAS_H__ -#define __IMGUI_EX_CANVAS_H__ -#pragma once - -#include -#include // ImRect, ImFloor - -#ifndef IMGUIEX_CANVAS_API -#define IMGUIEX_CANVAS_API -#endif - -namespace ImGuiEx { - -struct CanvasView { - ImVec2 Origin; - float Scale = 1.0f; - float InvScale = 1.0f; - - CanvasView() = default; - CanvasView(const ImVec2& origin, float scale) - : Origin(origin), Scale(scale), InvScale(scale ? 1.0f / scale : 0.0f) {} - - void Set(const ImVec2& origin, float scale) { - *this = CanvasView(origin, scale); - } -}; - -// Canvas widget represent view over infinite plane. -// -// It acts like a child window without scroll bars with -// ability to zoom to specific part of canvas plane. -// -// Widgets are clipped according to current view exactly -// same way ImGui do. To avoid `missing widgets` artifacts first -// setup visible region with SetView() then draw content. -// -// Everything drawn with ImDrawList betwen calls to Begin()/End() -// will be drawn on canvas plane. This behavior can be suspended -// by calling Suspend() and resumed by calling Resume(). -// -// Warning: -// Please do not interleave canvas with use of channel splitter. -// Keep channel splitter contained inside canvas or always -// call canvas functions from same channel. -struct Canvas { - // Begins drawing content of canvas plane. - // - // When false is returned that mean canvas is not visible to the - // user can drawing should be skipped and End() not called. - // When true is returned drawing must be ended with call to End(). - // - // If any size component is equal to zero or less canvas will - // automatically expand to all available area on that axis. - // So (0, 300) will take horizontal space and have height - // of 300 points. (0, 0) will take all remaining space of - // the window. - // - // You can query size of the canvas while it is being drawn - // by calling Rect(). - IMGUIEX_CANVAS_API bool Begin(const char* id, const ImVec2& size); - IMGUIEX_CANVAS_API bool Begin(ImGuiID id, const ImVec2& size); - - // Ends interaction with canvas plane. - // - // Must be called only when Begin() retuned true. - IMGUIEX_CANVAS_API void End(); - - // Sets visible region of canvas plane. - // - // Origin is an offset of infinite plane origin from top left - // corner of the canvas. - // - // Scale greater than 1 make canvas content be bigger, less than 1 smaller. - IMGUIEX_CANVAS_API void SetView(const ImVec2& origin, float scale); - IMGUIEX_CANVAS_API void SetView(const CanvasView& view); - - // Centers view over specific point on canvas plane. - // - // View will be centered on specific point by changing origin - // but not scale. - IMGUIEX_CANVAS_API void CenterView(const ImVec2& canvasPoint); - - // Calculates view over specific point on canvas plane. - IMGUIEX_CANVAS_API CanvasView CalcCenterView(const ImVec2& canvasPoint) const; - - // Centers view over specific rectangle on canvas plane. - // - // Whole rectangle will fit in canvas view. This will affect both - // origin and scale. - IMGUIEX_CANVAS_API void CenterView(const ImRect& canvasRect); - - // Calculates view over specific rectangle on canvas plane. - IMGUIEX_CANVAS_API CanvasView CalcCenterView(const ImRect& canvasRect) const; - - // Suspends canvas by returning to normal ImGui transformation space. - // While suspended UI will not be drawn on canvas plane. - // - // Calls to Suspend()/Resume() are symetrical. Each call to Suspend() - // must be matched with call to Resume(). - IMGUIEX_CANVAS_API void Suspend(); - IMGUIEX_CANVAS_API void Resume(); - - // Transforms point from canvas plane to ImGui. - IMGUIEX_CANVAS_API ImVec2 FromLocal(const ImVec2& point) const; - IMGUIEX_CANVAS_API ImVec2 FromLocal(const ImVec2& point, - const CanvasView& view) const; - - // Transforms vector from canvas plant to ImGui. - IMGUIEX_CANVAS_API ImVec2 FromLocalV(const ImVec2& vector) const; - IMGUIEX_CANVAS_API ImVec2 FromLocalV(const ImVec2& vector, - const CanvasView& view) const; - - // Transforms point from ImGui to canvas plane. - IMGUIEX_CANVAS_API ImVec2 ToLocal(const ImVec2& point) const; - IMGUIEX_CANVAS_API ImVec2 ToLocal(const ImVec2& point, - const CanvasView& view) const; - - // Transforms vector from ImGui to canvas plane. - IMGUIEX_CANVAS_API ImVec2 ToLocalV(const ImVec2& vector) const; - IMGUIEX_CANVAS_API ImVec2 ToLocalV(const ImVec2& vector, - const CanvasView& view) const; - - // Returns widget bounds. - // - // Note: - // Rect is valid after call to Begin(). - const ImRect& Rect() const { return m_WidgetRect; } - - // Returns visible region on canvas plane (in canvas plane coordinates). - const ImRect& ViewRect() const { return m_ViewRect; } - - // Calculates visible region for view. - IMGUIEX_CANVAS_API ImRect CalcViewRect(const CanvasView& view) const; - - // Returns current view. - const CanvasView& View() const { return m_View; } - - // Returns origin of the view. - // - // Origin is an offset of infinite plane origin from top left - // corner of the canvas. - const ImVec2& ViewOrigin() const { return m_View.Origin; } - - // Returns scale of the view. - float ViewScale() const { return m_View.Scale; } - - // Returns true if canvas is suspended. - // - // See: Suspend()/Resume() - bool IsSuspended() const { return m_SuspendCounter > 0; } - - private: -#define IMGUI_EX_CANVAS_DEFERED() 0 - -#if IMGUI_EX_CANVAS_DEFERED() - struct Range { - int BeginVertexIndex = 0; - int EndVertexIndex = 0; - int BeginComandIndex = 0; - int EndCommandIndex = 0; - }; -#endif - - void UpdateViewTransformPosition(); - - void SaveInputState(); - void RestoreInputState(); - - void SaveViewportState(); - void RestoreViewportState(); - - void EnterLocalSpace(); - void LeaveLocalSpace(); - - bool m_InBeginEnd = false; - - ImVec2 m_WidgetPosition; - ImVec2 m_WidgetSize; - ImRect m_WidgetRect; - - ImDrawList* m_DrawList = nullptr; - int m_ExpectedChannel = 0; - -#if IMGUI_EX_CANVAS_DEFERED() - ImVector m_Ranges; - Range* m_CurrentRange = nullptr; -#endif - - int m_DrawListCommadBufferSize = 0; - int m_DrawListStartVertexIndex = 0; - - CanvasView m_View; - ImRect m_ViewRect; - - ImVec2 m_ViewTransformPosition; - - int m_SuspendCounter = 0; - - float m_LastFringeScale = 1.0f; - - ImVec2 m_MousePosBackup; - ImVec2 m_MousePosPrevBackup; - ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)]; - ImVec2 m_WindowCursorMaxBackup; - -#if defined(IMGUI_HAS_VIEWPORT) - ImVec2 m_WindowPosBackup; - ImVec2 m_ViewportPosBackup; - ImVec2 m_ViewportSizeBackup; -#if IMGUI_VERSION_NUM > 18002 - ImVec2 m_ViewportWorkPosBackup; - ImVec2 m_ViewportWorkSizeBackup; -#else - ImVec2 m_ViewportWorkOffsetMinBackup; - ImVec2 m_ViewportWorkOffsetMaxBackup; -#endif -#endif -}; - -} // namespace ImGuiEx - -#endif // __IMGUI_EX_CANVAS_H__ diff --git a/symmetri/gui/extensions/imgui_extra_math.h b/symmetri/gui/extensions/imgui_extra_math.h deleted file mode 100644 index d28e2ca..0000000 --- a/symmetri/gui/extensions/imgui_extra_math.h +++ /dev/null @@ -1,67 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.9.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -#ifndef __IMGUI_EXTRA_MATH_H__ -#define __IMGUI_EXTRA_MATH_H__ -#pragma once - -//------------------------------------------------------------------------------ -#ifndef IMGUI_DEFINE_MATH_OPERATORS -#define IMGUI_DEFINE_MATH_OPERATORS -#endif -#include -#include - -//------------------------------------------------------------------------------ -struct ImLine { - ImVec2 A, B; -}; - -//------------------------------------------------------------------------------ -inline bool operator==(const ImVec2& lhs, const ImVec2& rhs); -inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs); -inline ImVec2 operator*(const float lhs, const ImVec2& rhs); -#if IMGUI_VERSION_NUM < 18955 -inline ImVec2 operator-(const ImVec2& lhs); -#endif - -//------------------------------------------------------------------------------ -inline float ImLength(float v); -inline float ImLength(const ImVec2& v); -inline float ImLengthSqr(float v); -inline ImVec2 ImNormalized(const ImVec2& v); - -//------------------------------------------------------------------------------ -inline bool ImRect_IsEmpty(const ImRect& rect); -inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, - bool snap_to_edge); -inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, - bool snap_to_edge, float radius); -inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& b); -inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b); -inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, - float radius_a, float radius_b); - -//------------------------------------------------------------------------------ -namespace ImEasing { - -template -inline V EaseOutQuad(V b, V c, T t) { - return b - c * (t * (t - 2)); -} - -} // namespace ImEasing - -//------------------------------------------------------------------------------ -#include "imgui_extra_math.inl" - -//------------------------------------------------------------------------------ -#endif // __IMGUI_EXTRA_MATH_H__ diff --git a/symmetri/gui/extensions/imgui_extra_math.inl b/symmetri/gui/extensions/imgui_extra_math.inl deleted file mode 100644 index 8b1b071..0000000 --- a/symmetri/gui/extensions/imgui_extra_math.inl +++ /dev/null @@ -1,191 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.9.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -# ifndef __IMGUI_EXTRA_MATH_INL__ -# define __IMGUI_EXTRA_MATH_INL__ -# pragma once - - -//------------------------------------------------------------------------------ -# include "imgui_extra_math.h" - - -//------------------------------------------------------------------------------ -inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) -{ - return lhs.x == rhs.x && lhs.y == rhs.y; -} - -inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) -{ - return lhs.x != rhs.x || lhs.y != rhs.y; -} - -inline ImVec2 operator*(const float lhs, const ImVec2& rhs) -{ - return ImVec2(lhs * rhs.x, lhs * rhs.y); -} - -# if IMGUI_VERSION_NUM < 18955 -inline ImVec2 operator-(const ImVec2& lhs) -{ - return ImVec2(-lhs.x, -lhs.y); -} -# endif - - -//------------------------------------------------------------------------------ -inline float ImLength(float v) -{ - return v; -} - -inline float ImLength(const ImVec2& v) -{ - return ImSqrt(ImLengthSqr(v)); -} - -inline float ImLengthSqr(float v) -{ - return v * v; -} - -inline ImVec2 ImNormalized(const ImVec2& v) -{ - return v * ImInvLength(v, 0.0f); -} - - - - -//------------------------------------------------------------------------------ -inline bool ImRect_IsEmpty(const ImRect& rect) -{ - return rect.Min.x >= rect.Max.x - || rect.Min.y >= rect.Max.y; -} - -inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge) -{ - if (!snap_to_edge && rect.Contains(p)) - return p; - - return ImVec2( - (p.x > rect.Max.x) ? rect.Max.x : (p.x < rect.Min.x ? rect.Min.x : p.x), - (p.y > rect.Max.y) ? rect.Max.y : (p.y < rect.Min.y ? rect.Min.y : p.y) - ); -} - -inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImVec2& p, bool snap_to_edge, float radius) -{ - auto point = ImRect_ClosestPoint(rect, p, snap_to_edge); - - const auto offset = p - point; - const auto distance_sq = offset.x * offset.x + offset.y * offset.y; - if (distance_sq <= 0) - return point; - - const auto distance = ImSqrt(distance_sq); - - return point + offset * (ImMin(distance, radius) * (1.0f / distance)); -} - -inline ImVec2 ImRect_ClosestPoint(const ImRect& rect, const ImRect& other) -{ - ImVec2 result; - if (other.Min.x >= rect.Max.x) - result.x = rect.Max.x; - else if (other.Max.x <= rect.Min.x) - result.x = rect.Min.x; - else - result.x = (ImMax(rect.Min.x, other.Min.x) + ImMin(rect.Max.x, other.Max.x)) / 2; - - if (other.Min.y >= rect.Max.y) - result.y = rect.Max.y; - else if (other.Max.y <= rect.Min.y) - result.y = rect.Min.y; - else - result.y = (ImMax(rect.Min.y, other.Min.y) + ImMin(rect.Max.y, other.Max.y)) / 2; - - return result; -} - -inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b) -{ - ImLine result; - result.A = ImRect_ClosestPoint(rect_a, rect_b); - result.B = ImRect_ClosestPoint(rect_b, rect_a); - - auto distribute = [](float& a, float& b, float a0, float a1, float b0, float b1) - { - if (a0 >= b1 || a1 <= b0) - return; - - const auto aw = a1 - a0; - const auto bw = b1 - b0; - - if (aw > bw) - { - b = b0 + bw - bw * (a - a0) / aw; - a = b; - } - else if (aw < bw) - { - a = a0 + aw - aw * (b - b0) / bw; - b = a; - } - }; - - distribute(result.A.x, result.B.x, rect_a.Min.x, rect_a.Max.x, rect_b.Min.x, rect_b.Max.x); - distribute(result.A.y, result.B.y, rect_a.Min.y, rect_a.Max.y, rect_b.Min.y, rect_b.Max.y); - - return result; -} - -inline ImLine ImRect_ClosestLine(const ImRect& rect_a, const ImRect& rect_b, float radius_a, float radius_b) -{ - auto line = ImRect_ClosestLine(rect_a, rect_b); - if (radius_a < 0) - radius_a = 0; - if (radius_b < 0) - radius_b = 0; - - if (radius_a == 0 && radius_b == 0) - return line; - - const auto offset = line.B - line.A; - const auto length_sq = offset.x * offset.x + offset.y * offset.y; - const auto radius_a_sq = radius_a * radius_a; - const auto radius_b_sq = radius_b * radius_b; - - if (length_sq <= 0) - return line; - - const auto length = ImSqrt(length_sq); - const auto direction = ImVec2(offset.x / length, offset.y / length); - - const auto total_radius_sq = radius_a_sq + radius_b_sq; - if (total_radius_sq > length_sq) - { - const auto scale = length / (radius_a + radius_b); - radius_a *= scale; - radius_b *= scale; - } - - line.A = line.A + (direction * radius_a); - line.B = line.B - (direction * radius_b); - - return line; -} - - -//------------------------------------------------------------------------------ -# endif // __IMGUI_EXTRA_MATH_INL__ diff --git a/symmetri/gui/extensions/imgui_node_editor.cpp b/symmetri/gui/extensions/imgui_node_editor.cpp deleted file mode 100644 index 7959a68..0000000 --- a/symmetri/gui/extensions/imgui_node_editor.cpp +++ /dev/null @@ -1,5368 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.9.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -#include -#include -#include -#include // snprintf -#include -#include -#include -#include -#include - -#include "imgui_node_editor_internal.h" - -// https://stackoverflow.com/a/8597498 -#define DECLARE_HAS_NESTED(Name, Member) \ - \ - template \ - struct has_nested_##Name { \ - typedef char yes; \ - typedef yes (&no)[2]; \ - \ - template \ - static yes test(decltype(U::Member)*); \ - template \ - static no test(...); \ - \ - static bool const value = sizeof(test(0)) == sizeof(yes); \ - }; - -namespace ax { -namespace NodeEditor { -namespace Detail { - -#if !defined(IMGUI_VERSION_NUM) || (IMGUI_VERSION_NUM < 18822) -#define DECLARE_KEY_TESTER(Key) \ - DECLARE_HAS_NESTED(Key, Key) \ - struct KeyTester_##Key { \ - template \ - static int Get(typename std::enable_if::value, \ - T>::type*) { \ - return ImGui::GetKeyIndex(T::Key); \ - } \ - \ - template \ - static int Get(typename std::enable_if< \ - !has_nested_##Key::value, T>::type*) { \ - return -1; \ - } \ - } - -DECLARE_KEY_TESTER(ImGuiKey_F); -DECLARE_KEY_TESTER(ImGuiKey_D); - -static inline int GetKeyIndexForF() { - return KeyTester_ImGuiKey_F::Get(nullptr); -} - -static inline int GetKeyIndexForD() { - return KeyTester_ImGuiKey_D::Get(nullptr); -} -#else -static inline ImGuiKey GetKeyIndexForF() { return ImGuiKey_F; } - -static inline ImGuiKey GetKeyIndexForD() { return ImGuiKey_D; } -#endif - -} // namespace Detail -} // namespace NodeEditor -} // namespace ax - -//------------------------------------------------------------------------------ -namespace ed = ax::NodeEditor::Detail; - -//------------------------------------------------------------------------------ -static const int c_BackgroundChannelCount = 1; -static const int c_LinkChannelCount = 4; -static const int c_UserLayersCount = 5; - -static const int c_UserLayerChannelStart = 0; -static const int c_BackgroundChannelStart = - c_UserLayerChannelStart + c_UserLayersCount; -static const int c_LinkStartChannel = - c_BackgroundChannelStart + c_BackgroundChannelCount; -static const int c_NodeStartChannel = c_LinkStartChannel + c_LinkChannelCount; - -static const int c_BackgroundChannel_SelectionRect = - c_BackgroundChannelStart + 0; - -static const int c_UserChannel_Content = c_UserLayerChannelStart + 1; -static const int c_UserChannel_Grid = c_UserLayerChannelStart + 2; -static const int c_UserChannel_HintsBackground = c_UserLayerChannelStart + 3; -static const int c_UserChannel_Hints = c_UserLayerChannelStart + 4; - -static const int c_LinkChannel_Selection = c_LinkStartChannel + 0; -static const int c_LinkChannel_Links = c_LinkStartChannel + 1; -static const int c_LinkChannel_Flow = c_LinkStartChannel + 2; -static const int c_LinkChannel_NewLink = c_LinkStartChannel + 3; - -static const int c_ChannelsPerNode = 5; -static const int c_NodeBaseChannel = 0; -static const int c_NodeBackgroundChannel = 1; -static const int c_NodeUserBackgroundChannel = 2; -static const int c_NodePinChannel = 3; -static const int c_NodeContentChannel = 4; - -static const float c_GroupSelectThickness = 6.0f; // canvas pixels -static const float c_LinkSelectThickness = 5.0f; // canvas pixels -static const float c_NavigationZoomMargin = - 0.1f; // percentage of visible bounds -static const float c_MouseZoomDuration = 0.15f; // seconds -static const float c_SelectionFadeOutDuration = 0.15f; // seconds - -static const auto c_MaxMoveOverEdgeSpeed = 10.0f; -static const auto c_MaxMoveOverEdgeDistance = 300.0f; - -#if IMGUI_VERSION_NUM > 18101 -static const auto c_AllRoundCornersFlags = ImDrawFlags_RoundCornersAll; -#else -static const auto c_AllRoundCornersFlags = 15; -#endif - -//------------------------------------------------------------------------------ -#if defined(_DEBUG) && defined(_WIN32) -extern "C" __declspec(dllimport) void __stdcall OutputDebugStringA( - const char* string); - -static void LogV(const char* fmt, va_list args) { - const int buffer_size = 1024; - static char buffer[1024]; - - vsnprintf(buffer, buffer_size - 1, fmt, args); - buffer[buffer_size - 1] = 0; - - ImGui::LogText("\nNode Editor: %s", buffer); - - OutputDebugStringA("NodeEditor: "); - OutputDebugStringA(buffer); - OutputDebugStringA("\n"); -} -#endif - -void ed::Log(const char* fmt, ...) { -#if defined(_DEBUG) && defined(_WIN32) - va_list args; - va_start(args, fmt); - LogV(fmt, args); - va_end(args); -#endif -} - -//------------------------------------------------------------------------------ -static bool IsGroup(const ed::Node* node) { - if (node && node->m_Type == ed::NodeType::Group) - return true; - else - return false; -} - -//------------------------------------------------------------------------------ -static void ImDrawListSplitter_Grow(ImDrawList* draw_list, - ImDrawListSplitter* splitter, - int channels_count) { - IM_ASSERT(splitter != nullptr); - IM_ASSERT(splitter->_Count <= channels_count); - - if (splitter->_Count == 1) { - splitter->Split(draw_list, channels_count); - return; - } - - int old_channels_count = splitter->_Channels.Size; - if (old_channels_count < channels_count) { - splitter->_Channels.reserve(channels_count); - splitter->_Channels.resize(channels_count); - } - int old_used_channels_count = splitter->_Count; - splitter->_Count = channels_count; - - for (int i = old_used_channels_count; i < channels_count; i++) { - if (i >= old_channels_count) { - IM_PLACEMENT_NEW(&splitter->_Channels[i]) ImDrawChannel(); - } else { - splitter->_Channels[i]._CmdBuffer.resize(0); - splitter->_Channels[i]._IdxBuffer.resize(0); - } - } -} - -static void ImDrawList_ChannelsGrow(ImDrawList* draw_list, int channels_count) { - ImDrawListSplitter_Grow(draw_list, &draw_list->_Splitter, channels_count); -} - -static void ImDrawListSplitter_SwapChannels(ImDrawListSplitter* splitter, - int left, int right) { - IM_ASSERT(left < splitter->_Count && right < splitter->_Count); - if (left == right) return; - - auto currentChannel = splitter->_Current; - - auto* leftCmdBuffer = &splitter->_Channels[left]._CmdBuffer; - auto* leftIdxBuffer = &splitter->_Channels[left]._IdxBuffer; - auto* rightCmdBuffer = &splitter->_Channels[right]._CmdBuffer; - auto* rightIdxBuffer = &splitter->_Channels[right]._IdxBuffer; - - leftCmdBuffer->swap(*rightCmdBuffer); - leftIdxBuffer->swap(*rightIdxBuffer); - - if (currentChannel == left) - splitter->_Current = right; - else if (currentChannel == right) - splitter->_Current = left; -} - -static void ImDrawList_SwapChannels(ImDrawList* drawList, int left, int right) { - ImDrawListSplitter_SwapChannels(&drawList->_Splitter, left, right); -} - -static void ImDrawList_SwapSplitter(ImDrawList* drawList, - ImDrawListSplitter& splitter) { - auto& currentSplitter = drawList->_Splitter; - - std::swap(currentSplitter._Current, splitter._Current); - std::swap(currentSplitter._Count, splitter._Count); - currentSplitter._Channels.swap(splitter._Channels); -} - -// static void ImDrawList_TransformChannel_Inner(ImVector& -// vtxBuffer, const ImVector& idxBuffer, const ImVector& -// cmdBuffer, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& -// postOffset) -//{ -// auto idxRead = idxBuffer.Data; -// -// int indexOffset = 0; -// for (auto& cmd : cmdBuffer) -// { -// auto idxCount = cmd.ElemCount; -// -// if (idxCount == 0) continue; -// -// auto minIndex = idxRead[indexOffset]; -// auto maxIndex = idxRead[indexOffset]; -// -// for (auto i = 1u; i < idxCount; ++i) -// { -// auto idx = idxRead[indexOffset + i]; -// minIndex = std::min(minIndex, idx); -// maxIndex = ImMax(maxIndex, idx); -// } -// -// for (auto vtx = vtxBuffer.Data + minIndex, vtxEnd = vtxBuffer.Data + -// maxIndex + 1; vtx < vtxEnd; ++vtx) -// { -// vtx->pos.x = (vtx->pos.x + preOffset.x) * scale.x + postOffset.x; -// vtx->pos.y = (vtx->pos.y + preOffset.y) * scale.y + postOffset.y; -// } -// -// indexOffset += idxCount; -// } -// } - -// static void ImDrawList_TransformChannels(ImDrawList* drawList, int begin, int -// end, const ImVec2& preOffset, const ImVec2& scale, const ImVec2& postOffset) -//{ -// int lastCurrentChannel = drawList->_ChannelsCurrent; -// if (lastCurrentChannel != 0) -// drawList->ChannelsSetCurrent(0); -// -// auto& vtxBuffer = drawList->VtxBuffer; -// -// if (begin == 0 && begin != end) -// { -// ImDrawList_TransformChannel_Inner(vtxBuffer, drawList->IdxBuffer, -// drawList->CmdBuffer, preOffset, scale, postOffset); -// ++begin; -// } -// -// for (int channelIndex = begin; channelIndex < end; ++channelIndex) -// { -// auto& channel = drawList->_Channels[channelIndex]; -// ImDrawList_TransformChannel_Inner(vtxBuffer, channel.IdxBuffer, -// channel.CmdBuffer, preOffset, scale, postOffset); -// } -// -// if (lastCurrentChannel != 0) -// drawList->ChannelsSetCurrent(lastCurrentChannel); -// } - -// static void ImDrawList_ClampClipRects_Inner(ImVector& cmdBuffer, -// const ImVec4& clipRect, const ImVec2& offset) -//{ -// for (auto& cmd : cmdBuffer) -// { -// cmd.ClipRect.x = ImMax(cmd.ClipRect.x + offset.x, clipRect.x); -// cmd.ClipRect.y = ImMax(cmd.ClipRect.y + offset.y, clipRect.y); -// cmd.ClipRect.z = std::min(cmd.ClipRect.z + offset.x, clipRect.z); -// cmd.ClipRect.w = std::min(cmd.ClipRect.w + offset.y, clipRect.w); -// } -// } - -// static void ImDrawList_TranslateAndClampClipRects(ImDrawList* drawList, int -// begin, int end, const ImVec2& offset) -//{ -// int lastCurrentChannel = drawList->_ChannelsCurrent; -// if (lastCurrentChannel != 0) -// drawList->ChannelsSetCurrent(0); -// -// auto clipRect = drawList->_ClipRectStack.back(); -// -// if (begin == 0 && begin != end) -// { -// ImDrawList_ClampClipRects_Inner(drawList->CmdBuffer, clipRect, -// offset); -// ++begin; -// } -// -// for (int channelIndex = begin; channelIndex < end; ++channelIndex) -// { -// auto& channel = drawList->_Channels[channelIndex]; -// ImDrawList_ClampClipRects_Inner(channel.CmdBuffer, clipRect, offset); -// } -// -// if (lastCurrentChannel != 0) -// drawList->ChannelsSetCurrent(lastCurrentChannel); -// } - -static void ImDrawList_PathBezierOffset(ImDrawList* drawList, float offset, - const ImVec2& p0, const ImVec2& p1, - const ImVec2& p2, const ImVec2& p3) { - using namespace ed; - - auto acceptPoint = [drawList, offset](const ImCubicBezierSubdivideSample& r) { - drawList->PathLineTo( - r.Point + ImNormalized(ImVec2(-r.Tangent.y, r.Tangent.x)) * offset); - }; - - ImCubicBezierSubdivide(acceptPoint, p0, p1, p2, p3); -} - -/* -static void ImDrawList_PolyFillScanFlood(ImDrawList *draw, std::vector* -poly, ImColor color, int gap = 1, float strokeWidth = 1.0f) -{ - std::vector scanHits; - ImVec2 min, max; // polygon min/max points - auto io = ImGui::GetIO(); - float y; - bool isMinMaxDone = false; - unsigned int polysize = poly->size(); - - // find the orthagonal bounding box - // probably can put this as a predefined - if (!isMinMaxDone) - { - min.x = min.y = FLT_MAX; - max.x = max.y = FLT_MIN; - for (auto p : *poly) - { - if (p.x < min.x) min.x = p.x; - if (p.y < min.y) min.y = p.y; - if (p.x > max.x) max.x = p.x; - if (p.y > max.y) max.y = p.y; - } - isMinMaxDone = true; - } - - // Bounds check - if ((max.x < 0) || (min.x > io.DisplaySize.x) || (max.y < 0) || (min.y > -io.DisplaySize.y)) return; - - // Vertically clip - if (min.y < 0) min.y = 0; - if (max.y > io.DisplaySize.y) max.y = io.DisplaySize.y; - - // so we know we start on the outside of the object we step out by 1. - min.x -= 1; - max.x += 1; - - // Initialise our starting conditions - y = min.y; - - // Go through each scan line iteratively, jumping by 'gap' pixels each time - while (y < max.y) - { - scanHits.clear(); - - { - int jump = 1; - ImVec2 fp = poly->at(0); - - for (size_t i = 0; i < polysize - 1; i++) - { - ImVec2 pa = poly->at(i); - ImVec2 pb = poly->at(i + 1); - - // jump double/dud points - if (pa.x == pb.x && pa.y == pb.y) continue; - - // if we encounter our hull/poly start point, then we've now -created the - // closed - // hull, jump the next segment and reset the first-point - if ((!jump) && (fp.x == pb.x) && (fp.y == pb.y)) - { - if (i < polysize - 2) - { - fp = poly->at(i + 2); - jump = 1; - i++; - } - } - else - { - jump = 0; - } - - // test to see if this segment makes the scan-cut. - if ((pa.y > pb.y && y < pa.y && y > pb.y) || (pa.y < pb.y && y > -pa.y && y < pb.y)) - { - ImVec2 intersect; - - intersect.y = y; - if (pa.x == pb.x) - { - intersect.x = pa.x; - } - else - { - intersect.x = (pb.x - pa.x) / (pb.y - pa.y) * (y - pa.y) -+ pa.x; - } - scanHits.push_back(intersect); - } - } - - // Sort the scan hits by X, so we have a proper left->right ordering - sort(scanHits.begin(), scanHits.end(), [](ImVec2 const &a, ImVec2 -const &b) { return a.x < b.x; }); - - // generate the line segments. - { - int i = 0; - int l = scanHits.size() - 1; // we need pairs of points, this -prevents segfault. for (i = 0; i < l; i += 2) - { - draw->AddLine(scanHits[i], scanHits[i + 1], color, -strokeWidth); - } - } - } - y += gap; - } // for each scan line - scanHits.clear(); -} -*/ - -static void ImDrawList_AddBezierWithArrows( - ImDrawList* drawList, const ImCubicBezierPoints& curve, float thickness, - float startArrowSize, float startArrowWidth, float endArrowSize, - float endArrowWidth, bool fill, ImU32 color, float strokeThickness, - const ImVec2* startDirHint = nullptr, const ImVec2* endDirHint = nullptr) { - using namespace ax; - - if ((color >> 24) == 0) return; - - const auto half_thickness = thickness * 0.5f; - - if (fill) { - drawList->AddBezierCubic(curve.P0, curve.P1, curve.P2, curve.P3, color, - thickness); - - if (startArrowSize > 0.0f) { - const auto start_dir = ImNormalized( - startDirHint ? *startDirHint - : ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, - curve.P3, 0.0f)); - const auto start_n = ImVec2(-start_dir.y, start_dir.x); - const auto half_width = startArrowWidth * 0.5f; - const auto tip = curve.P0 - start_dir * startArrowSize; - - drawList->PathLineTo(curve.P0 - - start_n * ImMax(half_width, half_thickness)); - drawList->PathLineTo(curve.P0 + - start_n * ImMax(half_width, half_thickness)); - drawList->PathLineTo(tip); - drawList->PathFillConvex(color); - } - - if (endArrowSize > 0.0f) { - const auto end_dir = ImNormalized( - endDirHint ? -*endDirHint - : ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, - curve.P3, 1.0f)); - const auto end_n = ImVec2(-end_dir.y, end_dir.x); - const auto half_width = endArrowWidth * 0.5f; - const auto tip = curve.P3 + end_dir * endArrowSize; - - drawList->PathLineTo(curve.P3 + - end_n * ImMax(half_width, half_thickness)); - drawList->PathLineTo(curve.P3 - - end_n * ImMax(half_width, half_thickness)); - drawList->PathLineTo(tip); - drawList->PathFillConvex(color); - } - } else { - if (startArrowSize > 0.0f) { - const auto start_dir = ImNormalized( - ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f)); - const auto start_n = ImVec2(-start_dir.y, start_dir.x); - const auto half_width = startArrowWidth * 0.5f; - const auto tip = curve.P0 - start_dir * startArrowSize; - - if (half_width > half_thickness) - drawList->PathLineTo(curve.P0 - start_n * half_width); - drawList->PathLineTo(tip); - if (half_width > half_thickness) - drawList->PathLineTo(curve.P0 + start_n * half_width); - } - - ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P0, curve.P1, - curve.P2, curve.P3); - - if (endArrowSize > 0.0f) { - const auto end_dir = ImNormalized( - ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f)); - const auto end_n = ImVec2(-end_dir.y, end_dir.x); - const auto half_width = endArrowWidth * 0.5f; - const auto tip = curve.P3 + end_dir * endArrowSize; - - if (half_width > half_thickness) - drawList->PathLineTo(curve.P3 + end_n * half_width); - drawList->PathLineTo(tip); - if (half_width > half_thickness) - drawList->PathLineTo(curve.P3 - end_n * half_width); - } - - ImDrawList_PathBezierOffset(drawList, half_thickness, curve.P3, curve.P2, - curve.P1, curve.P0); - - drawList->PathStroke(color, true, strokeThickness); - } -} - -//------------------------------------------------------------------------------ -// -// Pin -// -//------------------------------------------------------------------------------ -void ed::Pin::Draw(ImDrawList* drawList, DrawFlags flags) { - if (flags & Hovered) { - drawList->ChannelsSetCurrent(m_Node->m_Channel + c_NodePinChannel); - - drawList->AddRectFilled(m_Bounds.Min, m_Bounds.Max, m_Color, m_Rounding, - m_Corners); - - if (m_BorderWidth > 0.0f) { - FringeScaleScope fringe(1.0f); - drawList->AddRect(m_Bounds.Min, m_Bounds.Max, m_BorderColor, m_Rounding, - m_Corners, m_BorderWidth); - } - - if (!Editor->IsSelected(m_Node)) m_Node->Draw(drawList, flags); - } -} - -ImVec2 ed::Pin::GetClosestPoint(const ImVec2& p) const { - auto pivot = m_Pivot; - auto extent = m_Radius + m_ArrowSize; - - if (m_SnapLinkToDir && extent > 0.0f) { - pivot.Min += m_Dir * extent; - pivot.Max += m_Dir * extent; - - extent = 0; - } - - return ImRect_ClosestPoint(pivot, p, true, extent); -} - -ImLine ed::Pin::GetClosestLine(const Pin* pin) const { - auto pivotA = m_Pivot; - auto pivotB = pin->m_Pivot; - auto extentA = m_Radius + m_ArrowSize; - auto extentB = pin->m_Radius + pin->m_ArrowSize; - - if (m_SnapLinkToDir && extentA > 0.0f) { - pivotA.Min += m_Dir * extentA; - pivotA.Max += m_Dir * extentA; - - extentA = 0; - } - - if (pin->m_SnapLinkToDir && extentB > 0.0f) { - pivotB.Min += pin->m_Dir * extentB; - pivotB.Max += pin->m_Dir * extentB; - - extentB = 0; - } - - return ImRect_ClosestLine(pivotA, pivotB, extentA, extentB); -} - -//------------------------------------------------------------------------------ -// -// Node -// -//------------------------------------------------------------------------------ -bool ed::Node::AcceptDrag() { - m_DragStart = m_Bounds.Min; - return true; -} - -void ed::Node::UpdateDrag(const ImVec2& offset) { - auto size = m_Bounds.GetSize(); - m_Bounds.Min = ImFloor(m_DragStart + offset); - m_Bounds.Max = m_Bounds.Min + size; -} - -bool ed::Node::EndDrag() { return m_Bounds.Min != m_DragStart; } - -void ed::Node::Draw(ImDrawList* drawList, DrawFlags flags) { - if (flags == Detail::Object::None) { - drawList->ChannelsSetCurrent(m_Channel + c_NodeBackgroundChannel); - - drawList->AddRectFilled(m_Bounds.Min, m_Bounds.Max, m_Color, m_Rounding); - - if (IsGroup(this)) { - drawList->AddRectFilled(m_GroupBounds.Min, m_GroupBounds.Max, - m_GroupColor, m_GroupRounding); - - if (m_GroupBorderWidth > 0.0f) { - FringeScaleScope fringe(1.0f); - - drawList->AddRect(m_GroupBounds.Min, m_GroupBounds.Max, - m_GroupBorderColor, m_GroupRounding, - c_AllRoundCornersFlags, m_GroupBorderWidth); - } - } - -#if 0 - // #debug: highlight group regions - auto drawRect = [drawList](const ImRect& rect, ImU32 color) - { - if (ImRect_IsEmpty(rect)) return; - drawList->AddRectFilled(rect.Min, rect.Max, color); - }; - - drawRect(GetRegionBounds(NodeRegion::Top), IM_COL32(255, 0, 0, 64)); - drawRect(GetRegionBounds(NodeRegion::Bottom), IM_COL32(255, 0, 0, 64)); - drawRect(GetRegionBounds(NodeRegion::Left), IM_COL32(0, 255, 0, 64)); - drawRect(GetRegionBounds(NodeRegion::Right), IM_COL32(0, 255, 0, 64)); - drawRect(GetRegionBounds(NodeRegion::TopLeft), IM_COL32(255, 0, 255, 64)); - drawRect(GetRegionBounds(NodeRegion::TopRight), IM_COL32(255, 0, 255, 64)); - drawRect(GetRegionBounds(NodeRegion::BottomLeft), IM_COL32(255, 0, 255, 64)); - drawRect(GetRegionBounds(NodeRegion::BottomRight), IM_COL32(255, 0, 255, 64)); - drawRect(GetRegionBounds(NodeRegion::Center), IM_COL32(0, 0, 255, 64)); - drawRect(GetRegionBounds(NodeRegion::Header), IM_COL32(0, 255, 255, 64)); -#endif - - DrawBorder(drawList, m_BorderColor, m_BorderWidth); - } else if (flags & Selected) { - const auto borderColor = Editor->GetColor(StyleColor_SelNodeBorder); - const auto& editorStyle = Editor->GetStyle(); - - drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel); - - DrawBorder(drawList, borderColor, editorStyle.SelectedNodeBorderWidth, - editorStyle.SelectedNodeBorderOffset); - } else if (!IsGroup(this) && (flags & Hovered)) { - const auto borderColor = Editor->GetColor(StyleColor_HovNodeBorder); - const auto& editorStyle = Editor->GetStyle(); - - drawList->ChannelsSetCurrent(m_Channel + c_NodeBaseChannel); - - DrawBorder(drawList, borderColor, editorStyle.HoveredNodeBorderWidth, - editorStyle.HoverNodeBorderOffset); - } -} - -void ed::Node::DrawBorder(ImDrawList* drawList, ImU32 color, float thickness, - float offset) { - if (thickness > 0.0f) { - const ImVec2 extraOffset = ImVec2(offset, offset); - - drawList->AddRect(m_Bounds.Min - extraOffset, m_Bounds.Max + extraOffset, - color, ImMax(0.0f, m_Rounding + offset), - c_AllRoundCornersFlags, thickness); - } -} - -void ed::Node::GetGroupedNodes(std::vector& result, bool append) { - if (!append) result.resize(0); - - if (!IsGroup(this)) return; - - const auto firstNodeIndex = result.size(); - Editor->FindNodesInRect(m_GroupBounds, result, true, false); - - for (auto index = firstNodeIndex; index < result.size(); ++index) - result[index]->GetGroupedNodes(result, true); -} - -ImRect ed::Node::GetRegionBounds(NodeRegion region) const { - if (m_Type == NodeType::Node) { - if (region == NodeRegion::Header) return m_Bounds; - } else if (m_Type == NodeType::Group) { - const float activeAreaMinimumSize = - ImMax(ImMax(Editor->GetView().InvScale * c_GroupSelectThickness, - m_GroupBorderWidth), - c_GroupSelectThickness); - const float minimumSize = activeAreaMinimumSize * 5; - - auto bounds = m_Bounds; - if (bounds.GetWidth() < minimumSize) - bounds.Expand(ImVec2(minimumSize - bounds.GetWidth(), 0.0f)); - if (bounds.GetHeight() < minimumSize) - bounds.Expand(ImVec2(0.0f, minimumSize - bounds.GetHeight())); - - if (region == NodeRegion::Top) { - bounds.Max.y = bounds.Min.y + activeAreaMinimumSize; - bounds.Min.x += activeAreaMinimumSize; - bounds.Max.x -= activeAreaMinimumSize; - return bounds; - } else if (region == NodeRegion::Bottom) { - bounds.Min.y = bounds.Max.y - activeAreaMinimumSize; - bounds.Min.x += activeAreaMinimumSize; - bounds.Max.x -= activeAreaMinimumSize; - return bounds; - } else if (region == NodeRegion::Left) { - bounds.Max.x = bounds.Min.x + activeAreaMinimumSize; - bounds.Min.y += activeAreaMinimumSize; - bounds.Max.y -= activeAreaMinimumSize; - return bounds; - } else if (region == NodeRegion::Right) { - bounds.Min.x = bounds.Max.x - activeAreaMinimumSize; - bounds.Min.y += activeAreaMinimumSize; - bounds.Max.y -= activeAreaMinimumSize; - return bounds; - } else if (region == NodeRegion::TopLeft) { - bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2; - bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2; - return bounds; - } else if (region == NodeRegion::TopRight) { - bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2; - bounds.Max.y = bounds.Min.y + activeAreaMinimumSize * 2; - return bounds; - } else if (region == NodeRegion::BottomRight) { - bounds.Min.x = bounds.Max.x - activeAreaMinimumSize * 2; - bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2; - return bounds; - } else if (region == NodeRegion::BottomLeft) { - bounds.Max.x = bounds.Min.x + activeAreaMinimumSize * 2; - bounds.Min.y = bounds.Max.y - activeAreaMinimumSize * 2; - return bounds; - } else if (region == NodeRegion::Header) { - bounds.Min.x += activeAreaMinimumSize; - bounds.Max.x -= activeAreaMinimumSize; - bounds.Min.y += activeAreaMinimumSize; - bounds.Max.y = - ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y); - return bounds; - } else if (region == NodeRegion::Center) { - bounds.Max.x -= activeAreaMinimumSize; - bounds.Min.y = - ImMax(bounds.Min.y + activeAreaMinimumSize, m_GroupBounds.Min.y); - bounds.Min.x += activeAreaMinimumSize; - bounds.Max.y -= activeAreaMinimumSize; - return bounds; - } - } - - return ImRect(); -} - -ed::NodeRegion ed::Node::GetRegion(const ImVec2& point) const { - if (m_Type == NodeType::Node) { - if (m_Bounds.Contains(point)) - return NodeRegion::Header; - else - return NodeRegion::None; - } else if (m_Type == NodeType::Group) { - static const NodeRegion c_Regions[] = { - // Corners first, they may overlap other regions. - NodeRegion::TopLeft, NodeRegion::TopRight, NodeRegion::BottomLeft, - NodeRegion::BottomRight, NodeRegion::Header, NodeRegion::Top, - NodeRegion::Bottom, NodeRegion::Left, NodeRegion::Right, - NodeRegion::Center}; - - for (auto region : c_Regions) { - auto bounds = GetRegionBounds(region); - if (bounds.Contains(point)) return region; - } - } - - return NodeRegion::None; -} - -//------------------------------------------------------------------------------ -// -// Link -// -//------------------------------------------------------------------------------ -void ed::Link::Draw(ImDrawList* drawList, DrawFlags flags) { - if (flags == None) { - drawList->ChannelsSetCurrent(c_LinkChannel_Links); - - Draw(drawList, m_Color, 0.0f); - } else if (flags & Selected) { - const auto borderColor = Editor->GetColor(StyleColor_SelLinkBorder); - - drawList->ChannelsSetCurrent(c_LinkChannel_Selection); - - Draw(drawList, borderColor, 4.5f); - } else if (flags & Hovered) { - const auto borderColor = Editor->GetColor(StyleColor_HovLinkBorder); - - drawList->ChannelsSetCurrent(c_LinkChannel_Selection); - - Draw(drawList, borderColor, 2.0f); - } else if (flags & Highlighted) { - drawList->ChannelsSetCurrent(c_LinkChannel_Selection); - - Draw(drawList, m_HighlightColor, 3.5f); - } -} - -void ed::Link::Draw(ImDrawList* drawList, ImU32 color, - float extraThickness) const { - if (!m_IsLive) return; - - const auto curve = GetCurve(); - - ImDrawList_AddBezierWithArrows( - drawList, curve, m_Thickness + extraThickness, - m_StartPin && m_StartPin->m_ArrowSize > 0.0f - ? m_StartPin->m_ArrowSize + extraThickness - : 0.0f, - m_StartPin && m_StartPin->m_ArrowWidth > 0.0f - ? m_StartPin->m_ArrowWidth + extraThickness - : 0.0f, - m_EndPin && m_EndPin->m_ArrowSize > 0.0f - ? m_EndPin->m_ArrowSize + extraThickness - : 0.0f, - m_EndPin && m_EndPin->m_ArrowWidth > 0.0f - ? m_EndPin->m_ArrowWidth + extraThickness - : 0.0f, - true, color, 1.0f, - m_StartPin && m_StartPin->m_SnapLinkToDir ? &m_StartPin->m_Dir : nullptr, - m_EndPin && m_EndPin->m_SnapLinkToDir ? &m_EndPin->m_Dir : nullptr); -} - -void ed::Link::UpdateEndpoints() { - const auto line = m_StartPin->GetClosestLine(m_EndPin); - m_Start = line.A; - m_End = line.B; -} - -ImCubicBezierPoints ed::Link::GetCurve() const { - auto easeLinkStrength = [](const ImVec2& a, const ImVec2& b, float strength) { - const auto distanceX = b.x - a.x; - const auto distanceY = b.y - a.y; - const auto distance = ImSqrt(distanceX * distanceX + distanceY * distanceY); - const auto halfDistance = distance * 0.5f; - - if (halfDistance < strength) - strength = strength * ImSin(IM_PI * 0.5f * halfDistance / strength); - - return strength; - }; - - const auto startStrength = - easeLinkStrength(m_Start, m_End, m_StartPin->m_Strength); - const auto endStrength = - easeLinkStrength(m_Start, m_End, m_EndPin->m_Strength); - const auto cp0 = m_Start + m_StartPin->m_Dir * startStrength; - const auto cp1 = m_End + m_EndPin->m_Dir * endStrength; - - ImCubicBezierPoints result; - result.P0 = m_Start; - result.P1 = cp0; - result.P2 = cp1; - result.P3 = m_End; - - return result; -} - -bool ed::Link::TestHit(const ImVec2& point, float extraThickness) const { - if (!m_IsLive) return false; - - auto bounds = GetBounds(); - if (extraThickness > 0.0f) bounds.Expand(extraThickness); - - if (!bounds.Contains(point)) return false; - - const auto bezier = GetCurve(); - const auto result = ImProjectOnCubicBezier(point, bezier.P0, bezier.P1, - bezier.P2, bezier.P3, 50); - - return result.Distance <= m_Thickness + extraThickness; -} - -bool ed::Link::TestHit(const ImRect& rect, bool allowIntersect) const { - if (!m_IsLive) return false; - - const auto bounds = GetBounds(); - - if (rect.Contains(bounds)) return true; - - if (!allowIntersect || !rect.Overlaps(bounds)) return false; - - const auto bezier = GetCurve(); - - const auto p0 = rect.GetTL(); - const auto p1 = rect.GetTR(); - const auto p2 = rect.GetBR(); - const auto p3 = rect.GetBL(); - - if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p0, - p1) - .Count > 0) - return true; - if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p1, - p2) - .Count > 0) - return true; - if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p2, - p3) - .Count > 0) - return true; - if (ImCubicBezierLineIntersect(bezier.P0, bezier.P1, bezier.P2, bezier.P3, p3, - p0) - .Count > 0) - return true; - - return false; -} - -ImRect ed::Link::GetBounds() const { - if (m_IsLive) { - const auto curve = GetCurve(); - auto bounds = - ImCubicBezierBoundingRect(curve.P0, curve.P1, curve.P2, curve.P3); - - if (bounds.GetWidth() == 0.0f) { - bounds.Min.x -= 0.5f; - bounds.Max.x += 0.5f; - } - - if (bounds.GetHeight() == 0.0f) { - bounds.Min.y -= 0.5f; - bounds.Max.y += 0.5f; - } - - if (m_StartPin->m_ArrowSize) { - const auto start_dir = ImNormalized( - ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 0.0f)); - const auto p0 = curve.P0; - const auto p1 = curve.P0 - start_dir * m_StartPin->m_ArrowSize; - const auto min = ImMin(p0, p1); - const auto max = ImMax(p0, p1); - auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1))); - bounds.Add(arrowBounds); - } - - if (m_EndPin->m_ArrowSize) { - const auto end_dir = ImNormalized( - ImCubicBezierTangent(curve.P0, curve.P1, curve.P2, curve.P3, 1.0f)); - const auto p0 = curve.P3; - const auto p1 = curve.P3 + end_dir * m_EndPin->m_ArrowSize; - const auto min = ImMin(p0, p1); - const auto max = ImMax(p0, p1); - auto arrowBounds = ImRect(min, ImMax(max, min + ImVec2(1, 1))); - bounds.Add(arrowBounds); - } - - return bounds; - } else - return ImRect(); -} - -//------------------------------------------------------------------------------ -// -// Editor Context -// -//------------------------------------------------------------------------------ -ed::EditorContext::EditorContext(const ax::NodeEditor::Config* config) - : m_Config(config), - m_EditorActiveId(0), - m_IsFirstFrame(true), - m_IsFocused(false), - m_IsHovered(false), - m_IsHoveredWithoutOverlapp(false), - m_ShortcutsEnabled(true), - m_Style(), - m_Nodes(), - m_Pins(), - m_Links(), - m_SelectionId(1), - m_LastActiveLink(nullptr), - m_Canvas(), - m_IsCanvasVisible(false), - m_NodeBuilder(this), - m_HintBuilder(this), - m_CurrentAction(nullptr), - m_NavigateAction(this, m_Canvas), - m_SizeAction(this), - m_DragAction(this), - m_SelectAction(this), - m_ContextMenuAction(this), - m_ShortcutAction(this), - m_CreateItemAction(this), - m_DeleteItemsAction(this), - m_AnimationControllers{&m_FlowAnimationController}, - m_FlowAnimationController(this), - m_HoveredNode(0), - m_HoveredPin(0), - m_HoveredLink(0), - m_DoubleClickedNode(0), - m_DoubleClickedPin(0), - m_DoubleClickedLink(0), - m_BackgroundClickButtonIndex(-1), - m_BackgroundDoubleClickButtonIndex(-1), - m_IsInitialized(false), - m_Settings(), - m_DrawList(nullptr), - m_ExternalChannel(0) {} - -ed::EditorContext::~EditorContext() { - if (m_IsInitialized) SaveSettings(); - - for (auto link : m_Links) delete link.m_Object; - for (auto pin : m_Pins) delete pin.m_Object; - for (auto node : m_Nodes) delete node.m_Object; - - m_Splitter.ClearFreeMemory(); -} - -void ed::EditorContext::Begin(const char* id, const ImVec2& size) { - m_EditorActiveId = ImGui::GetID(id); - ImGui::PushID(id); - - auto availableContentSize = ImGui::GetContentRegionAvail(); - ImVec2 canvasSize = ImFloor(size); - if (canvasSize.x <= 0.0f) canvasSize.x = ImMax(4.0f, availableContentSize.x); - if (canvasSize.y <= 0.0f) canvasSize.y = ImMax(4.0f, availableContentSize.y); - - if (!m_IsInitialized) { - // Cycle canvas, so it has a chance to initialize its size before settings - // are loaded - if (m_Canvas.Begin(id, canvasSize)) m_Canvas.End(); - - LoadSettings(); - m_IsInitialized = true; - } - - // ImGui::LogToClipboard(); - // Log("---- begin ----"); - - static auto resetAndCollect = [](auto& objects) { - objects.erase(std::remove_if(objects.begin(), objects.end(), - [](auto objectWrapper) { - if (objectWrapper->m_DeleteOnNewFrame) { - delete objectWrapper.m_Object; - return true; - } else { - objectWrapper->Reset(); - return false; - } - }), - objects.end()); - }; - - resetAndCollect(m_Nodes); - resetAndCollect(m_Pins); - resetAndCollect(m_Links); - - m_DrawList = ImGui::GetWindowDrawList(); - - ImDrawList_SwapSplitter(m_DrawList, m_Splitter); - m_ExternalChannel = m_DrawList->_Splitter._Current; - - if (m_CurrentAction && m_CurrentAction->IsDragging() && - m_NavigateAction.MoveOverEdge(canvasSize)) { - auto& io = ImGui::GetIO(); - auto offset = m_NavigateAction.GetMoveScreenOffset(); - for (int i = 0; i < 5; ++i) - io.MouseClickedPos[i] = io.MouseClickedPos[i] - offset; - } else - m_NavigateAction.StopMoveOverEdge(); - - auto previousSize = m_Canvas.Rect().GetSize(); - auto previousVisibleRect = m_Canvas.ViewRect(); - m_IsCanvasVisible = m_Canvas.Begin(id, canvasSize); - - // ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); - // ImGui::BeginChild(id, size, false, - // ImGuiWindowFlags_NoMove | - // ImGuiWindowFlags_NoScrollbar | - // ImGuiWindowFlags_NoScrollWithMouse); - - m_IsFocused = ImGui::IsWindowFocused(); - - // - m_NavigateAction.SetWindow(m_Canvas.ViewRect().Min, - m_Canvas.ViewRect().GetSize()); - - // Handle canvas size change. Scale to Y axis, center on X. - if (!ImRect_IsEmpty(previousVisibleRect) && previousSize != canvasSize) { - m_NavigateAction.FinishNavigation(); - - auto centerX = - (previousVisibleRect.Max.x + previousVisibleRect.Min.x) * 0.5f; - auto centerY = - (previousVisibleRect.Max.y + previousVisibleRect.Min.y) * 0.5f; - auto currentVisibleRect = m_Canvas.ViewRect(); - auto currentAspectRatio = - currentVisibleRect.GetHeight() - ? (currentVisibleRect.GetWidth() / currentVisibleRect.GetHeight()) - : 0.0f; - auto width = previousVisibleRect.GetWidth(); - auto height = previousVisibleRect.GetHeight(); - - if (m_Config.CanvasSizeMode == - ax::NodeEditor::CanvasSizeMode::FitVerticalView) { - height = previousVisibleRect.GetHeight(); - width = height * currentAspectRatio; - } else if (m_Config.CanvasSizeMode == - ax::NodeEditor::CanvasSizeMode::FitHorizontalView) { - width = previousVisibleRect.GetWidth(); - height = width / currentAspectRatio; - } else if (m_Config.CanvasSizeMode == - ax::NodeEditor::CanvasSizeMode::CenterOnly) { - width = currentVisibleRect.GetWidth(); - height = currentVisibleRect.GetHeight(); - } - - previousVisibleRect.Min.x = centerX - 0.5f * width; - previousVisibleRect.Max.x = centerX + 0.5f * width; - previousVisibleRect.Min.y = centerY - 0.5f * height; - previousVisibleRect.Max.y = centerY + 0.5f * height; - - m_NavigateAction.NavigateTo(previousVisibleRect, - Detail::NavigateAction::ZoomMode::Exact, 0.0f); - } - - m_Canvas.SetView(m_NavigateAction.GetView()); - - // #debug #clip - // ImGui::Text("CLIP = { x=%g y=%g w=%g h=%g r=%g b=%g }", - // clipMin.x, clipMin.y, clipMax.x - clipMin.x, clipMax.y - clipMin.y, - // clipMax.x, clipMax.y); - - // Reserve channels for background and links - ImDrawList_ChannelsGrow(m_DrawList, c_NodeStartChannel); - - if (HasSelectionChanged()) ++m_SelectionId; - - m_LastSelectedObjects = m_SelectedObjects; -} - -void ed::EditorContext::End() { - // auto& io = ImGui::GetIO(); - auto control = BuildControl( - m_CurrentAction && - m_CurrentAction->IsDragging()); // NavigateAction.IsMovingOverEdge() - // auto& editorStyle = GetStyle(); - - m_HoveredNode = - control.HotNode && m_CurrentAction == nullptr ? control.HotNode->m_ID : 0; - m_HoveredPin = - control.HotPin && m_CurrentAction == nullptr ? control.HotPin->m_ID : 0; - m_HoveredLink = - control.HotLink && m_CurrentAction == nullptr ? control.HotLink->m_ID : 0; - m_DoubleClickedNode = - control.DoubleClickedNode ? control.DoubleClickedNode->m_ID : 0; - m_DoubleClickedPin = - control.DoubleClickedPin ? control.DoubleClickedPin->m_ID : 0; - m_DoubleClickedLink = - control.DoubleClickedLink ? control.DoubleClickedLink->m_ID : 0; - m_BackgroundClickButtonIndex = control.BackgroundClickButtonIndex; - m_BackgroundDoubleClickButtonIndex = control.BackgroundDoubleClickButtonIndex; - - // if (DoubleClickedNode) LOG_TRACE(0, "DOUBLE CLICK NODE: %d", - // DoubleClickedNode); if (DoubleClickedPin) LOG_TRACE(0, "DOUBLE CLICK PIN: - // %d", DoubleClickedPin); if (DoubleClickedLink) LOG_TRACE(0, "DOUBLE CLICK - // LINK: %d", DoubleClickedLink); if (BackgroundDoubleClicked) LOG_TRACE(0, - // "DOUBLE CLICK BACKGROUND", DoubleClickedLink); - - const bool isSelecting = - m_CurrentAction && m_CurrentAction->AsSelect() != nullptr; - const bool isDragging = - m_CurrentAction && m_CurrentAction->AsDrag() != nullptr; - // const bool isSizing = CurrentAction && CurrentAction->AsSize() != - // nullptr; - - // Draw nodes - for (auto node : m_Nodes) - if (node->m_IsLive && node->IsVisible()) node->Draw(m_DrawList); - - // Draw links - for (auto link : m_Links) - if (link->m_IsLive && link->IsVisible()) link->Draw(m_DrawList); - - // Highlight selected objects - { - auto selectedObjects = &m_SelectedObjects; - if (auto selectAction = - m_CurrentAction ? m_CurrentAction->AsSelect() : nullptr) - selectedObjects = &selectAction->m_CandidateObjects; - - for (auto selectedObject : *selectedObjects) { - if (selectedObject->IsVisible()) - selectedObject->Draw(m_DrawList, Object::Selected); - } - - // Highlight adjacent links - static auto isLinkHighlightedForPin = [](const Pin& pin) { - return pin.m_Node->m_HighlightConnectedLinks && pin.m_Node->m_IsSelected; - }; - - for (auto& link : m_Links) { - if (!link->m_IsLive || !link->IsVisible()) continue; - - auto isLinkHighlighted = isLinkHighlightedForPin(*link->m_StartPin) || - isLinkHighlightedForPin(*link->m_EndPin); - if (!isLinkHighlighted) continue; - - link->Draw(m_DrawList, Object::Highlighted); - } - } - - if (!isSelecting) { - auto hoveredObject = control.HotObject; - if (auto dragAction = m_CurrentAction ? m_CurrentAction->AsDrag() : nullptr) - hoveredObject = dragAction->m_DraggedObject; - if (auto sizeAction = m_CurrentAction ? m_CurrentAction->AsSize() : nullptr) - hoveredObject = sizeAction->m_SizedNode; - - if (hoveredObject && !IsSelected(hoveredObject) && - hoveredObject->IsVisible()) - hoveredObject->Draw(m_DrawList, Object::Hovered); - } - - // Draw animations - for (auto controller : m_AnimationControllers) controller->Draw(m_DrawList); - - if (m_CurrentAction && !m_CurrentAction->Process(control)) - m_CurrentAction = nullptr; - - if (m_NavigateAction.m_IsActive) - m_NavigateAction.Process(control); - else - m_NavigateAction.Accept(control); - - if (nullptr == m_CurrentAction) { - EditorAction* possibleAction = nullptr; - - auto accept = [&possibleAction, &control](EditorAction& action) { - auto result = action.Accept(control); - - if (result == EditorAction::True) - return true; - else if (/*!possibleAction &&*/ result == EditorAction::Possible) - possibleAction = &action; - else if (result == EditorAction::Possible) - action.Reject(); - - return false; - }; - - if (accept(m_ContextMenuAction)) - m_CurrentAction = &m_ContextMenuAction; - else if (accept(m_ShortcutAction)) - m_CurrentAction = &m_ShortcutAction; - else if (accept(m_SizeAction)) - m_CurrentAction = &m_SizeAction; - else if (accept(m_DragAction)) - m_CurrentAction = &m_DragAction; - else if (accept(m_CreateItemAction)) - m_CurrentAction = &m_CreateItemAction; - else if (accept(m_DeleteItemsAction)) - m_CurrentAction = &m_DeleteItemsAction; - else if (accept(m_SelectAction)) - m_CurrentAction = &m_SelectAction; - - if (possibleAction) ImGui::SetMouseCursor(possibleAction->GetCursor()); - - if (m_CurrentAction && possibleAction) possibleAction->Reject(); - } - - if (m_CurrentAction) ImGui::SetMouseCursor(m_CurrentAction->GetCursor()); - - // Draw selection rectangle - m_SelectAction.Draw(m_DrawList); - - bool sortGroups = false; - if (control.ActiveNode) { - if (!IsGroup(control.ActiveNode)) { - // Bring active node to front - auto activeNodeIt = - std::find(m_Nodes.begin(), m_Nodes.end(), control.ActiveNode); - std::rotate(activeNodeIt, activeNodeIt + 1, m_Nodes.end()); - } else if (!isDragging && m_CurrentAction && m_CurrentAction->AsDrag()) { - // Bring content of dragged group to front - std::vector nodes; - control.ActiveNode->GetGroupedNodes(nodes); - - std::stable_partition( - m_Nodes.begin(), m_Nodes.end(), [&nodes](Node* node) { - return std::find(nodes.begin(), nodes.end(), node) == nodes.end(); - }); - - sortGroups = true; - } - } - - // Sort nodes if bounds of node changed - if (sortGroups || ((m_Settings.m_DirtyReason & - (SaveReasonFlags::Position | SaveReasonFlags::Size)) != - SaveReasonFlags::None)) { - // Bring all groups before regular nodes - auto groupsItEnd = - std::stable_partition(m_Nodes.begin(), m_Nodes.end(), IsGroup); - - // Sort groups by area - std::sort(m_Nodes.begin(), groupsItEnd, [this](Node* lhs, Node* rhs) { - const auto& lhsSize = lhs == m_SizeAction.m_SizedNode - ? m_SizeAction.GetStartGroupBounds().GetSize() - : lhs->m_GroupBounds.GetSize(); - const auto& rhsSize = rhs == m_SizeAction.m_SizedNode - ? m_SizeAction.GetStartGroupBounds().GetSize() - : rhs->m_GroupBounds.GetSize(); - - const auto lhsArea = lhsSize.x * lhsSize.y; - const auto rhsArea = rhsSize.x * rhsSize.y; - - return lhsArea > rhsArea; - }); - } - - // Apply Z order - std::stable_sort(m_Nodes.begin(), m_Nodes.end(), - [](const auto& lhs, const auto& rhs) { - return lhs->m_ZPosition < rhs->m_ZPosition; - }); - -#if 1 - // Every node has few channels assigned. Grow channel list - // to hold twice as much of channels and place them in - // node drawing order. - { - // Copy group nodes - auto liveNodeCount = static_cast( - std::count_if(m_Nodes.begin(), m_Nodes.end(), - [](Node* node) { return node->m_IsLive; })); - - // Reserve two additional channels for sorted list of channels - auto nodeChannelCount = m_DrawList->_Splitter._Count; - ImDrawList_ChannelsGrow(m_DrawList, m_DrawList->_Splitter._Count + - c_ChannelsPerNode * liveNodeCount + - c_LinkChannelCount); - - int targetChannel = nodeChannelCount; - - auto copyNode = [this, &targetChannel](Node* node) { - if (!node->m_IsLive) return; - - for (int i = 0; i < c_ChannelsPerNode; ++i) - ImDrawList_SwapChannels(m_DrawList, node->m_Channel + i, - targetChannel + i); - - node->m_Channel = targetChannel; - targetChannel += c_ChannelsPerNode; - }; - - auto groupsItEnd = std::find_if(m_Nodes.begin(), m_Nodes.end(), - [](Node* node) { return !IsGroup(node); }); - - // Copy group nodes - std::for_each(m_Nodes.begin(), groupsItEnd, copyNode); - - // Copy links - for (int i = 0; i < c_LinkChannelCount; ++i, ++targetChannel) - ImDrawList_SwapChannels(m_DrawList, c_LinkStartChannel + i, - targetChannel); - - // Copy normal nodes - std::for_each(groupsItEnd, m_Nodes.end(), copyNode); - } -#endif - - // ImGui::PopClipRect(); - - // Draw grid -#if 1 // #FIXME - { - // auto& style = ImGui::GetStyle(); - - m_DrawList->ChannelsSetCurrent(c_UserChannel_Grid); - - ImVec2 offset = m_Canvas.ViewOrigin() * (1.0f / m_Canvas.ViewScale()); - ImU32 GRID_COLOR = GetColor( - StyleColor_Grid, - ImClamp(m_Canvas.ViewScale() * m_Canvas.ViewScale(), 0.0f, 1.0f)); - float GRID_SX = 32.0f; // * m_Canvas.ViewScale(); - float GRID_SY = 32.0f; // * m_Canvas.ViewScale(); - ImVec2 VIEW_POS = m_Canvas.ViewRect().Min; - ImVec2 VIEW_SIZE = m_Canvas.ViewRect().GetSize(); - - m_DrawList->AddRectFilled(VIEW_POS, VIEW_POS + VIEW_SIZE, - GetColor(StyleColor_Bg)); - - for (float x = fmodf(offset.x, GRID_SX); x < VIEW_SIZE.x; x += GRID_SX) - m_DrawList->AddLine(ImVec2(x, 0.0f) + VIEW_POS, - ImVec2(x, VIEW_SIZE.y) + VIEW_POS, GRID_COLOR); - for (float y = fmodf(offset.y, GRID_SY); y < VIEW_SIZE.y; y += GRID_SY) - m_DrawList->AddLine(ImVec2(0.0f, y) + VIEW_POS, - ImVec2(VIEW_SIZE.x, y) + VIEW_POS, GRID_COLOR); - } -#endif - -#if 0 - { - auto userChannel = drawList->_Splitter._Count; - auto channelsToCopy = c_UserLayersCount; - ImDrawList_ChannelsGrow(drawList, userChannel + channelsToCopy); - for (int i = 0; i < channelsToCopy; ++i) - ImDrawList_SwapChannels(drawList, userChannel + i, c_UserLayerChannelStart + i); - } -#endif - -#if 0 - { - auto preOffset = ImVec2(0, 0); - auto postOffset = m_OldCanvas.WindowScreenPos + m_OldCanvas.ClientOrigin; - auto scale = m_OldCanvas.Zoom; - - ImDrawList_TransformChannels(drawList, 0, 1, preOffset, scale, postOffset); - ImDrawList_TransformChannels(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, preOffset, scale, postOffset); - - auto clipTranslation = m_OldCanvas.WindowScreenPos - m_OldCanvas.FromScreen(m_OldCanvas.WindowScreenPos); - ImGui::PushClipRect(m_OldCanvas.WindowScreenPos + ImVec2(1, 1), m_OldCanvas.WindowScreenPos + m_OldCanvas.WindowScreenSize - ImVec2(1, 1), false); - ImDrawList_TranslateAndClampClipRects(drawList, 0, 1, clipTranslation); - ImDrawList_TranslateAndClampClipRects(drawList, c_BackgroundChannelStart, drawList->_ChannelsCount - 1, clipTranslation); - ImGui::PopClipRect(); - - // #debug: Static grid in local space - //for (float x = 0; x < Canvas.WindowScreenSize.x; x += 100) - // drawList->AddLine(ImVec2(x, 0.0f) + Canvas.WindowScreenPos, ImVec2(x, Canvas.WindowScreenSize.y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128)); - //for (float y = 0; y < Canvas.WindowScreenSize.y; y += 100) - // drawList->AddLine(ImVec2(0.0f, y) + Canvas.WindowScreenPos, ImVec2(Canvas.WindowScreenSize.x, y) + Canvas.WindowScreenPos, IM_COL32(255, 0, 0, 128)); - } -#endif - -#if 1 - // Move user and hint channels to top - { - // Clip plane is transformed to global space. - // These channels already have clip planes in global space, so - // we move them to clip plane. Batch transformation in canvas - // will bring them back to global space. - auto preTransformClipRect = [this](int channelIndex) { - ImDrawChannel& channel = m_DrawList->_Splitter._Channels[channelIndex]; - for (ImDrawCmd& cmd : channel._CmdBuffer) { - auto a = ToCanvas(ImVec2(cmd.ClipRect.x, cmd.ClipRect.y)); - auto b = ToCanvas(ImVec2(cmd.ClipRect.z, cmd.ClipRect.w)); - cmd.ClipRect = ImVec4(a.x, a.y, b.x, b.y); - } - }; - - m_DrawList->ChannelsSetCurrent(0); - - auto channelCount = m_DrawList->_Splitter._Count; - ImDrawList_ChannelsGrow(m_DrawList, channelCount + 3); - ImDrawList_SwapChannels(m_DrawList, c_UserChannel_HintsBackground, - channelCount + 0); - ImDrawList_SwapChannels(m_DrawList, c_UserChannel_Hints, channelCount + 1); - ImDrawList_SwapChannels(m_DrawList, c_UserChannel_Content, - channelCount + 2); - - preTransformClipRect(channelCount + 0); - preTransformClipRect(channelCount + 1); - preTransformClipRect(channelCount + 2); - } -#endif - - UpdateAnimations(); - - m_DrawList->ChannelsMerge(); - - // #debug - // drawList->AddRectFilled(ImVec2(-10.0f, -10.0f), ImVec2(10.0f, 10.0f), - // IM_COL32(255, 0, 255, 255)); - - // ImGui::EndChild(); - // ImGui::PopStyleColor(); - if (m_IsCanvasVisible) m_Canvas.End(); - - ImDrawList_SwapSplitter(m_DrawList, m_Splitter); - - // Draw border - { - auto& style = ImGui::GetStyle(); - auto borderShadoColor = style.Colors[ImGuiCol_BorderShadow]; - auto borderColor = style.Colors[ImGuiCol_Border]; - m_DrawList->AddRect(m_Canvas.Rect().Min + ImVec2(1, 1), - m_Canvas.Rect().Max - ImVec2(1, 1), - ImColor(borderShadoColor)); - m_DrawList->AddRect(m_Canvas.Rect().Min, m_Canvas.Rect().Max, - ImColor(borderColor)); - } - - // #metrics - // ShowMetrics(control); - - ImGui::PopID(); - - if (!m_CurrentAction && m_IsFirstFrame && !m_Settings.m_Selection.empty()) { - ClearSelection(); - for (auto id : m_Settings.m_Selection) - if (auto object = FindObject(id)) SelectObject(object); - } - - if (HasSelectionChanged()) MakeDirty(SaveReasonFlags::Selection); - - if (m_Settings.m_IsDirty && !m_CurrentAction) SaveSettings(); - - m_DrawList = nullptr; - m_IsFirstFrame = false; -} - -bool ed::EditorContext::DoLink(LinkId id, PinId startPinId, PinId endPinId, - ImU32 color, float thickness) { - // auto& editorStyle = GetStyle(); - - auto startPin = FindPin(startPinId); - auto endPin = FindPin(endPinId); - - if (!startPin || !startPin->m_IsLive || !endPin || !endPin->m_IsLive) - return false; - - startPin->m_HasConnection = true; - endPin->m_HasConnection = true; - - auto link = GetLink(id); - link->m_StartPin = startPin; - link->m_EndPin = endPin; - link->m_Color = color; - link->m_HighlightColor = GetColor(StyleColor_HighlightLinkBorder); - link->m_Thickness = thickness; - link->m_IsLive = true; - - link->UpdateEndpoints(); - - return true; -} - -void ed::EditorContext::SetNodePosition(NodeId nodeId, const ImVec2& position) { - auto node = FindNode(nodeId); - if (!node) { - node = CreateNode(nodeId); - node->m_IsLive = false; - } - - if (node->m_Bounds.Min != position) { - node->m_Bounds.Translate(position - node->m_Bounds.Min); - node->m_Bounds.Floor(); - MakeDirty(NodeEditor::SaveReasonFlags::Position, node); - } -} - -void ed::EditorContext::SetGroupSize(NodeId nodeId, const ImVec2& size) { - auto node = FindNode(nodeId); - if (!node) { - node = CreateNode(nodeId); - node->m_IsLive = false; - } - - node->m_Type = NodeType::Group; - - if (node->m_GroupBounds.GetSize() != size) { - node->m_GroupBounds.Min = node->m_Bounds.Min; - node->m_GroupBounds.Max = node->m_Bounds.Min + size; - node->m_GroupBounds.Floor(); - MakeDirty(NodeEditor::SaveReasonFlags::Size, node); - } -} - -ImVec2 ed::EditorContext::GetNodePosition(NodeId nodeId) { - auto node = FindNode(nodeId); - if (!node) return ImVec2(FLT_MAX, FLT_MAX); - - return node->m_Bounds.Min; -} - -ImVec2 ed::EditorContext::GetNodeSize(NodeId nodeId) { - auto node = FindNode(nodeId); - if (!node) return ImVec2(0, 0); - - return node->m_Bounds.GetSize(); -} - -void ed::EditorContext::SetNodeZPosition(NodeId nodeId, float z) { - auto node = FindNode(nodeId); - if (!node) { - node = CreateNode(nodeId); - node->m_IsLive = false; - } - - node->m_ZPosition = z; -} - -float ed::EditorContext::GetNodeZPosition(NodeId nodeId) { - auto node = FindNode(nodeId); - if (!node) return 0.0f; - - return node->m_ZPosition; -} - -void ed::EditorContext::MarkNodeToRestoreState(Node* node) { - node->m_RestoreState = true; -} - -void ed::EditorContext::UpdateNodeState(Node* node) { - bool tryLoadState = node->m_RestoreState; - - node->m_RestoreState = false; - - auto settings = m_Settings.FindNode(node->m_ID); - if (!settings) return; - - if (!tryLoadState && settings->m_WasUsed) return; - - if (!settings->m_WasUsed) { - MakeDirty(SaveReasonFlags::AddNode, node); - settings->m_WasUsed = true; - } - - // Load state from config (if possible) - if (tryLoadState) { - NodeSettings newSettings = *settings; - if (NodeSettings::Parse(m_Config.LoadNode(node->m_ID), newSettings)) - *settings = newSettings; - } - - node->m_Bounds.Min = settings->m_Location; - node->m_Bounds.Max = node->m_Bounds.Min + settings->m_Size; - node->m_Bounds.Floor(); - node->m_GroupBounds.Min = settings->m_Location; - node->m_GroupBounds.Max = node->m_GroupBounds.Min + settings->m_GroupSize; - node->m_GroupBounds.Floor(); -} - -void ed::EditorContext::RemoveSettings(Object* object) { - if (auto node = object->AsNode()) { - m_Settings.RemoveNode(node->m_ID); - MakeDirty(SaveReasonFlags::RemoveNode, node); - } -} - -void ed::EditorContext::ClearSelection() { - for (auto& object : m_SelectedObjects) object->m_IsSelected = false; - - m_SelectedObjects.clear(); -} - -void ed::EditorContext::SelectObject(Object* object) { - m_SelectedObjects.push_back(object); - object->m_IsSelected = true; -} - -void ed::EditorContext::DeselectObject(Object* object) { - auto objectIt = - std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), object); - if (objectIt == m_SelectedObjects.end()) return; - - object->m_IsSelected = false; - m_SelectedObjects.erase(objectIt); -} - -void ed::EditorContext::SetSelectedObject(Object* object) { - ClearSelection(); - SelectObject(object); -} - -void ed::EditorContext::ToggleObjectSelection(Object* object) { - if (IsSelected(object)) - DeselectObject(object); - else - SelectObject(object); -} - -bool ed::EditorContext::IsSelected(Object* object) { - return object && object->m_IsSelected; - // return std::find(m_SelectedObjects.begin(), m_SelectedObjects.end(), - // object) != m_SelectedObjects.end(); -} - -const ed::vector& ed::EditorContext::GetSelectedObjects() { - return m_SelectedObjects; -} - -bool ed::EditorContext::IsAnyNodeSelected() { - for (auto object : m_SelectedObjects) - if (object->AsNode()) return true; - - return false; -} - -bool ed::EditorContext::IsAnyLinkSelected() { - for (auto object : m_SelectedObjects) - if (object->AsLink()) return true; - - return false; -} - -bool ed::EditorContext::HasSelectionChanged() { - return m_LastSelectedObjects != m_SelectedObjects; -} - -ed::Node* ed::EditorContext::FindNodeAt(const ImVec2& p) { - for (auto node : m_Nodes) - if (node->TestHit(p)) return node; - - return nullptr; -} - -void ed::EditorContext::FindNodesInRect(const ImRect& r, vector& result, - bool append, bool includeIntersecting) { - if (!append) result.resize(0); - - if (ImRect_IsEmpty(r)) return; - - for (auto node : m_Nodes) - if (node->TestHit(r, includeIntersecting)) result.push_back(node); -} - -void ed::EditorContext::FindLinksInRect(const ImRect& r, vector& result, - bool append) { - if (!append) result.resize(0); - - if (ImRect_IsEmpty(r)) return; - - for (auto link : m_Links) - if (link->TestHit(r)) result.push_back(link); -} - -bool ed::EditorContext::HasAnyLinks(NodeId nodeId) const { - for (auto link : m_Links) { - if (!link->m_IsLive) continue; - - if (link->m_StartPin->m_Node->m_ID == nodeId || - link->m_EndPin->m_Node->m_ID == nodeId) - return true; - } - - return false; -} - -bool ed::EditorContext::HasAnyLinks(PinId pinId) const { - for (auto link : m_Links) { - if (!link->m_IsLive) continue; - - if (link->m_StartPin->m_ID == pinId || link->m_EndPin->m_ID == pinId) - return true; - } - - return false; -} - -int ed::EditorContext::BreakLinks(NodeId nodeId) { - int result = 0; - for (auto link : m_Links) { - if (!link->m_IsLive) continue; - - if (link->m_StartPin->m_Node->m_ID == nodeId || - link->m_EndPin->m_Node->m_ID == nodeId) { - if (GetItemDeleter().Add(link)) ++result; - } - } - return result; -} - -int ed::EditorContext::BreakLinks(PinId pinId) { - int result = 0; - for (auto link : m_Links) { - if (!link->m_IsLive) continue; - - if (link->m_StartPin->m_ID == pinId || link->m_EndPin->m_ID == pinId) { - if (GetItemDeleter().Add(link)) ++result; - } - } - return result; -} - -void ed::EditorContext::FindLinksForNode(NodeId nodeId, vector& result, - bool add) { - if (!add) result.clear(); - - for (auto link : m_Links) { - if (!link->m_IsLive) continue; - - if (link->m_StartPin->m_Node->m_ID == nodeId || - link->m_EndPin->m_Node->m_ID == nodeId) - result.push_back(link); - } -} - -bool ed::EditorContext::PinHadAnyLinks(PinId pinId) { - auto pin = FindPin(pinId); - if (!pin || !pin->m_IsLive) return false; - - return pin->m_HasConnection || pin->m_HadConnection; -} - -void ed::EditorContext::NotifyLinkDeleted(Link* link) { - if (m_LastActiveLink == link) m_LastActiveLink = nullptr; -} - -void ed::EditorContext::Suspend(SuspendFlags flags) { - IM_ASSERT(m_DrawList != nullptr && - "Suspend was called outiside of Begin/End."); - auto lastChannel = m_DrawList->_Splitter._Current; - m_DrawList->ChannelsSetCurrent(m_ExternalChannel); - if (m_IsCanvasVisible) m_Canvas.Suspend(); - m_DrawList->ChannelsSetCurrent(lastChannel); - if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter) - ImDrawList_SwapSplitter(m_DrawList, m_Splitter); -} - -void ed::EditorContext::Resume(SuspendFlags flags) { - IM_ASSERT(m_DrawList != nullptr && - "Reasume was called outiside of Begin/End."); - if ((flags & SuspendFlags::KeepSplitter) != SuspendFlags::KeepSplitter) - ImDrawList_SwapSplitter(m_DrawList, m_Splitter); - auto lastChannel = m_DrawList->_Splitter._Current; - m_DrawList->ChannelsSetCurrent(m_ExternalChannel); - if (m_IsCanvasVisible) m_Canvas.Resume(); - m_DrawList->ChannelsSetCurrent(lastChannel); -} - -bool ed::EditorContext::IsSuspended() { return m_Canvas.IsSuspended(); } - -bool ed::EditorContext::IsFocused() { return m_IsFocused; } - -bool ed::EditorContext::IsHovered() const { return m_IsHovered; } - -bool ed::EditorContext::IsHoveredWithoutOverlapp() const { - return m_IsHoveredWithoutOverlapp; -} - -bool ed::EditorContext::CanAcceptUserInput() const { - return m_IsFocused && m_IsHovered; -} - -int ed::EditorContext::CountLiveNodes() const { - return (int)std::count_if(m_Nodes.begin(), m_Nodes.end(), - [](const Node* node) { return node->m_IsLive; }); -} - -int ed::EditorContext::CountLivePins() const { - return (int)std::count_if(m_Pins.begin(), m_Pins.end(), - [](const Pin* pin) { return pin->m_IsLive; }); -} - -int ed::EditorContext::CountLiveLinks() const { - return (int)std::count_if(m_Links.begin(), m_Links.end(), - [](const Link* link) { return link->m_IsLive; }); -} - -ed::Pin* ed::EditorContext::CreatePin(PinId id, PinKind kind) { - IM_ASSERT(nullptr == FindObject(id)); - auto pin = new Pin(this, id, kind); - m_Pins.push_back({id, pin}); - std::sort(m_Pins.begin(), m_Pins.end()); - return pin; -} - -ed::Node* ed::EditorContext::CreateNode(NodeId id) { - IM_ASSERT(nullptr == FindObject(id)); - auto node = new Node(this, id); - m_Nodes.push_back({id, node}); - // std::sort(Nodes.begin(), Nodes.end()); - - auto settings = m_Settings.FindNode(id); - if (!settings) settings = m_Settings.AddNode(id); - - UpdateNodeState(node); - - if (settings->m_GroupSize.x > 0 || settings->m_GroupSize.y > 0) - node->m_Type = NodeType::Group; - - node->m_IsLive = false; - - return node; -} - -ed::Link* ed::EditorContext::CreateLink(LinkId id) { - IM_ASSERT(nullptr == FindObject(id)); - auto link = new Link(this, id); - m_Links.push_back({id, link}); - std::sort(m_Links.begin(), m_Links.end()); - - return link; -} - -template -static inline auto FindItemInLinear(C& container, Id id) { -#if defined(_DEBUG) - auto start = container.data(); - auto end = container.data() + container.size(); - for (auto it = start; it < end; ++it) - if ((*it).m_ID == id) return it->m_Object; -#else - for (auto item : container) - if (item.m_ID == id) return item.m_Object; -#endif - - return static_cast(nullptr); -} - -template -static inline auto FindItemIn(C& container, Id id) { - // # if defined(_DEBUG) - // auto start = container.data(); - // auto end = container.data() + container.size(); - // for (auto it = start; it < end; ++it) - // if ((*it)->ID == id) - // return *it; - // # else - // for (auto item : container) - // if (item->ID == id) - // return item; - // # endif - auto key = typename C::value_type{id, nullptr}; - auto first = container.cbegin(); - auto last = container.cend(); - auto it = std::lower_bound(first, last, key); - if (it != last && (key.m_ID == it->m_ID)) - return it->m_Object; - else - return static_castm_Object)>(nullptr); -} - -ed::Node* ed::EditorContext::FindNode(NodeId id) { - return FindItemInLinear(m_Nodes, id); -} - -ed::Pin* ed::EditorContext::FindPin(PinId id) { return FindItemIn(m_Pins, id); } - -ed::Link* ed::EditorContext::FindLink(LinkId id) { - return FindItemIn(m_Links, id); -} - -ed::Object* ed::EditorContext::FindObject(ObjectId id) { - if (id.IsNodeId()) - return FindNode(id.AsNodeId()); - else if (id.IsLinkId()) - return FindLink(id.AsLinkId()); - else if (id.IsPinId()) - return FindPin(id.AsPinId()); - else - return nullptr; -} - -ed::Node* ed::EditorContext::GetNode(NodeId id) { - auto node = FindNode(id); - if (!node) node = CreateNode(id); - return node; -} - -ed::Pin* ed::EditorContext::GetPin(PinId id, PinKind kind) { - if (auto pin = FindPin(id)) { - pin->m_Kind = kind; - return pin; - } else - return CreatePin(id, kind); -} - -ed::Link* ed::EditorContext::GetLink(LinkId id) { - if (auto link = FindLink(id)) - return link; - else - return CreateLink(id); -} - -void ed::EditorContext::LoadSettings() { - ed::Settings::Parse(m_Config.Load(), m_Settings); - - if (ImRect_IsEmpty(m_Settings.m_VisibleRect)) { - m_NavigateAction.m_Scroll = m_Settings.m_ViewScroll; - m_NavigateAction.m_Zoom = m_Settings.m_ViewZoom; - } else { - m_NavigateAction.NavigateTo(m_Settings.m_VisibleRect, - NavigateAction::ZoomMode::Exact, 0.0f); - } -} - -void ed::EditorContext::SaveSettings() { - m_Config.BeginSave(); - - for (auto& node : m_Nodes) { - auto settings = m_Settings.FindNode(node->m_ID); - settings->m_Location = node->m_Bounds.Min; - settings->m_Size = node->m_Bounds.GetSize(); - if (IsGroup(node)) settings->m_GroupSize = node->m_GroupBounds.GetSize(); - - if (!node->m_RestoreState && settings->m_IsDirty && - m_Config.SaveNodeSettings) { - if (m_Config.SaveNode(node->m_ID, settings->Serialize().dump(), - settings->m_DirtyReason)) - settings->ClearDirty(); - } - } - - m_Settings.m_Selection.resize(0); - for (auto& object : m_SelectedObjects) - m_Settings.m_Selection.push_back(object->ID()); - - m_Settings.m_ViewScroll = m_NavigateAction.m_Scroll; - m_Settings.m_ViewZoom = m_NavigateAction.m_Zoom; - m_Settings.m_VisibleRect = m_NavigateAction.m_VisibleRect; - - if (m_Config.Save(m_Settings.Serialize(), m_Settings.m_DirtyReason)) - m_Settings.ClearDirty(); - - m_Config.EndSave(); -} - -void ed::EditorContext::MakeDirty(SaveReasonFlags reason) { - m_Settings.MakeDirty(reason); -} - -void ed::EditorContext::MakeDirty(SaveReasonFlags reason, Node* node) { - m_Settings.MakeDirty(reason, node); -} - -ed::Link* ed::EditorContext::FindLinkAt(const ImVec2& p) { - for (auto& link : m_Links) - if (link->TestHit(p, c_LinkSelectThickness)) return link; - - return nullptr; -} - -ImU32 ed::EditorContext::GetColor(StyleColor colorIndex) const { - return ImColor(m_Style.Colors[colorIndex]); -} - -ImU32 ed::EditorContext::GetColor(StyleColor colorIndex, float alpha) const { - auto color = m_Style.Colors[colorIndex]; - return ImColor(color.x, color.y, color.z, color.w * alpha); -} - -int ed::EditorContext::GetNodeIds(NodeId* nodes, int size) const { - if (size <= 0) return 0; - - int result = 0; - for (auto node : m_Nodes) { - if (!node->m_IsLive) continue; - - *nodes++ = node->m_ID; - ++result; - if (--size == 0) break; - } - - return result; -} - -void ed::EditorContext::RegisterAnimation(Animation* animation) { - m_LiveAnimations.push_back(animation); -} - -void ed::EditorContext::UnregisterAnimation(Animation* animation) { - auto it = - std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), animation); - if (it != m_LiveAnimations.end()) m_LiveAnimations.erase(it); -} - -void ed::EditorContext::UpdateAnimations() { - m_LastLiveAnimations = m_LiveAnimations; - - for (auto animation : m_LastLiveAnimations) { - const bool isLive = - (std::find(m_LiveAnimations.begin(), m_LiveAnimations.end(), - animation) != m_LiveAnimations.end()); - - if (isLive) animation->Update(); - } -} - -void ed::EditorContext::Flow(Link* link, FlowDirection direction) { - m_FlowAnimationController.Flow(link, direction); -} - -void ed::EditorContext::SetUserContext(bool globalSpace) { - const auto mousePos = ImGui::GetMousePos(); - - // Move drawing cursor to mouse location and prepare layer for - // content added by user. - if (globalSpace) - ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); - else - ImGui::SetCursorScreenPos(m_Canvas.FromLocal(mousePos)); - // ImGui::SetCursorScreenPos(ImFloor(mousePos)); - // ImGui::SetCursorScreenPos(ImVec2(floorf(mousePos.x), floorf(mousePos.y))); - - if (!IsSuspended()) { - m_DrawList->ChannelsSetCurrent(c_UserChannel_Content); - } - - // #debug - // drawList->AddCircleFilled(ImGui::GetMousePos(), 4, IM_COL32(0, 255, 0, - // 255)); -} - -void ed::EditorContext::EnableShortcuts(bool enable) { - m_ShortcutsEnabled = enable; -} - -bool ed::EditorContext::AreShortcutsEnabled() { return m_ShortcutsEnabled; } - -ed::Control ed::EditorContext::BuildControl(bool allowOffscreen) { - m_IsHovered = false; - m_IsHoveredWithoutOverlapp = false; - - const auto windowHovered = ImGui::IsWindowHovered(); - const auto widgetHovered = ImGui::IsMouseHoveringRect( - m_Canvas.ViewRect().Min, m_Canvas.ViewRect().Max, true); - - if (!allowOffscreen && !windowHovered && !widgetHovered) return Control(); - - const auto mousePos = ImGui::GetMousePos(); - - // Expand clip rectangle to always contain cursor - auto editorRect = m_Canvas.ViewRect(); - auto isMouseOffscreen = allowOffscreen && !editorRect.Contains(mousePos); - if (isMouseOffscreen) { - // Extend clip rect to capture off-screen mouse cursor - editorRect.Add(ImFloor(mousePos)); - editorRect.Add(ImVec2(ImCeil(mousePos.x), ImCeil(mousePos.y))); - - ImGui::PushClipRect(editorRect.Min, editorRect.Max, false); - } - - ImGuiID activeId = 0; - Object* hotObject = nullptr; - Object* activeObject = nullptr; - Object* clickedObject = nullptr; - Object* doubleClickedObject = nullptr; - - ImGuiButtonFlags extraFlags = ImGuiButtonFlags_None; - extraFlags |= ImGuiButtonFlags_MouseButtonLeft; - extraFlags |= ImGuiButtonFlags_MouseButtonRight; - extraFlags |= ImGuiButtonFlags_MouseButtonMiddle; - - static auto invisibleButtonEx = [](const char* str_id, const ImVec2& size_arg, - ImGuiButtonFlags extraFlags) -> int { - using namespace ImGui; - - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) return -1; - - if (size_arg.x == 0.0f || size_arg.y == 0.0f) return false; - - const ImGuiID id = window->GetID(str_id); - 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)) return -1; - - auto buttonIndex = ImGui::GetCurrentContext()->ActiveIdMouseButton; - - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held, extraFlags); - - return pressed ? buttonIndex : -1; - }; - - // Emits invisible button and returns true if it is clicked. - auto emitInteractiveAreaEx = [&activeId](ObjectId id, const ImRect& rect, - ImGuiButtonFlags extraFlags) -> int { - char idString[33] = {0}; // itoa can output 33 bytes maximum - snprintf(idString, 32, "%p", id.AsPointer()); - ImGui::SetCursorScreenPos(rect.Min); - - // debug - // if (id < 0) return ImGui::Button(idString, to_imvec(rect.size)); - - auto buttonIndex = invisibleButtonEx(idString, rect.GetSize(), extraFlags); - - // #debug - // ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), - // ImGui::GetItemRectMax(), IM_COL32(0, 255, 0, 64)); - - if (ImGui::IsItemActive()) activeId = ImGui::GetActiveID(); - - return buttonIndex; - }; - - auto emitInteractiveArea = [&emitInteractiveAreaEx, extraFlags]( - ObjectId id, const ImRect& rect) { - return emitInteractiveAreaEx(id, rect, extraFlags); - }; - - // Check input interactions over area. - auto checkInteractionsInArea = - [this, &emitInteractiveArea, &hotObject, &activeObject, &clickedObject, - &doubleClickedObject](ObjectId id, const ImRect& rect, Object* object) { - if (emitInteractiveArea(id, rect) >= 0) clickedObject = object; - if (!doubleClickedObject && - ImGui::IsMouseDoubleClicked(m_Config.DragButtonIndex) && - ImGui::IsItemHovered()) - doubleClickedObject = object; - - if (!hotObject && ImGui::IsItemHovered( - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) - hotObject = object; - - if (ImGui::IsItemActive()) activeObject = object; - }; - - // Process live nodes and pins. - for (auto nodeIt = m_Nodes.rbegin(), nodeItEnd = m_Nodes.rend(); - nodeIt != nodeItEnd; ++nodeIt) { - auto node = *nodeIt; - - if (!node->m_IsLive) continue; - - // Check for interactions with live pins in node before - // processing node itself. Pins does not overlap each other - // and all are within node bounds. - for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin) { - if (!pin->m_IsLive) continue; - - checkInteractionsInArea(pin->m_ID, pin->m_Bounds, pin); - } - - // Check for interactions with node. - if (node->m_Type == NodeType::Group) { - // Node with a hole - ImGui::PushID(node->m_ID.AsPointer()); - - static const NodeRegion c_Regions[] = { - NodeRegion::TopLeft, NodeRegion::TopRight, NodeRegion::BottomLeft, - NodeRegion::BottomRight, NodeRegion::Top, NodeRegion::Bottom, - NodeRegion::Left, NodeRegion::Right, NodeRegion::Header, - }; - - for (auto region : c_Regions) { - auto bounds = node->GetRegionBounds(region); - if (ImRect_IsEmpty(bounds)) continue; - checkInteractionsInArea(NodeId(static_cast(region)), bounds, node); - } - - ImGui::PopID(); - } else - checkInteractionsInArea(node->m_ID, node->m_Bounds, node); - } - - // Links are not regular widgets and must be done manually since - // ImGui does not support interactive elements with custom hit maps. - // - // Links can steal input from background. - - // Links are just over background. So if anything else - // is hovered we can skip them. - if (nullptr == hotObject) hotObject = FindLinkAt(mousePos); - - ImGuiButtonFlags backgroundExtraFlags = ImGuiButtonFlags_None; - if (m_Config.DragButtonIndex == 0 || m_Config.SelectButtonIndex == 0 || - m_Config.NavigateButtonIndex == 0) - backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonLeft; - if (m_Config.DragButtonIndex == 1 || m_Config.SelectButtonIndex == 1 || - m_Config.NavigateButtonIndex == 1) - backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonRight; - if (m_Config.DragButtonIndex == 2 || m_Config.SelectButtonIndex == 2 || - m_Config.NavigateButtonIndex == 2) - backgroundExtraFlags |= ImGuiButtonFlags_MouseButtonMiddle; - - auto isMouseDoubleClickOverBackground = [doubleClickedObject, - backgroundExtraFlags]() -> int { - if (doubleClickedObject) return -1; - - if (!ImGui::IsItemHovered()) return -1; - - if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonLeft) && - ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) - return ImGuiButtonFlags_MouseButtonLeft; - if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonRight) && - ImGui::IsMouseDoubleClicked(ImGuiButtonFlags_MouseButtonRight)) - return ImGuiButtonFlags_MouseButtonRight; - if ((backgroundExtraFlags & ImGuiButtonFlags_MouseButtonMiddle) && - ImGui::IsMouseDoubleClicked(ImGuiButtonFlags_MouseButtonMiddle)) - return ImGuiButtonFlags_MouseButtonMiddle; - - return -1; - }; - - // Check for interaction with background. - auto backgroundClickButonIndex = - emitInteractiveAreaEx(NodeId(0), editorRect, backgroundExtraFlags); - auto backgroundDoubleClickButtonIndex = isMouseDoubleClickOverBackground(); - auto isBackgroundActive = ImGui::IsItemActive(); - auto isBackgroundHot = !hotObject; - auto isDragging = ImGui::IsMouseDragging(0, 1) || - ImGui::IsMouseDragging(1, 1) || - ImGui::IsMouseDragging(2, 1); - - if (backgroundDoubleClickButtonIndex >= 0) backgroundClickButonIndex = -1; - - if (isMouseOffscreen) ImGui::PopClipRect(); - - // Process link input using background interactions. - auto hotLink = hotObject ? hotObject->AsLink() : nullptr; - - // ImGui take care of tracking active items. With link - // we must do this ourself. - if (!isDragging && isBackgroundActive && hotLink && !m_LastActiveLink) - m_LastActiveLink = hotLink; - if (isBackgroundActive && m_LastActiveLink) { - activeObject = m_LastActiveLink; - isBackgroundActive = false; - } else if (!isBackgroundActive && m_LastActiveLink) - m_LastActiveLink = nullptr; - - // Steal click from backgrounds if link is hovered. - if (!isDragging && backgroundClickButonIndex >= 0 && hotLink) { - clickedObject = hotLink; - backgroundClickButonIndex = -1; - } - - // Steal double-click from backgrounds if link is hovered. - if (!isDragging && backgroundDoubleClickButtonIndex >= 0 && hotLink) { - doubleClickedObject = hotLink; - backgroundDoubleClickButtonIndex = -1; - } - - if (activeId) m_EditorActiveId = activeId; - - if (ImGui::IsAnyItemActive() && ImGui::GetActiveID() != m_EditorActiveId) - return Control(); - - m_IsHovered = ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly); - m_IsHoveredWithoutOverlapp = ImGui::IsItemHovered(); - if (!allowOffscreen && !m_IsHovered) return Control(); - -#if IMGUI_VERSION_NUM >= 18836 - if (m_IsHoveredWithoutOverlapp) ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY); -#elif IMGUI_VERSION_NUM >= 17909 - if (m_IsHoveredWithoutOverlapp) ImGui::SetItemUsingMouseWheel(); -#endif - - return Control(hotObject, activeObject, clickedObject, doubleClickedObject, - isBackgroundHot, isBackgroundActive, backgroundClickButonIndex, - backgroundDoubleClickButtonIndex); -} - -void ed::EditorContext::ShowMetrics(const Control& control) { - auto& io = ImGui::GetIO(); - - auto getObjectName = [](Object* object) { - if (!object) - return ""; - else if (object->AsNode()) - return "Node"; - else if (object->AsPin()) - return "Pin"; - else if (object->AsLink()) - return "Link"; - else - return ""; - }; - - auto getHotObjectName = [&control, &getObjectName]() { - if (control.HotObject) - return getObjectName(control.HotObject); - else if (control.BackgroundHot) - return "Background"; - else - return ""; - }; - - auto getActiveObjectName = [&control, &getObjectName]() { - if (control.ActiveObject) - return getObjectName(control.ActiveObject); - else if (control.BackgroundActive) - return "Background"; - else - return ""; - }; - - auto liveNodeCount = CountLiveNodes(); - auto livePinCount = CountLivePins(); - auto liveLinkCount = CountLiveLinks(); - - auto canvasRect = m_Canvas.Rect(); - auto viewRect = m_Canvas.ViewRect(); - auto localMousePos = m_Canvas.ToLocal(io.MousePos); - auto globalMousePos = io.MousePos; - - ImGui::SetCursorScreenPos(canvasRect.Min + ImVec2(5, 5)); - ImGui::BeginGroup(); - ImGui::Text("Is Focused: %s", m_IsFocused ? "true" : "false"); - ImGui::Text("Is Hovered: %s", m_IsHovered ? "true" : "false"); - ImGui::Text("Is Hovered (without overlapp): %s", - m_IsHoveredWithoutOverlapp ? "true" : "false"); - ImGui::Text("Accept Input: %s", CanAcceptUserInput() ? "true" : "false"); - ImGui::Text("View Position: { x=%g y=%g }", viewRect.Min.x, viewRect.Min.y); - ImGui::Text("View Size: { w=%g h=%g }", viewRect.GetWidth(), - viewRect.GetHeight()); - ImGui::Text("Canvas Size: { w=%g h=%g }", canvasRect.GetWidth(), - canvasRect.GetHeight()); - ImGui::Text("Mouse: { x=%.0f y=%.0f } global: { x=%g y=%g }", localMousePos.x, - localMousePos.y, globalMousePos.x, globalMousePos.y); - ImGui::Text("Live Nodes: %d", liveNodeCount); - ImGui::Text("Live Pins: %d", livePinCount); - ImGui::Text("Live Links: %d", liveLinkCount); - ImGui::Text( - "Hot Object: %s (%p)", getHotObjectName(), - control.HotObject ? control.HotObject->ID().AsPointer() : nullptr); - if (auto node = control.HotObject ? control.HotObject->AsNode() : nullptr) { - ImGui::SameLine(); - ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, - node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), - node->m_Bounds.GetHeight()); - } - ImGui::Text( - "Active Object: %s (%p)", getActiveObjectName(), - control.ActiveObject ? control.ActiveObject->ID().AsPointer() : nullptr); - if (auto node = - control.ActiveObject ? control.ActiveObject->AsNode() : nullptr) { - ImGui::SameLine(); - ImGui::Text("{ x=%g y=%g w=%g h=%g }", node->m_Bounds.Min.x, - node->m_Bounds.Min.y, node->m_Bounds.GetWidth(), - node->m_Bounds.GetHeight()); - } - ImGui::Text("Action: %s", - m_CurrentAction ? m_CurrentAction->GetName() : ""); - ImGui::Text("Action Is Dragging: %s", - m_CurrentAction && m_CurrentAction->IsDragging() ? "Yes" : "No"); - m_NavigateAction.ShowMetrics(); - m_SizeAction.ShowMetrics(); - m_DragAction.ShowMetrics(); - m_SelectAction.ShowMetrics(); - m_ContextMenuAction.ShowMetrics(); - m_CreateItemAction.ShowMetrics(); - m_DeleteItemsAction.ShowMetrics(); - ImGui::EndGroup(); -} - -//------------------------------------------------------------------------------ -// -// Node Settings -// -//------------------------------------------------------------------------------ -void ed::NodeSettings::ClearDirty() { - m_IsDirty = false; - m_DirtyReason = SaveReasonFlags::None; -} - -void ed::NodeSettings::MakeDirty(SaveReasonFlags reason) { - m_IsDirty = true; - m_DirtyReason = m_DirtyReason | reason; -} - -ed::json::value ed::NodeSettings::Serialize() { - json::value result; - result["location"]["x"] = m_Location.x; - result["location"]["y"] = m_Location.y; - - if (m_GroupSize.x > 0 || m_GroupSize.y > 0) { - result["group_size"]["x"] = m_GroupSize.x; - result["group_size"]["y"] = m_GroupSize.y; - } - - return result; -} - -bool ed::NodeSettings::Parse(const std::string& string, - NodeSettings& settings) { - auto settingsValue = json::value::parse(string); - if (settingsValue.is_discarded()) return false; - - return Parse(settingsValue, settings); -} - -bool ed::NodeSettings::Parse(const json::value& data, NodeSettings& result) { - if (!data.is_object()) return false; - - auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool { - if (v.is_object()) { - auto xValue = v["x"]; - auto yValue = v["y"]; - - if (xValue.is_number() && yValue.is_number()) { - result.x = static_cast(xValue.get()); - result.y = static_cast(yValue.get()); - - return true; - } - } - - return false; - }; - - if (!tryParseVector(data["location"], result.m_Location)) return false; - - if (data.contains("group_size") && - !tryParseVector(data["group_size"], result.m_GroupSize)) - return false; - - return true; -} - -//------------------------------------------------------------------------------ -// -// Settings -// -//------------------------------------------------------------------------------ -ed::NodeSettings* ed::Settings::AddNode(NodeId id) { - m_Nodes.push_back(NodeSettings(id)); - return &m_Nodes.back(); -} - -ed::NodeSettings* ed::Settings::FindNode(NodeId id) { - for (auto& settings : m_Nodes) - if (settings.m_ID == id) return &settings; - - return nullptr; -} - -void ed::Settings::RemoveNode(NodeId id) { - auto node = FindNode(id); - if (!node) return; - - *node = NodeSettings(id); -} - -void ed::Settings::ClearDirty(Node* node) { - if (node) { - auto settings = FindNode(node->m_ID); - IM_ASSERT(settings); - settings->ClearDirty(); - } else { - m_IsDirty = false; - m_DirtyReason = SaveReasonFlags::None; - - for (auto& knownNode : m_Nodes) knownNode.ClearDirty(); - } -} - -void ed::Settings::MakeDirty(SaveReasonFlags reason, Node* node) { - m_IsDirty = true; - m_DirtyReason = m_DirtyReason | reason; - - if (node) { - auto settings = FindNode(node->m_ID); - IM_ASSERT(settings); - - settings->MakeDirty(reason); - } -} - -std::string ed::Settings::Serialize() { - json::value result; - - auto serializeObjectId = [](ObjectId id) { - auto value = std::to_string(reinterpret_cast(id.AsPointer())); - switch (id.Type()) { - default: - case NodeEditor::Detail::ObjectType::None: - return value; - case NodeEditor::Detail::ObjectType::Node: - return "node:" + value; - case NodeEditor::Detail::ObjectType::Link: - return "link:" + value; - case NodeEditor::Detail::ObjectType::Pin: - return "pin:" + value; - } - }; - - auto& nodes = result["nodes"]; - for (auto& node : m_Nodes) { - if (node.m_WasUsed) nodes[serializeObjectId(node.m_ID)] = node.Serialize(); - } - - auto& selection = result["selection"]; - for (auto& id : m_Selection) selection.push_back(serializeObjectId(id)); - - auto& view = result["view"]; - view["scroll"]["x"] = m_ViewScroll.x; - view["scroll"]["y"] = m_ViewScroll.y; - view["zoom"] = m_ViewZoom; - view["visible_rect"]["min"]["x"] = m_VisibleRect.Min.x; - view["visible_rect"]["min"]["y"] = m_VisibleRect.Min.y; - view["visible_rect"]["max"]["x"] = m_VisibleRect.Max.x; - view["visible_rect"]["max"]["y"] = m_VisibleRect.Max.y; - - return result.dump(); -} - -bool ed::Settings::Parse(const std::string& string, Settings& settings) { - Settings result = settings; - - auto settingsValue = json::value::parse(string); - if (settingsValue.is_discarded()) return false; - - if (!settingsValue.is_object()) return false; - - auto tryParseVector = [](const json::value& v, ImVec2& result) -> bool { - if (v.is_object() && v.contains("x") && v.contains("y")) { - auto xValue = v["x"]; - auto yValue = v["y"]; - - if (xValue.is_number() && yValue.is_number()) { - result.x = static_cast(xValue.get()); - result.y = static_cast(yValue.get()); - - return true; - } - } - - return false; - }; - - auto deserializeObjectId = [](const std::string& str) { - auto separator = str.find_first_of(':'); - auto idStart = - str.c_str() + ((separator != std::string::npos) ? separator + 1 : 0); - auto id = reinterpret_cast(strtoull(idStart, nullptr, 10)); - if (str.compare(0, separator, "node") == 0) - return ObjectId(NodeId(id)); - else if (str.compare(0, separator, "link") == 0) - return ObjectId(LinkId(id)); - else if (str.compare(0, separator, "pin") == 0) - return ObjectId(PinId(id)); - else - // fallback to old format - return ObjectId(NodeId(id)); // return ObjectId(); - }; - - // auto& settingsObject = settingsValue.get(); - - auto& nodesValue = settingsValue["nodes"]; - if (nodesValue.is_object()) { - for (auto& node : nodesValue.get()) { - auto id = deserializeObjectId(node.first.c_str()).AsNodeId(); - - auto nodeSettings = result.FindNode(id); - if (!nodeSettings) nodeSettings = result.AddNode(id); - - NodeSettings::Parse(node.second, *nodeSettings); - } - } - - auto& selectionValue = settingsValue["selection"]; - if (selectionValue.is_array()) { - const auto selectionArray = selectionValue.get(); - - result.m_Selection.reserve(selectionArray.size()); - result.m_Selection.resize(0); - for (auto& selection : selectionArray) { - if (selection.is_string()) - result.m_Selection.push_back( - deserializeObjectId(selection.get())); - } - } - - auto& viewValue = settingsValue["view"]; - if (viewValue.is_object()) { - auto& viewScrollValue = viewValue["scroll"]; - auto& viewZoomValue = viewValue["zoom"]; - - if (!tryParseVector(viewScrollValue, result.m_ViewScroll)) - result.m_ViewScroll = ImVec2(0, 0); - - result.m_ViewZoom = viewZoomValue.is_number() - ? static_cast(viewZoomValue.get()) - : 1.0f; - - if (!viewValue.contains("visible_rect") || - !tryParseVector(viewValue["visible_rect"]["min"], - result.m_VisibleRect.Min) || - !tryParseVector(viewValue["visible_rect"]["max"], - result.m_VisibleRect.Max)) - result.m_VisibleRect = {}; - } - - settings = std::move(result); - - return true; -} - -//------------------------------------------------------------------------------ -// -// Animation -// -//------------------------------------------------------------------------------ -ed::Animation::Animation(EditorContext* editor) - : Editor(editor), m_State(Stopped), m_Time(0.0f), m_Duration(0.0f) {} - -ed::Animation::~Animation() { Stop(); } - -void ed::Animation::Play(float duration) { - if (IsPlaying()) Stop(); - - m_State = Playing; - if (duration < 0) duration = 0.0f; - - m_Time = 0.0f; - m_Duration = duration; - - OnPlay(); - - Editor->RegisterAnimation(this); - - if (duration == 0.0f) Finish(); -} - -void ed::Animation::Stop() { - if (!IsPlaying()) return; - - m_State = Stopped; - - Editor->UnregisterAnimation(this); - - OnStop(); -} - -void ed::Animation::Finish() { - if (!IsPlaying()) return; - - OnFinish(); - - Stop(); -} - -void ed::Animation::Update() { - if (!IsPlaying()) return; - - m_Time += ImMax(0.0f, ImGui::GetIO().DeltaTime); - if (m_Time < m_Duration) { - const float progress = GetProgress(); - OnUpdate(progress); - } else { - OnFinish(); - Stop(); - } -} - -//------------------------------------------------------------------------------ -// -// Navigate Animation -// -//------------------------------------------------------------------------------ -ed::NavigateAnimation::NavigateAnimation(EditorContext* editor, - NavigateAction& scrollAction) - : Animation(editor), Action(scrollAction) {} - -void ed::NavigateAnimation::NavigateTo(const ImRect& target, float duration) { - Stop(); - - m_Start = Action.GetViewRect(); - m_Target = target; - - // Skip tiny animations - auto minoffset = m_Target.Min - m_Start.Min; - auto maxOffset = m_Target.Max - m_Start.Max; - auto epsilon = 1e-4f; - if (ImFabs(minoffset.x) < epsilon && ImFabs(minoffset.y) < epsilon && - ImFabs(maxOffset.x) < epsilon && ImFabs(maxOffset.y) < epsilon) { - duration = 0; - } - - Play(duration); -} - -void ed::NavigateAnimation::OnUpdate(float progress) { - ImRect current; - current.Min = - ImEasing::EaseOutQuad(m_Start.Min, m_Target.Min - m_Start.Min, progress); - current.Max = - ImEasing::EaseOutQuad(m_Start.Max, m_Target.Max - m_Start.Max, progress); - Action.SetViewRect(current); -} - -void ed::NavigateAnimation::OnStop() { - Editor->MakeDirty(SaveReasonFlags::Navigation); -} - -void ed::NavigateAnimation::OnFinish() { - Action.SetViewRect(m_Target); - - Editor->MakeDirty(SaveReasonFlags::Navigation); -} - -//------------------------------------------------------------------------------ -// -// Flow Animation -// -//------------------------------------------------------------------------------ -ed::FlowAnimation::FlowAnimation(FlowAnimationController* controller) - : Animation(controller->Editor), - Controller(controller), - m_Link(nullptr), - m_Offset(0.0f), - m_PathLength(0.0f) {} - -void ed::FlowAnimation::Flow(ed::Link* link, float markerDistance, float speed, - float duration) { - Stop(); - - if (m_Link != link) { - m_Offset = 0.0f; - ClearPath(); - } - - if (m_MarkerDistance != markerDistance) ClearPath(); - - m_MarkerDistance = markerDistance; - m_Speed = speed; - m_Link = link; - - Play(duration); -} - -void ed::FlowAnimation::Draw(ImDrawList* drawList) { - if (!IsPlaying() || !IsLinkValid() || !m_Link->IsVisible()) return; - - if (!IsPathValid()) UpdatePath(); - - m_Offset = fmodf(m_Offset, m_MarkerDistance); - if (m_Offset < 0) m_Offset += m_MarkerDistance; - - const auto progress = GetProgress(); - - const auto flowAlpha = 1.0f - progress * progress; - const auto flowColor = Editor->GetColor(StyleColor_Flow, flowAlpha); - // const auto flowPath = Link->GetCurve(); - - m_Link->Draw(drawList, flowColor, 2.0f); - - if (IsPathValid()) { - // Offset = 0; - - const auto markerAlpha = powf(1.0f - progress, 0.35f); - const auto markerRadius = 4.0f * (1.0f - progress) + 2.0f; - const auto markerColor = - Editor->GetColor(StyleColor_FlowMarker, markerAlpha); - - for (float d = m_Offset; d < m_PathLength; d += m_MarkerDistance) - drawList->AddCircleFilled(SamplePath(d), markerRadius, markerColor); - } -} - -bool ed::FlowAnimation::IsLinkValid() const { - return m_Link && m_Link->m_IsLive; -} - -bool ed::FlowAnimation::IsPathValid() const { - return m_Path.size() > 1 && m_PathLength > 0.0f && - m_Link->m_Start == m_LastStart && m_Link->m_End == m_LastEnd; -} - -void ed::FlowAnimation::UpdatePath() { - if (!IsLinkValid()) { - ClearPath(); - return; - } - - const auto curve = m_Link->GetCurve(); - - m_LastStart = m_Link->m_Start; - m_LastEnd = m_Link->m_End; - m_PathLength = ImCubicBezierLength(curve.P0, curve.P1, curve.P2, curve.P3); - - auto collectPointsCallback = [this](ImCubicBezierFixedStepSample& result) { - m_Path.push_back(CurvePoint{result.Length, result.Point}); - }; - - const auto step = ImMax(m_MarkerDistance * 0.5f, 15.0f); - - m_Path.resize(0); - ImCubicBezierFixedStep(collectPointsCallback, curve, step, false, 0.5f, - 0.001f); -} - -void ed::FlowAnimation::ClearPath() { - vector().swap(m_Path); - m_PathLength = 0.0f; -} - -ImVec2 ed::FlowAnimation::SamplePath(float distance) const { - // distance = ImMax(0.0f, std::min(distance, PathLength)); - - auto endPointIt = std::find_if( - m_Path.begin(), m_Path.end(), - [distance](const CurvePoint& p) { return distance < p.Distance; }); - if (endPointIt == m_Path.end()) - endPointIt = m_Path.end() - 1; - else if (endPointIt == m_Path.begin()) - endPointIt = m_Path.begin() + 1; - - const auto& start = endPointIt[-1]; - const auto& end = *endPointIt; - const auto t = (distance - start.Distance) / (end.Distance - start.Distance); - - return start.Point + (end.Point - start.Point) * t; -} - -void ed::FlowAnimation::OnUpdate(float progress) { - IM_UNUSED(progress); - - m_Offset += m_Speed * ImGui::GetIO().DeltaTime; -} - -void ed::FlowAnimation::OnStop() { Controller->Release(this); } - -//------------------------------------------------------------------------------ -// -// Flow Animation Controller -// -//------------------------------------------------------------------------------ -ed::FlowAnimationController::FlowAnimationController(EditorContext* editor) - : AnimationController(editor) {} - -ed::FlowAnimationController::~FlowAnimationController() { - for (auto animation : m_Animations) delete animation; -} - -void ed::FlowAnimationController::Flow(Link* link, FlowDirection direction) { - if (!link || !link->m_IsLive) return; - - auto& editorStyle = GetStyle(); - - auto animation = GetOrCreate(link); - - float speedDirection = 1.0f; - if (direction == FlowDirection::Backward) speedDirection = -1.0f; - - animation->Flow(link, editorStyle.FlowMarkerDistance, - editorStyle.FlowSpeed * speedDirection, - editorStyle.FlowDuration); -} - -void ed::FlowAnimationController::Draw(ImDrawList* drawList) { - if (m_Animations.empty()) return; - - drawList->ChannelsSetCurrent(c_LinkChannel_Flow); - - for (auto animation : m_Animations) animation->Draw(drawList); -} - -ed::FlowAnimation* ed::FlowAnimationController::GetOrCreate(Link* link) { - // Return live animation which match target link - { - auto animationIt = std::find_if( - m_Animations.begin(), m_Animations.end(), - [link](FlowAnimation* animation) { return animation->m_Link == link; }); - if (animationIt != m_Animations.end()) return *animationIt; - } - - // There are no live animations for target link, try to reuse inactive old one - if (!m_FreePool.empty()) { - auto animation = m_FreePool.back(); - m_FreePool.pop_back(); - return animation; - } - - // Cache miss, allocate new one - auto animation = new FlowAnimation(this); - m_Animations.push_back(animation); - - return animation; -} - -void ed::FlowAnimationController::Release(FlowAnimation* animation) { - IM_UNUSED(animation); -} - -//------------------------------------------------------------------------------ -// -// Navigate Action -// -//------------------------------------------------------------------------------ -const float ed::NavigateAction::s_DefaultZoomLevels[] = { - 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f, 1.25f, - 1.50f, 2.0f, 2.5f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f}; - -const int ed::NavigateAction::s_DefaultZoomLevelCount = - sizeof(s_DefaultZoomLevels) / sizeof(*s_DefaultZoomLevels); - -ed::NavigateAction::NavigateAction(EditorContext* editor, - ImGuiEx::Canvas& canvas) - : EditorAction(editor), - m_IsActive(false), - m_Zoom(1), - m_VisibleRect(), - m_Scroll(0, 0), - m_ScrollStart(0, 0), - m_ScrollDelta(0, 0), - m_Canvas(canvas), - m_WindowScreenPos(0, 0), - m_WindowScreenSize(0, 0), - m_Animation(editor, *this), - m_Reason(NavigationReason::Unknown), - m_LastSelectionId(0), - m_LastObject(nullptr), - m_MovingOverEdge(false), - m_MoveScreenOffset(0, 0), - m_ZoomLevels(editor->GetConfig().CustomZoomLevels.Size > 0 - ? editor->GetConfig().CustomZoomLevels.Data - : s_DefaultZoomLevels), - m_ZoomLevelCount(editor->GetConfig().CustomZoomLevels.Size > 0 - ? editor->GetConfig().CustomZoomLevels.Size - : s_DefaultZoomLevelCount) {} - -ed::EditorAction::AcceptResult ed::NavigateAction::Accept( - const Control& control) { - IM_ASSERT(!m_IsActive); - - if (m_IsActive) return False; - - if (Editor->CanAcceptUserInput() /*&& !ImGui::IsAnyItemActive()*/ && - ImGui::IsMouseDragging(Editor->GetConfig().NavigateButtonIndex, 0.0f)) { - m_IsActive = true; - m_ScrollStart = m_Scroll; - m_ScrollDelta = - ImGui::GetMouseDragDelta(Editor->GetConfig().NavigateButtonIndex); - m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom; - } - - auto& io = ImGui::GetIO(); - - if (Editor->CanAcceptUserInput() && ImGui::IsKeyPressed(GetKeyIndexForF()) && - Editor->AreShortcutsEnabled()) { - const auto zoomMode = io.KeyShift ? NavigateAction::ZoomMode::WithMargin - : NavigateAction::ZoomMode::None; - - auto findHotObjectToZoom = [this, &control, &io]() -> Object* { - if (control.HotObject) { - if (auto pin = control.HotObject->AsPin()) - return pin->m_Node; - else - return control.HotObject; - } else if (control.BackgroundHot) { - auto node = Editor->FindNodeAt(io.MousePos); - if (IsGroup(node)) return node; - } - - return nullptr; - }; - - bool navigateToContent = false; - if (!Editor->GetSelectedObjects().empty()) { - if (m_Reason != NavigationReason::Selection || - m_LastSelectionId != Editor->GetSelectionId() || - (zoomMode != NavigateAction::ZoomMode::None)) { - m_LastSelectionId = Editor->GetSelectionId(); - NavigateTo(Editor->GetSelectionBounds(), zoomMode, -1.0f, - NavigationReason::Selection); - } else - navigateToContent = true; - } else if (auto hotObject = findHotObjectToZoom()) { - if (m_Reason != NavigationReason::Object || m_LastObject != hotObject || - (zoomMode != NavigateAction::ZoomMode::None)) { - m_LastObject = hotObject; - auto bounds = hotObject->GetBounds(); - NavigateTo(bounds, zoomMode, -1.0f, NavigationReason::Object); - } else - navigateToContent = true; - } else - navigateToContent = true; - - if (navigateToContent) - NavigateTo(Editor->GetContentBounds(), - NavigateAction::ZoomMode::WithMargin, -1.0f, - NavigationReason::Content); - } - - auto visibleRect = GetViewRect(); - if (m_VisibleRect.Min != visibleRect.Min || - m_VisibleRect.Max != visibleRect.Max) { - m_VisibleRect = visibleRect; - Editor->MakeDirty(SaveReasonFlags::Navigation); - } - - // // #debug - // if (m_DrawList) - // m_DrawList->AddCircleFilled(io.MousePos, 4.0f, IM_COL32(255, 0, 255, - // 255)); - - if (HandleZoom(control)) return True; - - return m_IsActive ? True : False; -} - -bool ed::NavigateAction::Process(const Control& control) { - IM_UNUSED(control); - - if (!m_IsActive) return false; - - if (ImGui::IsMouseDragging(Editor->GetConfig().NavigateButtonIndex, 0.0f)) { - m_ScrollDelta = - ImGui::GetMouseDragDelta(Editor->GetConfig().NavigateButtonIndex); - m_Scroll = m_ScrollStart - m_ScrollDelta * m_Zoom; - m_VisibleRect = GetViewRect(); - // if (IsActive && Animation.IsPlaying()) - // Animation.Target = Animation.Target - ScrollDelta * - // Animation.TargetZoom; - } else { - if (m_Scroll != m_ScrollStart) - Editor->MakeDirty(SaveReasonFlags::Navigation); - - m_IsActive = false; - } - - // #TODO: Handle zoom while scrolling - // HandleZoom(control); - - return m_IsActive; -} - -bool ed::NavigateAction::HandleZoom(const Control& control) { - IM_UNUSED(control); - - const auto currentAction = Editor->GetCurrentAction(); - const auto allowOffscreen = currentAction && currentAction->IsDragging(); - - auto& io = ImGui::GetIO(); - - if (!io.MouseWheel || - (!allowOffscreen && - !Editor->IsHoveredWithoutOverlapp())) // && !ImGui::IsAnyItemActive()) - return false; - - auto savedScroll = m_Scroll; - auto savedZoom = m_Zoom; - - m_Animation.Finish(); - - auto mousePos = io.MousePos; - auto steps = (int)io.MouseWheel; - auto newZoom = - MatchZoom(steps, m_ZoomLevels[steps < 0 ? 0 : m_ZoomLevelCount - 1]); - - auto oldView = GetView(); - m_Zoom = newZoom; - auto newView = GetView(); - - auto screenPos = m_Canvas.FromLocal(mousePos, oldView); - auto canvasPos = m_Canvas.ToLocal(screenPos, newView); - - auto offset = (canvasPos - mousePos) * m_Zoom; - auto targetScroll = m_Scroll - offset; - - auto visibleRect = GetViewRect(); - - if (m_Scroll != savedScroll || m_Zoom != savedZoom || - m_VisibleRect.Min != visibleRect.Min || - m_VisibleRect.Max != visibleRect.Max) { - m_Scroll = savedScroll; - m_Zoom = savedZoom; - m_VisibleRect = visibleRect; - - Editor->MakeDirty(SaveReasonFlags::Navigation); - } - - auto targetRect = - m_Canvas.CalcViewRect(ImGuiEx::CanvasView(-targetScroll, newZoom)); - - NavigateTo(targetRect, c_MouseZoomDuration, NavigationReason::MouseZoom); - - return true; -} - -void ed::NavigateAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); - ImGui::Text(" Scroll: { x=%g y=%g }", m_Scroll.x, m_Scroll.y); - ImGui::Text(" Zoom: %g", m_Zoom); - ImGui::Text(" Visible Rect: { l=%g t=%g, r=%g b=%g w=%g h=%g }", - m_VisibleRect.Min.x, m_VisibleRect.Min.y, m_VisibleRect.Max.x, - m_VisibleRect.Max.y, m_VisibleRect.Max.x - m_VisibleRect.Min.x, - m_VisibleRect.Max.y - m_VisibleRect.Min.y); -} - -void ed::NavigateAction::NavigateTo(const ImRect& bounds, ZoomMode zoomMode, - float duration, NavigationReason reason) { - if (ImRect_IsEmpty(bounds)) return; - - if (duration < 0.0f) duration = GetStyle().ScrollDuration; - - if (zoomMode == ZoomMode::None) { - auto viewRect = m_Canvas.ViewRect(); - auto viewRectCenter = viewRect.GetCenter(); - auto targetCenter = bounds.GetCenter(); - - viewRect.Translate(targetCenter - viewRectCenter); - - NavigateTo(viewRect, duration, reason); - } else { - // Grow rect by 5% to leave some reasonable margin - // from the edges of the canvas. - auto rect = bounds; - - if (zoomMode == ZoomMode::WithMargin) { - auto extend = ImMax(rect.GetWidth(), rect.GetHeight()); - rect.Expand(extend * c_NavigationZoomMargin * 0.5f); - } - - NavigateTo(rect, duration, reason); - } -} - -void ed::NavigateAction::NavigateTo(const ImRect& target, float duration, - NavigationReason reason) { - m_Reason = reason; - - m_Animation.NavigateTo(target, duration); -} - -void ed::NavigateAction::StopNavigation() { m_Animation.Stop(); } - -void ed::NavigateAction::FinishNavigation() { m_Animation.Finish(); } - -bool ed::NavigateAction::MoveOverEdge(const ImVec2& canvasSize) { - // Don't interrupt non-edge animations - if (m_Animation.IsPlaying()) return false; - - auto& io = ImGui::GetIO(); - - const auto screenMousePos = io.MousePos; - const auto screenRect = ImRect(ImGui::GetCursorScreenPos(), - ImGui::GetCursorScreenPos() + canvasSize); - - // Mouse is over screen, do nothing - if (screenRect.Contains(screenMousePos)) return false; - - // Several backend move mouse position to -FLT_MAX to indicate - // uninitialized/unknown state. To prevent all sorts - // of math problems, we just ignore such state. - if (screenMousePos.x <= -FLT_MAX || screenMousePos.y <= -FLT_MAX) - return false; - - const auto minDistance = - ImVec2(-c_MaxMoveOverEdgeDistance, -c_MaxMoveOverEdgeDistance); - const auto maxDistance = - ImVec2(c_MaxMoveOverEdgeDistance, c_MaxMoveOverEdgeDistance); - - const auto screenPointOnEdge = - ImRect_ClosestPoint(screenRect, screenMousePos, true); - const auto offset = ImMin( - ImMax(screenPointOnEdge - screenMousePos, minDistance), maxDistance); - const auto relativeOffset = -offset * io.DeltaTime * c_MaxMoveOverEdgeSpeed; - - m_Scroll = m_Scroll + relativeOffset; - - m_MoveScreenOffset = relativeOffset; - m_MovingOverEdge = true; - - return true; -} - -void ed::NavigateAction::StopMoveOverEdge() { - if (m_MovingOverEdge) { - Editor->MakeDirty(SaveReasonFlags::Navigation); - - m_MoveScreenOffset = ImVec2(0, 0); - m_MovingOverEdge = false; - } -} - -void ed::NavigateAction::SetWindow(ImVec2 position, ImVec2 size) { - m_WindowScreenPos = position; - m_WindowScreenSize = size; -} - -ImGuiEx::CanvasView ed::NavigateAction::GetView() const { - return ImGuiEx::CanvasView(-m_Scroll, m_Zoom); -} - -ImVec2 ed::NavigateAction::GetViewOrigin() const { return -m_Scroll; } - -float ed::NavigateAction::GetViewScale() const { return m_Zoom; } - -void ed::NavigateAction::SetViewRect(const ImRect& rect) { - auto view = m_Canvas.CalcCenterView(rect); - m_Scroll = -view.Origin; - m_Zoom = view.Scale; -} - -ImRect ed::NavigateAction::GetViewRect() const { - return m_Canvas.CalcViewRect(GetView()); -} - -float ed::NavigateAction::MatchZoom(int steps, float fallbackZoom) { - auto currentZoomIndex = MatchZoomIndex(steps); - if (currentZoomIndex < 0) return fallbackZoom; - - auto currentZoom = m_ZoomLevels[currentZoomIndex]; - if (fabsf(currentZoom - m_Zoom) > 0.001f) return currentZoom; - - auto newIndex = currentZoomIndex + steps; - if (newIndex >= 0 && newIndex < m_ZoomLevelCount) - return m_ZoomLevels[newIndex]; - else - return fallbackZoom; -} - -int ed::NavigateAction::MatchZoomIndex(int direction) { - int bestIndex = -1; - float bestDistance = 0.0f; - - for (int i = 0; i < m_ZoomLevelCount; ++i) { - auto distance = fabsf(m_ZoomLevels[i] - m_Zoom); - if (distance < bestDistance || bestIndex < 0) { - bestDistance = distance; - bestIndex = i; - } - } - - if (bestDistance > 0.001f) { - if (direction > 0) { - ++bestIndex; - - if (bestIndex >= m_ZoomLevelCount) bestIndex = m_ZoomLevelCount - 1; - } else if (direction < 0) { - --bestIndex; - - if (bestIndex < 0) bestIndex = 0; - } - } - - return bestIndex; -} - -//------------------------------------------------------------------------------ -// -// Size Action -// -//------------------------------------------------------------------------------ -ed::SizeAction::SizeAction(EditorContext* editor) - : EditorAction(editor), - m_IsActive(false), - m_Clean(false), - m_SizedNode(nullptr), - m_Pivot(NodeRegion::None), - m_Cursor(ImGuiMouseCursor_Arrow) {} - -ed::EditorAction::AcceptResult ed::SizeAction::Accept(const Control& control) { - IM_ASSERT(!m_IsActive); - - if (m_IsActive) return False; - - if (control.ActiveNode && IsGroup(control.ActiveNode) && - ImGui::IsMouseDragging(Editor->GetConfig().DragButtonIndex, 1)) { - // const auto mousePos = to_point(ImGui::GetMousePos()); - // const auto closestPoint = - // control.ActiveNode->Bounds.get_closest_point_hollow(mousePos, - // static_cast(control.ActiveNode->Rounding)); - - auto pivot = GetRegion(control.ActiveNode); - if (pivot != NodeRegion::Header && pivot != NodeRegion::Center) { - m_StartBounds = control.ActiveNode->m_Bounds; - m_StartGroupBounds = control.ActiveNode->m_GroupBounds; - m_LastSize = control.ActiveNode->m_Bounds.GetSize(); - m_MinimumSize = ImVec2(0, 0); - m_LastDragOffset = ImVec2(0, 0); - m_Pivot = pivot; - m_Cursor = ChooseCursor(m_Pivot); - m_SizedNode = control.ActiveNode; - m_IsActive = true; - } - } else if (control.HotNode && IsGroup(control.HotNode)) { - m_Cursor = ChooseCursor(GetRegion(control.HotNode)); - return Possible; - } - - return m_IsActive ? True : False; -} - -bool ed::SizeAction::Process(const Control& control) { - if (m_Clean) { - m_Clean = false; - - if (m_SizedNode->m_Bounds.Min != m_StartBounds.Min || - m_SizedNode->m_GroupBounds.Min != m_StartGroupBounds.Min) - Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, - m_SizedNode); - - if (m_SizedNode->m_Bounds.GetSize() != m_StartBounds.GetSize() || - m_SizedNode->m_GroupBounds.GetSize() != m_StartGroupBounds.GetSize()) - Editor->MakeDirty(SaveReasonFlags::Size | SaveReasonFlags::User, - m_SizedNode); - - m_SizedNode = nullptr; - } - - if (!m_IsActive) return false; - - if (control.ActiveNode == m_SizedNode) { - const auto dragOffset = (control.ActiveNode == m_SizedNode) - ? ImGui::GetMouseDragDelta(0, 0.0f) - : m_LastDragOffset; - m_LastDragOffset = dragOffset; - - if (m_MinimumSize.x == 0.0f && - m_LastSize.x != m_SizedNode->m_Bounds.GetWidth()) - m_MinimumSize.x = m_SizedNode->m_Bounds.GetWidth(); - if (m_MinimumSize.y == 0.0f && - m_LastSize.y != m_SizedNode->m_Bounds.GetHeight()) - m_MinimumSize.y = m_SizedNode->m_Bounds.GetHeight(); - - auto minimumSize = ImMax( - m_MinimumSize, m_StartBounds.GetSize() - m_StartGroupBounds.GetSize()); - - auto newBounds = m_StartBounds; - - if ((m_Pivot & NodeRegion::Top) == NodeRegion::Top) - newBounds.Min.y = - ImMin(newBounds.Max.y - minimumSize.y, - Editor->AlignPointToGrid(newBounds.Min.y + dragOffset.y)); - if ((m_Pivot & NodeRegion::Bottom) == NodeRegion::Bottom) - newBounds.Max.y = - ImMax(newBounds.Min.y + minimumSize.y, - Editor->AlignPointToGrid(newBounds.Max.y + dragOffset.y)); - if ((m_Pivot & NodeRegion::Left) == NodeRegion::Left) - newBounds.Min.x = - ImMin(newBounds.Max.x - minimumSize.x, - Editor->AlignPointToGrid(newBounds.Min.x + dragOffset.x)); - if ((m_Pivot & NodeRegion::Right) == NodeRegion::Right) - newBounds.Max.x = - ImMax(newBounds.Min.x + minimumSize.x, - Editor->AlignPointToGrid(newBounds.Max.x + dragOffset.x)); - - newBounds.Floor(); - - m_LastSize = newBounds.GetSize(); - - m_SizedNode->m_Bounds = newBounds; - m_SizedNode->m_GroupBounds = newBounds; - m_SizedNode->m_GroupBounds.Min.x -= - m_StartBounds.Min.x - m_StartGroupBounds.Min.x; - m_SizedNode->m_GroupBounds.Min.y -= - m_StartBounds.Min.y - m_StartGroupBounds.Min.y; - m_SizedNode->m_GroupBounds.Max.x -= - m_StartBounds.Max.x - m_StartGroupBounds.Max.x; - m_SizedNode->m_GroupBounds.Max.y -= - m_StartBounds.Max.y - m_StartGroupBounds.Max.y; - } else if (!control.ActiveNode) { - m_Clean = true; - m_IsActive = false; - return true; - } - - return m_IsActive; -} - -void ed::SizeAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - auto getObjectName = [](Object* object) { - if (!object) - return ""; - else if (object->AsNode()) - return "Node"; - else if (object->AsPin()) - return "Pin"; - else if (object->AsLink()) - return "Link"; - else - return ""; - }; - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); - ImGui::Text(" Node: %s (%p)", getObjectName(m_SizedNode), - m_SizedNode ? m_SizedNode->m_ID.AsPointer() : nullptr); - if (m_SizedNode && m_IsActive) { - ImGui::Text(" Bounds: { x=%g y=%g w=%g h=%g }", - m_SizedNode->m_Bounds.Min.x, m_SizedNode->m_Bounds.Min.y, - m_SizedNode->m_Bounds.GetWidth(), - m_SizedNode->m_Bounds.GetHeight()); - ImGui::Text(" Group Bounds: { x=%g y=%g w=%g h=%g }", - m_SizedNode->m_GroupBounds.Min.x, - m_SizedNode->m_GroupBounds.Min.y, - m_SizedNode->m_GroupBounds.GetWidth(), - m_SizedNode->m_GroupBounds.GetHeight()); - ImGui::Text(" Start Bounds: { x=%g y=%g w=%g h=%g }", - m_StartBounds.Min.x, m_StartBounds.Min.y, - m_StartBounds.GetWidth(), m_StartBounds.GetHeight()); - ImGui::Text(" Start Group Bounds: { x=%g y=%g w=%g h=%g }", - m_StartGroupBounds.Min.x, m_StartGroupBounds.Min.y, - m_StartGroupBounds.GetWidth(), m_StartGroupBounds.GetHeight()); - ImGui::Text(" Minimum Size: { w=%g h=%g }", m_MinimumSize.x, - m_MinimumSize.y); - ImGui::Text(" Last Size: { w=%g h=%g }", m_LastSize.x, m_LastSize.y); - } -} - -ed::NodeRegion ed::SizeAction::GetRegion(Node* node) { - return node->GetRegion(ImGui::GetMousePos()); -} - -ImGuiMouseCursor ed::SizeAction::ChooseCursor(NodeRegion region) { - switch (region) { - default: - case NodeRegion::Center: - return ImGuiMouseCursor_Arrow; - - case NodeRegion::Top: - case NodeRegion::Bottom: - return ImGuiMouseCursor_ResizeNS; - - case NodeRegion::Left: - case NodeRegion::Right: - return ImGuiMouseCursor_ResizeEW; - - case NodeRegion::TopLeft: - case NodeRegion::BottomRight: - return ImGuiMouseCursor_ResizeNWSE; - - case NodeRegion::TopRight: - case NodeRegion::BottomLeft: - return ImGuiMouseCursor_ResizeNESW; - } -} - -//------------------------------------------------------------------------------ -// -// Drag Action -// -//------------------------------------------------------------------------------ -ed::DragAction::DragAction(EditorContext* editor) - : EditorAction(editor), - m_IsActive(false), - m_Clear(false), - m_DraggedObject(nullptr) {} - -ed::EditorAction::AcceptResult ed::DragAction::Accept(const Control& control) { - IM_ASSERT(!m_IsActive); - - if (m_IsActive) return False; - - if (Editor->CanAcceptUserInput() && control.ActiveObject && - ImGui::IsMouseDragging(Editor->GetConfig().DragButtonIndex, 1)) { - if (!control.ActiveObject->AcceptDrag()) return False; - - m_DraggedObject = control.ActiveObject; - - m_Objects.resize(0); - m_Objects.push_back(m_DraggedObject); - - if (Editor->IsSelected(m_DraggedObject)) { - for (auto selectedObject : Editor->GetSelectedObjects()) - if (auto selectedNode = selectedObject->AsNode()) - if (selectedNode != m_DraggedObject && selectedNode->AcceptDrag()) - m_Objects.push_back(selectedNode); - } - - auto& io = ImGui::GetIO(); - if (!io.KeyShift) { - std::vector groupedNodes; - for (auto object : m_Objects) - if (auto node = object->AsNode()) - node->GetGroupedNodes(groupedNodes, true); - - auto isAlreadyPicked = [this](Node* node) { - return std::find(m_Objects.begin(), m_Objects.end(), node) != - m_Objects.end(); - }; - - for (auto candidate : groupedNodes) - if (!isAlreadyPicked(candidate) && candidate->AcceptDrag()) - m_Objects.push_back(candidate); - } - - m_IsActive = true; - } else if (control.HotNode && IsGroup(control.HotNode) && - control.HotNode->GetRegion(ImGui::GetMousePos()) == - NodeRegion::Header) { - return Possible; - } - - return m_IsActive ? True : False; -} - -bool ed::DragAction::Process(const Control& control) { - if (m_Clear) { - m_Clear = false; - - for (auto object : m_Objects) { - if (object->EndDrag()) - Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, - object->AsNode()); - } - - m_Objects.resize(0); - - m_DraggedObject = nullptr; - } - - if (!m_IsActive) return false; - - if (control.ActiveObject == m_DraggedObject) { - auto dragOffset = - ImGui::GetMouseDragDelta(Editor->GetConfig().DragButtonIndex, 0.0f); - - auto draggedOrigin = m_DraggedObject->DragStartLocation(); - auto alignPivot = ImVec2(0, 0); - - // TODO: Move this experimental alignment to closes pivot out of internals - // to node API - if (auto draggedNode = m_DraggedObject->AsNode()) { - float x = FLT_MAX; - float y = FLT_MAX; - - auto testPivot = [this, &x, &y, &draggedOrigin, &dragOffset, - &alignPivot](const ImVec2& pivot) { - auto initial = draggedOrigin + dragOffset + pivot; - auto candidate = - Editor->AlignPointToGrid(initial) - draggedOrigin - pivot; - - if (ImFabs(candidate.x) < ImFabs(ImMin(x, FLT_MAX))) { - x = candidate.x; - alignPivot.x = pivot.x; - } - - if (ImFabs(candidate.y) < ImFabs(ImMin(y, FLT_MAX))) { - y = candidate.y; - alignPivot.y = pivot.y; - } - }; - - for (auto pin = draggedNode->m_LastPin; pin; pin = pin->m_PreviousPin) { - auto pivot = pin->m_Pivot.GetCenter() - draggedNode->m_Bounds.Min; - testPivot(pivot); - } - - // testPivot(point(0, 0)); - } - - auto alignedOffset = - Editor->AlignPointToGrid(draggedOrigin + dragOffset + alignPivot) - - draggedOrigin - alignPivot; - - if (!ImGui::GetIO().KeyAlt) dragOffset = alignedOffset; - - for (auto object : m_Objects) object->UpdateDrag(dragOffset); - } else if (!control.ActiveObject) { - m_Clear = true; - - m_IsActive = false; - return true; - } - - return m_IsActive; -} - -void ed::DragAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - auto getObjectName = [](Object* object) { - if (!object) - return ""; - else if (object->AsNode()) - return "Node"; - else if (object->AsPin()) - return "Pin"; - else if (object->AsLink()) - return "Link"; - else - return ""; - }; - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); - ImGui::Text(" Node: %s (%p)", getObjectName(m_DraggedObject), - m_DraggedObject ? m_DraggedObject->ID().AsPointer() : nullptr); -} - -//------------------------------------------------------------------------------ -// -// Select Action -// -//------------------------------------------------------------------------------ -ed::SelectAction::SelectAction(EditorContext* editor) - : EditorAction(editor), - m_IsActive(false), - m_SelectGroups(false), - m_SelectLinkMode(false), - m_CommitSelection(false), - m_StartPoint(), - m_Animation(editor) {} - -ed::EditorAction::AcceptResult ed::SelectAction::Accept( - const Control& control) { - IM_ASSERT(!m_IsActive); - - if (m_IsActive) return False; - - auto& io = ImGui::GetIO(); - m_SelectGroups = io.KeyShift; - m_SelectLinkMode = io.KeyAlt; - - m_SelectedObjectsAtStart.clear(); - - if (Editor->CanAcceptUserInput() && control.BackgroundHot && - ImGui::IsMouseDragging(Editor->GetConfig().SelectButtonIndex, 1)) { - m_IsActive = true; - m_StartPoint = - ImGui_GetMouseClickPos(Editor->GetConfig().SelectButtonIndex); - m_EndPoint = m_StartPoint; - - // Links and nodes cannot be selected together - if ((m_SelectLinkMode && Editor->IsAnyNodeSelected()) || - (!m_SelectLinkMode && Editor->IsAnyLinkSelected())) { - Editor->ClearSelection(); - } - - if (io.KeyCtrl) m_SelectedObjectsAtStart = Editor->GetSelectedObjects(); - } else if (control.BackgroundClickButtonIndex == - Editor->GetConfig().SelectButtonIndex) { - Editor->ClearSelection(); - } else { - Object* clickedObject = control.ClickedNode - ? static_cast(control.ClickedNode) - : static_cast(control.ClickedLink); - - if (clickedObject) { - // Links and nodes cannot be selected together - if ((clickedObject->AsLink() && Editor->IsAnyNodeSelected()) || - (clickedObject->AsNode() && Editor->IsAnyLinkSelected())) { - Editor->ClearSelection(); - } - - if (io.KeyCtrl) - Editor->ToggleObjectSelection(clickedObject); - else - Editor->SetSelectedObject(clickedObject); - } - } - - if (m_IsActive) m_Animation.Stop(); - - return m_IsActive ? True : False; -} - -bool ed::SelectAction::Process(const Control& control) { - IM_UNUSED(control); - - if (m_CommitSelection) { - Editor->ClearSelection(); - for (auto object : m_CandidateObjects) Editor->SelectObject(object); - - m_CandidateObjects.clear(); - - m_CommitSelection = false; - } - - if (!m_IsActive) return false; - - if (ImGui::IsMouseDragging(Editor->GetConfig().SelectButtonIndex, 0)) { - m_EndPoint = ImGui::GetMousePos(); - - auto topLeft = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), - std::min(m_StartPoint.y, m_EndPoint.y)); - auto bottomRight = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), - ImMax(m_StartPoint.y, m_EndPoint.y)); - auto rect = ImRect(topLeft, bottomRight); - if (rect.GetWidth() <= 0) rect.Max.x = rect.Min.x + 1; - if (rect.GetHeight() <= 0) rect.Max.y = rect.Min.y + 1; - - vector nodes; - vector links; - - if (m_SelectLinkMode) { - Editor->FindLinksInRect(rect, links); - m_CandidateObjects.assign(links.begin(), links.end()); - } else { - Editor->FindNodesInRect(rect, nodes); - m_CandidateObjects.assign(nodes.begin(), nodes.end()); - - if (m_SelectGroups) { - auto endIt = std::remove_if( - m_CandidateObjects.begin(), m_CandidateObjects.end(), - [](Object* object) { return !IsGroup(object->AsNode()); }); - m_CandidateObjects.erase(endIt, m_CandidateObjects.end()); - } else { - auto endIt = std::remove_if( - m_CandidateObjects.begin(), m_CandidateObjects.end(), - [](Object* object) { return IsGroup(object->AsNode()); }); - m_CandidateObjects.erase(endIt, m_CandidateObjects.end()); - } - } - - m_CandidateObjects.insert(m_CandidateObjects.end(), - m_SelectedObjectsAtStart.begin(), - m_SelectedObjectsAtStart.end()); - std::sort(m_CandidateObjects.begin(), m_CandidateObjects.end()); - m_CandidateObjects.erase( - std::unique(m_CandidateObjects.begin(), m_CandidateObjects.end()), - m_CandidateObjects.end()); - } else { - m_IsActive = false; - - m_Animation.Play(c_SelectionFadeOutDuration); - - m_CommitSelection = true; - - return true; - } - - return m_IsActive; -} - -void ed::SelectAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); -} - -void ed::SelectAction::Draw(ImDrawList* drawList) { - if (!m_IsActive && !m_Animation.IsPlaying()) return; - - const auto alpha = - m_Animation.IsPlaying() - ? ImEasing::EaseOutQuad(1.0f, -1.0f, m_Animation.GetProgress()) - : 1.0f; - - const auto fillColor = Editor->GetColor( - m_SelectLinkMode ? StyleColor_LinkSelRect : StyleColor_NodeSelRect, - alpha); - const auto outlineColor = - Editor->GetColor(m_SelectLinkMode ? StyleColor_LinkSelRectBorder - : StyleColor_NodeSelRectBorder, - alpha); - - drawList->ChannelsSetCurrent(c_BackgroundChannel_SelectionRect); - - auto min = ImVec2(std::min(m_StartPoint.x, m_EndPoint.x), - std::min(m_StartPoint.y, m_EndPoint.y)); - auto max = ImVec2(ImMax(m_StartPoint.x, m_EndPoint.x), - ImMax(m_StartPoint.y, m_EndPoint.y)); - - drawList->AddRectFilled(min, max, fillColor); - drawList->AddRect(min, max, outlineColor); -} - -//------------------------------------------------------------------------------ -// -// Context Menu Action -// -//------------------------------------------------------------------------------ -ed::ContextMenuAction::ContextMenuAction(EditorContext* editor) - : EditorAction(editor), - m_CandidateMenu(Menu::None), - m_CurrentMenu(Menu::None), - m_ContextId() {} - -ed::EditorAction::AcceptResult ed::ContextMenuAction::Accept( - const Control& control) { - const auto isPressed = - ImGui::IsMouseClicked(Editor->GetConfig().ContextMenuButtonIndex); - const auto isReleased = - ImGui::IsMouseReleased(Editor->GetConfig().ContextMenuButtonIndex); - const auto isDragging = - ImGui::IsMouseDragging(Editor->GetConfig().ContextMenuButtonIndex, 1); - - if (isPressed || isReleased || isDragging) { - Menu candidateMenu = ContextMenuAction::None; - ObjectId contextId; - - if (auto hotObejct = control.HotObject) { - if (hotObejct->AsNode()) - candidateMenu = Node; - else if (hotObejct->AsPin()) - candidateMenu = Pin; - else if (hotObejct->AsLink()) - candidateMenu = Link; - - if (candidateMenu != None) contextId = hotObejct->ID(); - } else if (control.BackgroundHot) - candidateMenu = Background; - - if (isPressed) { - m_CandidateMenu = candidateMenu; - m_ContextId = contextId; - return Possible; - } else if (isReleased && m_CandidateMenu == candidateMenu && - m_ContextId == contextId) { - m_CurrentMenu = m_CandidateMenu; - m_CandidateMenu = ContextMenuAction::None; - return True; - } else { - m_CandidateMenu = None; - m_CurrentMenu = None; - m_ContextId = ObjectId(); - return False; - } - } - - return False; -} - -bool ed::ContextMenuAction::Process(const Control& control) { - IM_UNUSED(control); - - m_CandidateMenu = None; - m_CurrentMenu = None; - m_ContextId = ObjectId(); - return false; -} - -void ed::ContextMenuAction::Reject() { - m_CandidateMenu = None; - m_CurrentMenu = None; - m_ContextId = ObjectId(); -} - -void ed::ContextMenuAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - auto getMenuName = [](Menu menu) { - switch (menu) { - default: - case None: - return "None"; - case Node: - return "Node"; - case Pin: - return "Pin"; - case Link: - return "Link"; - case Background: - return "Background"; - } - }; - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Menu: %s", getMenuName(m_CurrentMenu)); -} - -bool ed::ContextMenuAction::ShowNodeContextMenu(NodeId* nodeId) { - if (m_CurrentMenu != Node) return false; - - *nodeId = m_ContextId.AsNodeId(); - Editor->SetUserContext(); - return true; -} - -bool ed::ContextMenuAction::ShowPinContextMenu(PinId* pinId) { - if (m_CurrentMenu != Pin) return false; - - *pinId = m_ContextId.AsPinId(); - Editor->SetUserContext(); - return true; -} - -bool ed::ContextMenuAction::ShowLinkContextMenu(LinkId* linkId) { - if (m_CurrentMenu != Link) return false; - - *linkId = m_ContextId.AsLinkId(); - Editor->SetUserContext(); - return true; -} - -bool ed::ContextMenuAction::ShowBackgroundContextMenu() { - if (m_CurrentMenu != Background) return false; - - Editor->SetUserContext(); - return true; -} - -//------------------------------------------------------------------------------ -// -// Cut/Copy/Paste Action -// -//------------------------------------------------------------------------------ -ed::ShortcutAction::ShortcutAction(EditorContext* editor) - : EditorAction(editor), - m_IsActive(false), - m_InAction(false), - m_CurrentAction(Action::None), - m_Context() {} - -ed::EditorAction::AcceptResult ed::ShortcutAction::Accept( - const Control& control) { - if (!Editor->IsFocused() || !Editor->AreShortcutsEnabled()) return False; - - Action candidateAction = None; - - auto& io = ImGui::GetIO(); - if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && - ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X))) - candidateAction = Cut; - if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && - ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C))) - candidateAction = Copy; - if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && - ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V))) - candidateAction = Paste; - if (io.KeyCtrl && !io.KeyShift && !io.KeyAlt && - ImGui::IsKeyPressed(GetKeyIndexForD())) - candidateAction = Duplicate; - if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt && - ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Space))) - candidateAction = CreateNode; - - if (candidateAction != None) { - if (candidateAction != Paste && candidateAction != CreateNode) { - auto& selection = Editor->GetSelectedObjects(); - if (!selection.empty()) { - // #TODO: Find a way to simplify logic. - - m_Context.assign(selection.begin(), selection.end()); - - // Expand groups - vector extra; - for (auto object : m_Context) { - auto node = object->AsNode(); - if (IsGroup(node)) node->GetGroupedNodes(extra, true); - } - - // Apply groups and remove duplicates - if (!extra.empty()) { - m_Context.insert(m_Context.end(), extra.begin(), extra.end()); - std::sort(m_Context.begin(), m_Context.end()); - m_Context.erase(std::unique(m_Context.begin(), m_Context.end()), - m_Context.end()); - } - } else if (control.HotObject && control.HotObject->IsSelectable() && - !IsGroup(control.HotObject->AsNode())) { - m_Context.push_back(control.HotObject); - } - - if (m_Context.empty()) return False; - - // Does copying only links make sense? - // const auto hasOnlyLinks = std::all_of(Context.begin(), Context.end(), - // [](Object* object) { return object->AsLink() != nullptr; }); if - // (hasOnlyLinks) - // return False; - - // If no links are selected, pick all links between nodes within context - const auto hasAnyLinks = std::any_of( - m_Context.begin(), m_Context.end(), - [](Object* object) { return object->AsLink() != nullptr; }); - if (!hasAnyLinks && - m_Context.size() > 1) // one node cannot make connection to anything - { - // Collect nodes in sorted vector viable for binary search - std::vector> nodes; - - nodes.reserve(m_Context.size()); - std::for_each(m_Context.begin(), m_Context.end(), - [&nodes](Object* object) { - if (auto node = object->AsNode()) - nodes.push_back({node->m_ID, node}); - }); - - std::sort(nodes.begin(), nodes.end()); - - auto isNodeInContext = [&nodes](NodeId nodeId) { - return std::binary_search(nodes.begin(), nodes.end(), - ObjectWrapper{nodeId, nullptr}); - }; - - // Collect links connected to nodes and drop those reaching out of - // context - std::vector links; - - for (auto node : nodes) - Editor->FindLinksForNode(node.m_ID, links, true); - - // Remove duplicates - std::sort(links.begin(), links.end()); - links.erase(std::unique(links.begin(), links.end()), links.end()); - - // Drop out of context links - links.erase( - std::remove_if( - links.begin(), links.end(), - [&isNodeInContext](Link* link) { - return !isNodeInContext(link->m_StartPin->m_Node->m_ID) || - !isNodeInContext(link->m_EndPin->m_Node->m_ID); - }), - links.end()); - - // Append links and remove duplicates - m_Context.insert(m_Context.end(), links.begin(), links.end()); - } - } else - m_Context.resize(0); - - m_IsActive = true; - m_CurrentAction = candidateAction; - - return True; - } - - return False; -} - -bool ed::ShortcutAction::Process(const Control& control) { - IM_UNUSED(control); - - m_IsActive = false; - m_CurrentAction = None; - m_Context.resize(0); - return false; -} - -void ed::ShortcutAction::Reject() { - m_IsActive = false; - m_CurrentAction = None; - m_Context.resize(0); -} - -void ed::ShortcutAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - auto getActionName = [](Action action) { - switch (action) { - default: - case None: - return "None"; - case Cut: - return "Cut"; - case Copy: - return "Copy"; - case Paste: - return "Paste"; - case Duplicate: - return "Duplicate"; - case CreateNode: - return "CreateNode"; - } - }; - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Action: %s", getActionName(m_CurrentAction)); -} - -bool ed::ShortcutAction::Begin() { - if (m_IsActive) m_InAction = true; - return m_IsActive; -} - -void ed::ShortcutAction::End() { - if (m_IsActive) m_InAction = false; -} - -bool ed::ShortcutAction::AcceptCut() { - IM_ASSERT(m_InAction); - return m_CurrentAction == Cut; -} - -bool ed::ShortcutAction::AcceptCopy() { - IM_ASSERT(m_InAction); - return m_CurrentAction == Copy; -} - -bool ed::ShortcutAction::AcceptPaste() { - IM_ASSERT(m_InAction); - return m_CurrentAction == Paste; -} - -bool ed::ShortcutAction::AcceptDuplicate() { - IM_ASSERT(m_InAction); - return m_CurrentAction == Duplicate; -} - -bool ed::ShortcutAction::AcceptCreateNode() { - IM_ASSERT(m_InAction); - return m_CurrentAction == CreateNode; -} - -//------------------------------------------------------------------------------ -// -// Create Item Action -// -//------------------------------------------------------------------------------ -ed::CreateItemAction::CreateItemAction(EditorContext* editor) - : EditorAction(editor), - m_InActive(false), - m_NextStage(None), - m_CurrentStage(None), - m_ItemType(NoItem), - m_UserAction(Unknown), - m_LinkColor(IM_COL32_WHITE), - m_LinkThickness(1.0f), - m_LinkStart(nullptr), - m_LinkEnd(nullptr), - - m_IsActive(false), - m_DraggedPin(nullptr), - - m_IsInGlobalSpace(false) {} - -ed::EditorAction::AcceptResult ed::CreateItemAction::Accept( - const Control& control) { - IM_ASSERT(!m_IsActive); - - if (m_IsActive) return EditorAction::False; - - if (control.ActivePin && - ImGui::IsMouseDragging(Editor->GetConfig().DragButtonIndex, 1)) { - m_DraggedPin = control.ActivePin; - DragStart(m_DraggedPin); - - Editor->ClearSelection(); - } else if (control.HotPin) { - return EditorAction::Possible; - } else - return EditorAction::False; - - m_IsActive = true; - - return EditorAction::True; -} - -bool ed::CreateItemAction::Process(const Control& control) { - IM_ASSERT(m_IsActive); - - if (!m_IsActive) return false; - - if (m_DraggedPin && control.ActivePin == m_DraggedPin && - (m_CurrentStage == Possible)) { - const auto draggingFromSource = (m_DraggedPin->m_Kind == PinKind::Output); - - ed::Pin cursorPin(Editor, 0, - draggingFromSource ? PinKind::Input : PinKind::Output); - cursorPin.m_Pivot = ImRect(ImGui::GetMousePos(), ImGui::GetMousePos()); - cursorPin.m_Dir = -m_DraggedPin->m_Dir; - cursorPin.m_Strength = m_DraggedPin->m_Strength; - - ed::Link candidate(Editor, 0); - candidate.m_Color = m_LinkColor; - candidate.m_StartPin = draggingFromSource ? m_DraggedPin : &cursorPin; - candidate.m_EndPin = draggingFromSource ? &cursorPin : m_DraggedPin; - - ed::Pin*& freePin = - draggingFromSource ? candidate.m_EndPin : candidate.m_StartPin; - - if (control.HotPin) { - DropPin(control.HotPin); - - if (m_UserAction == UserAccept) freePin = control.HotPin; - } else if (control.BackgroundHot) - DropNode(); - else - DropNothing(); - - auto drawList = Editor->GetDrawList(); - drawList->ChannelsSetCurrent(c_LinkChannel_NewLink); - - candidate.UpdateEndpoints(); - candidate.Draw(drawList, m_LinkColor, m_LinkThickness); - } else if (m_CurrentStage == Possible || !control.ActivePin) { - if (!Editor->CanAcceptUserInput()) { - m_DraggedPin = nullptr; - DropNothing(); - } - - DragEnd(); - m_IsActive = false; - } - - return m_IsActive; -} - -void ed::CreateItemAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - auto getStageName = [](Stage stage) { - switch (stage) { - case None: - return "None"; - case Possible: - return "Possible"; - case Create: - return "Create"; - default: - return ""; - } - }; - - auto getActionName = [](Action action) { - switch (action) { - default: - case Unknown: - return "Unknown"; - case UserReject: - return "Reject"; - case UserAccept: - return "Accept"; - } - }; - - auto getItemName = [](Type item) { - switch (item) { - default: - case NoItem: - return "None"; - case Node: - return "Node"; - case Link: - return "Link"; - } - }; - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Stage: %s", getStageName(m_CurrentStage)); - ImGui::Text(" User Action: %s", getActionName(m_UserAction)); - ImGui::Text(" Item Type: %s", getItemName(m_ItemType)); -} - -void ed::CreateItemAction::SetStyle(ImU32 color, float thickness) { - m_LinkColor = color; - m_LinkThickness = thickness; -} - -bool ed::CreateItemAction::Begin() { - IM_ASSERT(false == m_InActive); - - m_InActive = true; - m_CurrentStage = m_NextStage; - m_UserAction = Unknown; - m_LinkColor = IM_COL32_WHITE; - m_LinkThickness = 1.0f; - - if (m_CurrentStage == None) return false; - - m_LastChannel = Editor->GetDrawList()->_Splitter._Current; - - return true; -} - -void ed::CreateItemAction::End() { - IM_ASSERT(m_InActive); - - if (m_IsInGlobalSpace) { - ImGui::PopClipRect(); - Editor->Resume(SuspendFlags::KeepSplitter); - - auto currentChannel = Editor->GetDrawList()->_Splitter._Current; - if (currentChannel != m_LastChannel) - Editor->GetDrawList()->ChannelsSetCurrent(m_LastChannel); - - m_IsInGlobalSpace = false; - } - - m_InActive = false; -} - -void ed::CreateItemAction::DragStart(Pin* startPin) { - IM_ASSERT(!m_InActive); - - m_NextStage = Possible; - m_LinkStart = startPin; - m_LinkEnd = nullptr; -} - -void ed::CreateItemAction::DragEnd() { - IM_ASSERT(!m_InActive); - - if (m_CurrentStage == Possible && m_UserAction == UserAccept) { - m_NextStage = Create; - } else { - m_NextStage = None; - m_ItemType = NoItem; - m_LinkStart = nullptr; - m_LinkEnd = nullptr; - } -} - -void ed::CreateItemAction::DropPin(Pin* endPin) { - IM_ASSERT(!m_InActive); - - m_ItemType = Link; - m_LinkEnd = endPin; -} - -void ed::CreateItemAction::DropNode() { - IM_ASSERT(!m_InActive); - - m_ItemType = Node; - m_LinkEnd = nullptr; -} - -void ed::CreateItemAction::DropNothing() { - IM_ASSERT(!m_InActive); - - m_ItemType = NoItem; - m_LinkEnd = nullptr; -} - -ed::CreateItemAction::Result ed::CreateItemAction::RejectItem() { - IM_ASSERT(m_InActive); - - if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem) - return Indeterminate; - - m_UserAction = UserReject; - - return True; -} - -ed::CreateItemAction::Result ed::CreateItemAction::AcceptItem() { - IM_ASSERT(m_InActive); - - if (!m_InActive || m_CurrentStage == None || m_ItemType == NoItem) - return Indeterminate; - - m_UserAction = UserAccept; - - if (m_CurrentStage == Create) { - m_NextStage = None; - m_ItemType = NoItem; - m_LinkStart = nullptr; - m_LinkEnd = nullptr; - return True; - } else - return False; -} - -ed::CreateItemAction::Result ed::CreateItemAction::QueryLink(PinId* startId, - PinId* endId) { - IM_ASSERT(m_InActive); - - if (!m_InActive || m_CurrentStage == None || m_ItemType != Link) - return Indeterminate; - - auto linkStartId = m_LinkStart->m_ID; - auto linkEndId = m_LinkEnd->m_ID; - - *startId = linkStartId; - *endId = linkEndId; - - Editor->SetUserContext(true); - - if (!m_IsInGlobalSpace) { - Editor->Suspend(SuspendFlags::KeepSplitter); - - auto rect = Editor->GetRect(); - ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), - false); - m_IsInGlobalSpace = true; - } - - return True; -} - -ed::CreateItemAction::Result ed::CreateItemAction::QueryNode(PinId* pinId) { - IM_ASSERT(m_InActive); - - if (!m_InActive || m_CurrentStage == None || m_ItemType != Node) - return Indeterminate; - - *pinId = m_LinkStart ? m_LinkStart->m_ID : 0; - - Editor->SetUserContext(true); - - if (!m_IsInGlobalSpace) { - Editor->Suspend(SuspendFlags::KeepSplitter); - - auto rect = Editor->GetRect(); - ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), - false); - m_IsInGlobalSpace = true; - } - - return True; -} - -//------------------------------------------------------------------------------ -// -// Delete Items Action -// -//------------------------------------------------------------------------------ -ed::DeleteItemsAction::DeleteItemsAction(EditorContext* editor) - : EditorAction(editor), - m_IsActive(false), - m_InInteraction(false), - m_CurrentItemType(Unknown), - m_UserAction(Undetermined) {} - -void ed::DeleteItemsAction::DeleteDeadLinks(NodeId nodeId) { - vector links; - Editor->FindLinksForNode(nodeId, links, true); - for (auto link : links) { - link->m_DeleteOnNewFrame = true; - - auto it = - std::find(m_CandidateObjects.begin(), m_CandidateObjects.end(), link); - if (it != m_CandidateObjects.end()) continue; - - m_CandidateObjects.push_back(link); - } -} - -void ed::DeleteItemsAction::DeleteDeadPins(NodeId nodeId) { - auto node = Editor->FindNode(nodeId); - if (!node) return; - - for (auto pin = node->m_LastPin; pin; pin = pin->m_PreviousPin) - pin->m_DeleteOnNewFrame = true; -} - -ed::EditorAction::AcceptResult ed::DeleteItemsAction::Accept( - const Control& control) { - IM_ASSERT(!m_IsActive); - - if (m_IsActive) return False; - - auto& io = ImGui::GetIO(); - if (Editor->CanAcceptUserInput() && - ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)) && - Editor->AreShortcutsEnabled()) { - auto& selection = Editor->GetSelectedObjects(); - if (!selection.empty()) { - m_CandidateObjects = selection; - m_IsActive = true; - return True; - } - } else if (control.ClickedLink && io.KeyAlt) { - m_CandidateObjects.clear(); - m_CandidateObjects.push_back(control.ClickedLink); - m_IsActive = true; - return True; - } - - else if (!m_ManuallyDeletedObjects.empty()) { - m_CandidateObjects = m_ManuallyDeletedObjects; - m_ManuallyDeletedObjects.clear(); - m_IsActive = true; - return True; - } - - return m_IsActive ? True : False; -} - -bool ed::DeleteItemsAction::Process(const Control& control) { - IM_UNUSED(control); - - if (!m_IsActive) return false; - - m_IsActive = false; - return true; -} - -void ed::DeleteItemsAction::ShowMetrics() { - EditorAction::ShowMetrics(); - - // auto getObjectName = [](Object* object) - //{ - // if (!object) return ""; - // else if (object->AsNode()) return "Node"; - // else if (object->AsPin()) return "Pin"; - // else if (object->AsLink()) return "Link"; - // else return ""; - // }; - - ImGui::Text("%s:", GetName()); - ImGui::Text(" Active: %s", m_IsActive ? "yes" : "no"); - // ImGui::Text(" Node: %s (%d)", getObjectName(DeleteItemsgedNode), - // DeleteItemsgedNode ? DeleteItemsgedNode->ID : 0); -} - -bool ed::DeleteItemsAction::Add(Object* object) { - if (Editor->GetCurrentAction() != nullptr) return false; - - m_ManuallyDeletedObjects.push_back(object); - - return true; -} - -bool ed::DeleteItemsAction::Begin() { - if (!m_IsActive) return false; - - IM_ASSERT(!m_InInteraction); - m_InInteraction = true; - - m_CurrentItemType = Unknown; - m_UserAction = Undetermined; - - return m_IsActive; -} - -void ed::DeleteItemsAction::End() { - if (!m_IsActive) return; - - IM_ASSERT(m_InInteraction); - m_InInteraction = false; -} - -bool ed::DeleteItemsAction::QueryLink(LinkId* linkId, PinId* startId, - PinId* endId) { - ObjectId objectId; - if (!QueryItem(&objectId, Link)) return false; - - if (auto id = objectId.AsLinkId()) - *linkId = id; - else - return false; - - if (startId || endId) { - auto link = Editor->FindLink(*linkId); - if (startId) *startId = link->m_StartPin->m_ID; - if (endId) *endId = link->m_EndPin->m_ID; - } - - return true; -} - -bool ed::DeleteItemsAction::QueryNode(NodeId* nodeId) { - ObjectId objectId; - if (!QueryItem(&objectId, Node)) return false; - - if (auto id = objectId.AsNodeId()) - *nodeId = id; - else - return false; - - return true; -} - -bool ed::DeleteItemsAction::QueryItem(ObjectId* itemId, IteratorType itemType) { - if (!m_InInteraction) return false; - - if (m_CurrentItemType != itemType) { - m_CurrentItemType = itemType; - m_CandidateItemIndex = 0; - } else if (m_UserAction == Undetermined) { - RejectItem(); - } - - m_UserAction = Undetermined; - - auto itemCount = (int)m_CandidateObjects.size(); - while (m_CandidateItemIndex < itemCount) { - auto item = m_CandidateObjects[m_CandidateItemIndex]; - if (itemType == Node) { - if (auto node = item->AsNode()) { - *itemId = node->m_ID; - return true; - } - } else if (itemType == Link) { - if (auto link = item->AsLink()) { - *itemId = link->m_ID; - return true; - } - } - - ++m_CandidateItemIndex; - } - - if (m_CandidateItemIndex == itemCount) m_CurrentItemType = Unknown; - - return false; -} - -bool ed::DeleteItemsAction::AcceptItem(bool deleteDependencies) { - if (!m_InInteraction) return false; - - m_UserAction = Accepted; - - RemoveItem(deleteDependencies); - - return true; -} - -void ed::DeleteItemsAction::RejectItem() { - if (!m_InInteraction) return; - - m_UserAction = Rejected; - - DropCurrentItem(); -} - -void ed::DeleteItemsAction::RemoveItem(bool deleteDependencies) { - auto item = DropCurrentItem(); - - Editor->DeselectObject(item); - - Editor->RemoveSettings(item); - - item->m_DeleteOnNewFrame = true; - - if (deleteDependencies && m_CurrentItemType == Node) { - auto node = item->ID().AsNodeId(); - DeleteDeadLinks(node); - DeleteDeadPins(node); - } - - if (m_CurrentItemType == Link) Editor->NotifyLinkDeleted(item->AsLink()); -} - -ed::Object* ed::DeleteItemsAction::DropCurrentItem() { - auto item = m_CandidateObjects[m_CandidateItemIndex]; - m_CandidateObjects.erase(m_CandidateObjects.begin() + m_CandidateItemIndex); - - return item; -} - -//------------------------------------------------------------------------------ -// -// Node Builder -// -//------------------------------------------------------------------------------ -ed::NodeBuilder::NodeBuilder(EditorContext* editor) - : Editor(editor), m_CurrentNode(nullptr), m_CurrentPin(nullptr) {} - -ed::NodeBuilder::~NodeBuilder() { - m_Splitter.ClearFreeMemory(); - m_PinSplitter.ClearFreeMemory(); -} - -void ed::NodeBuilder::Begin(NodeId nodeId) { - IM_ASSERT(nullptr == m_CurrentNode); - - m_CurrentNode = Editor->GetNode(nodeId); - - Editor->UpdateNodeState(m_CurrentNode); - - if (m_CurrentNode->m_CenterOnScreen) { - auto bounds = Editor->GetViewRect(); - auto offset = bounds.GetCenter() - m_CurrentNode->m_Bounds.GetCenter(); - - if (ImLengthSqr(offset) > 0) { - if (::IsGroup(m_CurrentNode)) { - std::vector groupedNodes; - m_CurrentNode->GetGroupedNodes(groupedNodes); - groupedNodes.push_back(m_CurrentNode); - - for (auto node : groupedNodes) { - node->m_Bounds.Translate(ImFloor(offset)); - node->m_GroupBounds.Translate(ImFloor(offset)); - Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, - node); - } - } else { - m_CurrentNode->m_Bounds.Translate(ImFloor(offset)); - m_CurrentNode->m_GroupBounds.Translate(ImFloor(offset)); - Editor->MakeDirty(SaveReasonFlags::Position | SaveReasonFlags::User, - m_CurrentNode); - } - } - - m_CurrentNode->m_CenterOnScreen = false; - } - - // Position node on screen - ImGui::SetCursorScreenPos(m_CurrentNode->m_Bounds.Min); - - auto& editorStyle = Editor->GetStyle(); - - const auto alpha = ImGui::GetStyle().Alpha; - - m_CurrentNode->m_IsLive = true; - m_CurrentNode->m_LastPin = nullptr; - m_CurrentNode->m_Color = Editor->GetColor(StyleColor_NodeBg, alpha); - m_CurrentNode->m_BorderColor = Editor->GetColor(StyleColor_NodeBorder, alpha); - m_CurrentNode->m_BorderWidth = editorStyle.NodeBorderWidth; - m_CurrentNode->m_Rounding = editorStyle.NodeRounding; - m_CurrentNode->m_GroupColor = Editor->GetColor(StyleColor_GroupBg, alpha); - m_CurrentNode->m_GroupBorderColor = - Editor->GetColor(StyleColor_GroupBorder, alpha); - m_CurrentNode->m_GroupBorderWidth = editorStyle.GroupBorderWidth; - m_CurrentNode->m_GroupRounding = editorStyle.GroupRounding; - m_CurrentNode->m_HighlightConnectedLinks = - editorStyle.HighlightConnectedLinks != 0.0f; - - m_IsGroup = false; - - // Grow channel list and select user channel - if (auto drawList = Editor->GetDrawList()) { - m_CurrentNode->m_Channel = drawList->_Splitter._Count; - ImDrawList_ChannelsGrow(drawList, - drawList->_Splitter._Count + c_ChannelsPerNode); - drawList->ChannelsSetCurrent(m_CurrentNode->m_Channel + - c_NodeContentChannel); - - m_Splitter.Clear(); - ImDrawList_SwapSplitter(drawList, m_Splitter); - } - - // Begin outer group - ImGui::BeginGroup(); - - // Apply frame padding. Begin inner group if necessary. - if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || - editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0) { - ImGui::SetCursorPos( - ImGui::GetCursorPos() + - ImVec2(editorStyle.NodePadding.x, editorStyle.NodePadding.y)); - ImGui::BeginGroup(); - } -} - -void ed::NodeBuilder::End() { - IM_ASSERT(nullptr != m_CurrentNode); - - if (auto drawList = Editor->GetDrawList()) { - IM_ASSERT(drawList->_Splitter._Count == - 1); // Did you forgot to call drawList->ChannelsMerge()? - ImDrawList_SwapSplitter(drawList, m_Splitter); - } - - // Apply frame padding. This must be done in this convoluted way if outer - // group size must contain inner group padding. - auto& editorStyle = Editor->GetStyle(); - if (editorStyle.NodePadding.x != 0 || editorStyle.NodePadding.y != 0 || - editorStyle.NodePadding.z != 0 || editorStyle.NodePadding.w != 0) { - ImGui::EndGroup(); - ImGui::SameLine(0, editorStyle.NodePadding.z); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); - ImGui::Dummy(ImVec2( - 0, 0)); // bump cursor at the end of the line and move to next one - ImGui::Dummy(ImVec2(0, editorStyle.NodePadding.w)); // apply padding - ImGui::PopStyleVar(); - } - - // End outer group. - ImGui::EndGroup(); - - m_NodeRect = ImGui_GetItemRect(); - m_NodeRect.Floor(); - - if (m_CurrentNode->m_Bounds.GetSize() != m_NodeRect.GetSize()) { - m_CurrentNode->m_Bounds.Max = - m_CurrentNode->m_Bounds.Min + m_NodeRect.GetSize(); - Editor->MakeDirty(SaveReasonFlags::Size, m_CurrentNode); - } - - if (m_IsGroup) { - // Groups cannot have pins. Discard them. - for (auto pin = m_CurrentNode->m_LastPin; pin; pin = pin->m_PreviousPin) - pin->Reset(); - - m_CurrentNode->m_Type = NodeType::Group; - m_CurrentNode->m_GroupBounds = m_GroupBounds; - m_CurrentNode->m_LastPin = nullptr; - } else - m_CurrentNode->m_Type = NodeType::Node; - - m_CurrentNode = nullptr; -} - -void ed::NodeBuilder::BeginPin(PinId pinId, PinKind kind) { - IM_ASSERT(nullptr != m_CurrentNode); - IM_ASSERT(nullptr == m_CurrentPin); - IM_ASSERT(false == m_IsGroup); - - auto& editorStyle = Editor->GetStyle(); - - m_CurrentPin = Editor->GetPin(pinId, kind); - m_CurrentPin->m_Node = m_CurrentNode; - - m_CurrentPin->m_IsLive = true; - m_CurrentPin->m_Color = Editor->GetColor(StyleColor_PinRect); - m_CurrentPin->m_BorderColor = Editor->GetColor(StyleColor_PinRectBorder); - m_CurrentPin->m_BorderWidth = editorStyle.PinBorderWidth; - m_CurrentPin->m_Rounding = editorStyle.PinRounding; - m_CurrentPin->m_Corners = static_cast(editorStyle.PinCorners); - m_CurrentPin->m_Radius = editorStyle.PinRadius; - m_CurrentPin->m_ArrowSize = editorStyle.PinArrowSize; - m_CurrentPin->m_ArrowWidth = editorStyle.PinArrowWidth; - m_CurrentPin->m_Dir = kind == PinKind::Output ? editorStyle.SourceDirection - : editorStyle.TargetDirection; - m_CurrentPin->m_Strength = editorStyle.LinkStrength; - m_CurrentPin->m_SnapLinkToDir = editorStyle.SnapLinkToPinDir != 0.0f; - - m_CurrentPin->m_PreviousPin = m_CurrentNode->m_LastPin; - m_CurrentNode->m_LastPin = m_CurrentPin; - - m_PivotAlignment = editorStyle.PivotAlignment; - m_PivotSize = editorStyle.PivotSize; - m_PivotScale = editorStyle.PivotScale; - m_ResolvePinRect = true; - m_ResolvePivot = true; - - if (auto drawList = Editor->GetDrawList()) { - m_PinSplitter.Clear(); - ImDrawList_SwapSplitter(drawList, m_PinSplitter); - } - - ImGui::BeginGroup(); -} - -void ed::NodeBuilder::EndPin() { - IM_ASSERT(nullptr != m_CurrentPin); - - if (auto drawList = Editor->GetDrawList()) { - IM_ASSERT(drawList->_Splitter._Count == - 1); // Did you forgot to call drawList->ChannelsMerge()? - ImDrawList_SwapSplitter(drawList, m_PinSplitter); - } - - ImGui::EndGroup(); - - if (m_ResolvePinRect) m_CurrentPin->m_Bounds = ImGui_GetItemRect(); - - if (m_ResolvePivot) { - auto& pinRect = m_CurrentPin->m_Bounds; - - if (m_PivotSize.x < 0) m_PivotSize.x = pinRect.GetWidth(); - if (m_PivotSize.y < 0) m_PivotSize.y = pinRect.GetHeight(); - - m_CurrentPin->m_Pivot.Min = - pinRect.Min + ImMul(pinRect.GetSize(), m_PivotAlignment); - m_CurrentPin->m_Pivot.Max = - m_CurrentPin->m_Pivot.Min + ImMul(m_PivotSize, m_PivotScale); - } - - // #debug: Draw pin bounds - // Editor->GetDrawList()->AddRect(m_CurrentPin->m_Bounds.Min, - // m_CurrentPin->m_Bounds.Max, IM_COL32(255, 255, 0, 255)); - - // #debug: Draw pin pivot rectangle - // Editor->GetDrawList()->AddRect(m_CurrentPin->m_Pivot.Min, - // m_CurrentPin->m_Pivot.Max, IM_COL32(255, 0, 255, 255)); - - m_CurrentPin = nullptr; -} - -void ed::NodeBuilder::PinRect(const ImVec2& a, const ImVec2& b) { - IM_ASSERT(nullptr != m_CurrentPin); - - m_CurrentPin->m_Bounds = ImRect(a, b); - m_CurrentPin->m_Bounds.Floor(); - m_ResolvePinRect = false; -} - -void ed::NodeBuilder::PinPivotRect(const ImVec2& a, const ImVec2& b) { - IM_ASSERT(nullptr != m_CurrentPin); - - m_CurrentPin->m_Pivot = ImRect(a, b); - m_ResolvePivot = false; -} - -void ed::NodeBuilder::PinPivotSize(const ImVec2& size) { - IM_ASSERT(nullptr != m_CurrentPin); - - m_PivotSize = size; - m_ResolvePivot = true; -} - -void ed::NodeBuilder::PinPivotScale(const ImVec2& scale) { - IM_ASSERT(nullptr != m_CurrentPin); - - m_PivotScale = scale; - m_ResolvePivot = true; -} - -void ed::NodeBuilder::PinPivotAlignment(const ImVec2& alignment) { - IM_ASSERT(nullptr != m_CurrentPin); - - m_PivotAlignment = alignment; - m_ResolvePivot = true; -} - -void ed::NodeBuilder::Group(const ImVec2& size) { - IM_ASSERT(nullptr != m_CurrentNode); - IM_ASSERT(nullptr == m_CurrentPin); - IM_ASSERT(false == m_IsGroup); - - m_IsGroup = true; - - if (IsGroup(m_CurrentNode)) - ImGui::Dummy(m_CurrentNode->m_GroupBounds.GetSize()); - else - ImGui::Dummy(size); - - m_GroupBounds = ImGui_GetItemRect(); - m_GroupBounds.Floor(); -} - -ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList() const { - return GetUserBackgroundDrawList(m_CurrentNode); -} - -ImDrawList* ed::NodeBuilder::GetUserBackgroundDrawList(Node* node) const { - if (node && node->m_IsLive) { - auto drawList = Editor->GetDrawList(); - drawList->ChannelsSetCurrent(node->m_Channel + c_NodeUserBackgroundChannel); - return drawList; - } else - return nullptr; -} - -//------------------------------------------------------------------------------ -// -// Node Builder -// -//------------------------------------------------------------------------------ -ed::HintBuilder::HintBuilder(EditorContext* editor) - : Editor(editor), m_IsActive(false), m_CurrentNode(nullptr) {} - -bool ed::HintBuilder::Begin(NodeId nodeId) { - IM_ASSERT(nullptr == m_CurrentNode); - - auto& view = Editor->GetView(); - auto& rect = Editor->GetRect(); - - const float c_min_zoom = 0.75f; - const float c_max_zoom = 0.50f; - - if (view.Scale > 0.75f) return false; - - auto node = Editor->FindNode(nodeId); - if (!IsGroup(node)) return false; - - m_CurrentNode = node; - - m_LastChannel = Editor->GetDrawList()->_Splitter._Current; - - Editor->Suspend(SuspendFlags::KeepSplitter); - - const auto alpha = ImMax(0.0f, std::min(1.0f, (view.Scale - c_min_zoom) / - (c_max_zoom - c_min_zoom))); - - Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground); - ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); - - Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_Hints); - ImGui::PushClipRect(rect.Min + ImVec2(1, 1), rect.Max - ImVec2(1, 1), false); - - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); - - m_IsActive = true; - - return true; -} - -void ed::HintBuilder::End() { - if (!m_IsActive) return; - - ImGui::PopStyleVar(); - - Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_Hints); - ImGui::PopClipRect(); - - Editor->GetDrawList()->ChannelsSetCurrent(c_UserChannel_HintsBackground); - ImGui::PopClipRect(); - - Editor->GetDrawList()->ChannelsSetCurrent(m_LastChannel); - - Editor->Resume(SuspendFlags::KeepSplitter); - - m_IsActive = false; - m_CurrentNode = nullptr; -} - -ImVec2 ed::HintBuilder::GetGroupMin() { - IM_ASSERT(nullptr != m_CurrentNode); - - return Editor->ToScreen(m_CurrentNode->m_Bounds.Min); -} - -ImVec2 ed::HintBuilder::GetGroupMax() { - IM_ASSERT(nullptr != m_CurrentNode); - - return Editor->ToScreen(m_CurrentNode->m_Bounds.Max); -} - -ImDrawList* ed::HintBuilder::GetForegroundDrawList() { - IM_ASSERT(nullptr != m_CurrentNode); - - auto drawList = Editor->GetDrawList(); - - drawList->ChannelsSetCurrent(c_UserChannel_Hints); - - return drawList; -} - -ImDrawList* ed::HintBuilder::GetBackgroundDrawList() { - IM_ASSERT(nullptr != m_CurrentNode); - - auto drawList = Editor->GetDrawList(); - - drawList->ChannelsSetCurrent(c_UserChannel_HintsBackground); - - return drawList; -} - -//------------------------------------------------------------------------------ -// -// Style -// -//------------------------------------------------------------------------------ -void ed::Style::PushColor(StyleColor colorIndex, const ImVec4& color) { - ColorModifier modifier; - modifier.Index = colorIndex; - modifier.Value = Colors[colorIndex]; - m_ColorStack.push_back(modifier); - Colors[colorIndex] = color; -} - -void ed::Style::PopColor(int count) { - while (count > 0) { - auto& modifier = m_ColorStack.back(); - Colors[modifier.Index] = modifier.Value; - m_ColorStack.pop_back(); - --count; - } -} - -void ed::Style::PushVar(StyleVar varIndex, float value) { - auto* var = GetVarFloatAddr(varIndex); - IM_ASSERT(var != nullptr); - VarModifier modifier; - modifier.Index = varIndex; - modifier.Value = ImVec4(*var, 0, 0, 0); - *var = value; - m_VarStack.push_back(modifier); -} - -void ed::Style::PushVar(StyleVar varIndex, const ImVec2& value) { - auto* var = GetVarVec2Addr(varIndex); - IM_ASSERT(var != nullptr); - VarModifier modifier; - modifier.Index = varIndex; - modifier.Value = ImVec4(var->x, var->y, 0, 0); - *var = value; - m_VarStack.push_back(modifier); -} - -void ed::Style::PushVar(StyleVar varIndex, const ImVec4& value) { - auto* var = GetVarVec4Addr(varIndex); - IM_ASSERT(var != nullptr); - VarModifier modifier; - modifier.Index = varIndex; - modifier.Value = *var; - *var = value; - m_VarStack.push_back(modifier); -} - -void ed::Style::PopVar(int count) { - while (count > 0) { - auto& modifier = m_VarStack.back(); - if (auto floatValue = GetVarFloatAddr(modifier.Index)) - *floatValue = modifier.Value.x; - else if (auto vec2Value = GetVarVec2Addr(modifier.Index)) - *vec2Value = ImVec2(modifier.Value.x, modifier.Value.y); - else if (auto vec4Value = GetVarVec4Addr(modifier.Index)) - *vec4Value = modifier.Value; - m_VarStack.pop_back(); - --count; - } -} - -const char* ed::Style::GetColorName(StyleColor colorIndex) const { - switch (colorIndex) { - case StyleColor_Bg: - return "Bg"; - case StyleColor_Grid: - return "Grid"; - case StyleColor_NodeBg: - return "NodeBg"; - case StyleColor_NodeBorder: - return "NodeBorder"; - case StyleColor_HovNodeBorder: - return "HoveredNodeBorder"; - case StyleColor_SelNodeBorder: - return "SelNodeBorder"; - case StyleColor_NodeSelRect: - return "NodeSelRect"; - case StyleColor_NodeSelRectBorder: - return "NodeSelRectBorder"; - case StyleColor_HovLinkBorder: - return "HoveredLinkBorder"; - case StyleColor_SelLinkBorder: - return "SelLinkBorder"; - case StyleColor_HighlightLinkBorder: - return "HighlightLinkBorder"; - case StyleColor_LinkSelRect: - return "LinkSelRect"; - case StyleColor_LinkSelRectBorder: - return "LinkSelRectBorder"; - case StyleColor_PinRect: - return "PinRect"; - case StyleColor_PinRectBorder: - return "PinRectBorder"; - case StyleColor_Flow: - return "Flow"; - case StyleColor_FlowMarker: - return "FlowMarker"; - case StyleColor_GroupBg: - return "GroupBg"; - case StyleColor_GroupBorder: - return "GroupBorder"; - case StyleColor_Count: - break; - } - - IM_ASSERT(0); - return "Unknown"; -} - -float* ed::Style::GetVarFloatAddr(StyleVar idx) { - switch (idx) { - case StyleVar_NodeRounding: - return &NodeRounding; - case StyleVar_NodeBorderWidth: - return &NodeBorderWidth; - case StyleVar_HoveredNodeBorderWidth: - return &HoveredNodeBorderWidth; - case StyleVar_SelectedNodeBorderWidth: - return &SelectedNodeBorderWidth; - case StyleVar_PinRounding: - return &PinRounding; - case StyleVar_PinBorderWidth: - return &PinBorderWidth; - case StyleVar_LinkStrength: - return &LinkStrength; - case StyleVar_ScrollDuration: - return &ScrollDuration; - case StyleVar_FlowMarkerDistance: - return &FlowMarkerDistance; - case StyleVar_FlowSpeed: - return &FlowSpeed; - case StyleVar_FlowDuration: - return &FlowDuration; - case StyleVar_PinCorners: - return &PinCorners; - case StyleVar_PinRadius: - return &PinRadius; - case StyleVar_PinArrowSize: - return &PinArrowSize; - case StyleVar_PinArrowWidth: - return &PinArrowWidth; - case StyleVar_GroupRounding: - return &GroupRounding; - case StyleVar_GroupBorderWidth: - return &GroupBorderWidth; - case StyleVar_HighlightConnectedLinks: - return &HighlightConnectedLinks; - case StyleVar_SnapLinkToPinDir: - return &SnapLinkToPinDir; - case StyleVar_HoveredNodeBorderOffset: - return &HoverNodeBorderOffset; - case StyleVar_SelectedNodeBorderOffset: - return &SelectedNodeBorderOffset; - default: - return nullptr; - } -} - -ImVec2* ed::Style::GetVarVec2Addr(StyleVar idx) { - switch (idx) { - case StyleVar_SourceDirection: - return &SourceDirection; - case StyleVar_TargetDirection: - return &TargetDirection; - case StyleVar_PivotAlignment: - return &PivotAlignment; - case StyleVar_PivotSize: - return &PivotSize; - case StyleVar_PivotScale: - return &PivotScale; - default: - return nullptr; - } -} - -ImVec4* ed::Style::GetVarVec4Addr(StyleVar idx) { - switch (idx) { - case StyleVar_NodePadding: - return &NodePadding; - default: - return nullptr; - } -} - -//------------------------------------------------------------------------------ -// -// Config -// -//------------------------------------------------------------------------------ -ed::Config::Config(const ax::NodeEditor::Config* config) { - if (config) *static_cast(this) = *config; -} - -std::string ed::Config::Load() { - std::string data; - - if (LoadSettings) { - const auto size = LoadSettings(nullptr, UserPointer); - if (size > 0) { - data.resize(size); - LoadSettings(const_cast(data.data()), UserPointer); - } - } else if (SettingsFile) { - std::ifstream file(SettingsFile); - if (file) { - file.seekg(0, std::ios_base::end); - auto size = static_cast(file.tellg()); - file.seekg(0, std::ios_base::beg); - - data.reserve(size); - data.assign(std::istreambuf_iterator(file), - std::istreambuf_iterator()); - } - } - - return data; -} - -std::string ed::Config::LoadNode(NodeId nodeId) { - std::string data; - - if (LoadNodeSettings) { - const auto size = LoadNodeSettings(nodeId, nullptr, UserPointer); - if (size > 0) { - data.resize(size); - LoadNodeSettings(nodeId, const_cast(data.data()), UserPointer); - } - } - - return data; -} - -void ed::Config::BeginSave() { - if (BeginSaveSession) BeginSaveSession(UserPointer); -} - -bool ed::Config::Save(const std::string& data, SaveReasonFlags flags) { - if (SaveSettings) { - return SaveSettings(data.c_str(), data.size(), flags, UserPointer); - } else if (SettingsFile) { - std::ofstream settingsFile(SettingsFile); - if (settingsFile) settingsFile << data; - - return !!settingsFile; - } - - return false; -} - -bool ed::Config::SaveNode(NodeId nodeId, const std::string& data, - SaveReasonFlags flags) { - if (SaveNodeSettings) - return SaveNodeSettings(nodeId, data.c_str(), data.size(), flags, - UserPointer); - - return false; -} - -void ed::Config::EndSave() { - if (EndSaveSession) EndSaveSession(UserPointer); -} diff --git a/symmetri/gui/extensions/imgui_node_editor.h b/symmetri/gui/extensions/imgui_node_editor.h deleted file mode 100644 index 4a77361..0000000 --- a/symmetri/gui/extensions/imgui_node_editor.h +++ /dev/null @@ -1,524 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.9.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -#ifndef __IMGUI_NODE_EDITOR_H__ -#define __IMGUI_NODE_EDITOR_H__ -#pragma once - -//------------------------------------------------------------------------------ -#include - -#include // std::uintXX_t -#include // std::move - -//------------------------------------------------------------------------------ -#define IMGUI_NODE_EDITOR_VERSION "0.9.3" -#define IMGUI_NODE_EDITOR_VERSION_NUM 000903 - -//------------------------------------------------------------------------------ -#ifndef IMGUI_NODE_EDITOR_API -#define IMGUI_NODE_EDITOR_API -#endif - -//------------------------------------------------------------------------------ -namespace ax { -namespace NodeEditor { - -//------------------------------------------------------------------------------ -struct NodeId; -struct LinkId; -struct PinId; - -//------------------------------------------------------------------------------ -enum class PinKind { Input, Output }; - -enum class FlowDirection { Forward, Backward }; - -enum class CanvasSizeMode { - FitVerticalView, // Previous view will be scaled to fit new view on Y axis - FitHorizontalView, // Previous view will be scaled to fit new view on X axis - CenterOnly, // Previous view will be centered on new view -}; - -//------------------------------------------------------------------------------ -enum class SaveReasonFlags : uint32_t { - None = 0x00000000, - Navigation = 0x00000001, - Position = 0x00000002, - Size = 0x00000004, - Selection = 0x00000008, - AddNode = 0x00000010, - RemoveNode = 0x00000020, - User = 0x00000040 -}; - -inline SaveReasonFlags operator|(SaveReasonFlags lhs, SaveReasonFlags rhs) { - return static_cast(static_cast(lhs) | - static_cast(rhs)); -} -inline SaveReasonFlags operator&(SaveReasonFlags lhs, SaveReasonFlags rhs) { - return static_cast(static_cast(lhs) & - static_cast(rhs)); -} - -using ConfigSaveSettings = bool (*)(const char* data, size_t size, - SaveReasonFlags reason, void* userPointer); -using ConfigLoadSettings = size_t (*)(char* data, void* userPointer); - -using ConfigSaveNodeSettings = bool (*)(NodeId nodeId, const char* data, - size_t size, SaveReasonFlags reason, - void* userPointer); -using ConfigLoadNodeSettings = size_t (*)(NodeId nodeId, char* data, - void* userPointer); - -using ConfigSession = void (*)(void* userPointer); - -struct Config { - using CanvasSizeModeAlias = ax::NodeEditor::CanvasSizeMode; - - const char* SettingsFile; - ConfigSession BeginSaveSession; - ConfigSession EndSaveSession; - ConfigSaveSettings SaveSettings; - ConfigLoadSettings LoadSettings; - ConfigSaveNodeSettings SaveNodeSettings; - ConfigLoadNodeSettings LoadNodeSettings; - void* UserPointer; - ImVector CustomZoomLevels; - CanvasSizeModeAlias CanvasSizeMode; - int DragButtonIndex; // Mouse button index drag action will react to (0-left, - // 1-right, 2-middle) - int SelectButtonIndex; // Mouse button index select action will react to - // (0-left, 1-right, 2-middle) - int NavigateButtonIndex; // Mouse button index navigate action will react to - // (0-left, 1-right, 2-middle) - int ContextMenuButtonIndex; // Mouse button index context menu action will - // react to (0-left, 1-right, 2-middle) - - Config() - : SettingsFile("NodeEditor.json"), - BeginSaveSession(nullptr), - EndSaveSession(nullptr), - SaveSettings(nullptr), - LoadSettings(nullptr), - SaveNodeSettings(nullptr), - LoadNodeSettings(nullptr), - UserPointer(nullptr), - CustomZoomLevels(), - CanvasSizeMode(CanvasSizeModeAlias::FitVerticalView), - DragButtonIndex(0), - SelectButtonIndex(0), - NavigateButtonIndex(1), - ContextMenuButtonIndex(1) {} -}; - -//------------------------------------------------------------------------------ -enum StyleColor { - StyleColor_Bg, - StyleColor_Grid, - StyleColor_NodeBg, - StyleColor_NodeBorder, - StyleColor_HovNodeBorder, - StyleColor_SelNodeBorder, - StyleColor_NodeSelRect, - StyleColor_NodeSelRectBorder, - StyleColor_HovLinkBorder, - StyleColor_SelLinkBorder, - StyleColor_HighlightLinkBorder, - StyleColor_LinkSelRect, - StyleColor_LinkSelRectBorder, - StyleColor_PinRect, - StyleColor_PinRectBorder, - StyleColor_Flow, - StyleColor_FlowMarker, - StyleColor_GroupBg, - StyleColor_GroupBorder, - - StyleColor_Count -}; - -enum StyleVar { - StyleVar_NodePadding, - StyleVar_NodeRounding, - StyleVar_NodeBorderWidth, - StyleVar_HoveredNodeBorderWidth, - StyleVar_SelectedNodeBorderWidth, - StyleVar_PinRounding, - StyleVar_PinBorderWidth, - StyleVar_LinkStrength, - StyleVar_SourceDirection, - StyleVar_TargetDirection, - StyleVar_ScrollDuration, - StyleVar_FlowMarkerDistance, - StyleVar_FlowSpeed, - StyleVar_FlowDuration, - StyleVar_PivotAlignment, - StyleVar_PivotSize, - StyleVar_PivotScale, - StyleVar_PinCorners, - StyleVar_PinRadius, - StyleVar_PinArrowSize, - StyleVar_PinArrowWidth, - StyleVar_GroupRounding, - StyleVar_GroupBorderWidth, - StyleVar_HighlightConnectedLinks, - StyleVar_SnapLinkToPinDir, - StyleVar_HoveredNodeBorderOffset, - StyleVar_SelectedNodeBorderOffset, - - StyleVar_Count -}; - -struct Style { - ImVec4 NodePadding; - float NodeRounding; - float NodeBorderWidth; - float HoveredNodeBorderWidth; - float HoverNodeBorderOffset; - float SelectedNodeBorderWidth; - float SelectedNodeBorderOffset; - float PinRounding; - float PinBorderWidth; - float LinkStrength; - ImVec2 SourceDirection; - ImVec2 TargetDirection; - float ScrollDuration; - float FlowMarkerDistance; - float FlowSpeed; - float FlowDuration; - ImVec2 PivotAlignment; - ImVec2 PivotSize; - ImVec2 PivotScale; - float PinCorners; - float PinRadius; - float PinArrowSize; - float PinArrowWidth; - float GroupRounding; - float GroupBorderWidth; - float HighlightConnectedLinks; - float SnapLinkToPinDir; // when true link will start on the line defined by - // pin direction - ImVec4 Colors[StyleColor_Count]; - - Style() { - NodePadding = ImVec4(8, 8, 8, 8); - NodeRounding = 12.0f; - NodeBorderWidth = 1.5f; - HoveredNodeBorderWidth = 3.5f; - HoverNodeBorderOffset = 0.0f; - SelectedNodeBorderWidth = 3.5f; - SelectedNodeBorderOffset = 0.0f; - PinRounding = 4.0f; - PinBorderWidth = 0.0f; - LinkStrength = 100.0f; - SourceDirection = ImVec2(1.0f, 0.0f); - TargetDirection = ImVec2(-1.0f, 0.0f); - ScrollDuration = 0.35f; - FlowMarkerDistance = 30.0f; - FlowSpeed = 150.0f; - FlowDuration = 2.0f; - PivotAlignment = ImVec2(0.5f, 0.5f); - PivotSize = ImVec2(0.0f, 0.0f); - PivotScale = ImVec2(1, 1); -#if IMGUI_VERSION_NUM > 18101 - PinCorners = ImDrawFlags_RoundCornersAll; -#else - PinCorners = ImDrawCornerFlags_All; -#endif - PinRadius = 0.0f; - PinArrowSize = 0.0f; - PinArrowWidth = 0.0f; - GroupRounding = 6.0f; - GroupBorderWidth = 1.0f; - HighlightConnectedLinks = 0.0f; - SnapLinkToPinDir = 0.0f; - - Colors[StyleColor_Bg] = ImColor(60, 60, 70, 200); - Colors[StyleColor_Grid] = ImColor(120, 120, 120, 40); - Colors[StyleColor_NodeBg] = ImColor(32, 32, 32, 200); - Colors[StyleColor_NodeBorder] = ImColor(255, 255, 255, 96); - Colors[StyleColor_HovNodeBorder] = ImColor(50, 176, 255, 255); - Colors[StyleColor_SelNodeBorder] = ImColor(255, 176, 50, 255); - Colors[StyleColor_NodeSelRect] = ImColor(5, 130, 255, 64); - Colors[StyleColor_NodeSelRectBorder] = ImColor(5, 130, 255, 128); - Colors[StyleColor_HovLinkBorder] = ImColor(50, 176, 255, 255); - Colors[StyleColor_SelLinkBorder] = ImColor(255, 176, 50, 255); - Colors[StyleColor_HighlightLinkBorder] = ImColor(204, 105, 0, 255); - Colors[StyleColor_LinkSelRect] = ImColor(5, 130, 255, 64); - Colors[StyleColor_LinkSelRectBorder] = ImColor(5, 130, 255, 128); - Colors[StyleColor_PinRect] = ImColor(60, 180, 255, 100); - Colors[StyleColor_PinRectBorder] = ImColor(60, 180, 255, 128); - Colors[StyleColor_Flow] = ImColor(255, 128, 64, 255); - Colors[StyleColor_FlowMarker] = ImColor(255, 128, 64, 255); - Colors[StyleColor_GroupBg] = ImColor(0, 0, 0, 160); - Colors[StyleColor_GroupBorder] = ImColor(255, 255, 255, 32); - } -}; - -//------------------------------------------------------------------------------ -struct EditorContext; - -//------------------------------------------------------------------------------ -IMGUI_NODE_EDITOR_API void SetCurrentEditor(EditorContext* ctx); -IMGUI_NODE_EDITOR_API EditorContext* GetCurrentEditor(); -IMGUI_NODE_EDITOR_API EditorContext* CreateEditor( - const Config* config = nullptr); -IMGUI_NODE_EDITOR_API void DestroyEditor(EditorContext* ctx); -IMGUI_NODE_EDITOR_API const Config& GetConfig(EditorContext* ctx = nullptr); - -IMGUI_NODE_EDITOR_API Style& GetStyle(); -IMGUI_NODE_EDITOR_API const char* GetStyleColorName(StyleColor colorIndex); - -IMGUI_NODE_EDITOR_API void PushStyleColor(StyleColor colorIndex, - const ImVec4& color); -IMGUI_NODE_EDITOR_API void PopStyleColor(int count = 1); - -IMGUI_NODE_EDITOR_API void PushStyleVar(StyleVar varIndex, float value); -IMGUI_NODE_EDITOR_API void PushStyleVar(StyleVar varIndex, const ImVec2& value); -IMGUI_NODE_EDITOR_API void PushStyleVar(StyleVar varIndex, const ImVec4& value); -IMGUI_NODE_EDITOR_API void PopStyleVar(int count = 1); - -IMGUI_NODE_EDITOR_API void Begin(const char* id, - const ImVec2& size = ImVec2(0, 0)); -IMGUI_NODE_EDITOR_API void End(); - -IMGUI_NODE_EDITOR_API void BeginNode(NodeId id); -IMGUI_NODE_EDITOR_API void BeginPin(PinId id, PinKind kind); -IMGUI_NODE_EDITOR_API void PinRect(const ImVec2& a, const ImVec2& b); -IMGUI_NODE_EDITOR_API void PinPivotRect(const ImVec2& a, const ImVec2& b); -IMGUI_NODE_EDITOR_API void PinPivotSize(const ImVec2& size); -IMGUI_NODE_EDITOR_API void PinPivotScale(const ImVec2& scale); -IMGUI_NODE_EDITOR_API void PinPivotAlignment(const ImVec2& alignment); -IMGUI_NODE_EDITOR_API void EndPin(); -IMGUI_NODE_EDITOR_API void Group(const ImVec2& size); -IMGUI_NODE_EDITOR_API void EndNode(); - -IMGUI_NODE_EDITOR_API bool BeginGroupHint(NodeId nodeId); -IMGUI_NODE_EDITOR_API ImVec2 GetGroupMin(); -IMGUI_NODE_EDITOR_API ImVec2 GetGroupMax(); -IMGUI_NODE_EDITOR_API ImDrawList* GetHintForegroundDrawList(); -IMGUI_NODE_EDITOR_API ImDrawList* GetHintBackgroundDrawList(); -IMGUI_NODE_EDITOR_API void EndGroupHint(); - -// TODO: Add a way to manage node background channels -IMGUI_NODE_EDITOR_API ImDrawList* GetNodeBackgroundDrawList(NodeId nodeId); - -IMGUI_NODE_EDITOR_API bool Link(LinkId id, PinId startPinId, PinId endPinId, - const ImVec4& color = ImVec4(1, 1, 1, 1), - float thickness = 1.0f); - -IMGUI_NODE_EDITOR_API void Flow( - LinkId linkId, FlowDirection direction = FlowDirection::Forward); - -IMGUI_NODE_EDITOR_API bool BeginCreate(const ImVec4& color = ImVec4(1, 1, 1, 1), - float thickness = 1.0f); -IMGUI_NODE_EDITOR_API bool QueryNewLink(PinId* startId, PinId* endId); -IMGUI_NODE_EDITOR_API bool QueryNewLink(PinId* startId, PinId* endId, - const ImVec4& color, - float thickness = 1.0f); -IMGUI_NODE_EDITOR_API bool QueryNewNode(PinId* pinId); -IMGUI_NODE_EDITOR_API bool QueryNewNode(PinId* pinId, const ImVec4& color, - float thickness = 1.0f); -IMGUI_NODE_EDITOR_API bool AcceptNewItem(); -IMGUI_NODE_EDITOR_API bool AcceptNewItem(const ImVec4& color, - float thickness = 1.0f); -IMGUI_NODE_EDITOR_API void RejectNewItem(); -IMGUI_NODE_EDITOR_API void RejectNewItem(const ImVec4& color, - float thickness = 1.0f); -IMGUI_NODE_EDITOR_API void EndCreate(); - -IMGUI_NODE_EDITOR_API bool BeginDelete(); -IMGUI_NODE_EDITOR_API bool QueryDeletedLink(LinkId* linkId, - PinId* startId = nullptr, - PinId* endId = nullptr); -IMGUI_NODE_EDITOR_API bool QueryDeletedNode(NodeId* nodeId); -IMGUI_NODE_EDITOR_API bool AcceptDeletedItem(bool deleteDependencies = true); -IMGUI_NODE_EDITOR_API void RejectDeletedItem(); -IMGUI_NODE_EDITOR_API void EndDelete(); - -IMGUI_NODE_EDITOR_API void SetNodePosition(NodeId nodeId, - const ImVec2& editorPosition); -IMGUI_NODE_EDITOR_API void SetGroupSize(NodeId nodeId, const ImVec2& size); -IMGUI_NODE_EDITOR_API ImVec2 GetNodePosition(NodeId nodeId); -IMGUI_NODE_EDITOR_API ImVec2 GetNodeSize(NodeId nodeId); -IMGUI_NODE_EDITOR_API void CenterNodeOnScreen(NodeId nodeId); -IMGUI_NODE_EDITOR_API void SetNodeZPosition( - NodeId nodeId, float z); // Sets node z position, nodes with higher value - // are drawn over nodes with lower value -IMGUI_NODE_EDITOR_API float GetNodeZPosition( - NodeId nodeId); // Returns node z position, defaults is 0.0f - -IMGUI_NODE_EDITOR_API void RestoreNodeState(NodeId nodeId); - -IMGUI_NODE_EDITOR_API void Suspend(); -IMGUI_NODE_EDITOR_API void Resume(); -IMGUI_NODE_EDITOR_API bool IsSuspended(); - -IMGUI_NODE_EDITOR_API bool IsActive(); - -IMGUI_NODE_EDITOR_API bool HasSelectionChanged(); -IMGUI_NODE_EDITOR_API int GetSelectedObjectCount(); -IMGUI_NODE_EDITOR_API int GetSelectedNodes(NodeId* nodes, int size); -IMGUI_NODE_EDITOR_API int GetSelectedLinks(LinkId* links, int size); -IMGUI_NODE_EDITOR_API bool IsNodeSelected(NodeId nodeId); -IMGUI_NODE_EDITOR_API bool IsLinkSelected(LinkId linkId); -IMGUI_NODE_EDITOR_API void ClearSelection(); -IMGUI_NODE_EDITOR_API void SelectNode(NodeId nodeId, bool append = false); -IMGUI_NODE_EDITOR_API void SelectLink(LinkId linkId, bool append = false); -IMGUI_NODE_EDITOR_API void DeselectNode(NodeId nodeId); -IMGUI_NODE_EDITOR_API void DeselectLink(LinkId linkId); - -IMGUI_NODE_EDITOR_API bool DeleteNode(NodeId nodeId); -IMGUI_NODE_EDITOR_API bool DeleteLink(LinkId linkId); - -IMGUI_NODE_EDITOR_API bool HasAnyLinks( - NodeId nodeId); // Returns true if node has any link connected -IMGUI_NODE_EDITOR_API bool HasAnyLinks( - PinId pinId); // Return true if pin has any link connected -IMGUI_NODE_EDITOR_API int BreakLinks( - NodeId nodeId); // Break all links connected to this node -IMGUI_NODE_EDITOR_API int BreakLinks( - PinId pinId); // Break all links connected to this pin - -IMGUI_NODE_EDITOR_API void NavigateToContent(float duration = -1); -IMGUI_NODE_EDITOR_API void NavigateToSelection(bool zoomIn = false, - float duration = -1); - -IMGUI_NODE_EDITOR_API bool ShowNodeContextMenu(NodeId* nodeId); -IMGUI_NODE_EDITOR_API bool ShowPinContextMenu(PinId* pinId); -IMGUI_NODE_EDITOR_API bool ShowLinkContextMenu(LinkId* linkId); -IMGUI_NODE_EDITOR_API bool ShowBackgroundContextMenu(); - -IMGUI_NODE_EDITOR_API void EnableShortcuts(bool enable); -IMGUI_NODE_EDITOR_API bool AreShortcutsEnabled(); - -IMGUI_NODE_EDITOR_API bool BeginShortcut(); -IMGUI_NODE_EDITOR_API bool AcceptCut(); -IMGUI_NODE_EDITOR_API bool AcceptCopy(); -IMGUI_NODE_EDITOR_API bool AcceptPaste(); -IMGUI_NODE_EDITOR_API bool AcceptDuplicate(); -IMGUI_NODE_EDITOR_API bool AcceptCreateNode(); -IMGUI_NODE_EDITOR_API int GetActionContextSize(); -IMGUI_NODE_EDITOR_API int GetActionContextNodes(NodeId* nodes, int size); -IMGUI_NODE_EDITOR_API int GetActionContextLinks(LinkId* links, int size); -IMGUI_NODE_EDITOR_API void EndShortcut(); - -IMGUI_NODE_EDITOR_API float GetCurrentZoom(); - -IMGUI_NODE_EDITOR_API NodeId GetHoveredNode(); -IMGUI_NODE_EDITOR_API PinId GetHoveredPin(); -IMGUI_NODE_EDITOR_API LinkId GetHoveredLink(); -IMGUI_NODE_EDITOR_API NodeId GetDoubleClickedNode(); -IMGUI_NODE_EDITOR_API PinId GetDoubleClickedPin(); -IMGUI_NODE_EDITOR_API LinkId GetDoubleClickedLink(); -IMGUI_NODE_EDITOR_API bool IsBackgroundClicked(); -IMGUI_NODE_EDITOR_API bool IsBackgroundDoubleClicked(); -IMGUI_NODE_EDITOR_API ImGuiMouseButton -GetBackgroundClickButtonIndex(); // -1 if none -IMGUI_NODE_EDITOR_API ImGuiMouseButton -GetBackgroundDoubleClickButtonIndex(); // -1 if none - -IMGUI_NODE_EDITOR_API bool GetLinkPins( - LinkId linkId, PinId* startPinId, - PinId* endPinId); // pass nullptr if particular pin do not interest you - -IMGUI_NODE_EDITOR_API bool PinHadAnyLinks(PinId pinId); - -IMGUI_NODE_EDITOR_API ImVec2 GetScreenSize(); -IMGUI_NODE_EDITOR_API ImVec2 ScreenToCanvas(const ImVec2& pos); -IMGUI_NODE_EDITOR_API ImVec2 CanvasToScreen(const ImVec2& pos); - -IMGUI_NODE_EDITOR_API int -GetNodeCount(); // Returns number of submitted nodes since Begin() call -IMGUI_NODE_EDITOR_API int GetOrderedNodeIds( - NodeId* nodes, - int size); // Fills an array with node id's in order they're drawn; up to - // 'size` elements are set. Returns actual size of filled id's. - -//------------------------------------------------------------------------------ -namespace Details { - -template -struct SafeType { - SafeType(T t) : m_Value(std::move(t)) {} - - SafeType(const SafeType&) = default; - - template - SafeType(const SafeType< - typename std::enable_if::value, T2>::type, - typename std::enable_if::value, - Tag2>::type>&) = delete; - - SafeType& operator=(const SafeType&) = default; - - explicit operator T() const { return Get(); } - - T Get() const { return m_Value; } - - private: - T m_Value; -}; - -template -struct SafePointerType : SafeType { - static const Tag Invalid; - - using SafeType::SafeType; - - SafePointerType() : SafePointerType(Invalid) {} - - template - explicit SafePointerType(T* ptr) - : SafePointerType(reinterpret_cast(ptr)) {} - template - T* AsPointer() const { - return reinterpret_cast(this->Get()); - } - - explicit operator bool() const { return *this != Invalid; } -}; - -template -const Tag SafePointerType::Invalid = {0}; - -template -inline bool operator==(const SafePointerType& lhs, - const SafePointerType& rhs) { - return lhs.Get() == rhs.Get(); -} - -template -inline bool operator!=(const SafePointerType& lhs, - const SafePointerType& rhs) { - return lhs.Get() != rhs.Get(); -} - -} // namespace Details - -struct NodeId final : Details::SafePointerType { - using SafePointerType::SafePointerType; -}; - -struct LinkId final : Details::SafePointerType { - using SafePointerType::SafePointerType; -}; - -struct PinId final : Details::SafePointerType { - using SafePointerType::SafePointerType; -}; - -//------------------------------------------------------------------------------ -} // namespace NodeEditor -} // namespace ax - -//------------------------------------------------------------------------------ -#endif // __IMGUI_NODE_EDITOR_H__ diff --git a/symmetri/gui/extensions/imgui_node_editor_api.cpp b/symmetri/gui/extensions/imgui_node_editor_api.cpp deleted file mode 100644 index 21084a8..0000000 --- a/symmetri/gui/extensions/imgui_node_editor_api.cpp +++ /dev/null @@ -1,596 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.9.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -#include - -#include "imgui_node_editor_internal.h" - -//------------------------------------------------------------------------------ -static ax::NodeEditor::Detail::EditorContext* s_Editor = nullptr; - -//------------------------------------------------------------------------------ -template -static int BuildIdList(C& container, I* list, int listSize, F&& accept) { - if (list != nullptr) { - int count = 0; - for (auto object : container) { - if (listSize <= 0) break; - - if (accept(object)) { - list[count] = I(object->ID().AsPointer()); - ++count; - --listSize; - } - } - - return count; - } else - return static_cast( - std::count_if(container.begin(), container.end(), accept)); -} - -//------------------------------------------------------------------------------ -ax::NodeEditor::EditorContext* ax::NodeEditor::CreateEditor( - const Config* config) { - return reinterpret_cast( - new ax::NodeEditor::Detail::EditorContext(config)); -} - -void ax::NodeEditor::DestroyEditor(EditorContext* ctx) { - auto lastContext = GetCurrentEditor(); - - // Set context we're about to destroy as current, to give callback valid - // context - if (lastContext != ctx) SetCurrentEditor(ctx); - - auto editor = reinterpret_cast(ctx); - - delete editor; - - if (lastContext != ctx) SetCurrentEditor(lastContext); -} - -const ax::NodeEditor::Config& ax::NodeEditor::GetConfig(EditorContext* ctx) { - if (ctx == nullptr) ctx = GetCurrentEditor(); - - if (ctx) { - auto editor = reinterpret_cast(ctx); - - return editor->GetConfig(); - } else { - static Config s_EmptyConfig; - return s_EmptyConfig; - } -} - -void ax::NodeEditor::SetCurrentEditor(EditorContext* ctx) { - s_Editor = reinterpret_cast(ctx); -} - -ax::NodeEditor::EditorContext* ax::NodeEditor::GetCurrentEditor() { - return reinterpret_cast(s_Editor); -} - -ax::NodeEditor::Style& ax::NodeEditor::GetStyle() { - return s_Editor->GetStyle(); -} - -const char* ax::NodeEditor::GetStyleColorName(StyleColor colorIndex) { - return s_Editor->GetStyle().GetColorName(colorIndex); -} - -void ax::NodeEditor::PushStyleColor(StyleColor colorIndex, - const ImVec4& color) { - s_Editor->GetStyle().PushColor(colorIndex, color); -} - -void ax::NodeEditor::PopStyleColor(int count) { - s_Editor->GetStyle().PopColor(count); -} - -void ax::NodeEditor::PushStyleVar(StyleVar varIndex, float value) { - s_Editor->GetStyle().PushVar(varIndex, value); -} - -void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec2& value) { - s_Editor->GetStyle().PushVar(varIndex, value); -} - -void ax::NodeEditor::PushStyleVar(StyleVar varIndex, const ImVec4& value) { - s_Editor->GetStyle().PushVar(varIndex, value); -} - -void ax::NodeEditor::PopStyleVar(int count) { - s_Editor->GetStyle().PopVar(count); -} - -void ax::NodeEditor::Begin(const char* id, const ImVec2& size) { - s_Editor->Begin(id, size); -} - -void ax::NodeEditor::End() { s_Editor->End(); } - -void ax::NodeEditor::BeginNode(NodeId id) { - s_Editor->GetNodeBuilder().Begin(id); -} - -void ax::NodeEditor::BeginPin(PinId id, PinKind kind) { - s_Editor->GetNodeBuilder().BeginPin(id, kind); -} - -void ax::NodeEditor::PinRect(const ImVec2& a, const ImVec2& b) { - s_Editor->GetNodeBuilder().PinRect(a, b); -} - -void ax::NodeEditor::PinPivotRect(const ImVec2& a, const ImVec2& b) { - s_Editor->GetNodeBuilder().PinPivotRect(a, b); -} - -void ax::NodeEditor::PinPivotSize(const ImVec2& size) { - s_Editor->GetNodeBuilder().PinPivotSize(size); -} - -void ax::NodeEditor::PinPivotScale(const ImVec2& scale) { - s_Editor->GetNodeBuilder().PinPivotScale(scale); -} - -void ax::NodeEditor::PinPivotAlignment(const ImVec2& alignment) { - s_Editor->GetNodeBuilder().PinPivotAlignment(alignment); -} - -void ax::NodeEditor::EndPin() { s_Editor->GetNodeBuilder().EndPin(); } - -void ax::NodeEditor::Group(const ImVec2& size) { - s_Editor->GetNodeBuilder().Group(size); -} - -void ax::NodeEditor::EndNode() { s_Editor->GetNodeBuilder().End(); } - -bool ax::NodeEditor::BeginGroupHint(NodeId nodeId) { - return s_Editor->GetHintBuilder().Begin(nodeId); -} - -ImVec2 ax::NodeEditor::GetGroupMin() { - return s_Editor->GetHintBuilder().GetGroupMin(); -} - -ImVec2 ax::NodeEditor::GetGroupMax() { - return s_Editor->GetHintBuilder().GetGroupMax(); -} - -ImDrawList* ax::NodeEditor::GetHintForegroundDrawList() { - return s_Editor->GetHintBuilder().GetForegroundDrawList(); -} - -ImDrawList* ax::NodeEditor::GetHintBackgroundDrawList() { - return s_Editor->GetHintBuilder().GetBackgroundDrawList(); -} - -void ax::NodeEditor::EndGroupHint() { s_Editor->GetHintBuilder().End(); } - -ImDrawList* ax::NodeEditor::GetNodeBackgroundDrawList(NodeId nodeId) { - if (auto node = s_Editor->FindNode(nodeId)) - return s_Editor->GetNodeBuilder().GetUserBackgroundDrawList(node); - else - return nullptr; -} - -bool ax::NodeEditor::Link(LinkId id, PinId startPinId, PinId endPinId, - const ImVec4& color /* = ImVec4(1, 1, 1, 1)*/, - float thickness /* = 1.0f*/) { - return s_Editor->DoLink(id, startPinId, endPinId, ImColor(color), thickness); -} - -void ax::NodeEditor::Flow(LinkId linkId, FlowDirection direction) { - if (auto link = s_Editor->FindLink(linkId)) s_Editor->Flow(link, direction); -} - -bool ax::NodeEditor::BeginCreate(const ImVec4& color, float thickness) { - auto& context = s_Editor->GetItemCreator(); - - if (context.Begin()) { - context.SetStyle(ImColor(color), thickness); - return true; - } else - return false; -} - -bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId) { - using Result = ax::NodeEditor::Detail::CreateItemAction::Result; - - auto& context = s_Editor->GetItemCreator(); - - return context.QueryLink(startId, endId) == Result::True; -} - -bool ax::NodeEditor::QueryNewLink(PinId* startId, PinId* endId, - const ImVec4& color, float thickness) { - using Result = ax::NodeEditor::Detail::CreateItemAction::Result; - - auto& context = s_Editor->GetItemCreator(); - - auto result = context.QueryLink(startId, endId); - if (result != Result::Indeterminate) - context.SetStyle(ImColor(color), thickness); - - return result == Result::True; -} - -bool ax::NodeEditor::QueryNewNode(PinId* pinId) { - using Result = ax::NodeEditor::Detail::CreateItemAction::Result; - - auto& context = s_Editor->GetItemCreator(); - - return context.QueryNode(pinId) == Result::True; -} - -bool ax::NodeEditor::QueryNewNode(PinId* pinId, const ImVec4& color, - float thickness) { - using Result = ax::NodeEditor::Detail::CreateItemAction::Result; - - auto& context = s_Editor->GetItemCreator(); - - auto result = context.QueryNode(pinId); - if (result != Result::Indeterminate) - context.SetStyle(ImColor(color), thickness); - - return result == Result::True; -} - -bool ax::NodeEditor::AcceptNewItem() { - using Result = ax::NodeEditor::Detail::CreateItemAction::Result; - - auto& context = s_Editor->GetItemCreator(); - - return context.AcceptItem() == Result::True; -} - -bool ax::NodeEditor::AcceptNewItem(const ImVec4& color, float thickness) { - using Result = ax::NodeEditor::Detail::CreateItemAction::Result; - - auto& context = s_Editor->GetItemCreator(); - - auto result = context.AcceptItem(); - if (result != Result::Indeterminate) - context.SetStyle(ImColor(color), thickness); - - return result == Result::True; -} - -void ax::NodeEditor::RejectNewItem() { - auto& context = s_Editor->GetItemCreator(); - - context.RejectItem(); -} - -void ax::NodeEditor::RejectNewItem(const ImVec4& color, float thickness) { - using Result = ax::NodeEditor::Detail::CreateItemAction::Result; - - auto& context = s_Editor->GetItemCreator(); - - if (context.RejectItem() != Result::Indeterminate) - context.SetStyle(ImColor(color), thickness); -} - -void ax::NodeEditor::EndCreate() { - auto& context = s_Editor->GetItemCreator(); - - context.End(); -} - -bool ax::NodeEditor::BeginDelete() { - auto& context = s_Editor->GetItemDeleter(); - - return context.Begin(); -} - -bool ax::NodeEditor::QueryDeletedLink(LinkId* linkId, PinId* startId, - PinId* endId) { - auto& context = s_Editor->GetItemDeleter(); - - return context.QueryLink(linkId, startId, endId); -} - -bool ax::NodeEditor::QueryDeletedNode(NodeId* nodeId) { - auto& context = s_Editor->GetItemDeleter(); - - return context.QueryNode(nodeId); -} - -bool ax::NodeEditor::AcceptDeletedItem(bool deleteDependencies) { - auto& context = s_Editor->GetItemDeleter(); - - return context.AcceptItem(deleteDependencies); -} - -void ax::NodeEditor::RejectDeletedItem() { - auto& context = s_Editor->GetItemDeleter(); - - context.RejectItem(); -} - -void ax::NodeEditor::EndDelete() { - auto& context = s_Editor->GetItemDeleter(); - - context.End(); -} - -void ax::NodeEditor::SetNodePosition(NodeId nodeId, const ImVec2& position) { - s_Editor->SetNodePosition(nodeId, position); -} - -void ax::NodeEditor::SetGroupSize(NodeId nodeId, const ImVec2& size) { - s_Editor->SetGroupSize(nodeId, size); -} - -ImVec2 ax::NodeEditor::GetNodePosition(NodeId nodeId) { - return s_Editor->GetNodePosition(nodeId); -} - -ImVec2 ax::NodeEditor::GetNodeSize(NodeId nodeId) { - return s_Editor->GetNodeSize(nodeId); -} - -void ax::NodeEditor::CenterNodeOnScreen(NodeId nodeId) { - if (auto node = s_Editor->FindNode(nodeId)) node->CenterOnScreenInNextFrame(); -} - -void ax::NodeEditor::SetNodeZPosition(NodeId nodeId, float z) { - s_Editor->SetNodeZPosition(nodeId, z); -} - -float ax::NodeEditor::GetNodeZPosition(NodeId nodeId) { - return s_Editor->GetNodeZPosition(nodeId); -} - -void ax::NodeEditor::RestoreNodeState(NodeId nodeId) { - if (auto node = s_Editor->FindNode(nodeId)) - s_Editor->MarkNodeToRestoreState(node); -} - -void ax::NodeEditor::Suspend() { s_Editor->Suspend(); } - -void ax::NodeEditor::Resume() { s_Editor->Resume(); } - -bool ax::NodeEditor::IsSuspended() { return s_Editor->IsSuspended(); } - -bool ax::NodeEditor::IsActive() { return s_Editor->IsFocused(); } - -bool ax::NodeEditor::HasSelectionChanged() { - return s_Editor->HasSelectionChanged(); -} - -int ax::NodeEditor::GetSelectedObjectCount() { - return (int)s_Editor->GetSelectedObjects().size(); -} - -int ax::NodeEditor::GetSelectedNodes(NodeId* nodes, int size) { - return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, - [](auto object) { return object->AsNode() != nullptr; }); -} - -int ax::NodeEditor::GetSelectedLinks(LinkId* links, int size) { - return BuildIdList(s_Editor->GetSelectedObjects(), links, size, - [](auto object) { return object->AsLink() != nullptr; }); -} - -bool ax::NodeEditor::IsNodeSelected(NodeId nodeId) { - if (auto node = s_Editor->FindNode(nodeId)) - return s_Editor->IsSelected(node); - else - return false; -} - -bool ax::NodeEditor::IsLinkSelected(LinkId linkId) { - if (auto link = s_Editor->FindLink(linkId)) - return s_Editor->IsSelected(link); - else - return false; -} - -void ax::NodeEditor::ClearSelection() { s_Editor->ClearSelection(); } - -void ax::NodeEditor::SelectNode(NodeId nodeId, bool append) { - if (auto node = s_Editor->FindNode(nodeId)) { - if (append) - s_Editor->SelectObject(node); - else - s_Editor->SetSelectedObject(node); - } -} - -void ax::NodeEditor::SelectLink(LinkId linkId, bool append) { - if (auto link = s_Editor->FindLink(linkId)) { - if (append) - s_Editor->SelectObject(link); - else - s_Editor->SetSelectedObject(link); - } -} - -void ax::NodeEditor::DeselectNode(NodeId nodeId) { - if (auto node = s_Editor->FindNode(nodeId)) s_Editor->DeselectObject(node); -} - -void ax::NodeEditor::DeselectLink(LinkId linkId) { - if (auto link = s_Editor->FindLink(linkId)) s_Editor->DeselectObject(link); -} - -bool ax::NodeEditor::DeleteNode(NodeId nodeId) { - if (auto node = s_Editor->FindNode(nodeId)) - return s_Editor->GetItemDeleter().Add(node); - else - return false; -} - -bool ax::NodeEditor::DeleteLink(LinkId linkId) { - if (auto link = s_Editor->FindLink(linkId)) - return s_Editor->GetItemDeleter().Add(link); - else - return false; -} - -bool ax::NodeEditor::HasAnyLinks(NodeId nodeId) { - return s_Editor->HasAnyLinks(nodeId); -} - -bool ax::NodeEditor::HasAnyLinks(PinId pinId) { - return s_Editor->HasAnyLinks(pinId); -} - -int ax::NodeEditor::BreakLinks(NodeId nodeId) { - return s_Editor->BreakLinks(nodeId); -} - -int ax::NodeEditor::BreakLinks(PinId pinId) { - return s_Editor->BreakLinks(pinId); -} - -void ax::NodeEditor::NavigateToContent(float duration) { - s_Editor->NavigateTo(s_Editor->GetContentBounds(), true, duration); -} - -void ax::NodeEditor::NavigateToSelection(bool zoomIn, float duration) { - s_Editor->NavigateTo(s_Editor->GetSelectionBounds(), zoomIn, duration); -} - -bool ax::NodeEditor::ShowNodeContextMenu(NodeId* nodeId) { - return s_Editor->GetContextMenu().ShowNodeContextMenu(nodeId); -} - -bool ax::NodeEditor::ShowPinContextMenu(PinId* pinId) { - return s_Editor->GetContextMenu().ShowPinContextMenu(pinId); -} - -bool ax::NodeEditor::ShowLinkContextMenu(LinkId* linkId) { - return s_Editor->GetContextMenu().ShowLinkContextMenu(linkId); -} - -bool ax::NodeEditor::ShowBackgroundContextMenu() { - return s_Editor->GetContextMenu().ShowBackgroundContextMenu(); -} - -void ax::NodeEditor::EnableShortcuts(bool enable) { - s_Editor->EnableShortcuts(enable); -} - -bool ax::NodeEditor::AreShortcutsEnabled() { - return s_Editor->AreShortcutsEnabled(); -} - -bool ax::NodeEditor::BeginShortcut() { return s_Editor->GetShortcut().Begin(); } - -bool ax::NodeEditor::AcceptCut() { return s_Editor->GetShortcut().AcceptCut(); } - -bool ax::NodeEditor::AcceptCopy() { - return s_Editor->GetShortcut().AcceptCopy(); -} - -bool ax::NodeEditor::AcceptPaste() { - return s_Editor->GetShortcut().AcceptPaste(); -} - -bool ax::NodeEditor::AcceptDuplicate() { - return s_Editor->GetShortcut().AcceptDuplicate(); -} - -bool ax::NodeEditor::AcceptCreateNode() { - return s_Editor->GetShortcut().AcceptCreateNode(); -} - -int ax::NodeEditor::GetActionContextSize() { - return static_cast(s_Editor->GetShortcut().m_Context.size()); -} - -int ax::NodeEditor::GetActionContextNodes(NodeId* nodes, int size) { - return BuildIdList(s_Editor->GetSelectedObjects(), nodes, size, - [](auto object) { return object->AsNode() != nullptr; }); -} - -int ax::NodeEditor::GetActionContextLinks(LinkId* links, int size) { - return BuildIdList(s_Editor->GetSelectedObjects(), links, size, - [](auto object) { return object->AsLink() != nullptr; }); -} - -void ax::NodeEditor::EndShortcut() { return s_Editor->GetShortcut().End(); } - -float ax::NodeEditor::GetCurrentZoom() { return s_Editor->GetView().InvScale; } - -ax::NodeEditor::NodeId ax::NodeEditor::GetHoveredNode() { - return s_Editor->GetHoveredNode(); -} - -ax::NodeEditor::PinId ax::NodeEditor::GetHoveredPin() { - return s_Editor->GetHoveredPin(); -} - -ax::NodeEditor::LinkId ax::NodeEditor::GetHoveredLink() { - return s_Editor->GetHoveredLink(); -} - -ax::NodeEditor::NodeId ax::NodeEditor::GetDoubleClickedNode() { - return s_Editor->GetDoubleClickedNode(); -} - -ax::NodeEditor::PinId ax::NodeEditor::GetDoubleClickedPin() { - return s_Editor->GetDoubleClickedPin(); -} - -ax::NodeEditor::LinkId ax::NodeEditor::GetDoubleClickedLink() { - return s_Editor->GetDoubleClickedLink(); -} - -bool ax::NodeEditor::IsBackgroundClicked() { - return s_Editor->IsBackgroundClicked(); -} - -bool ax::NodeEditor::IsBackgroundDoubleClicked() { - return s_Editor->IsBackgroundDoubleClicked(); -} - -ImGuiMouseButton ax::NodeEditor::GetBackgroundClickButtonIndex() { - return s_Editor->GetBackgroundClickButtonIndex(); -} - -ImGuiMouseButton ax::NodeEditor::GetBackgroundDoubleClickButtonIndex() { - return s_Editor->GetBackgroundDoubleClickButtonIndex(); -} - -bool ax::NodeEditor::GetLinkPins(LinkId linkId, PinId* startPinId, - PinId* endPinId) { - auto link = s_Editor->FindLink(linkId); - if (!link) return false; - - if (startPinId) *startPinId = link->m_StartPin->m_ID; - if (endPinId) *endPinId = link->m_EndPin->m_ID; - - return true; -} - -bool ax::NodeEditor::PinHadAnyLinks(PinId pinId) { - return s_Editor->PinHadAnyLinks(pinId); -} - -ImVec2 ax::NodeEditor::GetScreenSize() { return s_Editor->GetRect().GetSize(); } - -ImVec2 ax::NodeEditor::ScreenToCanvas(const ImVec2& pos) { - return s_Editor->ToCanvas(pos); -} - -ImVec2 ax::NodeEditor::CanvasToScreen(const ImVec2& pos) { - return s_Editor->ToScreen(pos); -} - -int ax::NodeEditor::GetNodeCount() { return s_Editor->CountLiveNodes(); } - -int ax::NodeEditor::GetOrderedNodeIds(NodeId* nodes, int size) { - return s_Editor->GetNodeIds(nodes, size); -} diff --git a/symmetri/gui/extensions/imgui_node_editor_internal.h b/symmetri/gui/extensions/imgui_node_editor_internal.h deleted file mode 100644 index e4d158e..0000000 --- a/symmetri/gui/extensions/imgui_node_editor_internal.h +++ /dev/null @@ -1,1488 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.9.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -#ifndef __IMGUI_NODE_EDITOR_INTERNAL_H__ -#define __IMGUI_NODE_EDITOR_INTERNAL_H__ -#pragma once - -//------------------------------------------------------------------------------ -#ifndef IMGUI_DEFINE_MATH_OPERATORS -#define IMGUI_DEFINE_MATH_OPERATORS -#endif -#include "imgui_node_editor.h" - -//------------------------------------------------------------------------------ -#include -#include - -#include -#include - -#include "crude_json.h" -#include "imgui_bezier_math.h" -#include "imgui_canvas.h" -#include "imgui_extra_math.h" - -//------------------------------------------------------------------------------ -namespace ax { -namespace NodeEditor { -namespace Detail { - -//------------------------------------------------------------------------------ -namespace ed = ax::NodeEditor::Detail; -namespace json = crude_json; - -//------------------------------------------------------------------------------ -using std::string; -using std::vector; - -//------------------------------------------------------------------------------ -void Log(const char* fmt, ...); - -//------------------------------------------------------------------------------ -// inline ImRect ToRect(const ax::rectf& rect); -// inline ImRect ToRect(const ax::rect& rect); -inline ImRect ImGui_GetItemRect(); -inline ImVec2 ImGui_GetMouseClickPos(ImGuiMouseButton buttonIndex); - -//------------------------------------------------------------------------------ -// https://stackoverflow.com/a/36079786 -#define DECLARE_HAS_MEMBER(__trait_name__, __member_name__) \ - \ - template \ - class __trait_name__ { \ - using check_type = ::std::remove_const_t<__boost_has_member_T__>; \ - struct no_type { \ - char x[2]; \ - }; \ - using yes_type = char; \ - \ - struct base { \ - void __member_name__() {} \ - }; \ - struct mixin : public base, public check_type {}; \ - \ - template \ - struct aux {}; \ - \ - template \ - static no_type test(aux<&U::__member_name__>*); \ - template \ - static yes_type test(...); \ - \ - public: \ - static constexpr bool value = \ - (sizeof(yes_type) == sizeof(test(0))); \ - } - -DECLARE_HAS_MEMBER(HasFringeScale, _FringeScale); - -#undef DECLARE_HAS_MEMBER - -struct FringeScaleRef { - // Overload is present when ImDrawList does have _FringeScale member variable. - template - static float& Get( - typename std::enable_if::value, T>::type* drawList) { - return drawList->_FringeScale; - } - - // Overload is present when ImDrawList does not have _FringeScale member - // variable. - template - static float& Get( - typename std::enable_if::value, T>::type*) { - static float placeholder = 1.0f; - return placeholder; - } -}; - -static inline float& ImFringeScaleRef(ImDrawList* drawList) { - return FringeScaleRef::Get(drawList); -} - -struct FringeScaleScope { - FringeScaleScope(float scale) - : m_LastFringeScale(ImFringeScaleRef(ImGui::GetWindowDrawList())) { - ImFringeScaleRef(ImGui::GetWindowDrawList()) = scale; - } - - ~FringeScaleScope() { - ImFringeScaleRef(ImGui::GetWindowDrawList()) = m_LastFringeScale; - } - - private: - float m_LastFringeScale; -}; - -//------------------------------------------------------------------------------ -enum class ObjectType { None, Node, Link, Pin }; - -using ax::NodeEditor::PinKind; -using ax::NodeEditor::SaveReasonFlags; -using ax::NodeEditor::StyleColor; -using ax::NodeEditor::StyleVar; - -using ax::NodeEditor::LinkId; -using ax::NodeEditor::NodeId; -using ax::NodeEditor::PinId; - -struct ObjectId final : Details::SafePointerType { - using Super = Details::SafePointerType; - using Super::Super; - - ObjectId() : Super(Invalid), m_Type(ObjectType::None) {} - ObjectId(PinId pinId) : Super(pinId.AsPointer()), m_Type(ObjectType::Pin) {} - ObjectId(NodeId nodeId) - : Super(nodeId.AsPointer()), m_Type(ObjectType::Node) {} - ObjectId(LinkId linkId) - : Super(linkId.AsPointer()), m_Type(ObjectType::Link) {} - - explicit operator PinId() const { return AsPinId(); } - explicit operator NodeId() const { return AsNodeId(); } - explicit operator LinkId() const { return AsLinkId(); } - - PinId AsPinId() const { - IM_ASSERT(IsPinId()); - return PinId(AsPointer()); - } - NodeId AsNodeId() const { - IM_ASSERT(IsNodeId()); - return NodeId(AsPointer()); - } - LinkId AsLinkId() const { - IM_ASSERT(IsLinkId()); - return LinkId(AsPointer()); - } - - bool IsPinId() const { return m_Type == ObjectType::Pin; } - bool IsNodeId() const { return m_Type == ObjectType::Node; } - bool IsLinkId() const { return m_Type == ObjectType::Link; } - - ObjectType Type() const { return m_Type; } - - private: - ObjectType m_Type; -}; - -struct EditorContext; - -struct Node; -struct Pin; -struct Link; - -template -struct ObjectWrapper { - Id m_ID; - T* m_Object; - - T* operator->() { return m_Object; } - const T* operator->() const { return m_Object; } - - operator T*() { return m_Object; } - operator const T*() const { return m_Object; } - - bool operator<(const ObjectWrapper& rhs) const { - return m_ID.AsPointer() < rhs.m_ID.AsPointer(); - } -}; - -struct Object { - enum DrawFlags { - None = 0, - Hovered = 1, - Selected = 2, - Highlighted = 4, - }; - - inline friend DrawFlags operator|(DrawFlags lhs, DrawFlags rhs) { - return static_cast(static_cast(lhs) | - static_cast(rhs)); - } - inline friend DrawFlags operator&(DrawFlags lhs, DrawFlags rhs) { - return static_cast(static_cast(lhs) & - static_cast(rhs)); - } - inline friend DrawFlags& operator|=(DrawFlags& lhs, DrawFlags rhs) { - lhs = lhs | rhs; - return lhs; - } - inline friend DrawFlags& operator&=(DrawFlags& lhs, DrawFlags rhs) { - lhs = lhs & rhs; - return lhs; - } - - EditorContext* const Editor; - - bool m_IsLive; - bool m_IsSelected; - bool m_DeleteOnNewFrame; - - Object(EditorContext* editor) - : Editor(editor), - m_IsLive(true), - m_IsSelected(false), - m_DeleteOnNewFrame(false) {} - - virtual ~Object() = default; - - virtual ObjectId ID() = 0; - - bool IsVisible() const { - if (!m_IsLive) return false; - - const auto bounds = GetBounds(); - - return ImGui::IsRectVisible(bounds.Min, bounds.Max); - } - - virtual void Reset() { m_IsLive = false; } - - virtual void Draw(ImDrawList* drawList, DrawFlags flags = None) = 0; - - virtual bool AcceptDrag() { return false; } - virtual void UpdateDrag(const ImVec2& offset) { IM_UNUSED(offset); } - virtual bool EndDrag() { return false; } - virtual ImVec2 DragStartLocation() { return GetBounds().Min; } - - virtual bool IsDraggable() { - bool result = AcceptDrag(); - EndDrag(); - return result; - } - virtual bool IsSelectable() { return false; } - - virtual bool TestHit(const ImVec2& point, float extraThickness = 0.0f) const { - if (!m_IsLive) return false; - - auto bounds = GetBounds(); - if (extraThickness > 0) bounds.Expand(extraThickness); - - return bounds.Contains(point); - } - - virtual bool TestHit(const ImRect& rect, bool allowIntersect = true) const { - if (!m_IsLive) return false; - - const auto bounds = GetBounds(); - - return !ImRect_IsEmpty(bounds) && - (allowIntersect ? bounds.Overlaps(rect) : rect.Contains(bounds)); - } - - virtual ImRect GetBounds() const = 0; - - virtual Node* AsNode() { return nullptr; } - virtual Pin* AsPin() { return nullptr; } - virtual Link* AsLink() { return nullptr; } -}; - -struct Pin final : Object { - using IdType = PinId; - - PinId m_ID; - PinKind m_Kind; - Node* m_Node; - ImRect m_Bounds; - ImRect m_Pivot; - Pin* m_PreviousPin; - ImU32 m_Color; - ImU32 m_BorderColor; - float m_BorderWidth; - float m_Rounding; - int m_Corners; - ImVec2 m_Dir; - float m_Strength; - float m_Radius; - float m_ArrowSize; - float m_ArrowWidth; - bool m_SnapLinkToDir; - bool m_HasConnection; - bool m_HadConnection; - - Pin(EditorContext* editor, PinId id, PinKind kind) - : Object(editor), - m_ID(id), - m_Kind(kind), - m_Node(nullptr), - m_Bounds(), - m_PreviousPin(nullptr), - m_Color(IM_COL32_WHITE), - m_BorderColor(IM_COL32_BLACK), - m_BorderWidth(0), - m_Rounding(0), - m_Corners(0), - m_Dir(0, 0), - m_Strength(0), - m_Radius(0), - m_ArrowSize(0), - m_ArrowWidth(0), - m_SnapLinkToDir(true), - m_HasConnection(false), - m_HadConnection(false) {} - - virtual ObjectId ID() override { return m_ID; } - - virtual void Reset() override final { - m_HadConnection = m_HasConnection && m_IsLive; - m_HasConnection = false; - - Object::Reset(); - } - - virtual void Draw(ImDrawList* drawList, - DrawFlags flags = None) override final; - - ImVec2 GetClosestPoint(const ImVec2& p) const; - ImLine GetClosestLine(const Pin* pin) const; - - virtual ImRect GetBounds() const override final { return m_Bounds; } - - virtual Pin* AsPin() override final { return this; } -}; - -enum class NodeType { Node, Group }; - -enum class NodeRegion : uint8_t { - None = 0x00, - Top = 0x01, - Bottom = 0x02, - Left = 0x04, - Right = 0x08, - Center = 0x10, - Header = 0x20, - TopLeft = Top | Left, - TopRight = Top | Right, - BottomLeft = Bottom | Left, - BottomRight = Bottom | Right, -}; - -inline NodeRegion operator|(NodeRegion lhs, NodeRegion rhs) { - return static_cast(static_cast(lhs) | - static_cast(rhs)); -} -inline NodeRegion operator&(NodeRegion lhs, NodeRegion rhs) { - return static_cast(static_cast(lhs) & - static_cast(rhs)); -} - -struct Node final : Object { - using IdType = NodeId; - - NodeId m_ID; - NodeType m_Type; - ImRect m_Bounds; - float m_ZPosition; - int m_Channel; - Pin* m_LastPin; - ImVec2 m_DragStart; - - ImU32 m_Color; - ImU32 m_BorderColor; - float m_BorderWidth; - float m_Rounding; - - ImU32 m_GroupColor; - ImU32 m_GroupBorderColor; - float m_GroupBorderWidth; - float m_GroupRounding; - ImRect m_GroupBounds; - - bool m_HighlightConnectedLinks; - - bool m_RestoreState; - bool m_CenterOnScreen; - - Node(EditorContext* editor, NodeId id) - : Object(editor), - m_ID(id), - m_Type(NodeType::Node), - m_Bounds(), - m_ZPosition(0.0f), - m_Channel(0), - m_LastPin(nullptr), - m_DragStart(), - m_Color(IM_COL32_WHITE), - m_BorderColor(IM_COL32_BLACK), - m_BorderWidth(0), - m_Rounding(0), - m_GroupBounds(), - m_HighlightConnectedLinks(false), - m_RestoreState(false), - m_CenterOnScreen(false) {} - - virtual ObjectId ID() override { return m_ID; } - - bool AcceptDrag() override; - void UpdateDrag(const ImVec2& offset) override; - bool EndDrag() override; // return true, when changed - ImVec2 DragStartLocation() override { return m_DragStart; } - - virtual bool IsSelectable() override { return true; } - - virtual void Draw(ImDrawList* drawList, - DrawFlags flags = None) override final; - void DrawBorder(ImDrawList* drawList, ImU32 color, float thickness = 1.0f, - float offset = 0.0f); - - void GetGroupedNodes(std::vector& result, bool append = false); - - void CenterOnScreenInNextFrame() { m_CenterOnScreen = true; } - - ImRect GetRegionBounds(NodeRegion region) const; - NodeRegion GetRegion(const ImVec2& point) const; - - virtual ImRect GetBounds() const override final { return m_Bounds; } - - virtual Node* AsNode() override final { return this; } -}; - -struct Link final : Object { - using IdType = LinkId; - - LinkId m_ID; - Pin* m_StartPin; - Pin* m_EndPin; - ImU32 m_Color; - ImU32 m_HighlightColor; - float m_Thickness; - ImVec2 m_Start; - ImVec2 m_End; - - Link(EditorContext* editor, LinkId id) - : Object(editor), - m_ID(id), - m_StartPin(nullptr), - m_EndPin(nullptr), - m_Color(IM_COL32_WHITE), - m_Thickness(1.0f) {} - - virtual ObjectId ID() override { return m_ID; } - - virtual bool IsSelectable() override { return true; } - - virtual void Draw(ImDrawList* drawList, - DrawFlags flags = None) override final; - void Draw(ImDrawList* drawList, ImU32 color, - float extraThickness = 0.0f) const; - - void UpdateEndpoints(); - - ImCubicBezierPoints GetCurve() const; - - virtual bool TestHit(const ImVec2& point, - float extraThickness = 0.0f) const override final; - virtual bool TestHit(const ImRect& rect, - bool allowIntersect = true) const override final; - - virtual ImRect GetBounds() const override final; - - virtual Link* AsLink() override final { return this; } -}; - -struct NodeSettings { - NodeId m_ID; - ImVec2 m_Location; - ImVec2 m_Size; - ImVec2 m_GroupSize; - bool m_WasUsed; - - bool m_Saved; - bool m_IsDirty; - SaveReasonFlags m_DirtyReason; - - NodeSettings(NodeId id) - : m_ID(id), - m_Location(0, 0), - m_Size(0, 0), - m_GroupSize(0, 0), - m_WasUsed(false), - m_Saved(false), - m_IsDirty(false), - m_DirtyReason(SaveReasonFlags::None) {} - - void ClearDirty(); - void MakeDirty(SaveReasonFlags reason); - - json::value Serialize(); - - static bool Parse(const std::string& string, NodeSettings& settings); - static bool Parse(const json::value& data, NodeSettings& result); -}; - -struct Settings { - bool m_IsDirty; - SaveReasonFlags m_DirtyReason; - - vector m_Nodes; - vector m_Selection; - ImVec2 m_ViewScroll; - float m_ViewZoom; - ImRect m_VisibleRect; - - Settings() - : m_IsDirty(false), - m_DirtyReason(SaveReasonFlags::None), - m_ViewScroll(0, 0), - m_ViewZoom(1.0f), - m_VisibleRect() {} - - NodeSettings* AddNode(NodeId id); - NodeSettings* FindNode(NodeId id); - void RemoveNode(NodeId id); - - void ClearDirty(Node* node = nullptr); - void MakeDirty(SaveReasonFlags reason, Node* node = nullptr); - - std::string Serialize(); - - static bool Parse(const std::string& string, Settings& settings); -}; - -struct Control { - Object* HotObject; - Object* ActiveObject; - Object* ClickedObject; - Object* DoubleClickedObject; - Node* HotNode; - Node* ActiveNode; - Node* ClickedNode; - Node* DoubleClickedNode; - Pin* HotPin; - Pin* ActivePin; - Pin* ClickedPin; - Pin* DoubleClickedPin; - Link* HotLink; - Link* ActiveLink; - Link* ClickedLink; - Link* DoubleClickedLink; - bool BackgroundHot; - bool BackgroundActive; - int BackgroundClickButtonIndex; - int BackgroundDoubleClickButtonIndex; - - Control() - : Control(nullptr, nullptr, nullptr, nullptr, false, false, -1, -1) {} - - Control(Object* hotObject, Object* activeObject, Object* clickedObject, - Object* doubleClickedObject, bool backgroundHot, - bool backgroundActive, int backgroundClickButtonIndex, - int backgroundDoubleClickButtonIndex) - : HotObject(hotObject), - ActiveObject(activeObject), - ClickedObject(clickedObject), - DoubleClickedObject(doubleClickedObject), - HotNode(nullptr), - ActiveNode(nullptr), - ClickedNode(nullptr), - DoubleClickedNode(nullptr), - HotPin(nullptr), - ActivePin(nullptr), - ClickedPin(nullptr), - DoubleClickedPin(nullptr), - HotLink(nullptr), - ActiveLink(nullptr), - ClickedLink(nullptr), - DoubleClickedLink(nullptr), - BackgroundHot(backgroundHot), - BackgroundActive(backgroundActive), - BackgroundClickButtonIndex(backgroundClickButtonIndex), - BackgroundDoubleClickButtonIndex(backgroundDoubleClickButtonIndex) { - if (hotObject) { - HotNode = hotObject->AsNode(); - HotPin = hotObject->AsPin(); - HotLink = hotObject->AsLink(); - - if (HotPin) HotNode = HotPin->m_Node; - } - - if (activeObject) { - ActiveNode = activeObject->AsNode(); - ActivePin = activeObject->AsPin(); - ActiveLink = activeObject->AsLink(); - } - - if (clickedObject) { - ClickedNode = clickedObject->AsNode(); - ClickedPin = clickedObject->AsPin(); - ClickedLink = clickedObject->AsLink(); - } - - if (doubleClickedObject) { - DoubleClickedNode = doubleClickedObject->AsNode(); - DoubleClickedPin = doubleClickedObject->AsPin(); - DoubleClickedLink = doubleClickedObject->AsLink(); - } - } -}; - -struct NavigateAction; -struct SizeAction; -struct DragAction; -struct SelectAction; -struct CreateItemAction; -struct DeleteItemsAction; -struct ContextMenuAction; -struct ShortcutAction; - -struct AnimationController; -struct FlowAnimationController; - -struct Animation { - enum State { Playing, Stopped }; - - EditorContext* Editor; - State m_State; - float m_Time; - float m_Duration; - - Animation(EditorContext* editor); - virtual ~Animation(); - - void Play(float duration); - void Stop(); - void Finish(); - void Update(); - - bool IsPlaying() const { return m_State == Playing; } - - float GetProgress() const { return m_Time / m_Duration; } - - protected: - virtual void OnPlay() {} - virtual void OnFinish() {} - virtual void OnStop() {} - - virtual void OnUpdate(float progress) { IM_UNUSED(progress); } -}; - -struct NavigateAnimation final : Animation { - NavigateAction& Action; - ImRect m_Start; - ImRect m_Target; - - NavigateAnimation(EditorContext* editor, NavigateAction& scrollAction); - - void NavigateTo(const ImRect& target, float duration); - - private: - void OnUpdate(float progress) override final; - void OnStop() override final; - void OnFinish() override final; -}; - -struct FlowAnimation final : Animation { - FlowAnimationController* Controller; - Link* m_Link; - float m_Speed; - float m_MarkerDistance; - float m_Offset; - - FlowAnimation(FlowAnimationController* controller); - - void Flow(Link* link, float markerDistance, float speed, float duration); - - void Draw(ImDrawList* drawList); - - private: - struct CurvePoint { - float Distance; - ImVec2 Point; - }; - - ImVec2 m_LastStart; - ImVec2 m_LastEnd; - float m_PathLength; - vector m_Path; - - bool IsLinkValid() const; - bool IsPathValid() const; - void UpdatePath(); - void ClearPath(); - - ImVec2 SamplePath(float distance) const; - - void OnUpdate(float progress) override final; - void OnStop() override final; -}; - -struct AnimationController { - EditorContext* Editor; - - AnimationController(EditorContext* editor) : Editor(editor) {} - - virtual ~AnimationController() {} - - virtual void Draw(ImDrawList* drawList) { IM_UNUSED(drawList); } -}; - -struct FlowAnimationController final : AnimationController { - FlowAnimationController(EditorContext* editor); - virtual ~FlowAnimationController(); - - void Flow(Link* link, FlowDirection direction = FlowDirection::Forward); - - virtual void Draw(ImDrawList* drawList) override final; - - void Release(FlowAnimation* animation); - - private: - FlowAnimation* GetOrCreate(Link* link); - - vector m_Animations; - vector m_FreePool; -}; - -struct EditorAction { - enum AcceptResult { False, True, Possible }; - - EditorAction(EditorContext* editor) : Editor(editor) {} - - virtual ~EditorAction() {} - - virtual const char* GetName() const = 0; - - virtual AcceptResult Accept(const Control& control) = 0; - virtual bool Process(const Control& control) = 0; - virtual void Reject() { - } // celled when Accept return 'Possible' and was rejected - - virtual ImGuiMouseCursor GetCursor() { return ImGuiMouseCursor_Arrow; } - - virtual bool IsDragging() { return false; } - - virtual void ShowMetrics() {} - - virtual NavigateAction* AsNavigate() { return nullptr; } - virtual SizeAction* AsSize() { return nullptr; } - virtual DragAction* AsDrag() { return nullptr; } - virtual SelectAction* AsSelect() { return nullptr; } - virtual CreateItemAction* AsCreateItem() { return nullptr; } - virtual DeleteItemsAction* AsDeleteItems() { return nullptr; } - virtual ContextMenuAction* AsContextMenu() { return nullptr; } - virtual ShortcutAction* AsCutCopyPaste() { return nullptr; } - - EditorContext* Editor; -}; - -struct NavigateAction final : EditorAction { - enum class ZoomMode { None, Exact, WithMargin }; - - enum class NavigationReason { - Unknown, - MouseZoom, - Selection, - Object, - Content, - Edge - }; - - bool m_IsActive; - float m_Zoom; - ImRect m_VisibleRect; - ImVec2 m_Scroll; - ImVec2 m_ScrollStart; - ImVec2 m_ScrollDelta; - - NavigateAction(EditorContext* editor, ImGuiEx::Canvas& canvas); - - virtual const char* GetName() const override final { return "Navigate"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - - virtual void ShowMetrics() override final; - - virtual NavigateAction* AsNavigate() override final { return this; } - - void NavigateTo(const ImRect& bounds, ZoomMode zoomMode, - float duration = -1.0f, - NavigationReason reason = NavigationReason::Unknown); - void StopNavigation(); - void FinishNavigation(); - - bool MoveOverEdge(const ImVec2& canvasSize); - void StopMoveOverEdge(); - bool IsMovingOverEdge() const { return m_MovingOverEdge; } - ImVec2 GetMoveScreenOffset() const { return m_MoveScreenOffset; } - - void SetWindow(ImVec2 position, ImVec2 size); - ImVec2 GetWindowScreenPos() const { return m_WindowScreenPos; }; - ImVec2 GetWindowScreenSize() const { return m_WindowScreenSize; }; - - ImGuiEx::CanvasView GetView() const; - ImVec2 GetViewOrigin() const; - float GetViewScale() const; - - void SetViewRect(const ImRect& rect); - ImRect GetViewRect() const; - - private: - ImGuiEx::Canvas& m_Canvas; - ImVec2 m_WindowScreenPos; - ImVec2 m_WindowScreenSize; - - NavigateAnimation m_Animation; - NavigationReason m_Reason; - uint64_t m_LastSelectionId; - Object* m_LastObject; - bool m_MovingOverEdge; - ImVec2 m_MoveScreenOffset; - - const float* m_ZoomLevels; - int m_ZoomLevelCount; - - bool HandleZoom(const Control& control); - - void NavigateTo(const ImRect& target, float duration = -1.0f, - NavigationReason reason = NavigationReason::Unknown); - - float MatchZoom(int steps, float fallbackZoom); - int MatchZoomIndex(int direction); - - static const float s_DefaultZoomLevels[]; - static const int s_DefaultZoomLevelCount; -}; - -struct SizeAction final : EditorAction { - bool m_IsActive; - bool m_Clean; - Node* m_SizedNode; - - SizeAction(EditorContext* editor); - - virtual const char* GetName() const override final { return "Size"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - - virtual ImGuiMouseCursor GetCursor() override final { return m_Cursor; } - - virtual void ShowMetrics() override final; - - virtual SizeAction* AsSize() override final { return this; } - - virtual bool IsDragging() override final { return m_IsActive; } - - const ImRect& GetStartGroupBounds() const { return m_StartGroupBounds; } - - private: - NodeRegion GetRegion(Node* node); - ImGuiMouseCursor ChooseCursor(NodeRegion region); - - ImRect m_StartBounds; - ImRect m_StartGroupBounds; - ImVec2 m_LastSize; - ImVec2 m_MinimumSize; - ImVec2 m_LastDragOffset; - ed::NodeRegion m_Pivot; - ImGuiMouseCursor m_Cursor; -}; - -struct DragAction final : EditorAction { - bool m_IsActive; - bool m_Clear; - Object* m_DraggedObject; - vector m_Objects; - - DragAction(EditorContext* editor); - - virtual const char* GetName() const override final { return "Drag"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - - virtual ImGuiMouseCursor GetCursor() override final { - return ImGuiMouseCursor_ResizeAll; - } - - virtual bool IsDragging() override final { return m_IsActive; } - - virtual void ShowMetrics() override final; - - virtual DragAction* AsDrag() override final { return this; } -}; - -struct SelectAction final : EditorAction { - bool m_IsActive; - - bool m_SelectGroups; - bool m_SelectLinkMode; - bool m_CommitSelection; - ImVec2 m_StartPoint; - ImVec2 m_EndPoint; - vector m_CandidateObjects; - vector m_SelectedObjectsAtStart; - - Animation m_Animation; - - SelectAction(EditorContext* editor); - - virtual const char* GetName() const override final { return "Select"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - - virtual void ShowMetrics() override final; - - virtual bool IsDragging() override final { return m_IsActive; } - - virtual SelectAction* AsSelect() override final { return this; } - - void Draw(ImDrawList* drawList); -}; - -struct ContextMenuAction final : EditorAction { - enum Menu { None, Node, Pin, Link, Background }; - - Menu m_CandidateMenu; - Menu m_CurrentMenu; - ObjectId m_ContextId; - - ContextMenuAction(EditorContext* editor); - - virtual const char* GetName() const override final { return "Context Menu"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - virtual void Reject() override final; - - virtual void ShowMetrics() override final; - - virtual ContextMenuAction* AsContextMenu() override final { return this; } - - bool ShowNodeContextMenu(NodeId* nodeId); - bool ShowPinContextMenu(PinId* pinId); - bool ShowLinkContextMenu(LinkId* linkId); - bool ShowBackgroundContextMenu(); -}; - -struct ShortcutAction final : EditorAction { - enum Action { None, Cut, Copy, Paste, Duplicate, CreateNode }; - - bool m_IsActive; - bool m_InAction; - Action m_CurrentAction; - vector m_Context; - - ShortcutAction(EditorContext* editor); - - virtual const char* GetName() const override final { return "Shortcut"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - virtual void Reject() override final; - - virtual void ShowMetrics() override final; - - virtual ShortcutAction* AsCutCopyPaste() override final { return this; } - - bool Begin(); - void End(); - - bool AcceptCut(); - bool AcceptCopy(); - bool AcceptPaste(); - bool AcceptDuplicate(); - bool AcceptCreateNode(); -}; - -struct CreateItemAction final : EditorAction { - enum Stage { None, Possible, Create }; - - enum Action { Unknown, UserReject, UserAccept }; - - enum Type { NoItem, Node, Link }; - - enum Result { True, False, Indeterminate }; - - bool m_InActive; - Stage m_NextStage; - - Stage m_CurrentStage; - Type m_ItemType; - Action m_UserAction; - ImU32 m_LinkColor; - float m_LinkThickness; - Pin* m_LinkStart; - Pin* m_LinkEnd; - - bool m_IsActive; - Pin* m_DraggedPin; - - int m_LastChannel = -1; - - CreateItemAction(EditorContext* editor); - - virtual const char* GetName() const override final { return "Create Item"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - - virtual ImGuiMouseCursor GetCursor() override final { - return ImGuiMouseCursor_Arrow; - } - - virtual void ShowMetrics() override final; - - virtual bool IsDragging() override final { return m_IsActive; } - - virtual CreateItemAction* AsCreateItem() override final { return this; } - - void SetStyle(ImU32 color, float thickness); - - bool Begin(); - void End(); - - Result RejectItem(); - Result AcceptItem(); - - Result QueryLink(PinId* startId, PinId* endId); - Result QueryNode(PinId* pinId); - - private: - bool m_IsInGlobalSpace; - - void DragStart(Pin* startPin); - void DragEnd(); - void DropPin(Pin* endPin); - void DropNode(); - void DropNothing(); -}; - -struct DeleteItemsAction final : EditorAction { - bool m_IsActive; - bool m_InInteraction; - - DeleteItemsAction(EditorContext* editor); - - virtual const char* GetName() const override final { return "Delete Items"; } - - virtual AcceptResult Accept(const Control& control) override final; - virtual bool Process(const Control& control) override final; - - virtual void ShowMetrics() override final; - - virtual DeleteItemsAction* AsDeleteItems() override final { return this; } - - bool Add(Object* object); - - bool Begin(); - void End(); - - bool QueryLink(LinkId* linkId, PinId* startId = nullptr, - PinId* endId = nullptr); - bool QueryNode(NodeId* nodeId); - - bool AcceptItem(bool deleteDependencies); - void RejectItem(); - - private: - enum IteratorType { Unknown, Link, Node }; - enum UserAction { Undetermined, Accepted, Rejected }; - - void DeleteDeadLinks(NodeId nodeId); - void DeleteDeadPins(NodeId nodeId); - - bool QueryItem(ObjectId* itemId, IteratorType itemType); - void RemoveItem(bool deleteDependencies); - Object* DropCurrentItem(); - - vector m_ManuallyDeletedObjects; - - IteratorType m_CurrentItemType; - UserAction m_UserAction; - vector m_CandidateObjects; - int m_CandidateItemIndex; -}; - -struct NodeBuilder { - EditorContext* const Editor; - - Node* m_CurrentNode; - Pin* m_CurrentPin; - - ImRect m_NodeRect; - - ImRect m_PivotRect; - ImVec2 m_PivotAlignment; - ImVec2 m_PivotSize; - ImVec2 m_PivotScale; - bool m_ResolvePinRect; - bool m_ResolvePivot; - - ImRect m_GroupBounds; - bool m_IsGroup; - - ImDrawListSplitter m_Splitter; - ImDrawListSplitter m_PinSplitter; - - NodeBuilder(EditorContext* editor); - ~NodeBuilder(); - - void Begin(NodeId nodeId); - void End(); - - void BeginPin(PinId pinId, PinKind kind); - void EndPin(); - - void PinRect(const ImVec2& a, const ImVec2& b); - void PinPivotRect(const ImVec2& a, const ImVec2& b); - void PinPivotSize(const ImVec2& size); - void PinPivotScale(const ImVec2& scale); - void PinPivotAlignment(const ImVec2& alignment); - - void Group(const ImVec2& size); - - ImDrawList* GetUserBackgroundDrawList() const; - ImDrawList* GetUserBackgroundDrawList(Node* node) const; -}; - -struct HintBuilder { - EditorContext* const Editor; - bool m_IsActive; - Node* m_CurrentNode; - float m_LastFringe = 1.0f; - int m_LastChannel = 0; - - HintBuilder(EditorContext* editor); - - bool Begin(NodeId nodeId); - void End(); - - ImVec2 GetGroupMin(); - ImVec2 GetGroupMax(); - - ImDrawList* GetForegroundDrawList(); - ImDrawList* GetBackgroundDrawList(); -}; - -struct Style : ax::NodeEditor::Style { - void PushColor(StyleColor colorIndex, const ImVec4& color); - void PopColor(int count = 1); - - void PushVar(StyleVar varIndex, float value); - void PushVar(StyleVar varIndex, const ImVec2& value); - void PushVar(StyleVar varIndex, const ImVec4& value); - void PopVar(int count = 1); - - const char* GetColorName(StyleColor colorIndex) const; - - private: - struct ColorModifier { - StyleColor Index; - ImVec4 Value; - }; - - struct VarModifier { - StyleVar Index; - ImVec4 Value; - }; - - float* GetVarFloatAddr(StyleVar idx); - ImVec2* GetVarVec2Addr(StyleVar idx); - ImVec4* GetVarVec4Addr(StyleVar idx); - - vector m_ColorStack; - vector m_VarStack; -}; - -struct Config : ax::NodeEditor::Config { - Config(const ax::NodeEditor::Config* config); - - std::string Load(); - std::string LoadNode(NodeId nodeId); - - void BeginSave(); - bool Save(const std::string& data, SaveReasonFlags flags); - bool SaveNode(NodeId nodeId, const std::string& data, SaveReasonFlags flags); - void EndSave(); -}; - -enum class SuspendFlags : uint8_t { None = 0, KeepSplitter = 1 }; - -inline SuspendFlags operator|(SuspendFlags lhs, SuspendFlags rhs) { - return static_cast(static_cast(lhs) | - static_cast(rhs)); -} -inline SuspendFlags operator&(SuspendFlags lhs, SuspendFlags rhs) { - return static_cast(static_cast(lhs) & - static_cast(rhs)); -} - -struct EditorContext { - EditorContext(const ax::NodeEditor::Config* config = nullptr); - ~EditorContext(); - - const Config& GetConfig() const { return m_Config; } - - Style& GetStyle() { return m_Style; } - - void Begin(const char* id, const ImVec2& size = ImVec2(0, 0)); - void End(); - - bool DoLink(LinkId id, PinId startPinId, PinId endPinId, ImU32 color, - float thickness); - - NodeBuilder& GetNodeBuilder() { return m_NodeBuilder; } - HintBuilder& GetHintBuilder() { return m_HintBuilder; } - - EditorAction* GetCurrentAction() { return m_CurrentAction; } - - CreateItemAction& GetItemCreator() { return m_CreateItemAction; } - DeleteItemsAction& GetItemDeleter() { return m_DeleteItemsAction; } - ContextMenuAction& GetContextMenu() { return m_ContextMenuAction; } - ShortcutAction& GetShortcut() { return m_ShortcutAction; } - - const ImGuiEx::CanvasView& GetView() const { return m_Canvas.View(); } - const ImRect& GetViewRect() const { return m_Canvas.ViewRect(); } - const ImRect& GetRect() const { return m_Canvas.Rect(); } - - void SetNodePosition(NodeId nodeId, const ImVec2& screenPosition); - void SetGroupSize(NodeId nodeId, const ImVec2& size); - ImVec2 GetNodePosition(NodeId nodeId); - ImVec2 GetNodeSize(NodeId nodeId); - - void SetNodeZPosition(NodeId nodeId, float z); - float GetNodeZPosition(NodeId nodeId); - - void MarkNodeToRestoreState(Node* node); - void UpdateNodeState(Node* node); - - void RemoveSettings(Object* object); - - void ClearSelection(); - void SelectObject(Object* object); - void DeselectObject(Object* object); - void SetSelectedObject(Object* object); - void ToggleObjectSelection(Object* object); - bool IsSelected(Object* object); - const vector& GetSelectedObjects(); - bool IsAnyNodeSelected(); - bool IsAnyLinkSelected(); - bool HasSelectionChanged(); - uint64_t GetSelectionId() const { return m_SelectionId; } - - Node* FindNodeAt(const ImVec2& p); - void FindNodesInRect(const ImRect& r, vector& result, - bool append = false, bool includeIntersecting = true); - void FindLinksInRect(const ImRect& r, vector& result, - bool append = false); - - bool HasAnyLinks(NodeId nodeId) const; - bool HasAnyLinks(PinId pinId) const; - - int BreakLinks(NodeId nodeId); - int BreakLinks(PinId pinId); - - void FindLinksForNode(NodeId nodeId, vector& result, bool add = false); - - bool PinHadAnyLinks(PinId pinId); - - ImVec2 ToCanvas(const ImVec2& point) const { return m_Canvas.ToLocal(point); } - ImVec2 ToScreen(const ImVec2& point) const { - return m_Canvas.FromLocal(point); - } - - void NotifyLinkDeleted(Link* link); - - void Suspend(SuspendFlags flags = SuspendFlags::None); - void Resume(SuspendFlags flags = SuspendFlags::None); - bool IsSuspended(); - - bool IsFocused(); - bool IsHovered() const; - bool IsHoveredWithoutOverlapp() const; - bool CanAcceptUserInput() const; - - void MakeDirty(SaveReasonFlags reason); - void MakeDirty(SaveReasonFlags reason, Node* node); - - int CountLiveNodes() const; - int CountLivePins() const; - int CountLiveLinks() const; - - Pin* CreatePin(PinId id, PinKind kind); - Node* CreateNode(NodeId id); - Link* CreateLink(LinkId id); - - Node* FindNode(NodeId id); - Pin* FindPin(PinId id); - Link* FindLink(LinkId id); - Object* FindObject(ObjectId id); - - Node* GetNode(NodeId id); - Pin* GetPin(PinId id, PinKind kind); - Link* GetLink(LinkId id); - - Link* FindLinkAt(const ImVec2& p); - - template - ImRect GetBounds(const std::vector& objects) { - ImRect bounds(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); - - for (auto object : objects) - if (object->m_IsLive) bounds.Add(object->GetBounds()); - - if (ImRect_IsEmpty(bounds)) bounds = ImRect(); - - return bounds; - } - - template - ImRect GetBounds(const std::vector>& objects) { - ImRect bounds(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); - - for (auto object : objects) - if (object.m_Object->m_IsLive) bounds.Add(object.m_Object->GetBounds()); - - if (ImRect_IsEmpty(bounds)) bounds = ImRect(); - - return bounds; - } - - ImRect GetSelectionBounds() { return GetBounds(m_SelectedObjects); } - ImRect GetContentBounds() { return GetBounds(m_Nodes); } - - ImU32 GetColor(StyleColor colorIndex) const; - ImU32 GetColor(StyleColor colorIndex, float alpha) const; - - int GetNodeIds(NodeId* nodes, int size) const; - - void NavigateTo(const ImRect& bounds, bool zoomIn = false, - float duration = -1) { - auto zoomMode = zoomIn ? NavigateAction::ZoomMode::WithMargin - : NavigateAction::ZoomMode::None; - m_NavigateAction.NavigateTo(bounds, zoomMode, duration); - } - - void RegisterAnimation(Animation* animation); - void UnregisterAnimation(Animation* animation); - - void Flow(Link* link, FlowDirection direction); - - void SetUserContext(bool globalSpace = false); - - void EnableShortcuts(bool enable); - bool AreShortcutsEnabled(); - - NodeId GetHoveredNode() const { return m_HoveredNode; } - PinId GetHoveredPin() const { return m_HoveredPin; } - LinkId GetHoveredLink() const { return m_HoveredLink; } - NodeId GetDoubleClickedNode() const { return m_DoubleClickedNode; } - PinId GetDoubleClickedPin() const { return m_DoubleClickedPin; } - LinkId GetDoubleClickedLink() const { return m_DoubleClickedLink; } - bool IsBackgroundClicked() const { return m_BackgroundClickButtonIndex >= 0; } - bool IsBackgroundDoubleClicked() const { - return m_BackgroundDoubleClickButtonIndex >= 0; - } - ImGuiMouseButton GetBackgroundClickButtonIndex() const { - return m_BackgroundClickButtonIndex; - } - ImGuiMouseButton GetBackgroundDoubleClickButtonIndex() const { - return m_BackgroundDoubleClickButtonIndex; - } - - float AlignPointToGrid(float p) const { - if (!ImGui::GetIO().KeyAlt) - return p - ImFmod(p, 16.0f); - else - return p; - } - - ImVec2 AlignPointToGrid(const ImVec2& p) const { - return ImVec2(AlignPointToGrid(p.x), AlignPointToGrid(p.y)); - } - - ImDrawList* GetDrawList() { return m_DrawList; } - - private: - void LoadSettings(); - void SaveSettings(); - - Control BuildControl(bool allowOffscreen); - - void ShowMetrics(const Control& control); - - void UpdateAnimations(); - - Config m_Config; - - ImGuiID m_EditorActiveId; - bool m_IsFirstFrame; - bool m_IsFocused; - bool m_IsHovered; - bool m_IsHoveredWithoutOverlapp; - - bool m_ShortcutsEnabled; - - Style m_Style; - - vector> m_Nodes; - vector> m_Pins; - vector> m_Links; - - vector m_SelectedObjects; - - vector m_LastSelectedObjects; - uint64_t m_SelectionId; - - Link* m_LastActiveLink; - - vector m_LiveAnimations; - vector m_LastLiveAnimations; - - ImGuiEx::Canvas m_Canvas; - bool m_IsCanvasVisible; - - NodeBuilder m_NodeBuilder; - HintBuilder m_HintBuilder; - - EditorAction* m_CurrentAction; - NavigateAction m_NavigateAction; - SizeAction m_SizeAction; - DragAction m_DragAction; - SelectAction m_SelectAction; - ContextMenuAction m_ContextMenuAction; - ShortcutAction m_ShortcutAction; - CreateItemAction m_CreateItemAction; - DeleteItemsAction m_DeleteItemsAction; - - vector m_AnimationControllers; - FlowAnimationController m_FlowAnimationController; - - NodeId m_HoveredNode; - PinId m_HoveredPin; - LinkId m_HoveredLink; - NodeId m_DoubleClickedNode; - PinId m_DoubleClickedPin; - LinkId m_DoubleClickedLink; - int m_BackgroundClickButtonIndex; - int m_BackgroundDoubleClickButtonIndex; - - bool m_IsInitialized; - Settings m_Settings; - - ImDrawList* m_DrawList; - int m_ExternalChannel; - ImDrawListSplitter m_Splitter; -}; - -//------------------------------------------------------------------------------ -} // namespace Detail -} // namespace NodeEditor -} // namespace ax - -//------------------------------------------------------------------------------ -#include "imgui_node_editor_internal.inl" - -//------------------------------------------------------------------------------ -#endif // __IMGUI_NODE_EDITOR_INTERNAL_H__ diff --git a/symmetri/gui/extensions/imgui_node_editor_internal.inl b/symmetri/gui/extensions/imgui_node_editor_internal.inl deleted file mode 100644 index df0dd25..0000000 --- a/symmetri/gui/extensions/imgui_node_editor_internal.inl +++ /dev/null @@ -1,65 +0,0 @@ -//------------------------------------------------------------------------------ -// VERSION 0.9.1 -// -// LICENSE -// This software is dual-licensed to the public domain and under the following -// license: you are granted a perpetual, irrevocable license to copy, modify, -// publish, and distribute this file as you see fit. -// -// CREDITS -// Written by Michal Cichon -//------------------------------------------------------------------------------ -# ifndef __IMGUI_NODE_EDITOR_INTERNAL_INL__ -# define __IMGUI_NODE_EDITOR_INTERNAL_INL__ -# pragma once - - -//------------------------------------------------------------------------------ -# include "imgui_node_editor_internal.h" - - -//------------------------------------------------------------------------------ -namespace ax { -namespace NodeEditor { -namespace Detail { - - -//------------------------------------------------------------------------------ -//inline ImRect ToRect(const ax::rectf& rect) -//{ -// return ImRect( -// to_imvec(rect.top_left()), -// to_imvec(rect.bottom_right()) -// ); -//} -// -//inline ImRect ToRect(const ax::rect& rect) -//{ -// return ImRect( -// to_imvec(rect.top_left()), -// to_imvec(rect.bottom_right()) -// ); -//} - -inline ImRect ImGui_GetItemRect() -{ - return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); -} - -inline ImVec2 ImGui_GetMouseClickPos(ImGuiMouseButton buttonIndex) -{ - if (ImGui::IsMouseDown(buttonIndex)) - return ImGui::GetIO().MouseClickedPos[buttonIndex]; - else - return ImGui::GetMousePos(); -} - - -//------------------------------------------------------------------------------ -} // namespace Detail -} // namespace Editor -} // namespace ax - - -//------------------------------------------------------------------------------ -# endif // __IMGUI_NODE_EDITOR_INTERNAL_INL__ From 0c3fcfca63126cd0d2a4686f778bfb9dc8db5073 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sun, 7 Jan 2024 17:54:06 +0100 Subject: [PATCH 016/142] remove nodes --- symmetri/gui/draw_graph.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index a5f3f8c..008737e 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -261,7 +261,7 @@ void draw(Graph& g) { // Create our child canvas ImGui::Text("Hold middle mouse button to scroll (%.2f,%.2f)", scrolling.x, scrolling.y); - ImGui::SameLine(ImGui::GetWindowWidth() - 440); + ImGui::SameLine(ImGui::GetWindowWidth() - 340); ImGui::Checkbox("Show grid", &show_grid); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); @@ -312,23 +312,34 @@ void draw(Graph& g) { if (node_selected) { ImGui::Text("Node '%s'", node_selected->name.c_str()); ImGui::Separator(); - if (ImGui::MenuItem("Delete", NULL, false, false)) { + if (ImGui::MenuItem("Delete")) { + MVC::push([&](Model&& m) { + const auto idx = std::distance( + g.nodes.begin(), + std::find_if(g.nodes.begin(), g.nodes.end(), + [=](const Node& n) { return node_selected == &n; })); + const auto swap_idx = std::distance( + g.n_idx.begin(), std::find(g.n_idx.begin(), g.n_idx.end(), idx)); + std::swap(g.n_idx[swap_idx], g.n_idx.back()); + g.n_idx.pop_back(); + return m; + }); } } else { ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; if (ImGui::MenuItem("Add place")) { MVC::push([&, scene_pos](Model&& m) { - g.n_idx.push_back(g.nodes.size()); g.nodes.push_back( Node{"New node", Symbol('P', g.nodes.size()), scene_pos}); + g.n_idx.push_back(g.nodes.size() - 1); return m; }); } if (ImGui::MenuItem("Add transition")) { MVC::push([&, scene_pos](Model&& m) { - g.n_idx.push_back(g.nodes.size()); g.nodes.push_back( Node{"New node", Symbol('T', g.nodes.size()), scene_pos}); + g.n_idx.push_back(g.nodes.size() - 1); return m; }); } From 6fef5d7f136cf5656efb7521940e56183e27b2c0 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Tue, 9 Jan 2024 21:31:29 +0100 Subject: [PATCH 017/142] remove arcs --- symmetri/gui/draw_graph.cpp | 95 ++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 16 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 008737e..92d05e8 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -23,7 +23,8 @@ static bool open_context_menu = false; static const Node* node_selected = nullptr; static const Node* node_hovered_in_list = nullptr; static const Node* node_hovered_in_scene = nullptr; -static const Arc* active_arc = nullptr; +static const Arc* arc_selected = nullptr; +static const Arc* arc_hovered_in_scene = nullptr; static const float NODE_SLOT_RADIUS = 4.0f; static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); @@ -68,24 +69,32 @@ void draw_arc(const Arc& arc) { ImVec2 p2 = offset + Node::GetCenterPos(*from_to[1], size); // use with: using namespace ImGui; - ImU32 imcolor = getColor(color); const float max_distance = 2.f; const auto mouse_pos = ImGui::GetIO().MousePos; ImVec2 mouse_pos_projected_on_segment = ImLineClosestPoint(p1, p2, mouse_pos); ImVec2 mouse_pos_delta_to_segment = mouse_pos_projected_on_segment - mouse_pos; bool is_segment_hovered = - active_arc == &arc || + arc_selected == &arc || (ImLengthSqr(mouse_pos_delta_to_segment) <= max_distance * max_distance && node_hovered_in_scene == nullptr); - imcolor |= ((ImU32)IM_F32_TO_INT8_SAT(is_segment_hovered ? 1.0f : 0.65f)) - << IM_COL32_A_SHIFT; + if (is_segment_hovered) { + arc_hovered_in_scene = &arc; + } else if (arc_hovered_in_scene == &arc) { + arc_hovered_in_scene = nullptr; + } if (is_segment_hovered && ImGui::IsMouseClicked(0)) { - active_arc = &arc; + arc_selected = &arc; + node_selected = nullptr; } + ImU32 imcolor = + getColor(color) | + ((ImU32)IM_F32_TO_INT8_SAT(arc_hovered_in_scene == &arc ? 1.0f : 0.65f)) + << IM_COL32_A_SHIFT; + const auto d = p2 - p1; const auto theta = std::atan2(d.y, d.x) - M_PI_2; const float h = draw_list->_Data->FontSize * 1.00f; @@ -97,7 +106,8 @@ void draw_arc(const Arc& arc) { const auto b = ImRotate(ImVec2(-1.f, -1.f) * r, a_cos, a_sin); const auto c = ImRotate(ImVec2(+1.f, -1.f) * r, a_cos, a_sin); draw_list->AddTriangleFilled(center + a, center + b, center + c, imcolor); - draw_list->AddLine(p1, p2, imcolor, is_segment_hovered ? 3.0f : 2.0f); + draw_list->AddLine(p1, p2, imcolor, + arc_hovered_in_scene == &arc ? 3.0f : 2.0f); }; void draw_nodes(Node& node) { @@ -130,6 +140,7 @@ void draw_nodes(Node& node) { bool node_moving_active = ImGui::IsItemActive(); if (node_widgets_active || node_moving_active) { node_selected = &node; + arc_selected = nullptr; } if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { node.Pos = node.Pos + ImGui::GetIO().MouseDelta; @@ -188,14 +199,12 @@ void draw(Graph& g) { ImGui::SameLine(); ImGui::InputInt("##", &multiplicity); } - } - ImGui::Separator(); - if (active_arc != nullptr) { + } else if (arc_selected != nullptr) { constexpr std::size_t offset = offsetof(Node, Pos); const Node* from = reinterpret_cast( - reinterpret_cast(active_arc->from_to_pos[0]) - offset); + reinterpret_cast(arc_selected->from_to_pos[0]) - offset); const Node* to = reinterpret_cast( - reinterpret_cast(active_arc->from_to_pos[1]) - offset); + reinterpret_cast(arc_selected->from_to_pos[1]) - offset); ImGui::Text("From:"); ImGui::SameLine(); @@ -209,7 +218,7 @@ void draw(Graph& g) { ImGui::PopItemWidth(); ImGui::Text("Color"); ImGui::SameLine(); - ImGui::Text("%s", symmetri::Color::toString(active_arc->color).c_str()); + ImGui::Text("%s", symmetri::Color::toString(arc_selected->color).c_str()); } ImGui::EndChild(); @@ -225,6 +234,7 @@ void draw(Graph& g) { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { node_selected = &node; + arc_selected = nullptr; } node_hovered_in_list = ImGui::IsItemHovered() ? &node : ImGui::IsAnyItemHovered() ? node_hovered_in_list @@ -246,6 +256,7 @@ void draw(Graph& g) { ImGui::PushID(node.id.key()); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { node_selected = &node; + arc_selected = nullptr; } node_hovered_in_list = ImGui::IsItemHovered() ? &node : ImGui::IsAnyItemHovered() ? node_hovered_in_list @@ -294,11 +305,11 @@ void draw(Graph& g) { if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) || !ImGui::IsAnyItemHovered()) { node_selected = node_hovered_in_scene; - active_arc = nullptr; + arc_selected = arc_hovered_in_scene; open_context_menu = true; } else { node_selected = nullptr; - active_arc = nullptr; + arc_selected = nullptr; } } @@ -322,8 +333,60 @@ void draw(Graph& g) { g.n_idx.begin(), std::find(g.n_idx.begin(), g.n_idx.end(), idx)); std::swap(g.n_idx[swap_idx], g.n_idx.back()); g.n_idx.pop_back(); + + // delete arcs related to this + for (auto iter = g.a_idx.begin(); iter != g.a_idx.end();) { + const auto [color, from_to] = g.arcs[*iter]; + if (from_to[0] == &(node_selected->Pos) || + from_to[1] == &(node_selected->Pos)) { + g.a_idx.erase(iter); + } else { + ++iter; + } + } return m; }); + node_selected = nullptr; + } + } else if (arc_selected) { + ImGui::Text("Arc"); + ImGui::Separator(); + + if (ImGui::MenuItem("Delete")) { + MVC::push([&, ptr = arc_selected](Model&& m) { + const auto idx = std::distance( + g.arcs.begin(), + std::find_if(g.arcs.begin(), g.arcs.end(), + [=](const Arc& a) { return ptr == &a; })); + const auto swap_idx = std::distance( + g.a_idx.begin(), std::find(g.a_idx.begin(), g.a_idx.end(), idx)); + std::swap(g.a_idx[swap_idx], g.a_idx.back()); + g.a_idx.pop_back(); + return m; + }); + arc_selected = nullptr; + } + if (ImGui::BeginMenu("Change color")) { + for (const auto& arc : symmetri::Color::getColors()) { + if (ImGui::MenuItem(arc.second.c_str())) { + MVC::push([&, ptr = arc_selected](Model&& m) { + const auto idx = std::distance( + g.arcs.begin(), + std::find_if(g.arcs.begin(), g.arcs.end(), + [=](const Arc& a) { return ptr == &a; })); + const auto swap_idx = + std::distance(g.a_idx.begin(), + std::find(g.a_idx.begin(), g.a_idx.end(), idx)); + std::swap(g.a_idx[swap_idx], g.a_idx.back()); + g.a_idx.pop_back(); + g.a_idx.push_back(g.arcs.size()); + g.arcs.push_back({arc.first, ptr->from_to_pos}); + return m; + }); + arc_selected = nullptr; + } + } + ImGui::EndMenu(); } } else { ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; @@ -352,7 +415,7 @@ void draw(Graph& g) { MVC::push([&](Model&& m) { g.a_idx.push_back(g.arcs.size()); g.arcs.push_back( - Arc{symmetri::Color::Success, &from.Pos, &to.Pos}); + {symmetri::Color::Success, &from.Pos, &to.Pos}); return m; }); } From ced0213a1c8dc3340f8ff0cdceb901b6ec1f6231 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Wed, 10 Jan 2024 22:26:39 +0100 Subject: [PATCH 018/142] govis colors --- symmetri/gui/draw_graph.cpp | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 92d05e8..2a3292d 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -6,6 +6,7 @@ #include #include +#include "color.hpp" #include "drawable.h" #include "graph.hpp" #include "imgui.h" @@ -29,24 +30,17 @@ static const Arc* arc_hovered_in_scene = nullptr; static const float NODE_SLOT_RADIUS = 4.0f; static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); -inline ImU32 getColor(symmetri::Token token) { +ImU32 getColor(symmetri::Token token) { using namespace symmetri; - switch (token) { - case Color::Scheduled: - case Color::Started: - case Color::Deadlocked: - case Color::Canceled: - case Color::Paused: - case Color::Failed: - return IM_COL32(255, 0, 0, 128); - break; - case Color::Success: - return IM_COL32(0, 255, 0, 128); - break; - default: { - // create new color and lookup if it already exists. - return IM_COL32(255, 200, 0, 128); - } + static std::unordered_map color_table; + const auto ptr = color_table.find(token); + if (ptr != std::end(color_table)) { + return ptr->second; + } else { + const auto rgb = hsv_to_rgb(ratio(), 0.6, 0.95); + const auto color = IM_COL32(rgb[0], rgb[1], rgb[2], 128); + color_table.insert({token, color}); + return color; } }; @@ -149,7 +143,7 @@ void draw_nodes(Node& node) { auto select_color = &node == node_selected ? IM_COL32(255, 255, 0, opacity) : &node == node_hovered_in_list || &node == node_hovered_in_scene - ? IM_COL32(255, 255, 50, 100) + ? IM_COL32(0, 255, 0, opacity) : IM_COL32(100, 100, 100, opacity); if (node.id.chr() == 'P') { draw_list->AddCircleFilled(offset + Node::GetCenterPos(node.Pos, size), @@ -409,15 +403,21 @@ void draw(Graph& g) { if (ImGui::BeginMenu("Add arc")) { for (const auto& from : g.nodes) { if (ImGui::BeginMenu(from.name.c_str())) { + node_hovered_in_scene = &from; for (const auto& to : g.nodes) { if (from.id.chr() != to.id.chr() && - ImGui::MenuItem(to.name.c_str())) { - MVC::push([&](Model&& m) { - g.a_idx.push_back(g.arcs.size()); - g.arcs.push_back( - {symmetri::Color::Success, &from.Pos, &to.Pos}); - return m; - }); + ImGui::BeginMenu(to.name.c_str())) { + node_hovered_in_scene = &to; + for (const auto& color : symmetri::Color::getColors()) { + if (ImGui::MenuItem(color.second.c_str())) { + MVC::push([&](Model&& m) { + g.a_idx.push_back(g.arcs.size()); + g.arcs.push_back({color.first, &from.Pos, &to.Pos}); + return m; + }); + } + } + ImGui::EndMenu(); } } ImGui::EndMenu(); From a121983ae16a91cbd8dbd1c262e9069753ba168c Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Fri, 12 Jan 2024 13:22:21 +0100 Subject: [PATCH 019/142] git wips mooi --- symmetri/gui/CMakeLists.txt | 1 + symmetri/gui/color.hpp | 35 ++++++++ symmetri/gui/draw_graph.cpp | 157 +++++++++++++++++++++--------------- symmetri/gui/graph.hpp | 39 ++++++--- symmetri/gui/initialize.hpp | 6 +- symmetri/gui/main.mm | 4 - symmetri/gui/menu_bar.hpp | 3 +- symmetri/gui/redux.cpp | 14 ++++ symmetri/gui/redux.hpp | 20 +---- 9 files changed, 178 insertions(+), 101 deletions(-) create mode 100644 symmetri/gui/color.hpp create mode 100644 symmetri/gui/redux.cpp diff --git a/symmetri/gui/CMakeLists.txt b/symmetri/gui/CMakeLists.txt index 12de331..ecfd40f 100644 --- a/symmetri/gui/CMakeLists.txt +++ b/symmetri/gui/CMakeLists.txt @@ -23,6 +23,7 @@ include_directories( set(SOURCES main.mm + redux.cpp symbol.cpp draw_graph.cpp ) diff --git a/symmetri/gui/color.hpp b/symmetri/gui/color.hpp new file mode 100644 index 0000000..1700c23 --- /dev/null +++ b/symmetri/gui/color.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +// https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ +inline double ratio() { + const static double phi = (std::sqrt(5) + 1.0) / 2.0; + static double n = 0; + return std::fmod(++n * phi, 1.0); +} + +// https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/ +inline std::array hsv_to_rgb(float h, float s, float v) { + float H = h * 360.f; + float C = s * v; + float X = C * (1 - abs(fmod(H / 60.0, 2) - 1)); + float m = v - C; + float r, g, b; + if (H >= 0 && H < 60) { + r = C, g = X, b = 0; + } else if (H >= 60 && H < 120) { + r = X, g = C, b = 0; + } else if (H >= 120 && H < 180) { + r = 0, g = C, b = X; + } else if (H >= 180 && H < 240) { + r = 0, g = X, b = C; + } else if (H >= 240 && H < 300) { + r = X, g = 0, b = C; + } else { + r = C, g = 0, b = X; + } + int R = (r + m) * 255; + int G = (g + m) * 255; + int B = (b + m) * 255; + return {R, G, B}; +} diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 2a3292d..cb2338b 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -30,6 +30,17 @@ static const Arc* arc_hovered_in_scene = nullptr; static const float NODE_SLOT_RADIUS = 4.0f; static const ImVec2 NODE_WINDOW_PADDING(8.0f, 8.0f); +template +std::size_t GetIndexFromRef(std::vector const& vec, T const& item) { + T const* data = vec.data(); + + if (std::less{}(&item, data) || + std::greater_equal{}(&item, data + vec.size())) + throw std::out_of_range{"The given object is not part of the vector."}; + + return static_cast(&item - vec.data()); +}; + ImU32 getColor(symmetri::Token token) { using namespace symmetri; static std::unordered_map color_table; @@ -57,10 +68,11 @@ void draw_grid() { ImVec2(canvas_sz.x, y) + win_pos, GRID_COLOR); }; -void draw_arc(const Arc& arc) { - const auto& [color, from_to] = arc; - ImVec2 p1 = offset + Node::GetCenterPos(*from_to[0], size); - ImVec2 p2 = offset + Node::GetCenterPos(*from_to[1], size); +void draw_arc(const Arc& arc, const std::vector& nodes) { + const auto& [color, from_to_idx] = arc; + ImVec2 p1 = offset + Node::GetCenterPos(nodes[from_to_idx[0]].Pos, size); + ImVec2 p2 = offset + Node::GetCenterPos(nodes[from_to_idx[1]].Pos, size); + // use with: using namespace ImGui; const float max_distance = 2.f; @@ -137,7 +149,11 @@ void draw_nodes(Node& node) { arc_selected = nullptr; } if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - node.Pos = node.Pos + ImGui::GetIO().MouseDelta; + MVC::push( + [Pos = &node.Pos, d = ImGui::GetIO().MouseDelta](Model&& m) mutable { + *Pos += d; + return m; + }); } const int opacity = 255; auto select_color = @@ -176,39 +192,38 @@ void draw(Graph& g) { if (node_selected) { auto node = std::find_if(g.nodes.begin(), g.nodes.end(), - [=](const auto& n) { return n.id == node_selected->id; }); - ImGui::Text("Name"); - ImGui::SameLine(); - const auto id = std::string("##") + std::to_string(node->id.key()); - ImGui::PushItemWidth(-1); - ImGui::InputText(id.c_str(), node->name.data(), 30); - ImGui::PopItemWidth(); - static int priority = 1; - static int multiplicity = 1; - if (node->id.chr() == 'T') { - ImGui::Text("Priority"); + [=](const auto& n) { return &n == node_selected; }); + if (node != std::end(g.nodes)) { + ImGui::Text("Name"); ImGui::SameLine(); - ImGui::InputInt("##", &priority); - ImGui::Text("Weight"); - ImGui::SameLine(); - ImGui::InputInt("##", &multiplicity); + const auto id = std::string("##") + std::to_string(node->id.key()); + ImGui::PushItemWidth(-1); + ImGui::InputText(id.c_str(), node->name.data(), 30); + ImGui::PopItemWidth(); + static int priority = 1; + static int multiplicity = 1; + if (node_selected->id.chr() == 'T') { + ImGui::Text("Priority"); + ImGui::SameLine(); + ImGui::InputInt("##", &priority); + ImGui::Text("Weight"); + ImGui::SameLine(); + ImGui::InputInt("##", &multiplicity); + } } - } else if (arc_selected != nullptr) { - constexpr std::size_t offset = offsetof(Node, Pos); - const Node* from = reinterpret_cast( - reinterpret_cast(arc_selected->from_to_pos[0]) - offset); - const Node* to = reinterpret_cast( - reinterpret_cast(arc_selected->from_to_pos[1]) - offset); + } else if (arc_selected) { + const Node& from = g.nodes[arc_selected->from_to_pos_idx[0]]; + const Node& to = g.nodes[arc_selected->from_to_pos_idx[1]]; ImGui::Text("From:"); ImGui::SameLine(); ImGui::PushItemWidth(-1); - ImGui::Text("%s", from->name.data()); + ImGui::Text("%s", from.name.data()); ImGui::PopItemWidth(); ImGui::Text("To:"); ImGui::SameLine(); ImGui::PushItemWidth(-1); - ImGui::Text("%s", to->name.data()); + ImGui::Text("%s", to.name.data()); ImGui::PopItemWidth(); ImGui::Text("Color"); ImGui::SameLine(); @@ -266,6 +281,10 @@ void draw(Graph& g) { // Create our child canvas ImGui::Text("Hold middle mouse button to scroll (%.2f,%.2f)", scrolling.x, scrolling.y); + ImGui::SameLine(); + const auto io = ImGui::GetIO(); + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", + 1000.0f / io.Framerate, io.Framerate); ImGui::SameLine(ImGui::GetWindowWidth() - 340); ImGui::Checkbox("Show grid", &show_grid); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); @@ -285,8 +304,9 @@ void draw(Graph& g) { } // draw arcs - for (auto idx : g.a_idx) { - draw_arc(g.arcs[idx]); + int i = 0; + for (const auto& idx : g.a_idx) { + draw_arc(g.arcs[idx], g.nodes); } // draw places & transitions @@ -318,22 +338,23 @@ void draw(Graph& g) { ImGui::Text("Node '%s'", node_selected->name.c_str()); ImGui::Separator(); if (ImGui::MenuItem("Delete")) { - MVC::push([&](Model&& m) { + MVC::push([node_selected = node_selected](Model&& m) { const auto idx = std::distance( - g.nodes.begin(), - std::find_if(g.nodes.begin(), g.nodes.end(), + m.graph->nodes.begin(), + std::find_if(m.graph->nodes.begin(), m.graph->nodes.end(), [=](const Node& n) { return node_selected == &n; })); const auto swap_idx = std::distance( - g.n_idx.begin(), std::find(g.n_idx.begin(), g.n_idx.end(), idx)); - std::swap(g.n_idx[swap_idx], g.n_idx.back()); - g.n_idx.pop_back(); + m.graph->n_idx.begin(), + std::find(m.graph->n_idx.begin(), m.graph->n_idx.end(), idx)); + std::swap(m.graph->n_idx[swap_idx], m.graph->n_idx.back()); + m.graph->n_idx.pop_back(); // delete arcs related to this - for (auto iter = g.a_idx.begin(); iter != g.a_idx.end();) { - const auto [color, from_to] = g.arcs[*iter]; - if (from_to[0] == &(node_selected->Pos) || - from_to[1] == &(node_selected->Pos)) { - g.a_idx.erase(iter); + for (auto iter = m.graph->a_idx.begin(); + iter != m.graph->a_idx.end();) { + const auto [color, from_to_idx] = m.graph->arcs[*iter]; + if (from_to_idx[0] == idx || from_to_idx[1] == idx) { + m.graph->a_idx.erase(iter); } else { ++iter; } @@ -341,6 +362,10 @@ void draw(Graph& g) { return m; }); node_selected = nullptr; + // node_hovered_in_list = nullptr; + // node_hovered_in_scene = nullptr; + // arc_selected = nullptr; + // arc_hovered_in_scene = nullptr; } } else if (arc_selected) { ImGui::Text("Arc"); @@ -363,18 +388,18 @@ void draw(Graph& g) { if (ImGui::BeginMenu("Change color")) { for (const auto& arc : symmetri::Color::getColors()) { if (ImGui::MenuItem(arc.second.c_str())) { - MVC::push([&, ptr = arc_selected](Model&& m) { + MVC::push([color = arc.first, ptr = arc_selected](Model&& m) { const auto idx = std::distance( - g.arcs.begin(), - std::find_if(g.arcs.begin(), g.arcs.end(), + m.graph->arcs.begin(), + std::find_if(m.graph->arcs.begin(), m.graph->arcs.end(), [=](const Arc& a) { return ptr == &a; })); - const auto swap_idx = - std::distance(g.a_idx.begin(), - std::find(g.a_idx.begin(), g.a_idx.end(), idx)); - std::swap(g.a_idx[swap_idx], g.a_idx.back()); - g.a_idx.pop_back(); - g.a_idx.push_back(g.arcs.size()); - g.arcs.push_back({arc.first, ptr->from_to_pos}); + const auto swap_idx = std::distance( + m.graph->a_idx.begin(), + std::find(m.graph->a_idx.begin(), m.graph->a_idx.end(), idx)); + std::swap(m.graph->a_idx[swap_idx], m.graph->a_idx.back()); + m.graph->a_idx.pop_back(); + m.graph->a_idx.push_back(m.graph->arcs.size()); + m.graph->arcs.push_back({color, ptr->from_to_pos_idx}); return m; }); arc_selected = nullptr; @@ -385,36 +410,40 @@ void draw(Graph& g) { } else { ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; if (ImGui::MenuItem("Add place")) { - MVC::push([&, scene_pos](Model&& m) { - g.nodes.push_back( - Node{"New node", Symbol('P', g.nodes.size()), scene_pos}); - g.n_idx.push_back(g.nodes.size() - 1); + MVC::push([scene_pos](Model&& m) { + m.graph->n_idx.push_back(m.graph->nodes.size()); + m.graph->nodes.push_back( + Node{"New node", Symbol('P', m.graph->nodes.size()), scene_pos}); return m; }); } if (ImGui::MenuItem("Add transition")) { - MVC::push([&, scene_pos](Model&& m) { - g.nodes.push_back( - Node{"New node", Symbol('T', g.nodes.size()), scene_pos}); - g.n_idx.push_back(g.nodes.size() - 1); + MVC::push([scene_pos](Model&& m) { + m.graph->n_idx.push_back(m.graph->nodes.size()); + m.graph->nodes.push_back( + Node{"New node", Symbol('T', m.graph->nodes.size()), scene_pos}); return m; }); } if (ImGui::BeginMenu("Add arc")) { - for (const auto& from : g.nodes) { + for (const auto& from_idx : g.n_idx) { + const auto& from = g.nodes[from_idx]; if (ImGui::BeginMenu(from.name.c_str())) { node_hovered_in_scene = &from; - for (const auto& to : g.nodes) { + for (const auto& to_idx : g.n_idx) { + const auto& to = g.nodes[to_idx]; if (from.id.chr() != to.id.chr() && ImGui::BeginMenu(to.name.c_str())) { node_hovered_in_scene = &to; for (const auto& color : symmetri::Color::getColors()) { if (ImGui::MenuItem(color.second.c_str())) { - MVC::push([&](Model&& m) { - g.a_idx.push_back(g.arcs.size()); - g.arcs.push_back({color.first, &from.Pos, &to.Pos}); + MVC::push([arc = Arc{color.first, {from_idx, to_idx}}]( + Model&& m) { + m.graph->a_idx.push_back(m.graph->arcs.size()); + m.graph->arcs.push_back(arc); return m; }); + node_hovered_in_scene = nullptr; } } ImGui::EndMenu(); diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index 6e0b048..b91273a 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -40,16 +40,36 @@ struct Node { struct Arc { symmetri::Token color; - std::array from_to_pos; + std::array from_to_pos_idx; }; struct Graph { - std::vector arcs; - std::vector nodes; - std::vector a_idx, n_idx; + std::vector arcs = {}; + std::vector nodes = {}; + std::vector a_idx = {}; + std::vector n_idx = {}; + + void reset(const Graph& g) { + const auto offset_arcs = arcs.size(); + const auto offset_nodes = nodes.size(); + nodes.insert(nodes.end(), g.nodes.begin(), g.nodes.end()); + arcs.insert(arcs.end(), g.arcs.begin(), g.arcs.end()); + a_idx = g.a_idx; + n_idx = g.n_idx; + for (auto& idx : a_idx) { + idx += offset_arcs; + } + for (auto& idx : n_idx) { + idx += offset_nodes; + } + for (auto& arc : arcs) { + arc.from_to_pos_idx[0] += offset_nodes; + arc.from_to_pos_idx[1] += offset_nodes; + } + } }; -inline Graph createGraph(const symmetri::Net net) { +inline std::shared_ptr createGraph(const symmetri::Net net) { std::vector nodes; std::vector arcs; ogdf::Graph G; @@ -101,16 +121,14 @@ inline Graph createGraph(const symmetri::Net net) { const auto place_idx = toIndex( nodes, [=](const Node& n) { return s.first == n.name; }); G.newEdge(ogdf_nodes[place_idx], ogdf_nodes[transition_idx]); - arcs.push_back( - {s.second, {&(nodes[place_idx].Pos), &(nodes[transition_idx].Pos)}}); + arcs.push_back({s.second, {place_idx, transition_idx}}); } for (const auto& s : io.second) { const auto place_idx = toIndex( nodes, [=](const Node& n) { return s.first == n.name; }); G.newEdge(ogdf_nodes[transition_idx], ogdf_nodes[place_idx]); - arcs.push_back( - {s.second, {&(nodes[transition_idx].Pos), &(nodes[place_idx].Pos)}}); + arcs.push_back({s.second, {transition_idx, place_idx}}); } } @@ -135,5 +153,6 @@ inline Graph createGraph(const symmetri::Net net) { std::vector v(arcs.size()), w(nodes.size()); std::iota(v.begin(), v.end(), 0); std::iota(w.begin(), w.end(), 0); - return {std::move(arcs), std::move(nodes), std::move(v), std::move(w)}; + return std::make_shared( + Graph{std::move(arcs), std::move(nodes), std::move(v), std::move(w)}); } diff --git a/symmetri/gui/initialize.hpp b/symmetri/gui/initialize.hpp index 6b41686..adc6bdc 100644 --- a/symmetri/gui/initialize.hpp +++ b/symmetri/gui/initialize.hpp @@ -18,14 +18,12 @@ inline Model initializeModel(Model &&m) { const std::filesystem::path pn_file = m.active_file.value(); if (pn_file.extension() == std::string(".pnml")) { std::tie(net, marking) = symmetri::readPnml({pn_file}); - } else { std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); } - // current(m.documents).push_back(net); - // current(m.documents).push_back(marking); - current(m.documents).push_back({createGraph(net)}); + m.graph = std::make_shared(); + m.graph->reset(*createGraph(net)); // create a file browser instance ImGui::FileBrowser fileDialog; diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 358e4a3..96d7505 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -104,10 +104,6 @@ int main(int, char **) { [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; - if (ImGui::GetIO().Framerate > 61.f) { - std::this_thread::sleep_for( - std::chrono::milliseconds(200)); // drop to 5 FPS when invisible - } } } diff --git a/symmetri/gui/menu_bar.hpp b/symmetri/gui/menu_bar.hpp index 60ebf40..ae50bac 100644 --- a/symmetri/gui/menu_bar.hpp +++ b/symmetri/gui/menu_bar.hpp @@ -11,8 +11,7 @@ Reducer updateActiveFile(const std::filesystem::path &file) { return [=](Model &&m) { m.active_file = file; auto [net, marking] = symmetri::readPnml({file}); - m.documents.push_back({createGraph(net)}); - commit(m.documents); + m.graph->reset(*createGraph(net)); return m; }; } diff --git a/symmetri/gui/redux.cpp b/symmetri/gui/redux.cpp new file mode 100644 index 0000000..11d2524 --- /dev/null +++ b/symmetri/gui/redux.cpp @@ -0,0 +1,14 @@ +#include "redux.hpp" + +#include "graph.hpp" +void draw(Model& m) { + for (auto& drawable : m.statics) { + draw(drawable); + } + ImGui::SetNextWindowSize( + ImVec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y)); + ImGui::SetNextWindowPos(ImVec2(0, m.menu_height)); + ImGui::Begin("test", NULL, ImGuiWindowFlags_NoTitleBar); + draw(*m.graph); + ImGui::End(); +} diff --git a/symmetri/gui/redux.hpp b/symmetri/gui/redux.hpp index cbc2bff..e71be29 100644 --- a/symmetri/gui/redux.hpp +++ b/symmetri/gui/redux.hpp @@ -12,6 +12,7 @@ #include "blockingconcurrentqueue.h" #include "drawable.h" +#include "graph.hpp" #include "imgui.h" using View = std::vector; @@ -33,29 +34,14 @@ inline View& current(History& x) { struct ViewModel {}; struct Model { + std::shared_ptr graph; std::filesystem::path working_dir; std::optional active_file; int menu_height = 0; View statics; - History documents = History{1}; }; -inline void draw(Model& m) { - for (auto& drawable : m.statics) { - draw(drawable); - } - - ImGui::SetNextWindowSize( - ImVec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y)); - - ImGui::SetNextWindowPos(ImVec2(0, m.menu_height)); - - ImGui::Begin("test", NULL, ImGuiWindowFlags_NoTitleBar); - for (auto& drawable : current(m.documents)) { - draw(drawable); - } - ImGui::End(); -} +void draw(Model& m); using Reducer = std::function; From 9a11b21a9b33970c6168e78fda5f58a6bd261f9a Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Fri, 12 Jan 2024 19:46:36 +0100 Subject: [PATCH 020/142] rxcpp! --- symmetri/gui/CMakeLists.txt | 3 +- symmetri/gui/main.mm | 4 +- symmetri/gui/rpp/CMakeLists.txt | 41 +++ symmetri/gui/rpp/rpp/defs.hpp | 26 ++ symmetri/gui/rpp/rpp/fwd.hpp | 25 ++ symmetri/gui/rpp/rpp/memory_model.hpp | 20 ++ symmetri/gui/rpp/rpp/observables.hpp | 23 ++ .../rpp/observables/blocking_observable.hpp | 75 ++++ .../observables/connectable_observable.hpp | 109 ++++++ .../gui/rpp/rpp/observables/constraints.hpp | 42 +++ .../observables/details/member_overload.hpp | 20 ++ .../rpp/observables/dynamic_observable.hpp | 120 +++++++ symmetri/gui/rpp/rpp/observables/fwd.hpp | 75 ++++ .../rpp/observables/grouped_observable.hpp | 33 ++ .../rpp/observables/interface_observable.hpp | 122 +++++++ .../rpp/observables/specific_observable.hpp | 129 +++++++ symmetri/gui/rpp/rpp/observers.hpp | 20 ++ .../gui/rpp/rpp/observers/constraints.hpp | 57 +++ .../rpp/rpp/observers/dynamic_observer.hpp | 158 +++++++++ symmetri/gui/rpp/rpp/observers/fwd.hpp | 43 +++ .../rpp/rpp/observers/specific_observer.hpp | 129 +++++++ .../gui/rpp/rpp/observers/state_observer.hpp | 96 ++++++ symmetri/gui/rpp/rpp/operators.hpp | 123 +++++++ symmetri/gui/rpp/rpp/operators/buffer.hpp | 86 +++++ .../gui/rpp/rpp/operators/combine_latest.hpp | 170 +++++++++ symmetri/gui/rpp/rpp/operators/concat.hpp | 140 ++++++++ symmetri/gui/rpp/rpp/operators/debounce.hpp | 159 +++++++++ symmetri/gui/rpp/rpp/operators/delay.hpp | 168 +++++++++ .../operators/details/early_unsubscribe.hpp | 42 +++ .../details/serialized_subscriber.hpp | 75 ++++ .../details/subscriber_with_state.hpp | 49 +++ .../rpp/operators/distinct_until_changed.hpp | 65 ++++ symmetri/gui/rpp/rpp/operators/do.hpp | 49 +++ symmetri/gui/rpp/rpp/operators/filter.hpp | 46 +++ symmetri/gui/rpp/rpp/operators/first.hpp | 54 +++ symmetri/gui/rpp/rpp/operators/flat_map.hpp | 26 ++ symmetri/gui/rpp/rpp/operators/fwd.hpp | 49 +++ symmetri/gui/rpp/rpp/operators/fwd/buffer.hpp | 93 +++++ .../rpp/rpp/operators/fwd/combine_latest.hpp | 183 ++++++++++ symmetri/gui/rpp/rpp/operators/fwd/concat.hpp | 158 +++++++++ .../gui/rpp/rpp/operators/fwd/debounce.hpp | 88 +++++ symmetri/gui/rpp/rpp/operators/fwd/delay.hpp | 102 ++++++ .../operators/fwd/distinct_until_changed.hpp | 85 +++++ symmetri/gui/rpp/rpp/operators/fwd/do.hpp | 220 ++++++++++++ symmetri/gui/rpp/rpp/operators/fwd/filter.hpp | 78 +++++ symmetri/gui/rpp/rpp/operators/fwd/first.hpp | 84 +++++ .../gui/rpp/rpp/operators/fwd/flat_map.hpp | 77 +++++ .../gui/rpp/rpp/operators/fwd/group_by.hpp | 106 ++++++ symmetri/gui/rpp/rpp/operators/fwd/last.hpp | 82 +++++ symmetri/gui/rpp/rpp/operators/fwd/lift.hpp | 210 +++++++++++ symmetri/gui/rpp/rpp/operators/fwd/map.hpp | 86 +++++ symmetri/gui/rpp/rpp/operators/fwd/merge.hpp | 159 +++++++++ .../gui/rpp/rpp/operators/fwd/multicast.hpp | 63 ++++ .../gui/rpp/rpp/operators/fwd/observe_on.hpp | 79 +++++ .../operators/fwd/on_error_resume_next.hpp | 107 ++++++ .../gui/rpp/rpp/operators/fwd/publish.hpp | 58 ++++ symmetri/gui/rpp/rpp/operators/fwd/reduce.hpp | 326 ++++++++++++++++++ .../gui/rpp/rpp/operators/fwd/ref_count.hpp | 56 +++ symmetri/gui/rpp/rpp/operators/fwd/repeat.hpp | 120 +++++++ symmetri/gui/rpp/rpp/operators/fwd/sample.hpp | 88 +++++ symmetri/gui/rpp/rpp/operators/fwd/scan.hpp | 95 +++++ symmetri/gui/rpp/rpp/operators/fwd/skip.hpp | 76 ++++ .../gui/rpp/rpp/operators/fwd/start_with.hpp | 128 +++++++ .../gui/rpp/rpp/operators/fwd/subscribe.hpp | 125 +++++++ .../rpp/rpp/operators/fwd/subscribe_on.hpp | 62 ++++ .../gui/rpp/rpp/operators/fwd/switch_map.hpp | 72 ++++ .../rpp/rpp/operators/fwd/switch_on_next.hpp | 86 +++++ symmetri/gui/rpp/rpp/operators/fwd/take.hpp | 83 +++++ .../gui/rpp/rpp/operators/fwd/take_last.hpp | 76 ++++ .../gui/rpp/rpp/operators/fwd/take_until.hpp | 105 ++++++ .../gui/rpp/rpp/operators/fwd/take_while.hpp | 78 +++++ .../gui/rpp/rpp/operators/fwd/timeout.hpp | 123 +++++++ symmetri/gui/rpp/rpp/operators/fwd/window.hpp | 87 +++++ .../rpp/operators/fwd/with_latest_from.hpp | 150 ++++++++ symmetri/gui/rpp/rpp/operators/group_by.hpp | 161 +++++++++ symmetri/gui/rpp/rpp/operators/last.hpp | 68 ++++ symmetri/gui/rpp/rpp/operators/lift.hpp | 131 +++++++ symmetri/gui/rpp/rpp/operators/map.hpp | 48 +++ symmetri/gui/rpp/rpp/operators/merge.hpp | 99 ++++++ symmetri/gui/rpp/rpp/operators/multicast.hpp | 28 ++ symmetri/gui/rpp/rpp/operators/observe_on.hpp | 18 + .../rpp/operators/on_error_resume_next.hpp | 57 +++ symmetri/gui/rpp/rpp/operators/publish.hpp | 26 ++ symmetri/gui/rpp/rpp/operators/reduce.hpp | 181 ++++++++++ symmetri/gui/rpp/rpp/operators/ref_count.hpp | 77 +++++ symmetri/gui/rpp/rpp/operators/repeat.hpp | 118 +++++++ symmetri/gui/rpp/rpp/operators/sample.hpp | 99 ++++++ symmetri/gui/rpp/rpp/operators/scan.hpp | 52 +++ symmetri/gui/rpp/rpp/operators/skip.hpp | 51 +++ symmetri/gui/rpp/rpp/operators/start_with.hpp | 39 +++ .../gui/rpp/rpp/operators/subscribe_on.hpp | 38 ++ symmetri/gui/rpp/rpp/operators/switch_map.hpp | 27 ++ .../gui/rpp/rpp/operators/switch_on_next.hpp | 107 ++++++ symmetri/gui/rpp/rpp/operators/take.hpp | 53 +++ symmetri/gui/rpp/rpp/operators/take_last.hpp | 83 +++++ symmetri/gui/rpp/rpp/operators/take_until.hpp | 87 +++++ symmetri/gui/rpp/rpp/operators/take_while.hpp | 47 +++ symmetri/gui/rpp/rpp/operators/timeout.hpp | 129 +++++++ symmetri/gui/rpp/rpp/operators/window.hpp | 98 ++++++ .../rpp/rpp/operators/with_latest_from.hpp | 145 ++++++++ symmetri/gui/rpp/rpp/rpp.hpp | 19 + symmetri/gui/rpp/rpp/schedulers.hpp | 22 ++ .../gui/rpp/rpp/schedulers/constraints.hpp | 40 +++ .../schedulers/details/queue_worker_state.hpp | 133 +++++++ .../gui/rpp/rpp/schedulers/details/utils.hpp | 47 +++ .../gui/rpp/rpp/schedulers/details/worker.hpp | 79 +++++ symmetri/gui/rpp/rpp/schedulers/fwd.hpp | 33 ++ .../rpp/schedulers/immediate_scheduler.hpp | 48 +++ .../rpp/schedulers/new_thread_scheduler.hpp | 126 +++++++ .../rpp/rpp/schedulers/run_loop_scheduler.hpp | 121 +++++++ .../rpp/schedulers/trampoline_scheduler.hpp | 173 ++++++++++ symmetri/gui/rpp/rpp/sources.hpp | 26 ++ symmetri/gui/rpp/rpp/sources/create.hpp | 81 +++++ symmetri/gui/rpp/rpp/sources/empty.hpp | 40 +++ symmetri/gui/rpp/rpp/sources/error.hpp | 41 +++ symmetri/gui/rpp/rpp/sources/from.hpp | 260 ++++++++++++++ symmetri/gui/rpp/rpp/sources/fwd.hpp | 104 ++++++ symmetri/gui/rpp/rpp/sources/interval.hpp | 97 ++++++ symmetri/gui/rpp/rpp/sources/just.hpp | 13 + symmetri/gui/rpp/rpp/sources/never.hpp | 39 +++ symmetri/gui/rpp/rpp/subjects.hpp | 21 ++ .../gui/rpp/rpp/subjects/behavior_subject.hpp | 129 +++++++ symmetri/gui/rpp/rpp/subjects/constraints.hpp | 24 ++ .../rpp/rpp/subjects/details/base_subject.hpp | 40 +++ .../rpp/subjects/details/subject_state.hpp | 151 ++++++++ symmetri/gui/rpp/rpp/subjects/fwd.hpp | 36 ++ .../gui/rpp/rpp/subjects/publish_subject.hpp | 80 +++++ symmetri/gui/rpp/rpp/subjects/type_traits.hpp | 28 ++ symmetri/gui/rpp/rpp/subscribers.hpp | 14 + .../gui/rpp/rpp/subscribers/constraints.hpp | 44 +++ .../subscribers/details/subscriber_base.hpp | 53 +++ .../rpp/subscribers/dynamic_subscriber.hpp | 52 +++ symmetri/gui/rpp/rpp/subscribers/fwd.hpp | 29 ++ .../rpp/subscribers/specific_subscriber.hpp | 121 +++++++ .../subscriptions/callback_subscription.hpp | 44 +++ .../subscriptions/composite_subscription.hpp | 166 +++++++++ .../gui/rpp/rpp/subscriptions/constraints.hpp | 20 ++ .../details/subscription_state.hpp | 46 +++ symmetri/gui/rpp/rpp/subscriptions/fwd.hpp | 18 + .../rpp/subscriptions/subscription_base.hpp | 70 ++++ .../rpp/subscriptions/subscription_guard.hpp | 44 +++ symmetri/gui/rpp/rpp/utils/constraints.hpp | 33 ++ symmetri/gui/rpp/rpp/utils/exceptions.hpp | 23 ++ .../gui/rpp/rpp/utils/function_traits.hpp | 74 ++++ symmetri/gui/rpp/rpp/utils/functors.hpp | 91 +++++ .../rpp/rpp/utils/operator_declaration.hpp | 30 ++ symmetri/gui/rpp/rpp/utils/overloaded.hpp | 21 ++ symmetri/gui/rpp/rpp/utils/spinlock.hpp | 36 ++ symmetri/gui/rpp/rpp/utils/utilities.hpp | 124 +++++++ 149 files changed, 11994 insertions(+), 2 deletions(-) create mode 100644 symmetri/gui/rpp/CMakeLists.txt create mode 100644 symmetri/gui/rpp/rpp/defs.hpp create mode 100644 symmetri/gui/rpp/rpp/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/memory_model.hpp create mode 100644 symmetri/gui/rpp/rpp/observables.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/blocking_observable.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/connectable_observable.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/constraints.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/details/member_overload.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/grouped_observable.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/interface_observable.hpp create mode 100644 symmetri/gui/rpp/rpp/observables/specific_observable.hpp create mode 100644 symmetri/gui/rpp/rpp/observers.hpp create mode 100644 symmetri/gui/rpp/rpp/observers/constraints.hpp create mode 100644 symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp create mode 100644 symmetri/gui/rpp/rpp/observers/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/observers/specific_observer.hpp create mode 100644 symmetri/gui/rpp/rpp/observers/state_observer.hpp create mode 100644 symmetri/gui/rpp/rpp/operators.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/buffer.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/combine_latest.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/concat.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/debounce.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/delay.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/do.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/filter.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/first.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/flat_map.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/buffer.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/combine_latest.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/concat.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/debounce.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/delay.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/distinct_until_changed.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/do.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/filter.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/first.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/flat_map.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/group_by.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/last.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/lift.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/map.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/merge.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/multicast.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/observe_on.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/on_error_resume_next.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/publish.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/reduce.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/ref_count.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/repeat.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/sample.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/scan.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/skip.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/start_with.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/subscribe.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/subscribe_on.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/switch_map.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/switch_on_next.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take_last.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take_until.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take_while.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/timeout.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/window.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/fwd/with_latest_from.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/group_by.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/last.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/lift.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/map.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/merge.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/multicast.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/observe_on.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/on_error_resume_next.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/publish.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/reduce.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/ref_count.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/repeat.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/sample.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/scan.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/skip.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/start_with.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/subscribe_on.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/switch_map.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/switch_on_next.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/take.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/take_last.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/take_until.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/take_while.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/timeout.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/window.hpp create mode 100644 symmetri/gui/rpp/rpp/operators/with_latest_from.hpp create mode 100644 symmetri/gui/rpp/rpp/rpp.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/constraints.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/details/queue_worker_state.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/details/utils.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/details/worker.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/immediate_scheduler.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/new_thread_scheduler.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/run_loop_scheduler.hpp create mode 100644 symmetri/gui/rpp/rpp/schedulers/trampoline_scheduler.hpp create mode 100644 symmetri/gui/rpp/rpp/sources.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/create.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/empty.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/error.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/from.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/interval.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/just.hpp create mode 100644 symmetri/gui/rpp/rpp/sources/never.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects/behavior_subject.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects/constraints.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects/details/base_subject.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects/details/subject_state.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects/publish_subject.hpp create mode 100644 symmetri/gui/rpp/rpp/subjects/type_traits.hpp create mode 100644 symmetri/gui/rpp/rpp/subscribers.hpp create mode 100644 symmetri/gui/rpp/rpp/subscribers/constraints.hpp create mode 100644 symmetri/gui/rpp/rpp/subscribers/details/subscriber_base.hpp create mode 100644 symmetri/gui/rpp/rpp/subscribers/dynamic_subscriber.hpp create mode 100644 symmetri/gui/rpp/rpp/subscribers/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/subscribers/specific_subscriber.hpp create mode 100644 symmetri/gui/rpp/rpp/subscriptions/callback_subscription.hpp create mode 100644 symmetri/gui/rpp/rpp/subscriptions/composite_subscription.hpp create mode 100644 symmetri/gui/rpp/rpp/subscriptions/constraints.hpp create mode 100644 symmetri/gui/rpp/rpp/subscriptions/details/subscription_state.hpp create mode 100644 symmetri/gui/rpp/rpp/subscriptions/fwd.hpp create mode 100644 symmetri/gui/rpp/rpp/subscriptions/subscription_base.hpp create mode 100644 symmetri/gui/rpp/rpp/subscriptions/subscription_guard.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/constraints.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/exceptions.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/function_traits.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/functors.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/operator_declaration.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/overloaded.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/spinlock.hpp create mode 100644 symmetri/gui/rpp/rpp/utils/utilities.hpp diff --git a/symmetri/gui/CMakeLists.txt b/symmetri/gui/CMakeLists.txt index ecfd40f..cf04d72 100644 --- a/symmetri/gui/CMakeLists.txt +++ b/symmetri/gui/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.26) project(Farbart) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) find_package(glfw3 REQUIRED) @@ -13,6 +13,7 @@ elseif(TSAN_BUILD AND NOT ASAN_BUILD) endif() include_directories( + rpp imgui imgui/backends ../externals diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 96d7505..2318749 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -4,6 +4,8 @@ #include "imgui_impl_glfw.h" #include "imgui_impl_metal.h" #include "initialize.hpp" +#include "rpp/rpp.hpp" + #define GLFW_INCLUDE_NONE #define GLFW_EXPOSE_NATIVE_COCOA #include @@ -56,12 +58,12 @@ int main(int, char **) { MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor new]; float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; - // Main loop while (!glfwWindowShouldClose(window)) { while (auto v = MVC::dequeue()) { if (v.has_value()) { MVC::update(v.value()); + // dispatcher.on_next(v.value()); } // should happen on a different thread? } diff --git a/symmetri/gui/rpp/CMakeLists.txt b/symmetri/gui/rpp/CMakeLists.txt new file mode 100644 index 0000000..ab7a043 --- /dev/null +++ b/symmetri/gui/rpp/CMakeLists.txt @@ -0,0 +1,41 @@ +# ReactivePlusPlus library +# +# Copyright Aleksey Loginov 2022 - present. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) +# +# Project home: https://github.com/victimsnino/ReactivePlusPlus +# + +file(GLOB_RECURSE FILES "*.hpp") + +if(${CMAKE_VERSION} VERSION_LESS "3.19.0") + add_library(rpp INTERFACE) +else() + add_library(rpp INTERFACE ${FILES}) +endif() + +add_library(RPP::rpp ALIAS rpp) + +target_include_directories(rpp ${RPP_WARNING_GUARD} + INTERFACE + "$" + # "$" +) + +target_link_libraries(rpp INTERFACE Threads::Threads) +target_compile_features(rpp INTERFACE cxx_std_20) + +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(rpp INTERFACE -fsized-deallocation) +endif() + +foreach(FILE ${FILES}) + get_filename_component(PARENT_DIR "${FILE}" PATH) + file(RELATIVE_PATH REL_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rpp" ${PARENT_DIR}) + set(REL_PARENT_DIR "Header Files\\${REL_PARENT_DIR}") + + string(REPLACE "/" "\\" GROUP ${REL_PARENT_DIR}) + source_group("${GROUP}" FILES "${FILE}") +endforeach() diff --git a/symmetri/gui/rpp/rpp/defs.hpp b/symmetri/gui/rpp/rpp/defs.hpp new file mode 100644 index 0000000..ed313e0 --- /dev/null +++ b/symmetri/gui/rpp/rpp/defs.hpp @@ -0,0 +1,26 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +// MSVC has bad support for the empty base classes, but provides specificator to +// enable full support. GCC/Clang works as expected +#if defined(_MSC_VER) && _MSC_FULL_VER >= 190023918 +#define RPP_EMPTY_BASES __declspec(empty_bases) +#else +#define RPP_EMPTY_BASES +#endif + +// MSVC has bad support for the no_unique_address from the box NOW, but provides +// specificator to enable full support. GCC/Clang works as expected +#if defined(_MSC_VER) && _MSC_FULL_VER >= 192829913 +#define RPP_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]] +#else +#define RPP_NO_UNIQUE_ADDRESS [[no_unique_address]] +#endif diff --git a/symmetri/gui/rpp/rpp/fwd.hpp b/symmetri/gui/rpp/rpp/fwd.hpp new file mode 100644 index 0000000..81c69ee --- /dev/null +++ b/symmetri/gui/rpp/rpp/fwd.hpp @@ -0,0 +1,25 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup rpp RPP + * \brief Rpp is Reactive extension for C++20 + */ + +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/memory_model.hpp b/symmetri/gui/rpp/rpp/memory_model.hpp new file mode 100644 index 0000000..d02dc55 --- /dev/null +++ b/symmetri/gui/rpp/rpp/memory_model.hpp @@ -0,0 +1,20 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +namespace rpp { +enum class memory_model { + // copy and move everywhere when needed + use_stack, + // make shared_ptr once and avoid any future copies/moves + use_shared, +}; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables.hpp b/symmetri/gui/rpp/rpp/observables.hpp new file mode 100644 index 0000000..98f4971 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables.hpp @@ -0,0 +1,23 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup observables Observables + * \brief Observable is the source of any Reactive Stream. Observable provides + * ability to subscribe observer on some events. \see + * https://reactivex.io/documentation/observable.html \ingroup rpp + */ + +#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/observables/blocking_observable.hpp b/symmetri/gui/rpp/rpp/observables/blocking_observable.hpp new file mode 100644 index 0000000..4b3cb92 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/blocking_observable.hpp @@ -0,0 +1,75 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include // RPP_EMPTY_BASES +#include // OriginalObservable type +#include // overload operators +#include // wrap subscribers +#include // create_subscriber_with_state +#include // forwarding of member_overaloads +#include // create subscriber + +namespace rpp { +/** + * \brief blocking alternative of observable: provides interface where each + * function do blocking subscribe on original observable (waits till + * on_completed and provides value) \tparam Type type of values emitted by this + * observable \tparam OriginalObservable original observable wrapped by this + * observable \ingroup observables + */ +template OriginalObservable> +class RPP_EMPTY_BASES blocking_observable final + : public details::member_overload< + Type, blocking_observable, + details::subscribe_tag> { + public: + blocking_observable(const OriginalObservable& original) + : m_original{original} {} + + blocking_observable(OriginalObservable&& original) + : m_original{std::move(original)} {} + + friend struct details::member_overload< + Type, blocking_observable, + details::subscribe_tag>; + + private: + template TSub> + void subscribe_impl(TSub&& subscriber) const noexcept { + if (!subscriber.is_subscribed()) return; + + std::promise is_success{}; + const auto future = is_success.get_future(); + auto sub = subscriber.get_subscription(); + m_original.subscribe(create_subscriber_with_state( + std::move(sub), utils::forwarding_on_next{}, + [&](const std::exception_ptr& err, const auto& sub) { + sub.on_error(err); + is_success.set_value(false); + }, + [&](const auto& sub) { + sub.on_completed(); + is_success.set_value(true); + }, + std::forward(subscriber))); + future.wait(); + } + + private: + OriginalObservable m_original; +}; + +template +blocking_observable(const TObs&) + -> blocking_observable, TObs>; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/connectable_observable.hpp b/symmetri/gui/rpp/rpp/observables/connectable_observable.hpp new file mode 100644 index 0000000..7275ce7 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/connectable_observable.hpp @@ -0,0 +1,109 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include // RPP_EMPTY_BASES +#include // OriginalObservable type +#include // create_subscriber_with_state +#include // include forwarding for member_overload +#include // type of subject used +#include // deduce observable type by subject type +#include // lifetime + +namespace rpp { +/** + * \brief connectable alternative of observable: extends interface with extra + * functionality. Common subscription will subscribe on underlying subject, but + * connect/ref_count will initiate subscription on original observable \tparam + * Type type of values emitted by this observable \tparam OriginalObservable + * original observable wrapped by this observable \see + * https://reactivex.io/documentation/operators/publish.html \ingroup + * observables + */ +template Subject, + constraint::observable_of_type OriginalObservable> +class RPP_EMPTY_BASES connectable_observable + : public decltype(std::declval().get_observable()), + public details::member_overload< + Type, connectable_observable, + details::ref_count_tag> { + using base = decltype(std::declval().get_observable()); + + public: + connectable_observable(const OriginalObservable& original_observable, + const Subject& subject = Subject{}) + : base{subject.get_observable()}, + m_original_observable{original_observable}, + m_state{std::make_shared(subject)} {} + + connectable_observable(OriginalObservable&& original_observable, + const Subject& subject = Subject{}) + : base{subject.get_observable()}, + m_original_observable{std::move(original_observable)}, + m_state{std::make_shared(subject)} {} + + composite_subscription connect(const composite_subscription& subscription = + composite_subscription{}) const { + auto subscriber = m_state->subject.get_subscriber(); + const auto& subscriber_subscription = subscriber.get_subscription(); + + { + std::lock_guard lock(m_state->mutex); + + if (!m_state->sub.is_empty()) return subscription; + + subscriber_subscription.add(subscription); + m_state->sub = subscription; + } + + subscription.add([state = std::weak_ptr{m_state}] { + if (const auto locked = state.lock()) { + auto current_sub = composite_subscription::empty(); + { + std::lock_guard lock(locked->mutex); + std::swap(current_sub, locked->sub); + } + current_sub.unsubscribe(); + locked->subject.get_subscriber().get_subscription().remove(current_sub); + } + }); + + m_original_observable.subscribe(create_subscriber_with_state( + m_state->sub, utils::forwarding_on_next{}, utils::forwarding_on_error{}, + utils::forwarding_on_completed{}, subscriber.get_observer(), + // capture state to be sure that state is alive while ANY subscriber is + // alive + m_state)); + + return subscription; + } + + private: + OriginalObservable m_original_observable; + struct state_t { + state_t(const Subject& subj) : subject{subj} {} + + Subject subject; + std::mutex mutex{}; + composite_subscription sub = composite_subscription::empty(); + }; + + std::shared_ptr m_state{}; +}; + +template +connectable_observable(const OriginalObservable&, const Subject&) + -> connectable_observable, + Subject, OriginalObservable>; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/constraints.hpp b/symmetri/gui/rpp/rpp/observables/constraints.hpp new file mode 100644 index 0000000..0e7a39c --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/constraints.hpp @@ -0,0 +1,42 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::constraint { +template +concept observable = + std::derived_from, details::observable_tag>; +} + +namespace rpp::utils { +namespace details { +template +struct extract_observable_type { + template + static TT deduce(const rpp::details::typed_observable_tag&); + + using type = decltype(deduce(std::declval>())); +}; +} // namespace details + +template +using extract_observable_type_t = + typename details::extract_observable_type::type; +} // namespace rpp::utils + +namespace rpp::constraint { +template +concept observable_of_type = + observable && std::same_as, Type>; +} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/observables/details/member_overload.hpp b/symmetri/gui/rpp/rpp/observables/details/member_overload.hpp new file mode 100644 index 0000000..51d4fb8 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/details/member_overload.hpp @@ -0,0 +1,20 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +template +struct member_overload; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp b/symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp new file mode 100644 index 0000000..a9315f6 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp @@ -0,0 +1,120 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include // base +#include // for header include + +IMPLEMENTATION_FILE(dynamic_observable_tag); + +namespace rpp::details { +template +class dynamic_observable_state { + public: + template TObs> + dynamic_observable_state(TObs&& obs) + : m_impl{ + std::make_shared>>( + std::forward(obs))} {} + + template TOnSub> + requires( + !constraint::decayed_same_as>) + dynamic_observable_state(TOnSub&& on_sub) + : m_impl{std::make_shared>>>( + std::forward(on_sub))} {} + + dynamic_observable_state(const dynamic_observable_state& other) = default; + dynamic_observable_state(dynamic_observable_state&& other) noexcept = default; + dynamic_observable_state& operator=(const dynamic_observable_state& other) = + default; + dynamic_observable_state& operator=( + dynamic_observable_state&& other) noexcept = default; + + composite_subscription operator()( + const dynamic_subscriber& subscriber) const { + return (*m_impl)(subscriber); + } + + private: + struct interface_dynamic_observable_state_impl { + virtual ~interface_dynamic_observable_state_impl() = default; + + virtual composite_subscription operator()( + const dynamic_subscriber& subscriber) const = 0; + }; + + template + class dynamic_observable_state_impl final + : public interface_dynamic_observable_state_impl { + public: + dynamic_observable_state_impl(TObs&& observable) + : m_observable{std::move(observable)} {} + + dynamic_observable_state_impl(const TObs& observable) + : m_observable{observable} {} + + composite_subscription operator()( + const dynamic_subscriber& subscriber) const override { + return m_observable.subscribe(subscriber); + } + + private: + RPP_NO_UNIQUE_ADDRESS TObs m_observable{}; + }; + + std::shared_ptr m_impl{}; +}; +} // namespace rpp::details + +namespace rpp { +/** + * \brief Type-less observable (or partially untyped) that has the notion of + * Type but hides the notion of on_subscribe for C++ compiler. + * + * \details This is a C++ technique called type-erasure. Multiple instances of + * the observable that may have different upstream graphs are considered + * homogeneous. i.e. They can be stored in the same container, e.g. std::vector. + * As a result, it uses heap to store on_subscribe and hide its type. + * + * \param Type is the value type. Observable of type means this source could + * emit a sequence of items of that "Type". \ingroup observables + */ +template +class dynamic_observable + : public specific_observable> { + public: + using base = + specific_observable>; + using base::base; + + explicit dynamic_observable( + constraint::on_subscribe_fn auto&& on_subscribe) + : base{std::forward(on_subscribe)} {} + + template TObs> + requires(!std::is_same_v, dynamic_observable>) + dynamic_observable(TObs&& observable) + : base{std::forward(observable)} {} +}; + +template +dynamic_observable(TObs obs) + -> dynamic_observable>; + +template +dynamic_observable(OnSub on_subscribe) -> dynamic_observable< + utils::extract_subscriber_type_t>>; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/fwd.hpp b/symmetri/gui/rpp/rpp/observables/fwd.hpp new file mode 100644 index 0000000..f34af68 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/fwd.hpp @@ -0,0 +1,75 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // on_subscribe_fn +#include // decayed type + +namespace rpp::details { +struct observable_tag {}; + +template +struct typed_observable_tag : public details::observable_tag {}; +struct dynamic_observable_tag; +} // namespace rpp::details + +namespace rpp::constraint { +template +concept on_subscribe_fn = + std::invocable, dynamic_subscriber>; +} // namespace rpp::constraint + +namespace rpp { + +/** + * \brief Type-full observable (or typed) that has the notion of Type and + * upstream observables for C++ compiler. e.g. observable> is different from observable>. + * + * \details This is a C++ technique about de-virtualization. To achieve + * polymorphic behavior, we could either go for function virtualization or + * function overload. However, virtualization is more expensive than function + * overload in both compile time and runtime. Therefore, we go for function + * overload. Actually, we use more advanced functor paradigm for better + * performance. As a result it has better performance comparing to + * rpp::dynamic_observable. Use it if possible. But it has worse usability due + * to OnSubscribeFn template parameter. + * + * \param Type is the value type. Observable of type means this source could + * emit a sequence of items of that "Type". \param OnSubscribeFn is the + * on_subscribe functor that is called when a subscriber subscribes to this + * observable. specific_observable stores OnSubscribeFn as member variable, so, + * it is stored on stack (instead of allocating it on heap). \ingroup + * observables + */ +template OnSubscribeFn> +class specific_observable; + +/** + * \brief Type-less observable (or partially untyped) that has the notion of + * Type but hides the notion of on_subscribe for C++ compiler. + * + * \details This is a C++ technique called type-erasure. Multiple instances of + * the observable that may have different upstream graphs are considered + * homogeneous. i.e. They can be stored in the same container, e.g. std::vector. + * As a result, it uses heap to store on_subscribe and hide its type. + * + * \param Type is the value type. Observable of type means this source could + * emit a sequence of items of that "Type". \ingroup observables + */ +template +class dynamic_observable; + +template OnSubscribeFn> +class grouped_observable; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/grouped_observable.hpp b/symmetri/gui/rpp/rpp/observables/grouped_observable.hpp new file mode 100644 index 0000000..9007143 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/grouped_observable.hpp @@ -0,0 +1,33 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include + +namespace rpp { +template OnSubscribeFn> +class grouped_observable final + : public specific_observable { + public: + grouped_observable(KeyType key, const OnSubscribeFn& on_subscribe) + : specific_observable{on_subscribe}, + m_key{std::move(key)} {} + + grouped_observable(KeyType key, OnSubscribeFn&& on_subscribe) + : specific_observable{std::move(on_subscribe)}, + m_key{std::move(key)} {} + + const KeyType& get_key() const { return m_key; } + + private: + KeyType m_key; +}; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/interface_observable.hpp b/symmetri/gui/rpp/rpp/observables/interface_observable.hpp new file mode 100644 index 0000000..245fc4f --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/interface_observable.hpp @@ -0,0 +1,122 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_EMPTY_BASES +#include // as_blocking +#include // own constraints +#include // own forwarding +#include // forwarding of member_overaloads +#include // decayed_invoke_result_t +#include + +namespace rpp::details { +template +concept op_fn = + constraint::observable>; +} // namespace rpp::details + +namespace rpp { +/** + * \brief Base part of observable. Mostly used to provide some interface + * functions used by all observables \tparam Type type provided by this + * observable \tparam SpecificObservable final type of observable inherited from + * this observable to successfully copy/move it + */ +template +struct RPP_EMPTY_BASES interface_observable + : public details::typed_observable_tag, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload, + details::member_overload { + public: + /** + * \brief The apply function to observable which returns observable of another + * type \tparam OperatorFn type of function which applies to this observable + * \return new specific_observable of NewType + * \ingroup operators + * + */ + template OperatorFn> + auto op(OperatorFn&& fn) const& { + return fn(CastThis()); + } + + template OperatorFn> + auto op(OperatorFn&& fn) && { + return fn(MoveThis()); + } + + /** + * \brief Converts existing observable to rpp::blocking_observable which has + * another interface and abilities + */ + auto as_blocking() const& { return blocking_observable{CastThis()}; } + + auto as_blocking() && { return blocking_observable{MoveThis()}; } + + private: + const SpecificObservable& CastThis() const { + return *static_cast(this); + } + + SpecificObservable&& MoveThis() { + return std::move(*static_cast(this)); + } +}; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/specific_observable.hpp b/symmetri/gui/rpp/rpp/observables/specific_observable.hpp new file mode 100644 index 0000000..fd466c8 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observables/specific_observable.hpp @@ -0,0 +1,129 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include // base_class +#include +#include +#include // for header include +#include // copy_assignable_callable +#include + +namespace rpp { +/** + * \brief Type-full observable (or typed) that has the notion of Type and + * upstream observables for C++ compiler. e.g. observable> is different from observable>. + * + * \details This is a C++ technique about de-virtualization. To achieve + * polymorphic behavior, we could either go for function virtualization or + * function overload. However, virtualization is more expensive than function + * overload in both compile time and runtime. Therefore, we go for function + * overload. Actually, we use more advanced functor paradigm for better + * performance. As a result it has better performance comparing to + * rpp::dynamic_observable. Use it if possible. But it has worse usability due + * to OnSubscribeFn template parameter. + * + * \param Type is the value type. Observable of type means this source could + * emit a sequence of items of that "Type". \param OnSubscribeFn is the + * on_subscribe functor that is called when a subscriber subscribes to this + * observable. specific_observable stores OnSubscribeFn as member variable, so, + * it is stored on stack (instead of allocating it on heap). \ingroup + * observables + */ +template OnSubscribeFn> +class specific_observable + : public interface_observable> { + public: + specific_observable(OnSubscribeFn&& on_subscribe) + : m_on_subscribe{std::move(on_subscribe)} {} + + specific_observable(const OnSubscribeFn& on_subscribe) + : m_on_subscribe{on_subscribe} {} + + specific_observable(const specific_observable& other) = default; + specific_observable(specific_observable&& other) noexcept = default; + specific_observable& operator=(const specific_observable& other) = default; + specific_observable& operator=(specific_observable&& other) noexcept = + default; + /** + * \brief Converts rpp::specific_observable to rpp::dynamic_observable via + * type-erasure mechanism. + */ + template + [[nodiscard]] auto as_dynamic() const& + requires details::is_header_included + { + return rpp::dynamic_observable{*this}; + } + template + [[nodiscard]] auto as_dynamic() && + requires details::is_header_included + { + return rpp::dynamic_observable{std::move(*this)}; + } + + friend struct details::member_overload< + Type, specific_observable, details::subscribe_tag>; + + private: + // used by rpp::details::member_overload, rpp::details::subscribe_tag>; + template TSub> + composite_subscription subscribe_impl(const TSub& subscriber) const { + if (subscriber.is_subscribed()) actual_subscribe(subscriber); + + return subscriber.get_subscription(); + } + + template TSub> + void actual_subscribe(const TSub& subscriber) const { + // take ownership over current thread as early as possible to delay all next + // "current_thread" schedulings. For example, scheduling of emissions from + // "just" to delay it till whole chain is subscribed and ready to listened + // emissions For example, if we have + // rpp::source::just(rpp::schedulers::current_thread{}, + // 1,2).combine_latest(rpp::source::just(rpp::schedulers::current_thread{}, + // 1,2)) + // + // then we expect to see emissions like (1,1) (2,1) (2,2) instead of (2,1) + // (2,2). TO do it we need to "take ownership" over queue to prevent ANY + // immediate schedulings from ANY next subscriptions + const auto drain_on_exit_if_needed = + schedulers::current_thread::own_queue_and_drain_finally_if_not_owned(); + try { + m_on_subscribe(subscriber); + } catch (...) { + if (subscriber.is_subscribed()) + subscriber.on_error(std::current_exception()); + else + throw; + } + } + + private: + /** + * \brief The on_subscribe functor, which has the operator()(const auto& + * subscriber) overload function. + */ + RPP_NO_UNIQUE_ADDRESS OnSubscribeFn m_on_subscribe; +}; + +template +specific_observable(OnSub on_subscribe) -> specific_observable< + utils::extract_subscriber_type_t>, OnSub>; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observers.hpp b/symmetri/gui/rpp/rpp/observers.hpp new file mode 100644 index 0000000..4066ac2 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observers.hpp @@ -0,0 +1,20 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup observers Observers + * \brief Observer subscribe on observable and obtains values provided by + * observable. \ingroup rpp + */ + +#include +#include diff --git a/symmetri/gui/rpp/rpp/observers/constraints.hpp b/symmetri/gui/rpp/rpp/observers/constraints.hpp new file mode 100644 index 0000000..4e51463 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observers/constraints.hpp @@ -0,0 +1,57 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::constraint { +template +concept observer_callbacks_exists = + requires(const T t) { + // t.on_next(...); + t.on_error(std::declval()); + t.on_completed(); + }; + +template +concept observer = std::is_base_of_v> && + observer_callbacks_exists; +template +concept decayed_observer = observer && decayed_type; +} // namespace rpp::constraint + +namespace rpp::utils { +namespace details { +template +struct extract_observer_type { + template + static TT deduce(const rpp::details::typed_observer_tag&); + + using type = decltype(deduce(std::declval>())); +}; + +} // namespace details +template +using extract_observer_type_t = + typename details::extract_observer_type::type; +} // namespace rpp::utils + +namespace rpp::constraint { +template +concept observer_on_next_exists = + requires(const T t) { t.on_next(std::declval()); }; + +template +concept observer_of_type = + observer && std::same_as, Type> && + observer_on_next_exists; +} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp b/symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp new file mode 100644 index 0000000..712e6b6 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp @@ -0,0 +1,158 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include // wrapping constructor +#include // base +#include // extract function args +#include // default arguments + +namespace rpp::details { +template +struct dynamic_observer_state_base { + virtual ~dynamic_observer_state_base() = default; + + virtual void on_next(const T& v) const = 0; + virtual void on_next(T&& v) const = 0; + virtual void on_error(const std::exception_ptr& err) const = 0; + virtual void on_completed() const = 0; +}; + +template TObserver> +class dynamic_observer_state final : public dynamic_observer_state_base { + public: + template + requires std::constructible_from + dynamic_observer_state(Args&&... args) + : m_observer{std::forward(args)...} {} + + void on_next(const T& v) const override { m_observer.on_next(v); } + void on_next(T&& v) const override { m_observer.on_next(std::move(v)); } + void on_error(const std::exception_ptr& err) const override { + m_observer.on_error(err); + } + void on_completed() const override { m_observer.on_completed(); } + + private: + RPP_NO_UNIQUE_ADDRESS TObserver m_observer; +}; + +template TObserver, + typename... Args> +std::shared_ptr> make_dynamic_observer_state( + Args&&... args) + requires std::constructible_from, Args...> +{ + return std::make_shared>>( + std::forward(args)...); +} + +template +std::shared_ptr> +make_dynamic_observer_state_from_fns(Args&&... args) { + return make_dynamic_observer_state< + T, details::state_observer...>>( + std::forward(args)...); +} + +template +class dynamic_state_observer + : public state_observer>> { + using base = state_observer>>; + + public: + template + requires(constraint::decayed_same_as && ...) + dynamic_state_observer( + std::invocable auto&& on_next, + std::invocable auto&& on_error, + std::invocable auto&& on_completed, TStates&&... states) + : base{utils::forwarding_on_next_for_pointer{}, + utils::forwarding_on_error_for_pointer{}, + utils::forwarding_on_completed_for_pointer{}, + make_dynamic_observer_state_from_fns( + std::forward(on_next), + std::forward(on_error), + std::forward(on_completed), + std::forward(states)...)} {} + + dynamic_state_observer( + std::shared_ptr> state) + : base{utils::forwarding_on_next_for_pointer{}, + utils::forwarding_on_error_for_pointer{}, + utils::forwarding_on_completed_for_pointer{}, std::move(state)} {} + + template TObserver> + requires(!std::is_same_v, + dynamic_state_observer>) + dynamic_state_observer(TObserver&& obs) + : dynamic_state_observer{ + details::make_dynamic_observer_state>( + std::forward(obs))} {} +}; +} // namespace rpp::details + +namespace rpp { +/** + * \brief Dynamic (type-erased) version of observer (comparing to + * specific_observer) \details It uses type-erasure mechanism to hide types of + * OnNext, OnError and OnCompleted callbacks. But it has higher cost in the + * terms of performance due to usage of heap. Use it only when you need to store + * observer as member variable or make copy of original subscriber. In other + * cases prefer using "auto" to avoid converting to dynamic_observer \tparam T + * is type of value handled by this observer \ingroup observers + */ +template +class dynamic_observer final : public details::dynamic_state_observer { + public: + template OnNext = utils::empty_function_t, + constraint::on_error_fn OnError = utils::rethrow_error_t, + constraint::on_completed_fn OnCompleted = utils::empty_function_t<>> + dynamic_observer(OnNext&& on_next = {}, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) + : details::dynamic_state_observer{ + std::forward(on_next), std::forward(on_error), + std::forward(on_completed)} {} + + dynamic_observer(constraint::on_next_fn auto&& on_next, + constraint::on_completed_fn auto&& on_completed) + : details::dynamic_state_observer{ + std::forward(on_next), utils::rethrow_error_t{}, + std::forward(on_completed)} {} + + template TObserver> + requires(!std::is_same_v, dynamic_observer>) + dynamic_observer(TObserver&& obs) + : details::dynamic_state_observer{std::forward(obs)} {} + + /** + * \brief Do nothing for rpp::dynamic_observer. Created only for unification + * of interfaces with rpp::specific_observer + */ + const dynamic_observer& as_dynamic() const { return *this; } +}; + +template +dynamic_observer(TObserver) + -> dynamic_observer>; + +template +dynamic_observer(OnNext, Args...) + -> dynamic_observer>; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observers/fwd.hpp b/symmetri/gui/rpp/rpp/observers/fwd.hpp new file mode 100644 index 0000000..11d5dc2 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observers/fwd.hpp @@ -0,0 +1,43 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct observer_tag {}; + +template +struct typed_observer_tag : public observer_tag {}; +} // namespace rpp::details + +namespace rpp::constraint { +template +concept on_next_fn = std::invocable, Type>; +template +concept on_error_fn = std::invocable, std::exception_ptr>; +template +concept on_completed_fn = std::invocable>; +} // namespace rpp::constraint + +namespace rpp { +template +class dynamic_observer; + +template OnNext, + constraint::on_error_fn OnError, + constraint::on_completed_fn OnCompleted> +class specific_observer; + +template +using specific_observer_with_decayed_args = + rpp::specific_observer...>; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observers/specific_observer.hpp b/symmetri/gui/rpp/rpp/observers/specific_observer.hpp new file mode 100644 index 0000000..cfdb480 --- /dev/null +++ b/symmetri/gui/rpp/rpp/observers/specific_observer.hpp @@ -0,0 +1,129 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // base class +#include // extract function args +#include // default arguments + +namespace rpp { +/** + * \brief Observer specified with specific template types of callbacks to avoid + * extra heap usage. + * + * \details It has better performance comparing to rpp::dynamic_observer due to + * using stack instead of heap as long as possible. It is default return type of + * all operators + * + * \tparam T is type of value handled by this observer + * \tparam OnNext type of on_next callback + * \tparam OnError type of on_error callback + * \tparam OnCompleted type of on_completed callback + * \ingroup observers + */ +template OnNext = utils::empty_function_t, + constraint::on_error_fn OnError = utils::rethrow_error_t, + constraint::on_completed_fn OnCompleted = utils::empty_function_t<>> +class specific_observer + : public details::state_observer { + using base = details::state_observer; + + using base::base; + + public: + template < + constraint::on_next_fn TOnNext = utils::empty_function_t, + constraint::on_error_fn TOnError = utils::rethrow_error_t, + constraint::on_completed_fn TOnCompleted = utils::empty_function_t<>> + specific_observer(TOnNext&& on_next = {}, TOnError&& on_error = {}, + TOnCompleted&& on_completed = {}) + : base{std::forward(on_next), std::forward(on_error), + std::forward(on_completed)} {} + + specific_observer(constraint::on_next_fn auto&& on_next, + constraint::on_completed_fn auto&& on_completed) + : base{std::forward(on_next), utils::rethrow_error_t{}, + std::forward(on_completed)} {} + + /** + * \brief Converting current rpp::specific_observer to rpp::dynamic_observer + * alternative with erasing of type (and using heap) \return converted + * rpp::dynamic_observer + */ + [[nodiscard]] auto as_dynamic() const& { return dynamic_observer{*this}; } + [[nodiscard]] auto as_dynamic() && { + return dynamic_observer{std::move(*this)}; + } +}; + +template +specific_observer(OnNext) + -> specific_observer, OnNext>; + +template +specific_observer(OnNext, OnError, Args...) + -> specific_observer, OnNext, + OnError, Args...>; + +template +specific_observer(OnNext, OnCompleted) + -> specific_observer, OnNext, + utils::rethrow_error_t, OnCompleted>; + +/** + * \brief Create specific_observer with manually specified Type. In case of type + * can be deduced from argument of OnNext use direct constructor of + * rpp::specific_observer \tparam Type manually specific type of observer + */ +template +auto make_specific_observer() -> specific_observer_with_decayed_args { + return {}; +} + +template OnNext> +auto make_specific_observer(OnNext&& on_next) + -> specific_observer_with_decayed_args { + return {std::forward(on_next)}; +} + +template OnNext, + constraint::on_completed_fn OnCompleted> +auto make_specific_observer(OnNext&& on_next, OnCompleted&& on_completed) + -> specific_observer_with_decayed_args { + return {std::forward(on_next), + std::forward(on_completed)}; +} + +template OnNext, + constraint::on_error_fn OnError> +auto make_specific_observer(OnNext&& on_next, OnError&& on_error) + -> specific_observer_with_decayed_args { + return {std::forward(on_next), std::forward(on_error)}; +} + +template OnNext, + constraint::on_error_fn OnError, + constraint::on_completed_fn OnCompleted> +auto make_specific_observer(OnNext&& on_next, OnError&& on_error, + OnCompleted&& on_completed) + -> specific_observer_with_decayed_args { + return {std::forward(on_next), std::forward(on_error), + std::forward(on_completed)}; +} + +namespace details { +template +using deduce_specific_observer_type_t = + decltype(make_specific_observer(std::declval()...)); +} // namespace details +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observers/state_observer.hpp b/symmetri/gui/rpp/rpp/observers/state_observer.hpp new file mode 100644 index 0000000..0cb94db --- /dev/null +++ b/symmetri/gui/rpp/rpp/observers/state_observer.hpp @@ -0,0 +1,96 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include // extract argument type +#include + +namespace rpp::details { +/** + * \brief Special type of specific_observer which has some state which this + * observer stores and pass to each callback. Actually it is base class for all + * observers. + */ +template + requires(std::invocable && + std::invocable && + std::invocable) +class state_observer : public details::typed_observer_tag { + public: + template + requires(constraint::decayed_same_as && ...) + state_observer(std::invocable auto&& on_next, + std::invocable auto&& on_error, + std::invocable auto&& on_completed, + TStates&&... states) + : m_on_next{std::forward(on_next)}, + m_on_err{std::forward(on_error)}, + m_on_completed{std::forward(on_completed)}, + m_state{std::forward(states)...} {} + + /** + * \brief Observable calls this methods to notify observer about new value. + * + * \note obtains value by const-reference to original object. + */ + void on_next(const T& v) const { + std::apply([&v, this](const States&... states) { m_on_next(v, states...); }, + m_state); + } + + /** + * \brief Observable calls this methods to notify observer about new value. + * + * \note obtains value by rvalue-reference to original object + */ + void on_next(T&& v) const { + std::apply( + [&v, this](const States&... states) { + m_on_next(std::move(v), states...); + }, + m_state); + } + + /** + * \brief Observable calls this method to notify observer about some error + * during generation next data. \warning Obtaining this call means no any + * further on_next or on_completed calls \param err details of error + */ + void on_error(const std::exception_ptr& err) const { + std::apply( + [&err, this](const States&... states) { m_on_err(err, states...); }, + m_state); + } + + /** + * \brief Observable calls this method to notify observer about finish of + * work. \warning Obtaining this call means no any further on_next calls + */ + void on_completed() const { std::apply(m_on_completed, m_state); } + + private: + RPP_NO_UNIQUE_ADDRESS OnNext m_on_next; + RPP_NO_UNIQUE_ADDRESS OnError m_on_err; + RPP_NO_UNIQUE_ADDRESS OnCompleted m_on_completed; + + RPP_NO_UNIQUE_ADDRESS std::tuple m_state; +}; + +template +state_observer(TOnNext, Args...) + -> state_observer>, + TOnNext, Args...>; + +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators.hpp b/symmetri/gui/rpp/rpp/operators.hpp new file mode 100644 index 0000000..3bfa251 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators.hpp @@ -0,0 +1,123 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup operators Operators + * \brief Operators is way to modify observables and extend with some extra + * custom logic \see https://reactivex.io/documentation/operators.html \ingroup + * rpp + */ + +/** + * \defgroup transforming_operators Transforming Operators + * \brief Transforming operators are operators that transform items provided by + * observable \see + * https://reactivex.io/documentation/operators.html#transforming \ingroup + * operators + */ + +#include +#include +#include +#include +#include +#include + +/** + * \defgroup filtering_operators Filtering Operators + * \brief Filtering operators are operators that emit only part of items that + * satisfies some condition \see + * https://reactivex.io/documentation/operators.html#filtering \ingroup + * operators + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * \defgroup conditional_operators Conditional Operators + * \brief Conditional operators are operators that emit items based on some + * condition including condition of items from other observables \see + * https://reactivex.io/documentation/operators.html#conditional \ingroup + * operators + */ + +#include +#include + +/** + * \defgroup combining_operators Combining Operators + * \brief Combining operators are operators that combines emissions of multiple + * observables into same observable by some rule \see + * https://reactivex.io/documentation/operators.html#combining \ingroup + * operators + */ + +#include +#include +#include +#include +#include +#include + +/** + * \defgroup aggregate_operators Aggregate Operators + * \brief Aggregate operators are operators on the entire sequence of items + * provided by observable \see + * https://reactivex.io/documentation/operators.html#mathematical \ingroup + * operators + */ + +#include +#include + +/** + * \defgroup utility_operators Utility Operators + * \brief Utility operators are operators that provide some extra functionality + * without changing of original values, but changing of behaviour \see + * https://reactivex.io/documentation/operators.html#utility \ingroup operators + */ + +#include +#include +#include +#include +#include +#include + +/** + * \defgroup connectable_operators Connectable Operators + * \brief Connectable operators are operators that provide extra functionality + * for multicasting of controlling of subscription \see + * https://reactivex.io/documentation/operators.html#connectable \ingroup + * operators + */ + +#include +#include +#include + +/** + * \defgroup error_handling_operators Error handling Operators + * \brief Error handling operators Operators that help to recover from error + * notifications from an Observable. \see + * https://reactivex.io/documentation/operators.html#error \ingroup operators + */ + +#include diff --git a/symmetri/gui/rpp/rpp/operators/buffer.hpp b/symmetri/gui/rpp/rpp/operators/buffer.hpp new file mode 100644 index 0000000..9d5758f --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/buffer.hpp @@ -0,0 +1,86 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include // create_subscriber_with_dynamic_state +#include // own forwarding +#include // required due to operator uses lift +#include // subscriber_of_type +#include // forwarding_on_error + +IMPLEMENTATION_FILE(buffer_tag); + +namespace rpp::details { +/// A non-copyable class that provides a copyable on_next for the subscriber and +/// allows copies of on_next(s) to share the same states. +template +struct buffer_state { + /// \param count Number of items being bundled. Note when count == 0, we'll + /// treat the behavior like when count == 1. + explicit buffer_state(size_t count) : max(std::max(size_t{1}, count)) { + clear_and_reserve_buckets(); + } + + buffer_state(const buffer_state& other) = delete; + buffer_state(buffer_state&&) noexcept = default; + buffer_state& operator=(const buffer_state&) = delete; + buffer_state& operator=(buffer_state&&) noexcept = default; + + void clear_and_reserve_buckets() const { + buckets.clear(); + buckets.reserve(max); + } + + const size_t max; + mutable buffer_bundle_type buckets; +}; + +struct buffer_on_next { + template + void operator()(auto&& value, const auto& subscriber, + const buffer_state& state) const { + state.buckets.push_back(std::forward(value)); + if (state.buckets.size() == state.max) { + subscriber.on_next(std::move(state.buckets)); + state.clear_and_reserve_buckets(); + } + } +}; + +struct buffer_on_completed { + template + void operator()(const auto& subscriber, + const buffer_state& state) const { + if (!state.buckets.empty()) subscriber.on_next(std::move(state.buckets)); + subscriber.on_completed(); + } +}; + +template +struct buffer_impl { + const size_t count; + + template > TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), buffer_on_next{}, utils::forwarding_on_error{}, + buffer_on_completed{}, std::forward(subscriber), + buffer_state{count}); + } +}; + +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/combine_latest.hpp b/symmetri/gui/rpp/rpp/operators/combine_latest.hpp new file mode 100644 index 0000000..dc60ff9 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/combine_latest.hpp @@ -0,0 +1,170 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include // RPP_NO_UNIQUE_ADDRESS +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // merge_state +#include // constraint::subscriber_of_type +#include // spinlock + +IMPLEMENTATION_FILE(combine_latest_tag); + +namespace rpp::details { +/** + * \brief The state that caches the values from all the observables and + * dispatches latest caches to the observer. Note the emission is only sent to + * the observer when all the observables at least emits once. + */ +template +struct combine_latest_state : public merge_state { + explicit combine_latest_state( + const TCombiner& combiner, + const composite_subscription& subscription_of_subscriber) + : merge_state(subscription_of_subscriber), combiner(combiner) {} + + // don't use NO_UNIQUE_ADDRESS there due to issue in MSVC base class becomes + // invalid + /*NO_UNIQUE_ADDRESS*/ TCombiner combiner; + std::mutex values_mutex{}; + std::tuple...> values{}; +}; + +template +struct combine_latest_on_next { + template + void operator()( + auto&& value, const auto& subscriber, + const std::shared_ptr>& state) + const { + // mutex need to be locked during changing of values, generating new values + // and sending of new values due to we can't update value while we are + // sending old one + std::scoped_lock lock{state->values_mutex}; + std::get(state->values) = std::forward(value); + + std::apply( + [&](const auto&... cached_values) { + if ((cached_values.has_value() && ...)) + subscriber.on_next(state->combiner(cached_values.value()...)); + }, + state->values); + } +}; + +using combine_latest_on_error = merge_on_error; +using combine_latest_on_completed = merge_on_completed; + +template +struct combine_latest_state_with_serialized_spinlock + : combine_latest_state { + using combine_latest_state::combine_latest_state; + + // we can use spinlock there because 99.9% of time only one ever thread would + // send values from on_next serialized (due to values_mutex), but we have + // small probability to get error from another observable immediately + utils::spinlock spinlock{}; +}; + +/** + * \brief "combine_latest" operator (an OperatorFn used by "lift"). + */ +template +struct combine_latest_impl { + RPP_NO_UNIQUE_ADDRESS TCombiner m_combiner; + RPP_NO_UNIQUE_ADDRESS std::tuple m_other_observables; + + private: + static constexpr size_t s_index_of_source_type = 0; + + /** + * \brief Templated helper function for subscribing to variadic 'other' + * observables. + * + * \param subscriber is the downstream subscriber. + * \param state manages the cache of emission from the observables and + * coordinate dispatching. + */ + template + void subscribe_other_observables( + std::index_sequence, + // Used in compile time for variadic expansion + const auto& subscriber, + const std::shared_ptr...>>& state) + const { + // +1 because the first element in tuple is the current observable, and you + // want to subscribe to the 'other' observables. (Use variadic expansion to + // iterate the observables) + (subscribe_observable(std::get(m_other_observables), subscriber, + state), + ...); + } + + template + static void subscribe_observable( + const TObservable& observable, const auto& subscriber, + const std::shared_ptr...>>& state) { + using ValueType = utils::extract_observable_type_t; + observable.subscribe( + create_inner_subscriber(subscriber, state)); + } + + template + static auto create_inner_subscriber( + auto&& subscriber, + std::shared_ptr...>> + state) { + auto subscription = state->children_subscriptions.make_child(); + return create_subscriber_with_state( + std::move(subscription), combine_latest_on_next{}, + combine_latest_on_error{}, combine_latest_on_completed{}, + std::forward(subscriber), std::move(state)); + } + + public: + using DownstreamType = utils::decayed_invoke_result_t< + TCombiner, Type, utils::extract_observable_type_t...>; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = std::make_shared...>>( + m_combiner, in_subscriber.get_subscription()); + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->spinlock}); + + state->count_of_on_completed_needed.store(sizeof...(TOtherObservable) + 1, + std::memory_order::relaxed); + + // Subscribe to other observables and redirect on_next event to state + subscribe_other_observables(std::index_sequence_for{}, + subscriber, state); + + // Redirect values from this observable to the state for value composition + return create_inner_subscriber( + std::move(subscriber), std::move(state)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/concat.hpp b/symmetri/gui/rpp/rpp/operators/concat.hpp new file mode 100644 index 0000000..c945ab2 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/concat.hpp @@ -0,0 +1,140 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include // dynamic_observable +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // merge_forwarding_on_next/merge_on_error +#include +#include +#include // constraint::subscriber_of_type +#include // composite_subscription +#include +#include + +IMPLEMENTATION_FILE(concat_tag); + +namespace rpp::details { +template +struct concat_state : public early_unsubscribe_state { + concat_state(const composite_subscription& subscription_of_subscriber) + : early_unsubscribe_state{subscription_of_subscriber}, + source_subscription{children_subscriptions.make_child()} {} + + composite_subscription source_subscription; + std::mutex queue_mutex{}; + std::queue> observables_to_subscribe{}; + std::atomic_bool inner_subscribed{}; +}; + +using concat_on_next_inner = merge_forwarding_on_next; +using concat_on_error = merge_on_error; + +template +struct concat_on_next_outer { + template + void operator()(TObs&& new_observable, const TSub& sub, + const std::shared_ptr>& state) const { + if (state->inner_subscribed.exchange(true, std::memory_order::acq_rel)) { + std::lock_guard lock{state->queue_mutex}; + if (state->inner_subscribed.exchange(true, std::memory_order::relaxed)) { + state->observables_to_subscribe.push( + std::forward(new_observable).as_dynamic()); + return; + } + } + subscribe_inner_subscriber(new_observable, sub, state); + } + + private: + static void subscribe_inner_subscriber( + const auto& observable, const constraint::subscriber auto& subscriber, + const std::shared_ptr>& state) { + observable.subscribe(create_subscriber_with_state( + state->children_subscriptions.make_child(), concat_on_next_inner{}, + concat_on_error{}, + [](const constraint::subscriber auto& sub, + const std::shared_ptr>& state) { + { + std::unique_lock lock{state->queue_mutex}; + if (!state->observables_to_subscribe.empty()) { + auto res = std::move(state->observables_to_subscribe.front()); + state->observables_to_subscribe.pop(); + lock.unlock(); + subscribe_inner_subscriber(res, sub, state); + return; + } + if (state->source_subscription.is_subscribed()) { + state->inner_subscribed.store(false, std::memory_order::relaxed); + return; + } + } + sub.on_completed(); + }, + subscriber, state)); + } +}; + +template +struct concat_on_completed { + void operator()(const constraint::subscriber auto& sub, + const std::shared_ptr>& state) const { + std::unique_lock lock{state->queue_mutex}; + if (!state->inner_subscribed.load(std::memory_order::relaxed)) + sub.on_completed(); + } +}; + +template +struct concat_state_with_serialized_spinlock : concat_state { + using concat_state::concat_state; + + // we can use spinlock there because 99.9% of time only one ever thread would + // send values from on_next (only one active observable), but we have small + // probability to get error from main observable immediately + utils::spinlock spinlock{}; +}; + +template +struct concat_impl { + using ValueType = utils::extract_observable_type_t; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = + std::make_shared>( + in_subscriber.get_subscription()); + + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->spinlock}); + + return create_subscriber_with_state( + state->source_subscription, concat_on_next_outer{}, + concat_on_error{}, concat_on_completed{}, + std::move(subscriber), std::move(state)); + } +}; + +template ... TObservables> +auto concat_with_impl(TObservables&&... observables) { + return source::just(rpp::schedulers::immediate{}, + std::forward(observables).as_dynamic()...) + .concat(); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/debounce.hpp b/symmetri/gui/rpp/rpp/operators/debounce.hpp new file mode 100644 index 0000000..d6c2a44 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/debounce.hpp @@ -0,0 +1,159 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include // required due to operator uses lift +#include +#include +#include +#include + +IMPLEMENTATION_FILE(debounce_tag); + +namespace rpp::details { +template +class debounce_state : public early_unsubscribe_state { + public: + debounce_state(schedulers::duration period, const Scheduler& scheduler, + const composite_subscription& subscription_of_subscriber) + : early_unsubscribe_state(subscription_of_subscriber), + m_period{period}, + m_worker{scheduler.create_worker(children_subscriptions)} {} + + std::optional emplace_safe(auto&& v) { + std::lock_guard lock{m_mutex}; + m_value_to_be_emitted.emplace(std::forward(v)); + const bool need_to_scheduled = + !m_time_when_value_should_be_emitted.has_value() || + !m_value_to_be_emitted.has_value(); + m_time_when_value_should_be_emitted = m_worker.now() + m_period; + return need_to_scheduled ? m_time_when_value_should_be_emitted + : std::optional{}; + } + + std::variant + extract_value_or_time() { + std::lock_guard lock{m_mutex}; + if (!m_time_when_value_should_be_emitted.has_value() || + !m_value_to_be_emitted.has_value()) + return std::monostate{}; + + const auto now = m_worker.now(); + if (m_time_when_value_should_be_emitted > now) + return m_time_when_value_should_be_emitted.value() - now; + + m_time_when_value_should_be_emitted.reset(); + auto v = std::move(m_value_to_be_emitted).value(); + m_value_to_be_emitted.reset(); + return v; + } + + std::optional extract_value() { + std::lock_guard lock{m_mutex}; + std::optional res{}; + m_value_to_be_emitted.swap(res); + return res; + } + + using Worker = decltype(std::declval().create_worker( + std::declval())); + const Worker& get_worker() const { return m_worker; } + + private: + schedulers::duration m_period; + Worker m_worker; + std::mutex m_mutex{}; + std::optional m_time_when_value_should_be_emitted{}; + std::optional m_value_to_be_emitted{}; +}; + +struct debounce_on_next { + template + void operator()(Value&& v, const auto& state_ptr) const { + if (const auto time_to_schedule = + state_ptr->emplace_safe(std::forward(v))) { + state_ptr->get_worker().schedule( + time_to_schedule.value(), + [state_ptr]() mutable -> schedulers::optional_duration { + auto value_or_duration = state_ptr->extract_value_or_time(); + if (auto* duration = + std::get_if(&value_or_duration)) + return *duration; + + if (auto* value = + std::get_if>(&value_or_duration)) + state_ptr->subscriber.on_next(std::move(*value)); + + return std::nullopt; + }); + } + } +}; + +struct debounce_on_error { + void operator()(const std::exception_ptr& err, const auto& state) const { + state->children_subscriptions.unsubscribe(); + state->subscriber.on_error(err); + } +}; + +struct debounce_on_completed { + void operator()(const auto& state_ptr) const { + state_ptr->children_subscriptions.unsubscribe(); + + if (auto v = state_ptr->extract_value()) + state_ptr->subscriber.on_next(std::move(v.value())); + + state_ptr->subscriber.on_completed(); + } +}; + +template +struct debounce_state_with_serialized_spinlock : debounce_state { + debounce_state_with_serialized_spinlock(auto&& sub, + schedulers::duration period, + const Scheduler& scheduler) + : debounce_state{std::move(period), scheduler, + sub.get_subscription()}, + subscriber(make_serialized_subscriber(std::forward(sub), + std::ref(spinlock))) {} + + // spinlock because most part of time there is only one thread would be active + utils::spinlock spinlock{}; + + using InnerSub = decltype(make_serialized_subscriber( + std::declval(), + std::declval>())); + InnerSub subscriber; +}; + +template +struct debounce_impl { + schedulers::duration period; + TScheduler scheduler; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = std::make_shared>>( + std::forward(in_subscriber), period, scheduler); + + return create_subscriber_with_state( + state->children_subscriptions, debounce_on_next{}, debounce_on_error{}, + debounce_on_completed{}, std::move(state)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/delay.hpp b/symmetri/gui/rpp/rpp/operators/delay.hpp new file mode 100644 index 0000000..f8a800d --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/delay.hpp @@ -0,0 +1,168 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // constraint::subscriber_of_type +#include +#include + +IMPLEMENTATION_FILE(delay_tag); + +namespace rpp::details { +struct completion {}; + +template +class queue_based_worker final + : public std::enable_shared_from_this< + queue_based_worker> { + public: + queue_based_worker(schedulers::duration delay, Worker&& worker, + const Subscriber& subscriber) + : m_delay{delay}, m_worker{std::move(worker)}, m_subscriber{subscriber} {} + + queue_based_worker(schedulers::duration delay, Worker&& worker, + Subscriber&& subscriber) + : m_delay{delay}, + m_worker{std::move(worker)}, + m_subscriber{std::move(subscriber)} {} + + struct on_next { + void operator()( + auto&& value, + const std::shared_ptr>& state) + const { + state->emplace(std::forward(value)); + } + }; + + struct on_error { + void operator()( + const std::exception_ptr& err, + const std::shared_ptr>& state) + const { + state->emplace(err); + } + }; + + struct on_completed { + void operator()( + const std::shared_ptr>& state) + const { + state->emplace(completion{}); + } + }; + + private: + template + void emplace(TT&& item) { + if (const auto timepoint = emplace_safe(std::forward(item))) { + m_worker.schedule( + timepoint.value(), + [state = + this->shared_from_this()]() -> schedulers::optional_duration { + return state->drain_queue(); + }); + } + } + + template + std::optional emplace_safe(TT&& item) { + std::lock_guard lock{m_mutex}; + const auto delay = std::is_same_v> + ? schedulers::duration{0} + : m_delay; + m_queue.emplace(++m_current_id, m_worker.now() + delay, + std::forward(item)); + if (!m_active && m_queue.size() == 1) { + m_active = true; + return m_queue.top().time; + } + return {}; + } + + schedulers::optional_duration drain_queue() { + while (true) { + std::unique_lock lock{m_mutex}; + if (m_queue.empty()) { + m_active = false; + return {}; + } + + auto& top = m_queue.top(); + const auto now = m_worker.now(); + if (top.time > now) return top.time - now; + + auto item = std::move(top.item); + m_queue.pop(); + lock.unlock(); + + std::visit( + utils::overloaded{[&](T&& v) { m_subscriber.on_next(std::move(v)); }, + [&](const std::exception_ptr& err) { + m_subscriber.on_error(err); + }, + [&](completion) { m_subscriber.on_completed(); }}, + std::move(item)); + } + } + + private: + struct emission { + template + emission(size_t id, schedulers::time_point time, TT&& item) + : id{id}, time{std::move(time)}, item{std::forward(item)} {} + + size_t id{}; + schedulers::time_point time{}; + std::variant item{}; + + bool operator<(const emission& other) const { + return std::tie(time, id) >= std::tie(other.time, other.id); + } + }; + + schedulers::duration m_delay; + Worker m_worker; + Subscriber m_subscriber; + std::mutex m_mutex{}; + size_t m_current_id{}; + std::priority_queue m_queue{}; + bool m_active{}; +}; + +template +struct delay_impl { + RPP_NO_UNIQUE_ADDRESS TScheduler scheduler; + schedulers::duration delay; + + template TSub> + auto operator()(TSub&& subscriber) const { + auto worker = scheduler.create_worker(subscriber.get_subscription()); + auto subscription = subscriber.get_subscription().make_child(); + + using state_t = queue_based_worker, + std::decay_t>; + auto state = std::make_shared(delay, std::move(worker), + std::forward(subscriber)); + + return create_subscriber_with_state( + std::move(subscription), typename state_t::on_next{}, + typename state_t::on_error{}, typename state_t::on_completed{}, + std::move(state)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp b/symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp new file mode 100644 index 0000000..21fa1e2 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp @@ -0,0 +1,42 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include + +namespace rpp::details { +struct early_unsubscribe_state { + early_unsubscribe_state( + const composite_subscription& subscription_of_subscriber) + : children_subscriptions(subscription_of_subscriber.make_child()) {} + + // use this subscription as source for any child subscription that should be + // early unsubscribed + composite_subscription children_subscriptions; +}; + +struct early_unsubscribe_on_error { + void operator()(const std::exception_ptr& err, + const constraint::subscriber auto& sub, + const std::shared_ptr& state) const { + state->children_subscriptions.unsubscribe(); + sub.on_error(err); + } +}; + +struct early_unsubscribe_on_completed { + void operator()(const constraint::subscriber auto& sub, + const std::shared_ptr& state) const { + state->children_subscriptions.unsubscribe(); + sub.on_completed(); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp b/symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp new file mode 100644 index 0000000..aa40a39 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp @@ -0,0 +1,75 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include // create_subscriber_with_state +#include +#include + +namespace rpp::details { +template +auto lock(const std::shared_ptr& ptr) { + return std::lock_guard{*ptr}; +} + +template +auto lock(const std::reference_wrapper& ref) { + return std::lock_guard{ref.get()}; +} + +struct forwarding_on_next_under_lock { + template + void operator()(T&& v, const auto& subscriber, const auto& primitive) const { + auto lock_guard = lock(primitive); + subscriber.on_next(std::forward(v)); + } +}; + +struct forwarding_on_error_under_lock { + void operator()(const std::exception_ptr& err, const auto& subscriber, + const auto& primitive) const { + auto lock_guard = lock(primitive); + subscriber.on_error(err); + } +}; + +struct forwarding_on_completed_under_lock { + void operator()(const auto& subscriber, const auto& primitive) const { + auto lock_guard = lock(primitive); + subscriber.on_completed(); + } +}; + +template +auto make_serialized_subscriber( + TSub&& subscriber, + const std::shared_ptr& primitive) { + auto sub = subscriber.get_subscription(); + return create_subscriber_with_state< + utils::extract_subscriber_type_t>>( + std::move(sub), forwarding_on_next_under_lock{}, + forwarding_on_error_under_lock{}, forwarding_on_completed_under_lock{}, + std::forward(subscriber), primitive); +} + +template +auto make_serialized_subscriber( + TSub&& subscriber, + std::reference_wrapper primitive) { + auto sub = subscriber.get_subscription(); + return create_subscriber_with_state< + utils::extract_subscriber_type_t>>( + std::move(sub), forwarding_on_next_under_lock{}, + forwarding_on_error_under_lock{}, forwarding_on_completed_under_lock{}, + std::forward(subscriber), primitive); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp b/symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp new file mode 100644 index 0000000..4db71e6 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp @@ -0,0 +1,49 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include + +namespace rpp::details { +template ...> OnNext, + std::invocable...> OnError, + std::invocable...> OnCompleted> +auto create_subscriber_with_state(composite_subscription sub, OnNext&& on_next, + OnError&& on_error, + OnCompleted&& on_completed, + States&&... states) { + using TObs = + state_observer, std::decay_t, + std::decay_t, std::decay_t...>; + return make_specific_subscriber( + std::move(sub), std::forward(on_next), + std::forward(on_error), std::forward(on_completed), + std::forward(states)...); +} + +template ...> OnNext, + std::invocable...> OnError, + std::invocable...> OnCompleted> +auto create_subscriber_with_dynamic_state(composite_subscription sub, + OnNext&& on_next, OnError&& on_error, + OnCompleted&& on_completed, + States&&... states) { + using TObs = dynamic_state_observer...>; + return make_specific_subscriber( + std::move(sub), std::forward(on_next), + std::forward(on_error), std::forward(on_completed), + std::forward(states)...); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp b/symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp new file mode 100644 index 0000000..217ddfc --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp @@ -0,0 +1,65 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include // RPP_NO_UNIQUE_ADDRESS +#include // create_subscriber_with_dynamic_state +#include // own forwarding +#include // required due to operator uses lift +#include // constraint::subscriber_of_type +#include // forwarding_on_error/forwarding_on_completed +#include // as_const + +IMPLEMENTATION_FILE(distinct_until_changed_tag); + +namespace rpp::details { +template EqualityFn> +struct distinct_until_changed_state { + RPP_NO_UNIQUE_ADDRESS EqualityFn equality_comparator; + mutable std::optional last_value{}; +}; + +struct distinct_until_changed_on_next { + template EqualityFn> + void operator()( + auto&& new_value, const constraint::subscriber auto& sub, + const distinct_until_changed_state& state) const { + if (state.last_value.has_value() && + state.equality_comparator(utils::as_const(state.last_value.value()), + utils::as_const(new_value))) + return; + + state.last_value.emplace(new_value); + sub.on_next(std::forward(new_value)); + } +}; + +template EqualityFn> +struct distinct_until_changed_impl { + RPP_NO_UNIQUE_ADDRESS EqualityFn equality_comparator; + + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), distinct_until_changed_on_next{}, + utils::forwarding_on_error{}, utils::forwarding_on_completed{}, + std::forward(subscriber), + distinct_until_changed_state{equality_comparator}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/do.hpp b/symmetri/gui/rpp/rpp/operators/do.hpp new file mode 100644 index 0000000..13e3373 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/do.hpp @@ -0,0 +1,49 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // constraint::subscriber_of_type +#include // utils::as_const + +IMPLEMENTATION_FILE(do_tag); + +namespace rpp::details { +template TObs> +struct do_impl { + TObs observer; + + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + + return create_subscriber_with_state( + std::move(subscription), + [](auto&& value, const TSub& subscriber, const TObs& do_observer) { + do_observer.on_next(utils::as_const(value)); + subscriber.on_next(std::forward(value)); + }, + [](const std::exception_ptr& err, const TSub& subscriber, + const TObs& do_observer) { + do_observer.on_error(err); + subscriber.on_error(err); + }, + [](const TSub& subscriber, const TObs& do_observer) { + do_observer.on_completed(); + subscriber.on_completed(); + }, + std::forward(subscriber), observer); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/filter.hpp b/symmetri/gui/rpp/rpp/operators/filter.hpp new file mode 100644 index 0000000..c938d94 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/filter.hpp @@ -0,0 +1,46 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include // own forwarding +#include // required due to operator uses lift +#include // constraint::subscriber_of_type +#include // utils::as_const +#include + +IMPLEMENTATION_FILE(filter_tag); + +namespace rpp::details { +template Predicate> +struct filter_impl_on_next { + RPP_NO_UNIQUE_ADDRESS Predicate predicate; + + template TSub> + void operator()(TVal&& value, const TSub& subscriber) const { + if (predicate(utils::as_const(value))) + subscriber.on_next(std::forward(value)); + } +}; + +template Predicate> +struct filter_impl { + RPP_NO_UNIQUE_ADDRESS filter_impl_on_next on_next; + + template + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + return create_subscriber_with_state( + std::move(subscription), on_next, utils::forwarding_on_error{}, + utils::forwarding_on_completed{}, std::forward(subscriber)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/first.hpp b/symmetri/gui/rpp/rpp/operators/first.hpp new file mode 100644 index 0000000..309adb9 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/first.hpp @@ -0,0 +1,54 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // take_state +#include // constraint::subscriber +#include // not_enough_emissions +#include // forwarding_on_error + +IMPLEMENTATION_FILE(first_tag); + +namespace rpp::details { +struct first_state : take_state { + first_state() : take_state{1} {} +}; + +using first_on_next = take_on_next; + +struct first_on_completed { + void operator()(const constraint::subscriber auto& subscriber, + const first_state&) const { + subscriber.on_error(std::make_exception_ptr(utils::not_enough_emissions{ + "first() operator expects at least one emission from observable before " + "completion"})); + } +}; + +template +struct first_impl { + public: + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), first_on_next{}, utils::forwarding_on_error{}, + first_on_completed{}, std::forward(subscriber), first_state{}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/flat_map.hpp b/symmetri/gui/rpp/rpp/operators/flat_map.hpp new file mode 100644 index 0000000..b10d513 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/flat_map.hpp @@ -0,0 +1,26 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(flat_map_tag); + +namespace rpp::details { +template Callable> +auto flat_map_impl(auto&& observable, Callable&& callable) { + return std::forward(observable) + .map(std::forward(callable)) + .merge(); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd.hpp b/symmetri/gui/rpp/rpp/operators/fwd.hpp new file mode 100644 index 0000000..556bd6d --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd.hpp @@ -0,0 +1,49 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/operators/fwd/buffer.hpp b/symmetri/gui/rpp/rpp/operators/fwd/buffer.hpp new file mode 100644 index 0000000..17efcfd --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/buffer.hpp @@ -0,0 +1,93 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct buffer_tag; +} + +namespace rpp::details { +template +using buffer_bundle_type = std::vector; + +template +struct buffer_impl; + +template +struct member_overload { + /** + * \brief Periodically gather emissions emitted by an original Observable into + bundles and emit these bundles rather than emitting + * the items one at a time + * + * \marble buffer + { + source observable : +-1-2-3-| + operator "buffer(2)" : +---{1,2}-{3}-| + } + * + * \details The resulting bundle is `std::vector` of requested size. + Actually it is similar to `window()` operator, but it emits vectors instead of + observables. + * + * \param count number of items being bundled. + * \return new specific_observable with the buffer operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet buffer.cpp buffer + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store `std::vector` of requested + size. + * - OnNext + * - Accumulates emissions inside current bundle and emits this bundle when + requested cound reached and starts new bundle. + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Emits current active bundle (if any) and just forwards on_completed + * + * \ingroup transforming_operators + * \see https://reactivex.io/documentation/operators/buffer.html + */ + template + auto buffer(size_t count) const& + requires is_header_included + { + return CastThis()->template lift>( + buffer_impl{count}); + } + + template + auto buffer(size_t count) && + requires is_header_included + { + return MoveThis().template lift>( + buffer_impl{count}); + } + + private: + const SpecificObservable* CastThis() const { + return static_cast(this); + } + + SpecificObservable&& MoveThis() { + return std::move(*static_cast(this)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/combine_latest.hpp b/symmetri/gui/rpp/rpp/operators/fwd/combine_latest.hpp new file mode 100644 index 0000000..b996401 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/combine_latest.hpp @@ -0,0 +1,183 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include // constraint::observable +#include // member_overload +#include // decayed_invoke_result_t +#include // pack_to_tuple +#include + +namespace rpp::details { +struct combine_latest_tag; +} + +namespace rpp::details { + +template +struct combine_latest_impl; + +template +struct member_overload { + /** + * \brief Combines latest emissions from original observable and other + observables when any of them emits. + * \warning According to observable contract + (https://reactivex.io/documentation/contract.html) emissions from any + observable should be serialized, so, resulting observable uses `std::mutex` + to satisfy this requirement + * + * \marble combine_latest_custom_combiner + { + source observable : +---1 -- -- -2 + -- -3 -| source other_observable : +-5-- -6 + -7 -- -8 - -| operator "combine_latest: x,y =>std::pair{x,y}" : + +---{1,5}-{1,6}-{1,7}-{2,7}-{2,8}-{3,8}-| + } + * \details Actually this operator subscribes on all of theses observables and + emits new combined value when any of them emits new emission (and each + observable emit values at least one to be able to provide combined value) + * + * \param combiner combines emissions from all the observables using custom + composition. + * \param observables are observables whose emissions would be combined with + the current observable's emissions + * \return new specific_observable with the combine_latest operator as most + recent operator. + * \warning #include + * + * \par Examples + * \snippet combine_latest.cpp combine_latest custom combiner + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store last emissions. + * - Wraps subscriber with serialization logic to be sure callbacks called + serialized + * - OnNext + * - Keeps last emission from each observable + * - Applies combiner function and emits result if there is last emissions + for each observable + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/combinelatest.html + */ + template ...> + TCombiner> + auto combine_latest(TCombiner&& combiner, + TOtherObservable&&... observables) const& + requires is_header_included + { + using DownstreamType = utils::decayed_invoke_result_t< + TCombiner, Type, utils::extract_observable_type_t...>; + + return cast_this()->template lift( + combine_latest_impl, + std::decay_t...>{ + std::forward(combiner), + std::tuple{std::forward(observables)...}}); + } + + template ...> + TCombiner> + auto combine_latest(TCombiner&& combiner, + TOtherObservable&&... observables) && + requires is_header_included + { + using DownstreamType = utils::decayed_invoke_result_t< + TCombiner, Type, utils::extract_observable_type_t...>; + + return move_this().template lift( + combine_latest_impl, + std::decay_t...>{ + std::forward(combiner), + std::tuple{std::forward(observables)...}}); + } + + /** + * \brief Combines latest emissions from current observable and other + observables when any of them emits. The combining result is std::tuple<...>. + * + * \marble combine_latest + { + source observable : +---1 -- -- -2 -- -3 -| + source other_observable : +-5-- -6 -7 -- -8 - -| + operator "combine_latest:tuple" : + +---{1,5}-{1,6}-{1,7}-{2,7}-{2,8}-{3,8}-| + } + * + * \details Actually this operator subscribes on all of theses observables and + emits `std::tuple` of last emissions when any of them emits new emission (and + each observable emit values at least one to be able to provide combined + value) + * + * \param observables are observables whose emissions would be combined with + the current observable's emissions + * \return new specific_observable with the combine_latest operator as most + recent operator. + * \warning #include + * + * \par Examples + * \snippet combine_latest.cpp combine_latest custom combiner + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store last emissions. + * - Wraps subscriber with serialization logic to be sure callbacks called + serialized + * - OnNext + * - Keeps last emission from each observable + * - Emits `std::tuple` of last emissions if there is last emissions for + each observable + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/combinelatest.html + */ + template + auto combine_latest(TOtherObservable&&... observables) const& + requires is_header_included + { + return cast_this()->combine_latest( + utils::pack_to_tuple{}, std::forward(observables)...); + } + + template + auto combine_latest(TOtherObservable&&... observables) && + requires is_header_included + { + return move_this().combine_latest( + utils::pack_to_tuple{}, std::forward(observables)...); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; + +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/concat.hpp b/symmetri/gui/rpp/rpp/operators/fwd/concat.hpp new file mode 100644 index 0000000..5af0bad --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/concat.hpp @@ -0,0 +1,158 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // constraint::observable_of_type +#include // member_overload + +namespace rpp::details { +struct concat_tag; +} + +namespace rpp::details { +template +struct concat_impl; + +template ... TObservables> +auto concat_with_impl(TObservables&&... observables); + +template +struct member_overload { + /** + * \brief Converts observable of observables of items into observable of items + via merging emissions but without overlapping (current observable completes + THEN next started to emit its values) + * + * \marble concat + { + source observable : + { + +--1-2-3-| + .....+4--6-| + } + operator "concat" : +--1-2-3-4--6-| + } + * + * \details Actually it subscribes on first observable from emissions. When + first observable completes, then it subscribes on second observable from + emissions and etc... + * + * \return new specific_observable with the concat operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet concat.cpp concat + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store observables (== emissions) and + some internal variables + * - Wraps subscriber with serialization logic to be sure callbacks called + serialized + * - OnNext for original observable + * - If no any active observable, then subscribes on new obtained + observable, else place it in queue + * - OnError + * - Just forwards original on_error + * - OnCompleted from original observable + * - Just forwards original on_completed if no any active observable (else + we need to processa all observables from queue and they would emit + on_completed for subscriber) + * - OnCompleted from inner observable + * - Subscribe on next observable from queue (if any) + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/concat.html + */ + template + auto concat() const& + requires(is_header_included && + rpp::constraint::observable) + { + return static_cast(this) + ->template lift>( + concat_impl{}); + } + + template + auto concat() && + requires(is_header_included&& + rpp::constraint::observable) { + return std::move(*static_cast(this)) + .template lift>( + concat_impl{}); + } + + /** + * \brief Combines submissions from current observable with other + observables into one but without overlapping (current observable completes + THEN next started to emit its values) + * + * \marble concat_with + { + source original_observable: +--1-2-3-| + source second: +-4--6-| + operator "concat_with" : +--1-2-3--4--6-| + } + * + * \details Actually this operator subscribes on original observable. When + original observable completes, then it subscribes on first observable from + arguments and etc... + * + * \return new specific_observable with the concat operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet concat.cpp concat_with + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store observables (== emissions) and + some internal variables + * - Wraps subscriber with serialization logic to be sure callbacks + called serialized + * - OnNext + * - Just forwards on_next + * - OnError + * - Just forwards original on_error + * - OnCompleted from original observable + * - Just forwards original on_completed if no any active observable + (else we need to processa all observables from queue and they would emit + on_completed for subscriber) + * - OnCompleted from inner observable + * - Subscribe on next observable from queue (if any) + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/concat.html + */ + template ... TObservables> + auto concat_with(TObservables&&... observables) const& + requires(is_header_included && + sizeof...(TObservables) >= 1) + { + return concat_with_impl( + static_cast(this)->as_dynamic(), + std::forward(observables).as_dynamic()...); + } + + template ... TObservables> + auto concat_with(TObservables&&... observables) && + requires(is_header_included && + sizeof...(TObservables) >= 1) { + return concat_with_impl( + std::move(*static_cast(this)).as_dynamic(), + std::forward(observables).as_dynamic()...); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/debounce.hpp b/symmetri/gui/rpp/rpp/operators/fwd/debounce.hpp new file mode 100644 index 0000000..48741b9 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/debounce.hpp @@ -0,0 +1,88 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct debounce_tag; +} + +namespace rpp::details { +template +struct debounce_impl; + +template +struct member_overload { + /** + * \brief Only emit emission if specified period of time has passed without + any other emission. On each new emission timer reset. + * + * \marble debounce + { + source observable : +--1-2-----3---| + operator "debounce(4)" : +--------2-----3| + } + * + * \details Actually this operator resets time of last emission, schedules + action to send this emission after specified period if no any new emissions + till this moment. + * + * \param period is duration of time should be passed since emission from + original observable without any new emissions to emit this emission. + * \param scheduler is scheduler used to run timer for debounce + * \return new specific_observable with the debounce operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet debounce.cpp debounce + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store last emission and time. + * - Wraps subscriber with serialization logic to prevent race-conditions + * - OnNext + * - Saves time when emission happened + * - Saves emission + * - Schedule action to send this emission with check if no any new + emissions + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * - Immediately send current active emission if any + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/debounce.html + */ + template + auto debounce(schedulers::duration period, + const TScheduler& scheduler = TScheduler{}) const& + requires is_header_included + { + return static_cast(this)->template lift( + debounce_impl{period, scheduler}); + } + + template + auto debounce(schedulers::duration period, + const TScheduler& scheduler = TScheduler{}) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift( + debounce_impl{period, scheduler}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/delay.hpp b/symmetri/gui/rpp/rpp/operators/fwd/delay.hpp new file mode 100644 index 0000000..8a30ecc --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/delay.hpp @@ -0,0 +1,102 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include // member_overload +#include // schedulers::constraint::scheduler + +namespace rpp::details { +struct delay_tag; +} + +namespace rpp::details { +template +struct delay_impl; + +template +struct member_overload { + /** + * \brief Shift the emissions from an Observable forward in time by a + particular amount. + * \details The delay operator modifies its source Observable by pausing for a + particular increment of time (that you specify) before emitting each of the + source Observable’s items. This has the effect of shifting the entire + sequence of items emitted by the Observable forward in time by that specified + increment. + * + * \marble delay + { + source observable : +-1-2-3-| + operator "delay: --" : +---1-2-3-| + } + * + * \details Actually this operator just schedules emissions via provided + scheduler with provided delay_duration. + * + * \param delay_duration is the delay duration for emitting items. Delay + duration should be able to cast to rpp::schedulers::duration. + * \param scheduler provides the threading model for delay. e.g. With a new + thread scheduler, the observer sees the values in a new thread after a delay + duration to the subscription. + * \return new specific_observable with the delay operator as most recent + operator. + * \warning #include + * + * \par Examples + * \snippet delay.cpp delay + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store internal state + * - OnNext + * - Move emission to queue and schedule action to drain queue (if not yet) + * - OnError + * - Just forwards original on_error via scheduling + * - OnCompleted + * - Just forwards original on_completed via scheduling + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/delay.html + */ + template + auto delay(auto&& delay_duration, TScheduler&& scheduler) const& + requires is_header_included + { + return cast_this()->template lift( + delay_impl>{ + std::forward(scheduler), + std::chrono::duration_cast( + delay_duration)}); + } + + template + auto delay(auto&& delay_duration, TScheduler&& scheduler) && + requires is_header_included + { + return move_this().template lift( + delay_impl>{ + std::forward(scheduler), + std::chrono::duration_cast( + delay_duration)}); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; + +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/distinct_until_changed.hpp b/symmetri/gui/rpp/rpp/operators/fwd/distinct_until_changed.hpp new file mode 100644 index 0000000..3837beb --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/distinct_until_changed.hpp @@ -0,0 +1,85 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include // member_overload + +namespace rpp::details { +struct distinct_until_changed_tag; +} + +namespace rpp::details { +template EqualityFn> +struct distinct_until_changed_impl; + +template +struct member_overload { + /** + * \brief Suppress consecutive duplicates of emissions from original + observable + * + * \marble distinct_until_changed + { + source observable : +--1-1-2-2-3-2-1-| + operator "distinct_until_changed" : +--1---2---3-2-1-| + } + * + * \details Actually this operator has `std::optional` with last item and + checks everytime where new emission is same or not. + * + * \param equality_fn optional equality comparator function + * \return new specific_observable with the distinct_until_changed operator as + most recent operator. + * \warning #include + * + * \par Example + * \snippet distinct_until_changed.cpp distinct_until_changed + * \snippet distinct_until_changed.cpp distinct_until_changed_with_comparator + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store last emission + * - OnNext + * - Checks if value in state same as new emission + * - If new emission is not same, then updates state and emit this emission + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup filtering_operators + * \see https://reactivex.io/documentation/operators/distinct.html + */ + template < + std::equivalence_relation EqualityFn = std::equal_to> + auto distinct_until_changed(EqualityFn&& equality_fn = EqualityFn{}) const& + requires is_header_included + { + return static_cast(this)->template lift( + distinct_until_changed_impl>{ + std::forward(equality_fn)}); + } + + template < + std::equivalence_relation EqualityFn = std::equal_to> + auto distinct_until_changed(EqualityFn&& equality_fn = EqualityFn{}) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift( + distinct_until_changed_impl>{ + std::forward(equality_fn)}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/do.hpp b/symmetri/gui/rpp/rpp/operators/fwd/do.hpp new file mode 100644 index 0000000..ebe2ab7 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/do.hpp @@ -0,0 +1,220 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // member_overload +#include // constraint::observer_of_type + +namespace rpp::details { +struct do_tag; +} + +namespace rpp::details { +template TObs> +struct do_impl; + +template +struct member_overload { + /** + * \brief Register an observer to be called when observable provides any + * events (on_next/on_error/on_completed) + * + * \note Callbacks from `tap` would be invoked BEFORE subscribed subscriber + * + * \param observer - observer which would accept callbacks + * + * \return new specific_observable with the tap operator as most recent + * operator. \warning #include + * + * \par Example + * \snippet do.cpp tap_observer + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/do.html + */ + template TObs> + auto tap(TObs&& observer) const& + requires is_header_included + { + return static_cast(this)->template lift( + do_impl>{std::forward(observer)}); + } + + template TObs> + auto tap(TObs&& observer) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift( + do_impl>{std::forward(observer)}); + } + + /** + * \brief Register an list of actions to be called when observable provides + * any events (on_next/on_error/on_completed) + * + * \note Callbacks from `tap` would be invoked BEFORE subscribed subscriber + * + * \param on_next - action over new emitted item + * \param on_error - action over std::exception_ptr in case of any error + * \param on_completed - action in case of completion + * + * \return new specific_observable with the tap operator as most recent + * operator. \warning #include + * + * \par Example + * \snippet do.cpp tap_callbacks + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/do.html + */ + template < + constraint::on_next_fn OnNextFn, + constraint::on_error_fn OnErrorFn = + utils::empty_function_t, + constraint::on_completed_fn OnCompletedFn = utils::empty_function_t<>> + auto tap(OnNextFn&& on_next, OnErrorFn&& on_error = OnErrorFn{}, + OnCompletedFn&& on_completed = OnCompletedFn{}) const& + requires is_header_included + { + using TObserver = + specific_observer_with_decayed_args; + return static_cast(this)->tap(TObserver{ + std::forward(on_next), std::forward(on_error), + std::forward(on_completed)}); + } + + template < + constraint::on_next_fn OnNextFn, + constraint::on_error_fn OnErrorFn = + utils::empty_function_t, + constraint::on_completed_fn OnCompletedFn = utils::empty_function_t<>> + auto tap(OnNextFn&& on_next, OnErrorFn&& on_error = OnErrorFn{}, + OnCompletedFn&& on_completed = OnCompletedFn{}) && + requires is_header_included + { + using TObserver = + specific_observer_with_decayed_args; + return std::move(*static_cast(this)) + .tap(TObserver{std::forward(on_next), + std::forward(on_error), + std::forward(on_completed)}); + } + + /** + * \brief Register an callback to be called when observable provides new item + * (on_next) + * + * \note on_next from `tap` would be invoked BEFORE on_next from subscriber + * + * \param on_next - action over new emitted item + * + * \return new specific_observable with the do_on_next operator as most recent + * operator. \warning #include + * + * \par Example + * \snippet do.cpp do_on_next + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/do.html + */ + template OnNextFn> + auto do_on_next(OnNextFn&& on_next) const& + requires is_header_included + { + return static_cast(this)->tap( + std::forward(on_next)); + } + + template OnNextFn> + auto do_on_next(OnNextFn&& on_next) && + requires is_header_included + { + return std::move(*static_cast(this)) + .tap(std::forward(on_next)); + } + + /** + * \brief Register an callback to be called when observable provides error + * (on_error) + * + * \note on_error from `tap` would be invoked BEFORE on_error from subscriber + * + * \param on_error - action over std::exception_ptr in case of any error + * + * \return new specific_observable with the do_on_error operator as most + * recent operator. \warning #include + * + * \par Example + * \snippet do.cpp do_on_error + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/do.html + */ + template + auto do_on_error(OnErrorFn&& on_error) const& + requires is_header_included + { + return static_cast(this)->tap( + utils::empty_function_t{}, std::forward(on_error)); + } + + template + auto do_on_error(OnErrorFn&& on_error) && + requires is_header_included + { + return std::move(*static_cast(this)) + .tap(utils::empty_function_t{}, + std::forward(on_error)); + } + + /** + * \brief Register an callback to be called when observable provides complete + * (on_completed) + * + * \note on_completed from `tap` would be invoked BEFORE on_completed from + * subscriber + * + * \param on_completed - action in case of completion + * + * \return new specific_observable with the do_on_completed operator as most + * recent operator. \warning #include + * + * \par Example + * \snippet do.cpp do_on_completed + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/do.html + */ + template + auto do_on_completed(OnCompletedFn&& on_completed) const& + requires is_header_included + { + return static_cast(this)->tap( + utils::empty_function_t{}, + utils::empty_function_t{}, + std::forward(on_completed)); + } + + template + auto do_on_completed(OnCompletedFn&& on_completed) && + requires is_header_included + { + return std::move(*static_cast(this)) + .tap(utils::empty_function_t{}, + utils::empty_function_t{}, + std::forward(on_completed)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/filter.hpp b/symmetri/gui/rpp/rpp/operators/fwd/filter.hpp new file mode 100644 index 0000000..53a303c --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/filter.hpp @@ -0,0 +1,78 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct filter_tag; +} + +namespace rpp::details { +template Predicate> +struct filter_impl; + +template +struct member_overload { + /** + * \brief Emit only those items from an Observable that satisfies a provided + predicate + * + * \marble filter + { + source observable : +--1-2-3-4-| + operator "filter: x=>x%2==0" : +----2---4-| + } + * + * \details Actually this operator just checks if predicate returns true, then + forwards emission + * + * \param predicate is predicate used to check emitted items. true -> items + satisfies condition, false -> not + * \return new specific_observable with the Filter operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet filter.cpp Filter + * + * \par Implementation details: + * - On subscribe + * - None + * - OnNext + * - Just forwards emission when predicate returns true + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup filtering_operators + * \see https://reactivex.io/documentation/operators/filter.html + */ + template Predicate> + auto filter(Predicate&& predicate) const& + requires is_header_included + { + return static_cast(this)->template lift( + filter_impl>{ + std::forward(predicate)}); + } + + template Predicate> + auto filter(Predicate&& predicate) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift(filter_impl>{ + std::forward(predicate)}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/first.hpp b/symmetri/gui/rpp/rpp/operators/fwd/first.hpp new file mode 100644 index 0000000..e44a3c7 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/first.hpp @@ -0,0 +1,84 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct first_tag; +} + +namespace rpp::details { + +template +struct first_impl; + +template +struct member_overload { + /** + * \brief emit only the first item. + * + * \marble first + { + source observable : +--1--2--3--| + operator "first" : +--1| + } + * + * \details Actually this operator is `take(1)` with exception during + `on_completed` if no any emision happens. So, it just forwards first obtained + emission and emits on_completed immediately + * \throws rpp::utils::not_enough_emissions in case of on_completed obtained + without any emissions + * + * \return new specific_observable with the first operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet first.cpp first + * \snippet first.cpp first_empty + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to keep internal state + * - OnNext + * - Just forwards 1 emission and emit on_completed + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - throws exception if no any emission before + * + * \ingroup filtering_operators + * \see https://reactivex.io/documentation/operators/first.html + */ + auto first() const& + requires is_header_included + { + return cast_this()->template lift(first_impl{}); + } + + auto first() && + requires is_header_included + { + return move_this().template lift(first_impl{}); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/flat_map.hpp b/symmetri/gui/rpp/rpp/operators/fwd/flat_map.hpp new file mode 100644 index 0000000..07e48ac --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/flat_map.hpp @@ -0,0 +1,77 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // constraint::observable +#include // member_overload +#include // decayed_invoke_result_t + +namespace rpp::details { +struct flat_map_tag; +} + +namespace rpp::details { +template +concept flat_map_callable = + std::invocable && + constraint::observable>; + +template Callable> +auto flat_map_impl(auto&& observable, Callable&& callable); + +template +struct member_overload { + /** + * \brief Transform emissions to observables via provided function and then + merge emissions from such an observables + * + * \warning According to observable contract + (https://reactivex.io/documentation/contract.html) emissions from any + observable should be serialized, so, resulting observable uses mutex to + satisfy this requirement + * + * \marble flat_map + { + source observable : +--1--2--3--| + operator "flat_map: x=>just(x,x+1)" : +--12-23-34-| + } + * + * \details Actually it makes `map(callable)` and then `merge`. + * + * \param callable Function to transform item to observable + * \return new specific_observable with the flat_map operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet flat_map.cpp flat_map + * + * \ingroup transforming_operators + * \see https://reactivex.io/documentation/operators/flatmap.html + */ + template Callable> + auto flat_map(Callable&& callable) const& + requires is_header_included + { + return flat_map_impl(*static_cast(this), + std::forward(callable)); + } + + template Callable> + auto flat_map(Callable&& callable) && + requires is_header_included + { + return flat_map_impl( + std::move(*static_cast(this)), + std::forward(callable)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/group_by.hpp b/symmetri/gui/rpp/rpp/operators/fwd/group_by.hpp new file mode 100644 index 0000000..da33994 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/group_by.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include + +namespace rpp::details { +struct group_by_tag; +} + +namespace rpp::details { +template KeySelector, std::invocable ValueSelector, + std::strict_weak_order KeyComparator> +auto group_by_impl(auto&& observable, KeySelector&& key_selector, + ValueSelector&& value_selector, KeyComparator&& comparator); + +template +struct member_overload { + /** + * \brief Divide original observable into multiple observables where each new + observable emits some group of values from original observable. + * + * \marble group_by + { + source observable : +--1-2-3-4-5-6-| + operator "group_by(x=>x%2==0)" : + { + ..+1---3---5---| + ....+2---4---6-| + } + } + * + * + * \details Actually this operator applies `key_selector` to emission to obtain + key, place rpp::grouped_observable to map with corresponding map and then send + observable with this key (if not yet). Original values emitted via this + grouped_observables + * + * \param key_selector Function which determines key for provided item + * \param value_selector Function which determines value to be emitted to + grouped observable + * \param comparator Function to provide strict_weak_order between key types + * + * \return new specific_observable with the group_by operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet group_by.cpp group_by + * \snippet group_by.cpp group_by selector + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to keep map + * - OnNext + * - Applies key_selector to obtained emission + * - For calculated key create new entry in map (if not yet) + * - Emit value via grouped_observable from map for corresponding key + * - OnError + * - Just forwards original on_error to both subscribers of observable of + grouped observables and grouped observables + * - OnCompleted + * - Just forwards original on_completed to both subscribers of observable + of grouped observables and grouped observables + * + * \ingroup transforming_operators + * \see https://reactivex.io/documentation/operators/groupby.html + */ + template < + std::invocable KeySelector, + std::invocable ValueSelector = std::identity, + typename TKey = rpp::utils::decayed_invoke_result_t, + std::strict_weak_order KeyComparator = std::less> + auto group_by(KeySelector&& key_selector, ValueSelector&& value_selector = {}, + KeyComparator&& comparator = {}) const& + requires is_header_included + { + return group_by_impl( + *static_cast(this), + std::forward(key_selector), + std::forward(value_selector), + std::forward(comparator)); + } + + template < + std::invocable KeySelector, + std::invocable ValueSelector = std::identity, + typename TKey = rpp::utils::decayed_invoke_result_t, + std::strict_weak_order KeyComparator = std::less> + auto group_by(KeySelector&& key_selector, + ValueSelector&& value_selector = {}, + KeyComparator&& comparator = {}) && + requires is_header_included + { + return group_by_impl( + std::move(*static_cast(this)), + std::forward(key_selector), + std::forward(value_selector), + std::forward(comparator)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/last.hpp b/symmetri/gui/rpp/rpp/operators/fwd/last.hpp new file mode 100644 index 0000000..c5c6e04 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/last.hpp @@ -0,0 +1,82 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct last_tag; +} + +namespace rpp::details { +template +struct last_impl; + +template +struct member_overload { + /** + * \brief Emit only the last item provided before on_completed. + * + * \marble last + { + source observable : +--1--2--3--| + operator "last" : +--3-| + } + * + * \details Actually this operator just updates `std::optional` on every new + emission and emits this value on_completed + * \throws rpp::utils::not_enough_emissions in case of on_completed obtained + without any emissions + * + * \return new specific_observable with the last operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet last.cpp last + * \snippet last.cpp last empty + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to keep `std::optional` + * - OnNext + * - Just saves emission to `std::optional` + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Emits saved emission or throws exception if no any emissions + * + * \ingroup filtering_operators + * \see https://reactivex.io/documentation/operators/last.html + */ + auto last() const& + requires is_header_included + { + return cast_this()->template lift(last_impl{}); + } + + auto last() && + requires is_header_included + { + return move_this().template lift(last_impl{}); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/lift.hpp b/symmetri/gui/rpp/rpp/operators/fwd/lift.hpp new file mode 100644 index 0000000..d3711e3 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/lift.hpp @@ -0,0 +1,210 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // member_overload +#include // constraint::subscriber +#include // extract_subscriber_type_t +#include // forwarding_on_error + +namespace rpp::details { +struct lift_tag; +} + +namespace rpp::details { +template +concept lift_fn = constraint::subscriber< + utils::decayed_invoke_result_t>>; + +template +struct lift_action_by_callbacks; + +template +using decayed_lift_action_by_callbacks = + lift_action_by_callbacks...>; + +template OperatorFn, + typename TObs> +auto lift_impl(OperatorFn&& op, TObs&& _this); + +template +struct member_overload { + /** + * \brief The lift operator provides ability to create your own operator and + * apply it to observable \tparam NewType manually specified new type of + * observable after applying of fn \param op represents operator logic in the + * form: accepts NEW subscriber and returns OLD subscriber \return new + * specific_observable of NewType + */ + template + auto lift(lift_fn auto&& op) const& + requires is_header_included + { + return details::lift_impl(std::forward(op), + CastThis()); + } + + template + auto lift(lift_fn auto&& op) && + requires is_header_included + { + return details::lift_impl(std::forward(op), + MoveThis()); + } + + // ********************************* LIFT OPERATOR: SUBSCRIBER -> SUBSCRIBER + // ******************// + /** + * \brief The lift operator provides ability to create your own operator and + * apply it to observable \tparam OperatorFn type of your custom functor + * \tparam NewType auto-deduced type of observable after applying of fn + * \param op represents operator logic in the form: accepts NEW subscriber and + * returns OLD subscriber \return new specific_observable of NewType + */ + template >> + auto lift(OperatorFn&& op) const& + requires(details::lift_fn && + is_header_included) + { + return details::lift_impl(std::forward(op), + CastThis()); + } + template >> + auto lift(OperatorFn&& op) && + requires(details::lift_fn&& + is_header_included) { + return details::lift_impl(std::forward(op), + MoveThis()); + } + + // ********************************* LIFT Direct type + OnNext, Onerror, + // OnCompleted ******************// + + /** + * \brief The lift operator provides ability to create your own operator + * and apply it to observable. \details This overload provides this + * ability via providing on_next, on_eror and on_completed with 2 params: + * old type of value + new subscriber \tparam NewType manually specified + * new type of observable after lift \tparam OnNext on_next of new + * subscriber accepting old value + new subscriber (logic how to transfer + * old value to new subscriber) \tparam OnError on_error of new subscriber + * accepting exception + new subscriber \tparam OnCompleted on_completed + * of new subscriber accepting new subscriber \return new + * specific_observable of NewType + */ + template > OnNext, + std::invocable> + OnError = utils::forwarding_on_error, + std::invocable> OnCompleted = + utils::forwarding_on_completed> + auto lift(OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) const& + requires is_header_included + { + return details::lift_impl( + details::decayed_lift_action_by_callbacks{ + std::forward(on_next), std::forward(on_error), + std::forward(on_completed)}, + CastThis()); + } + + template > OnNext, + std::invocable> + OnError = utils::forwarding_on_error, + std::invocable> OnCompleted = + utils::forwarding_on_completed> + auto lift(OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) && + requires is_header_included + { + return details::lift_impl( + details::decayed_lift_action_by_callbacks{ + std::forward(on_next), std::forward(on_error), + std::forward(on_completed)}, + MoveThis()); + } + + // ********************************* LIFT OnNext, Onerror, OnCompleted + // ******************// + + /** + * \brief The lift operator provides ability to create your own operator and + * apply it to observable. \details This overload provides this ability via + * providing on_next, on_eror and on_completed with 2 params: old type of + * value + new subscriber \tparam OnNext on_next of new subscriber accepting + * old value + new subscriber \tparam OnError on_error of new subscriber + * accepting exception + new subscriber \tparam OnCompleted on_completed of + * new subscriber accepting new subscriber \return new specific_observable of + * NewType + */ + template >>, + std::invocable> + OnError = utils::forwarding_on_error, + std::invocable> OnCompleted = + utils::forwarding_on_completed> + requires std::invocable> + auto lift(OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) const& + requires is_header_included + { + return details::lift_impl( + details::decayed_lift_action_by_callbacks{ + std::forward(on_next), std::forward(on_error), + std::forward(on_completed)}, + CastThis()); + } + + template >>, + std::invocable> + OnError = utils::forwarding_on_error, + std::invocable> OnCompleted = + utils::forwarding_on_completed> + requires std::invocable> + auto lift(OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) && + requires is_header_included + { + return details::lift_impl( + details::decayed_lift_action_by_callbacks{ + std::forward(on_next), std::forward(on_error), + std::forward(on_completed)}, + MoveThis()); + } + + private: + const SpecificObservable& CastThis() const { + return *static_cast(this); + } + + SpecificObservable&& MoveThis() { + return std::move(*static_cast(this)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/map.hpp b/symmetri/gui/rpp/rpp/operators/fwd/map.hpp new file mode 100644 index 0000000..4273319 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/map.hpp @@ -0,0 +1,86 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct map_tag; +} + +namespace rpp::details { +template Callable> +struct map_impl; + +template +struct member_overload { + /** + * \brief Transform the items emitted by an Observable via applying a function + to each item and emitting result + * \note The Map operator can keep same type of value or change it to some + another type. + * + * \marble map + { + source observable : +--1 -2 --3 -| + operator "map: x=>x+10" : +--(11)-(12)--(13)-| + } + * + * \details Actually this operator just applies callable to each obtained + emission and emit resulting value + * + * \param callable is callable used to provide this transformation. Should + accept Type of original observable and return type for new observable + * \return new specific_observable with the Map operator as most recent + operator. + * \warning #include + * + * \par Example with same type: + * \snippet map.cpp Same type + * + * \par Example with changed type: + * \snippet map.cpp Changed type + * + * \par Implementation details: + * - On subscribe + * - None + * - OnNext + * - Just forwards result of applying callable to emissions + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup transforming_operators + * \see https://reactivex.io/documentation/operators/map.html + */ + template Callable> + auto map(Callable&& callable) const& + requires is_header_included + { + return static_cast(this) + ->template lift>( + map_impl>{ + std::forward(callable)}); + } + + template Callable> + auto map(Callable&& callable) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift>( + map_impl>{ + std::forward(callable)}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/merge.hpp b/symmetri/gui/rpp/rpp/operators/fwd/merge.hpp new file mode 100644 index 0000000..2353da3 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/merge.hpp @@ -0,0 +1,159 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct merge_tag; +} + +namespace rpp::details { +template +struct merge_impl; + +template ... TObservables> +auto merge_with_impl(TObservables&&... observables); + +template +struct member_overload { + /** + * \brief Converts observable of observables of items into observable of items + via merging emissions. + * + * \warning According to observable contract + (https://reactivex.io/documentation/contract.html) emissions from any + observable should be serialized, so, resulting observable uses mutex to + satisfy this requirement + * + * \marble merge + { + source observable : + { + +--1-2-3-| + .....+4--6-| + } + operator "merge" : +--1-243-6-| + } + * + * \details Actually it subscribes on each observable from emissions. Resulting + observables completes when ALL observables completes + * + * \return new specific_observable with the merge operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet merge.cpp merge + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store interal state + * - Wraps subscriber with serialization logic to be sure callbacks called + serialized + * - OnNext for original observable + * - Subscribes on obtained observable + * - OnNext for inner observable + * - Just forwards original on_next + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed when all observables emit + on_completed + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/merge.html + */ + template + auto merge() const& + requires(is_header_included && + rpp::constraint::observable) + { + return static_cast(this) + ->template lift>( + merge_impl{}); + } + + template + auto merge() && + requires(is_header_included&& + rpp::constraint::observable) { + return std::move(*static_cast(this)) + .template lift>( + merge_impl{}); + } + + /** + * \brief Combines submissions from current observable with other + observables into one + * + * \warning According to observable contract + (https://reactivex.io/documentation/contract.html) emissions from any + observable should be serialized, so, resulting observable uses mutex to + satisfy this requirement + * + * \marble merge_with + { + source original_observable: +--1-2-3-| + source second: +-----4--6-| + operator "merge_with" : +--1-243-6-| + } + * + * \details Actually it subscribes on each observable. Resulting + observables completes when ALL observables completes + * + * \param observables are observables whose emissions would be merged with + current observable + * \return new specific_observable with the merge operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet merge.cpp merge_with + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store interal state + * - Wraps subscriber with serialization logic to be sure callbacks + called serialized + * - OnNext + * - Just forwards original on_next + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed when all observables emit + on_completed + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/merge.html + */ + template ... TObservables> + auto merge_with(TObservables&&... observables) const& + requires(is_header_included && + sizeof...(TObservables) >= 1) + { + return merge_with_impl( + static_cast(this)->as_dynamic(), + std::forward(observables).as_dynamic()...); + } + + template ... TObservables> + auto merge_with(TObservables&&... observables) && + requires(is_header_included && + sizeof...(TObservables) >= 1) { + return merge_with_impl( + std::move(*static_cast(this)).as_dynamic(), + std::forward(observables).as_dynamic()...); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/multicast.hpp b/symmetri/gui/rpp/rpp/operators/fwd/multicast.hpp new file mode 100644 index 0000000..b9ea89f --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/multicast.hpp @@ -0,0 +1,63 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +namespace rpp::details { +struct multicast_tag; +} + +namespace rpp::details { +template TObs, + subjects::constraint::subject_of_type TSubject> +auto multicast_impl(TObs&& observable, TSubject&& subject); + +template +struct member_overload { + /** + * \brief Converts ordinary observable to rpp::connectable_observable with + * help of provided subject \details Connectable observable is common + * observable, but actually it starts emissions of items only after call + * "connect", "ref_count" or any other available way. Also it uses subject to + * multicast values to subscribers + * + * \param subject is subject used to create rpp::connectable_observable + * \return new specific_observable with the multicast operator as most recent + * operator. \warning #include + * + * \par Example + * \snippet multicast.cpp multicast + * + * \ingroup connectable_operators + * \see https://reactivex.io/documentation/operators/publish.html + */ + template TSubject> + auto multicast(TSubject&& subject) const& + requires is_header_included + { + return multicast_impl(*static_cast(this), + std::forward(subject)); + } + + template TSubject> + auto multicast(TSubject&& subject) && + requires is_header_included + { + return multicast_impl( + std::move(*static_cast(this)), + std::forward(subject)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/observe_on.hpp b/symmetri/gui/rpp/rpp/operators/fwd/observe_on.hpp new file mode 100644 index 0000000..224fe55 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/observe_on.hpp @@ -0,0 +1,79 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include + +namespace rpp::details { +struct observe_on_tag; +} + +namespace rpp::details { +template +struct observe_on_impl; + +template +struct member_overload { + /** + * \brief Emit emissions of observable starting from this point via provided + * scheduler + * + * \details Actually this operator just schedules emissions via provided + * scheduler. So, actually it is delay(0) operator + * + * \param scheduler is scheduler used for scheduling of OnNext + * \return new specific_observable with the observe_on operator as most recent + * operator. \warning #include + * + * \par Example: + * \snippet observe_on.cpp observe_on + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store internal state + * - OnNext + * - Move emission to queue and schedule action to drain queue (if not yet) + * - OnError + * - Just forwards original on_error via scheduling + * - OnCompleted + * - Just forwards original on_completed via scheduling + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/observeon.html + */ + template + auto observe_on(TScheduler&& scheduler) const& + requires is_header_included + { + return cast_this()->delay(schedulers::duration{0}, + std::forward(scheduler)); + } + + template + auto observe_on(TScheduler&& scheduler) && + requires is_header_included + { + return move_this().delay(schedulers::duration{0}, + std::forward(scheduler)); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/on_error_resume_next.hpp b/symmetri/gui/rpp/rpp/operators/fwd/on_error_resume_next.hpp new file mode 100644 index 0000000..7248ae0 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/on_error_resume_next.hpp @@ -0,0 +1,107 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include +#include + +namespace rpp::details { +struct on_error_resume_next_tag; +} + +namespace rpp::details { + +template +concept resume_callable = + std::invocable && + constraint::observable< + utils::decayed_invoke_result_t>; + +template +struct on_error_resume_next_impl; + +template +struct member_overload { + /** + * \brief Recover from an on_error notification by continuing the sequence + without error. + * \details The operator intercepts an `on_error` notification from the source + Observable and, instead of passing it through to any observers, replaces it + with some other item or sequence of items. + * \warning This operator potentially allows the resulting Observable to + terminate normally or not to terminate at all. + * + * \marble on_error_resume_next + { + source observable : +-1-# + operator "on_error_resume_next: -9-9-|" : +-1-9-9-| + } + * + * \details Actually this operator just subscribes on observable calculated + from callback when `on_error` from original observable obtained. + * + * \param resume_callable A callable that is given an error pointer and shall + return an Observable. + * \return new specific_observable with the on_error_resume_next operator as + most recent operator. + * \warning #include + * + * \par Examples + * \snippet on_error_resume_next.cpp on_error_resume_next + * + * \par Implementation details: + * - On subscribe + * - None + * - OnNext + * - Just forwards original on_next + * - OnError from original observable + * - Subscribes subscriber on observable obtained from callable + * - OnError from calculated observable + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup error_handling_operators + * \see https://reactivex.io/documentation/operators/on_error_resume_next.html + */ + template + auto on_error_resume_next(ResumeCallable&& resume_callable) const& + requires is_header_included + { + return cast_this()->template lift( + on_error_resume_next_impl>{ + std::forward(resume_callable)}); + } + + template + auto on_error_resume_next(ResumeCallable&& resume_callable) && + requires is_header_included + { + return move_this().template lift( + on_error_resume_next_impl>{ + std::forward(resume_callable)}); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; + +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/publish.hpp b/symmetri/gui/rpp/rpp/operators/fwd/publish.hpp new file mode 100644 index 0000000..110ba5b --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/publish.hpp @@ -0,0 +1,58 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct publish_tag; +} + +namespace rpp::details { +template TObs> +auto publish_impl(TObs&& observable); + +template +struct member_overload { + /** + * \brief Converts ordinary observable to rpp::connectable_observable with + * help of rpp::subjects::publish_subject \details Connectable observable is + * common observable, but actually it starts emissions of items only after + * call "connect", "ref_count" or any other available way. Also it uses + * subject to multicast values to subscribers + * + * \return new specific_observable with the publish operator as most recent + * operator. \warning #include + * + * \par Example + * \snippet publish.cpp publish + * + * \ingroup connectable_operators + * \see https://reactivex.io/documentation/operators/publish.html + */ + template + auto publish() const& + requires is_header_included + { + return publish_impl(*static_cast(this)); + } + + template + auto publish() && + requires is_header_included + { + return publish_impl( + std::move(*static_cast(this))); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/reduce.hpp b/symmetri/gui/rpp/rpp/operators/fwd/reduce.hpp new file mode 100644 index 0000000..f4b7214 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/reduce.hpp @@ -0,0 +1,326 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct reduce_tag; +} + +namespace rpp::details { +// accepts Result and Type and returns Result +template +concept reduce_accumulator = + std::is_invocable_r_v, Fn, std::decay_t, + std::decay_t>; + +template +concept is_can_be_summed = requires(T t) { + { t + t } -> std::convertible_to; + }; + +template +concept is_can_be_averaged = + is_can_be_summed && requires(CastBeforeDrop nt) { + { nt / size_t{} }; + }; + +template AccumulatorFn, + std::invocable ResultSelectorFn> +struct reduce_impl; + +template +auto average_impl(TObs&& observable); + +template +auto sum_impl(TObs&& observable); + +template +auto count_impl(TObs&& observable); + +template +auto min_impl(TObs&& observable, Comparator&& comparator); + +template +auto max_impl(TObs&& observable, Comparator&& comparator); + +template +struct member_overload { + /** + * \brief Applies accumulator function to each emission from observable and + result of accumulator from previous step and emits final value + * + * \marble reduce + { + source observable : +--1-2-3-| + operator "reduce: s=1, (s,x)=>s+x" : +--------7| + } + * + * \details Actually this operator behaves like `scan()` + `take_last(1)`, so, + it just accumulates `seed` and emits it `on_completed` + * + * \param initial_seed initial value for seed which will be applied for first + value from observable. Then it will be replaced with result and etc. + * \param accumulator function which accepts seed value and new value from + observable and return new value of seed. Can accept seed by move-reference. + * + * \return new specific_observable with the reduce operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet reduce.cpp reduce + * \snippet reduce.cpp reduce_vector + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store internal state + * - OnNext + * - Applies accumulator to each emission + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Emits accumulated seed via applyting result_selector + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/reduce.html + */ + template AccumulatorFn, + std::invocable ResultSelectorFn = std::identity> + auto reduce(Seed&& initial_seed, AccumulatorFn&& accumulator, + ResultSelectorFn&& result_selector = {}) const& + requires is_header_included + { + return static_cast(this) + ->template lift>>( + reduce_impl, std::decay_t, + std::decay_t>{ + std::forward(initial_seed), + std::forward(accumulator), + std::forward(result_selector)}); + } + + template AccumulatorFn, + std::invocable ResultSelectorFn = std::identity> + auto reduce(Seed&& initial_seed, AccumulatorFn&& accumulator, + ResultSelectorFn&& result_selector = {}) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift>>( + reduce_impl, std::decay_t, + std::decay_t>{ + std::forward(initial_seed), + std::forward(accumulator), + std::forward(result_selector)}); + } + + /** + * \brief Calculates the average of emissions and emits final value + * + * \marble average + { + source observable : +--1-2-3-| + operator "average" : +--------2| + } + * + * \tparam CastBeforeDivide cast accumulated value to this type before + division + * \return new specific_observable with the average operator as most recent + operator. + * \throws rpp::utils::not_enough_emissions in case of no any emissions from + original observable + * + * \warning #include + * + * \par Example + * \snippet reduce.cpp average + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/average.html + */ + template + auto average() const& + requires(is_header_included && + is_can_be_averaged) + { + return average_impl( + *static_cast(this)); + } + + template + auto average() && + requires(is_header_included&& + is_can_be_averaged) { + return average_impl( + std::move(*static_cast(this))); + } + + /** + * \brief Calculates the sum of emissions and emits final value + * + * \marble sum + { + source observable : +--1-2-3-| + operator "sum" : +--------6| + } + * + * \return new specific_observable with the sum operator as most recent + operator. + * \throws rpp::utils::not_enough_emissions in case of no any emissions + from original observable + * + * \warning #include + * + * \par Example + * \snippet reduce.cpp sum + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/sum.html + */ + template + auto sum() const& + requires(is_header_included && + is_can_be_summed) + { + return sum_impl(*static_cast(this)); + } + + template + auto sum() && + requires( + is_header_included&& is_can_be_summed) { + return sum_impl(std::move(*static_cast(this))); + } + + /** + * \brief Calculates the amount of emitted emissions and emits this count + * + * \marble count + { + source observable : +--1-2-3-| + operator "count" : +--------3| + } + * + * \return new specific_observable with the count operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet reduce.cpp count + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/count.html + */ + template + auto count() const& + requires is_header_included + { + return count_impl(*static_cast(this)); + } + + template + auto count() && + requires is_header_included + { + return count_impl(std::move(*static_cast(this))); + } + + /** + * \brief Emits the emission which has minimal value from the whole observable + * + * \marble min + { + source observable : +-6-1-2-3-| + operator "min" : +---------1| + } + * + * \param comparator is function to deduce if left value is less than right + * \return new specific_observable with the min operator as most recent + operator. + * \throws rpp::utils::not_enough_emissions in case of no any emissions from + original observable + * + * \warning #include + * + * \par Example + * \snippet reduce.cpp min + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/min.html + */ + template Comparator = std::less, + typename... Args> + auto min(Comparator&& comparator = {}) const& + requires is_header_included + { + return min_impl(*static_cast(this), + std::forward(comparator)); + } + + template Comparator = std::less, + typename... Args> + auto min(Comparator&& comparator = {}) && + requires is_header_included + { + return min_impl(std::move(*static_cast(this)), + std::forward(comparator)); + } + + /** + * \brief Emits the emission which has maximal value from the whole observable + * + * \marble max + { + source observable : +-6-1-2-3-| + operator "max" : +---------6| + } + * + * \param comparator is function to deduce if left value is less than right + * \return new specific_observable with the max operator as most recent + operator. + * \throws rpp::utils::not_enough_emissions in case of no any emissions from + original observable + * + * \warning #include + * + * \par Example + * \snippet reduce.cpp max + * + * \ingroup aggregate_operators + * \see https://reactivex.io/documentation/operators/max.html + */ + template Comparator = std::less, + typename... Args> + auto max(Comparator&& comparator = {}) const& + requires is_header_included + { + return max_impl(*static_cast(this), + std::forward(comparator)); + } + + template Comparator = std::less, + typename... Args> + auto max(Comparator&& comparator = {}) && + requires is_header_included + { + return max_impl(std::move(*static_cast(this)), + std::forward(comparator)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/ref_count.hpp b/symmetri/gui/rpp/rpp/operators/fwd/ref_count.hpp new file mode 100644 index 0000000..89a9bce --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/ref_count.hpp @@ -0,0 +1,56 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct ref_count_tag; +} + +namespace rpp::details { +template TObs> +auto ref_count_impl(TObs&& observable); + +template +struct member_overload { + /** + * \brief Forces rpp::connectable_observable to behave like common observable + * \details Connects rpp::connectable_observable on the first subscription and + * unsubscribes on last unsubscription + * + * \return new specific_observable with the ref_count operator as most recent + * operator. \warning #include + * + * \par Example + * \snippet ref_count.cpp ref_count + * + * \ingroup connectable_operators + * \see https://reactivex.io/documentation/operators/refcount.html + */ + template + auto ref_count() const& + requires is_header_included + { + return ref_count_impl(*static_cast(this)); + } + + template + auto ref_count() && + requires is_header_included + { + return ref_count_impl( + std::move(*static_cast(this))); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/repeat.hpp b/symmetri/gui/rpp/rpp/operators/fwd/repeat.hpp new file mode 100644 index 0000000..8e9cf56 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/repeat.hpp @@ -0,0 +1,120 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct repeat_tag; +} + +namespace rpp::details { +template TObs> +auto repeat_impl(TObs&& observable, size_t count); + +template TObs> +auto repeat_impl(TObs&& observable); + +template +struct member_overload { + /** + * \brief Re-subscribes on current observable provided amount of times when + `on_completed` obtained + * + * \marble repeat + { + source observable : +-1-2-3-| + operator "repeat(2)" : +-1-2-3-1-2-3-| + } + * + * \details Actually this operator re-subscribes on same observable when + `on_completed` obtained while counter not reached zero + * + * \param count total amount of times subscription happens. For example: + * - `count(0)` - means no any subscription at all + * - `count(1)` - behave like ordinal observable + * - `count(10)` - 1 normal subscription and 9 re-subscriptions during + on_completed + * \return new specific_observable with the repeat operator as most recent + operator. + * \warning #include + * + * \par Examples: + * \snippet repeat.cpp repeat + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store counter + * - OnNext + * - Just forwards original on_next + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Decrements counter + * - If counter not zero, then re-subscribes on the same observable + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/repeat.html + */ + template + auto repeat(size_t count) const& + requires is_header_included + { + return repeat_impl(*static_cast(this), + count); + } + + template + auto repeat(size_t count) && + requires is_header_included + { + return repeat_impl(std::move(*static_cast(this)), + count); + } + + /** + * \brief Re-subscribes on current observable during `on_completed` infinitely + * + * \marble repeat_infinitely + { + source observable : +-1-2-3-| + operator "repeat" : +-1-2-3-1-2-3-1-2-3> + } + * + * \return new specific_observable with the repeat operator as most recent + operator. + * \warning #include + * + * \par Examples: + * \snippet repeat.cpp repeat_infinitely + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/repeat.html + */ + template + auto repeat() const& + requires is_header_included + { + return repeat_impl(*static_cast(this)); + } + + template + auto repeat() && + requires is_header_included + { + return repeat_impl( + std::move(*static_cast(this))); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/sample.hpp b/symmetri/gui/rpp/rpp/operators/fwd/sample.hpp new file mode 100644 index 0000000..5a1ae0b --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/sample.hpp @@ -0,0 +1,88 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +namespace rpp::details { +struct sample_tag; +} + +namespace rpp::details { +template +struct sample_with_time_impl; + +template +struct member_overload { + /** + * \brief Emit most recent emitted from original observable emission obtained + during last period of time. + * \details Emit item immediately in case of completion of the original + observable + * + * \marble sample_with_time + { + source observable : +--1---2-3-4---5-6-7-| + operator "sample_with_time(2)" : +--1---2---4---5---7-| + } + * + * \details Actually operator just schedules periodical action and on each + schedulable execution just emits last emitted emission (if any) + * + * \param period sampling period + * \scheduler scheduler to use to schedule emissions with provided sampling + period + * \return new specific_observable with the sample_with_time operator as most + recent operator. + * \warning #include + * + * \par Example + * \snippet sample.cpp sample_with_time + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store last emitted value + * - Schedules periodical action to emit stored value (if any) + * - OnNext + * - Updates stored value + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * - Emit last emitted value (if any) + * + * \ingroup filtering_operators + * \see + https://reactivex.io/documentation/operators/sample.htmlhttps://reactivex.io/documentation/operators/sample.html + */ + template + auto sample_with_time(schedulers::duration period, + const TScheduler& scheduler) const& + requires is_header_included + { + return static_cast(this)->template lift( + sample_with_time_impl{period, scheduler}); + } + + template + auto sample_with_time(schedulers::duration period, + const TScheduler& scheduler) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift( + sample_with_time_impl{period, scheduler}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/scan.hpp b/symmetri/gui/rpp/rpp/operators/fwd/scan.hpp new file mode 100644 index 0000000..d95adfc --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/scan.hpp @@ -0,0 +1,95 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct scan_tag; +} + +namespace rpp::details { +// accepts Result and Type and returns Result +template +concept scan_accumulator = reduce_accumulator; + +template AccumulatorFn> +struct scan_impl; + +template +struct member_overload { + /** + * \brief Apply accumulator function for each emission from observable and + result of accumulator from previous step and emit (and cache) resulting value + * + * \marble scan + { + source observable : +--1-2-3-| + operator "scan: s=1, (s,x)=>s+x" : +--2-4-7-| + } + * + * \details Acttually this operator applies provided accumulator function to + seed and new emission, emits resulting value and updates seed value for next + emission + * + * \param initial_value initial value for seed which will be applied for first + value from observable (instead of emitting this as first value). Then it will + be replaced with result and etc. + * \param accumulator function which accepts seed value and new value from + observable and return new value of seed. Can accept seed by move-reference. + * + * \return new specific_observable with the scan operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet scan.cpp scan + * \snippet scan.cpp scan_vector + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store seed + * - OnNext + * - Applies accumulator to each emission + * - Updates seed value + * - Emits new seed value + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup transforming_operators + * \see https://reactivex.io/documentation/operators/scan.html + */ + template AccumulatorFn> + auto scan(Result&& initial_value, AccumulatorFn&& accumulator) const& + requires is_header_included + { + return static_cast(this) + ->template lift>( + scan_impl, std::decay_t>{ + std::forward(initial_value), + std::forward(accumulator)}); + } + + template AccumulatorFn> + auto scan(Result&& initial_value, AccumulatorFn&& accumulator) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift>( + scan_impl, std::decay_t>{ + std::forward(initial_value), + std::forward(accumulator)}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/skip.hpp b/symmetri/gui/rpp/rpp/operators/fwd/skip.hpp new file mode 100644 index 0000000..2e0b51a --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/skip.hpp @@ -0,0 +1,76 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct skip_tag; +} + +namespace rpp::details { +template +struct skip_impl; + +template +struct member_overload { + /** + * \brief Skip first `count` items provided by observable then send rest items + as expected + * + * \marble skip + { + source observable : +--1-2-3-4-5-6-| + operator "skip(3)" : +--------4-5-6-| + } + * + * \details Actually this operator just decrements counter and starts to + forward emissions when counter reaches zero. + * + * \param count amount of items to be skipped + * \return new specific_observable with the skip operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet skip.cpp skip + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store counter + * - OnNext + * - Forwards emission if counter is zero + * - Decrements counter if not zero + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup filtering_operators + * \see https://reactivex.io/documentation/operators/skip.html + */ + template + auto skip(size_t count) const& + requires is_header_included + { + return static_cast(this)->template lift( + skip_impl{count}); + } + + template + auto skip(size_t count) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift(skip_impl{count}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/start_with.hpp b/symmetri/gui/rpp/rpp/operators/fwd/start_with.hpp new file mode 100644 index 0000000..850b8ca --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/start_with.hpp @@ -0,0 +1,128 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +namespace rpp::details { +struct start_with_tag; +} + +namespace rpp::details { +template TObservable, + constraint::observable_of_type... TObservables> +auto start_with_impl(TObservable&& observable, + TObservables&&... observables_to_start_with); + +template ... TTypes, + constraint::observable_of_type TObservable> +auto start_with_impl(TObservable&& observable, TTypes&&... vals_to_start_with); + +template +struct member_overload { + /** + * \brief Combines submissions from current observable with values into one but + without overlapping and starting from values provided as arguments + * + * \marble start_with + { + source original_observable : +-4--6-| + operator "start_with(1,2,3)" : +-1-2-3--4--6-| + } + * + * \details Actually it makes concat(rpp::source::just(vals_to_start_with)..., + current_observable) so observables from argument subscribed before current + observable + * + * \tparam memory_model memory_model strategy used to store provided values + * \param vals list of values which should be emitted before current observable + * + * \return new specific_observable with the start_with operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet start_with.cpp start_with + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/startwith.html + */ + template ... TTypes> + auto start_with(TTypes&&... vals_to_start_with) const& + requires is_header_included + { + return start_with_impl( + *static_cast(this), + std::forward(vals_to_start_with)...); + } + + template ... TTypes> + auto start_with(TTypes&&... vals_to_start_with) && + requires(is_header_included&& + constraint::variadic_is_same_type) { + return start_with_impl( + std::move(*static_cast(this)), + std::forward(vals_to_start_with)...); + } + + /** + * \brief Combines submissions from current observable with other + observables into one but without overlapping and starting from observables + provided as arguments + * + * \marble start_with_observable + { + source original_observable : +-4--6-| + operator "start_with(-1-2-3-|)" : +--1-2-3--4--6-| + } + * + * \details Actually it makes concat(observables_to_start_with..., + current_observable) so observables from argument subscribed before current + observable + * + * \param observables list of observables which should be used before + current observable + * + * \return new specific_observable with the start_with operator as most + recent operator. + * \warning #include + * + * \par Example + * \snippet start_with.cpp start_with observable + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/startwith.html + */ + template ... TObservables> + auto start_with(TObservables&&... observables_to_start_with) const& + requires is_header_included + { + return start_with_impl( + *static_cast(this), + std::forward(observables_to_start_with)...); + } + + template ... TObservables> + auto start_with(TObservables&&... observables_to_start_with) && + requires is_header_included + { + return start_with_impl( + std::move(*static_cast(this)), + std::forward(observables_to_start_with)...); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/subscribe.hpp b/symmetri/gui/rpp/rpp/operators/fwd/subscribe.hpp new file mode 100644 index 0000000..3161cf7 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/subscribe.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include + +namespace rpp::details { +struct subscribe_tag; + +template +struct member_overload { + /** + * \brief Main function of observable. Initiates subscription for provided + * subscriber and calls stored OnSubscribe function \details this overloading + * accepts subscriber as is to avoid construction of dynamic_subscriber + * \return subscription on this observable which can be used to unsubscribe + */ + template TSub> + auto subscribe(TSub&& subscriber) const { + return subscribe_impl(std::forward(subscriber)); + } + + /** + * \brief Main function of observable. Initiates subscription for provided + * subscriber and calls stored OnSubscribe function \details this overloading + * accepts observer to construct specific_subscriber without extra overheads + * \return subscription on this observable which can be used to unsubscribe + */ + template TObserver> + auto subscribe(TObserver&& observer) const { + return subscribe_impl>( + std::forward(observer)); + } + + /** + * \brief Main function of observable. Initiates subscription for provided + * subscriber and calls stored OnSubscribe function \details this overloading + * accepts subscription and observer to construct specific_subscriber without + * extra overheads \return subscription on this observable which can be used + * to unsubscribe + */ + template TObserver> + auto subscribe(composite_subscription sub, TObserver&& observer) const { + return subscribe_impl(specific_subscriber>{ + std::move(sub), std::forward(observer)}); + } + + /** + * \brief Main function of observable. Initiates subscription for provided + * subscriber and calls stored OnSubscribe function \details this overloading + * accepts raw functions to construct specific subscriber with specific + * observer \return subscription on this observable which can be used to + * unsubscribe + */ + template < + constraint::on_next_fn TOnNext = utils::empty_function_t, + constraint::on_error_fn TOnError = utils::rethrow_error_t, + constraint::on_completed_fn TOnCompleted = utils::empty_function_t<>> + auto subscribe(TOnNext&& on_next = {}, TOnError&& on_error = {}, + TOnCompleted&& on_completed = {}) const { + return subscribe_impl(make_specific_subscriber( + std::forward(on_next), std::forward(on_error), + std::forward(on_completed))); + } + + /** + * \brief Main function of observable. Initiates subscription for provided + * subscriber and calls stored OnSubscribe function \details this overloading + * accepts raw functions to construct specific subscriber with specific + * observer \return subscription on this observable which can be used to + * unsubscribe + */ + template TOnNext, + constraint::on_completed_fn TOnCompleted> + auto subscribe(TOnNext&& on_next, TOnCompleted&& on_completed) const { + return subscribe_impl(make_specific_subscriber( + std::forward(on_next), + std::forward(on_completed))); + } + + /** + * \brief Main function of observable. Initiates subscription for provided + * subscriber and calls stored OnSubscribe function \details this overloading + * accepts subscription and raw functions to construct specific subscriber + * with specific observer \return subscription on this observable which can be + * used to unsubscribe + */ + template < + constraint::on_next_fn TOnNext = utils::empty_function_t, + constraint::on_error_fn TOnError = utils::rethrow_error_t, + constraint::on_completed_fn TOnCompleted = utils::empty_function_t<>> + auto subscribe(composite_subscription sub, TOnNext&& on_next = {}, + TOnError&& on_error = {}, + TOnCompleted&& on_completed = {}) const { + return subscribe_impl(make_specific_subscriber( + std::move(sub), std::forward(on_next), + std::forward(on_error), + std::forward(on_completed))); + } + + /** + * \brief Main function of observable. Initiates subscription for provided + * subscriber and calls stored OnSubscribe function \details this overloading + * accepts subscription raw functions to construct specific subscriber with + * specific observer \return subscription on this observable which can be used + * to unsubscribe + */ + template TOnNext, + constraint::on_completed_fn TOnCompleted> + auto subscribe(composite_subscription sub, TOnNext&& on_next, + TOnCompleted&& on_completed) const { + return subscribe_impl(make_specific_subscriber( + std::move(sub), std::forward(on_next), + std::forward(on_completed))); + } + + private: + template Obs> + auto subscribe_impl(const specific_subscriber& subscriber) const { + return static_cast(this)->subscribe_impl( + subscriber); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/subscribe_on.hpp b/symmetri/gui/rpp/rpp/operators/fwd/subscribe_on.hpp new file mode 100644 index 0000000..07df7a4 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/subscribe_on.hpp @@ -0,0 +1,62 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +namespace rpp::details { +struct subscribe_on_tag; +} + +namespace rpp::details { +template TObs, + schedulers::constraint::scheduler TScheduler> +auto subscribe_on_impl(TObs&& obs, TScheduler&& scheduler); + +template +struct member_overload { + /** + * \brief OnSubscribe function for this observable will be scheduled via + * provided scheduler + * + * \details Actually this operator just schedules subscription on original + * observable to provided scheduler + * + * \param scheduler is scheduler used for scheduling of OnSubscribe + * \return new specific_observable with the subscribe_on operator as most + * recent operator. \warning #include + * + * \par Example: + * \snippet subscribe_on.cpp subscribe_on + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/subscribeon.html + */ + template + auto subscribe_on(const TScheduler& scheduler) const& + requires is_header_included + { + return subscribe_on_impl( + *static_cast(this), scheduler); + } + + template + auto subscribe_on(const TScheduler& scheduler) && + requires is_header_included + { + return subscribe_on_impl( + std::move(*static_cast(this)), scheduler); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/switch_map.hpp b/symmetri/gui/rpp/rpp/operators/fwd/switch_map.hpp new file mode 100644 index 0000000..4a1a741 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/switch_map.hpp @@ -0,0 +1,72 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct switch_map_tag; +} + +namespace rpp::details { +template +concept switch_map_callable = + std::invocable && + constraint::observable>; + +template Callable> +auto switch_map_impl(auto&& observable, Callable&& callable); + +template +struct member_overload { + /** + * \brief convert an Observable that emits Observables into a single Observable + that emits the items emitted by the most-recently-emitted of those Observables + * + * \marble switch_map + { + source observable : +--2-3-| + operator "switch_map: x=>x-x-x-|" : +--22333| + } + * + * \details Actually it makes `map` and then `switch_on_next`. + * + * \param callable Function to transform item to observable + * \return new specific_observable with the switch_map operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet switch_map.cpp switch_map + * + * \ingroup transforming_operators + * \see https://reactivex.io/documentation/operators/switchmap.html + */ + template Callable> + auto switch_map(Callable&& callable) const& + requires is_header_included + { + return switch_map_impl(*static_cast(this), + std::forward(callable)); + } + + template Callable> + auto switch_map(Callable&& callable) && + requires is_header_included + { + return switch_map_impl( + std::move(*static_cast(this)), + std::forward(callable)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/switch_on_next.hpp b/symmetri/gui/rpp/rpp/operators/fwd/switch_on_next.hpp new file mode 100644 index 0000000..386abdb --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/switch_on_next.hpp @@ -0,0 +1,86 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct switch_on_next_tag; +} + +namespace rpp::details { +template +struct switch_on_next_impl; + +template +struct member_overload { + /** + * \brief Converts observable of observables into observable of values which + emits values from most recent underlying observable till new observable + obtained + * + * \marble switch_on_next + { + source observable : + { + +--1-2-3-5--| + .....+4--6-9| + .......+7-8-| + } + operator "switch_on_next" : +--1-24-7-8| + } + * + * \details Actually this operator just unsubscribes from previous observable + and subscribes on new observable when obtained in `on_next` + * + * \return new specific_observable with the switch_on_next operator as most + recent operator. + * \warning #include + * + * \par Example: + * \snippet switch_on_next.cpp switch_on_next + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store internal state + * - OnNext + * - Unsubscribed from previous observable (if any) + * - Subscribed on new emitted observable + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed if no any active inner observable + or original observable yet + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/switch.html + */ + template + auto switch_on_next() const& + requires(is_header_included && + rpp::constraint::observable) + { + return static_cast(this) + ->template lift>( + switch_on_next_impl()); + } + + template + auto switch_on_next() && + requires(is_header_included&& + rpp::constraint::observable) { + return std::move(*static_cast(this)) + .template lift>( + switch_on_next_impl()); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/take.hpp b/symmetri/gui/rpp/rpp/operators/fwd/take.hpp new file mode 100644 index 0000000..54a19f4 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/take.hpp @@ -0,0 +1,83 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct take_tag; +} + +namespace rpp::details { +template +struct take_impl; + +template +struct member_overload { + /** + * \brief Emit only first `count` items provided by observable, then send + `on_completed` + * + * \marble take + { + source observable : +--1-2-3-4-5-6-| + operator "take(3)" : +--1-2-3| + } + * \details Actually this operator just emits emissions while counter is not + zero and decrements counter on each emission + * + * \param count amount of items to be emitted. 0 - instant complete + * \return new specific_observable with the Take operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet take.cpp take + * + * \par Implementation details: + * - On subscribe + * - Allocate one `shared_ptr` to store counter + * - OnNext + * - Just forwards emission if counter is not zero + * - Decrements counter if not zero + * - If counter reached zero, then emits OnCompleted + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup filtering_operators + * \see https://reactivex.io/documentation/operators/take.html + */ + template + auto take(size_t count) const& + requires is_header_included + { + return cast_this()->template lift(take_impl{count}); + } + + template + auto take(size_t count) && + requires is_header_included + { + return move_this().template lift(take_impl{count}); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/take_last.hpp b/symmetri/gui/rpp/rpp/operators/fwd/take_last.hpp new file mode 100644 index 0000000..4967307 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/take_last.hpp @@ -0,0 +1,76 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct take_last_tag; +} + +namespace rpp::details { +template +struct take_last_impl; + +template +struct member_overload { + /** + * \brief Emit only last `count` items provided by observable, then send + `on_completed` + * + * \marble take_last + { + source observable : +--1-2-3-4-5-6-| + operator "take_last(3)" : +--------------456| + } + * + * \details Actually this operator has buffer of requested size inside, keeps + last `count` values and emit stored values on `on_completed` + * + * \param count amount of last items to be emitted + * \return new specific_observable with the take_last operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet take_last.cpp take_last + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store internal buffer + * - OnNext + * - Place obtained value into queue + * - If queue contains more values than expected - remove oldest one + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Emits values stored in queue + * + * \ingroup filtering_operators + * \see https://reactivex.io/documentation/operators/takelast.html + */ + template + auto take_last(size_t count) const& + requires is_header_included + { + return static_cast(this)->template lift( + take_last_impl{count}); + } + + template + auto take_last(size_t count) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift(take_last_impl{count}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/take_until.hpp b/symmetri/gui/rpp/rpp/operators/fwd/take_until.hpp new file mode 100644 index 0000000..51b5264 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/take_until.hpp @@ -0,0 +1,105 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include + +namespace rpp::details { +struct take_until_tag; +} + +namespace rpp::details { + +template +struct take_until_impl; + +template +struct member_overload { + /** + * \brief Discard any items emitted by an Observable after a second Observable + emits an item or terminates. + * \warning The take_until subscribes and begins mirroring the source + Observable. It also monitors a second Observable that you provide. If this + second Observable emits an item or sends a on_error/on_completed + notification, the Observable returned by take_until stops mirroring the + source Observable and terminates. + * + * \marble take_until + { + source observable : +-1--2--3--| + source until_observable : +--s--s----| + operator "take_until" : +-1-| + } + * + * \details Actually this operator just subscribes on 2 observables and + completes original when `until_observable` emits any value + * + * \param until_observable is the observables that stops the source observable + from sending values when it emits one value or sends a on_error/on_completed + event. + * \return new specific_observable with the take_until operator as most recent + operator. + * \warning #include + * + * \par Examples + * \snippet take_until.cpp take_until + * \snippet take_until.cpp terminate + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to store internal state + * - OnNext for original observable + * - Just forward original on_next + * - OnError for original observable + * - Just forwards original on_error + * - OnCompleted for original observable + * - Just forwards original on_completed + * - OnNext for until observable + * - Emits on_completed + * - OnError for until observable + * - Just forwards on_error + * - OnCompleted for until observable + * - Just forwards on_completed + * + * \ingroup conditional_operators + * \see https://reactivex.io/documentation/operators/takeuntil.html + */ + template + auto take_until(TTriggerObservable&& until_observable) const& + requires is_header_included + { + return cast_this()->template lift( + take_until_impl>{ + std::forward(until_observable)}); + } + + template + auto take_until(TTriggerObservable&& until_observable) && + requires is_header_included + { + return move_this().template lift( + take_until_impl>{ + std::forward(until_observable)}); + } + + private: + const SpecificObservable* cast_this() const { + return static_cast(this); + } + + SpecificObservable&& move_this() { + return std::move(*static_cast(this)); + } +}; + +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/take_while.hpp b/symmetri/gui/rpp/rpp/operators/fwd/take_while.hpp new file mode 100644 index 0000000..4d9a770 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/take_while.hpp @@ -0,0 +1,78 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +struct take_while_tag; +} + +namespace rpp::details { +template Predicate> +struct take_while_impl; + +template +struct member_overload { + /** + * \brief Sends items provided by observable while items are satisfy + predicate. When condition becomes false -> sends `on_completed` + * + * \marble take_while + { + source observable : +--1-2-3-4-5-6-| + operator "take_while: x => x!=3" : +--1-2-| + } + * + * \details Actually this operator just emits values while predicate returns + true + * + * \param predicate is predicate used to check items + * \return new specific_observable with the take_while operator as most recent + operator. + * \warning #include + * + * \par Example: + * \snippet take_while.cpp take_while + * + * \par Implementation details: + * - On subscribe + * - None + * - OnNext + * - Just forwards emission if predicate returns true + * - Emits OnCompleted if predicate returns false + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup conditional_operators + * \see https://reactivex.io/documentation/operators/takewhile.html + */ + template Predicate> + auto take_while(Predicate&& predicate) const& + requires is_header_included + { + return static_cast(this)->template lift( + take_while_impl>{ + std::forward(predicate)}); + } + + template Predicate> + auto take_while(Predicate&& predicate) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift(take_while_impl>{ + std::forward(predicate)}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/timeout.hpp b/symmetri/gui/rpp/rpp/operators/fwd/timeout.hpp new file mode 100644 index 0000000..9503ea3 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/timeout.hpp @@ -0,0 +1,123 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include + +namespace rpp::details { +struct timeout_tag; +} + +namespace rpp::details { +template FallbackObs, + schedulers::constraint::scheduler TScheduler> +struct timeout_impl; + +template +struct member_overload { + /** + * \brief Forwards emissions from original observable, but subscribes on + fallback observable if no any events during specified period of time (since + last emission) + * + * \marble timeout_fallback_obs + { + source observable : +--1-2-3-4--------5-| + operator "timeout(4, -10-|)" : +--1-2-3-4----10-| + } + * + * \param period is maximum duration between emitted items before a timeout + occurs + * \param fallback_obs is observable to subscribe on when timeout reached + * \param scheduler is scheduler used to run timer for timeout + * \return new specific_observable with the timeout operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet timeout.cpp timeout_fallback_obs + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/timeout.html + */ + template FallbackObs, + schedulers::constraint::scheduler TScheduler> + auto timeout(schedulers::duration period, FallbackObs&& fallback_obs, + const TScheduler& scheduler = TScheduler{}) const& + requires is_header_included + { + return static_cast(this)->template lift( + timeout_impl, TScheduler>{ + period, std::forward(fallback_obs), scheduler}); + } + + template FallbackObs, + schedulers::constraint::scheduler TScheduler> + auto timeout(schedulers::duration period, FallbackObs&& fallback_obs, + const TScheduler& scheduler = TScheduler{}) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift( + timeout_impl, TScheduler>{ + period, std::forward(fallback_obs), scheduler}); + } + + /** + * \brief Forwards emissions from original observable, but emit error if no + any events during specified period of time (since last emission) + * + * \marble timeout + { + source observable : +--1-2-3-4------5-| + operator "timeout(4)" : +--1-2-3-4----# + } + * \param period is maximum duration between emitted items before a timeout + occurs + * \param scheduler is scheduler used to run timer for timeout + * \return new specific_observable with the timeout operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet timeout.cpp timeout + * + * \ingroup utility_operators + * \see https://reactivex.io/documentation/operators/timeout.html + */ + template + auto timeout(schedulers::duration period, + const TScheduler& scheduler = TScheduler{}) const& + requires is_header_included + { + return timeout(period, + rpp::source::error(std::make_exception_ptr( + utils::timeout{"Timeout reached"})), + scheduler); + } + + template + auto timeout(schedulers::duration period, + const TScheduler& scheduler = TScheduler{}) && + requires is_header_included + { + return std::move(*static_cast(this)) + .timeout(period, + rpp::source::error(std::make_exception_ptr( + utils::timeout{"Timeout reached"})), + scheduler); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/window.hpp b/symmetri/gui/rpp/rpp/operators/fwd/window.hpp new file mode 100644 index 0000000..bc408a1 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/window.hpp @@ -0,0 +1,87 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct window_tag; +} + +namespace rpp::details { +template TObs> +auto window_impl(TObs&& obs, size_t window_size); + +template +struct member_overload { + /** + * \brief Subdivide original observable into sub-observables (windowed + observables) and emit sub-observables of items instead of original items + * + * \marble window + { + source observable : +-1-2-3-4-5-| + + operator "window(2)" : + { + .+1-2| + .....+3-4| + .........+5-| + } + } + * + * \details Actually it is similar to `buffer` but it emits observable instead + of container. + * + * \param window_size amount of items which every observable would have + * + * \return new specific_observable with the window operator as most recent + operator. + * \warning #include + * + * \par Example + * \snippet window.cpp window + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to keep internal state + * - OnNext + * - Emits new window-observable if previous observable emitted requested + amound of emisions + * - Emits emission via active window-observable + * - Completes window-observable if requested amound of emisions reached + * - OnError + * - Just forwards original on_error + * - OnCompleted + * - Just forwards original on_completed + * + * \ingroup transforming_operators + * \see https://reactivex.io/documentation/operators/window.html + */ + template + auto window(size_t window_size) const& + requires is_header_included + { + return window_impl(*static_cast(this), + window_size); + } + + template + auto window(size_t window_size) && + requires is_header_included + { + return window_impl(std::move(*static_cast(this)), + window_size); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/fwd/with_latest_from.hpp b/symmetri/gui/rpp/rpp/operators/fwd/with_latest_from.hpp new file mode 100644 index 0000000..a99cb52 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/fwd/with_latest_from.hpp @@ -0,0 +1,150 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include +#include + +namespace rpp::details { +struct with_latest_from_tag; +} + +namespace rpp::details { +template +struct with_latest_from_impl; + +template +struct member_overload { + /** + * \brief Combines latest emissions from observables with emission from current + observable when it sends new value via applying selector + * + * \marble with_latest_from_custom_selector + { + source observable : +------1 -2 -3 + -| source other_observable : +-5-6-7- -- 8- -| + operator "with_latest_from: x,y =>std::pair{x,y}" : + +------{1,5}-{2,7}-{3,8}-| + } + * + * \details Actually this operator just keeps last values from all other + observables and combines them together with each new emission from original + observable + * + * \param selector is applied to current emission of current observable and + latests emissions from observables + * \param observables are observables whose emissions would be combined when + current observable sends new value + * \return new specific_observable with the with_latest_from operator as most + recent operator. + * \warning #include + * + * \par Examples + * \snippet with_latest_from.cpp with_latest_from custom selector + * + * \par Implementation details: + * - On subscribe + * - Allocates one `shared_ptr` to keep last values from all observables + * - OnNext for original observable + * - Applies selector to new emission and all saved last values from other + observable (if any value for all observables) + * - OnNext other original observables + * - Just updates last value for this observable + * - OnError + * - Just forwards original on_error + * - OnCompleted for original observable + * - Just forwards original on_completed + * - OnCompleted for other observables + * - None + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/combinelatest.html + */ + template < + constraint::observable... TObservables, + std::invocable...> + TSelector> + auto with_latest_from(TSelector&& selector, + TObservables&&... observables) const& + requires is_header_included + { + return static_cast(this) + ->template lift...>>( + with_latest_from_impl, + std::decay_t...>{ + std::forward(selector), + {std::forward(observables)...}}); + } + + template < + constraint::observable... TObservables, + std::invocable...> + TSelector> + auto with_latest_from(TSelector&& selector, + TObservables&&... observables) && + requires is_header_included + { + return std::move(*static_cast(this)) + .template lift...>>( + with_latest_from_impl, + std::decay_t...>{ + std::forward(selector), + {std::forward(observables)...}}); + } + + /** + * \brief Combines latest emissions from observables with emission from current + observable when it sends new value via making tuple + * + * \marble with_latest_from + { + source observable : +------1 -2 -3 -| + source other_observable : +-5-6-7- -- 8- -| + operator "with_latest_from: make_tuple" : +------{1,5}-{2,7}-{3,8}-| + } + * + * \param observables are observables whose emissions would be combined when + current observable sends new value + * \return new specific_observable with the with_latest_from operator as most + recent operator. + * \warning #include + * + * \par Examples + * \snippet with_latest_from.cpp with_latest_from + * + * \ingroup combining_operators + * \see https://reactivex.io/documentation/operators/combinelatest.html + */ + template + auto with_latest_from(TObservables&&... observables) const& + requires is_header_included + { + return static_cast(this)->with_latest_from( + utils::pack_to_tuple{}, std::forward(observables)...); + } + + template + auto with_latest_from(TObservables&&... observables) && + requires is_header_included + { + return std::move(*static_cast(this)) + .with_latest_from(utils::pack_to_tuple{}, + std::forward(observables)...); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/group_by.hpp b/symmetri/gui/rpp/rpp/operators/group_by.hpp new file mode 100644 index 0000000..370f319 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/group_by.hpp @@ -0,0 +1,161 @@ +#pragma once + +#include +#include +#include // RPP_NO_UNIQUE_ADDRESS +#include // grouped_observable +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // publish_subject +#include // constraint::subscriber +#include // utils::as_const + +IMPLEMENTATION_FILE(group_by_tag); + +namespace rpp::details { +class group_by_state_base + : public std::enable_shared_from_this { + public: + group_by_state_base() = default; + + virtual ~group_by_state_base() noexcept = default; + + void on_subscribe(const composite_subscription& dest) { + subscribers.fetch_add(1, std::memory_order::acq_rel); + dest.add([state = this->weak_from_this()] { + if (const auto locked = state.lock()) + if (locked->subscribers.fetch_sub(1, std::memory_order::acq_rel) == 1) + locked->lifetime.unsubscribe(); + }); + } + + const auto& get_source_lifetime() const { return lifetime; } + + private: + composite_subscription lifetime{}; + std::atomic_size_t subscribers{}; +}; + +template KeyComparator> +struct group_by_state final : group_by_state_base { + group_by_state(const KeyComparator& comparator) + : group_by_state_base{}, key_to_subject{comparator} {} + + std::map, KeyComparator> key_to_subject; + + void broadcast(const auto& action, const auto& subscriber) const { + for (const auto& [_, subject] : key_to_subject) + action(subject.get_subscriber()); + + action(subscriber); + } +}; + +template +struct group_by_on_subscribe { + subjects::publish_subject subject; + std::weak_ptr state; + + void operator()(auto&& subscriber) const { + if (const auto locked = state.lock()) { + locked->on_subscribe(subscriber.get_subscription()); + subject.get_observable().subscribe( + std::forward(subscriber)); + } + } +}; +} // namespace rpp::details + +namespace rpp { +template +using grouped_observable_group_by = + grouped_observable>; +} + +namespace rpp::details { +template KeySelector, std::invocable ValueSelector, + std::strict_weak_order KeyComparator> +struct group_by_lift_impl { + using ValueType = utils::decayed_invoke_result_t; + using StateType = + group_by_state, + KeyComparator>; + + RPP_NO_UNIQUE_ADDRESS KeySelector key_selector; + RPP_NO_UNIQUE_ADDRESS ValueSelector value_selector; + RPP_NO_UNIQUE_ADDRESS KeyComparator comparator; + + private: + struct observer_state { + std::shared_ptr state; + RPP_NO_UNIQUE_ADDRESS KeySelector key_selector; + RPP_NO_UNIQUE_ADDRESS ValueSelector value_selector; + }; + + struct on_next { + void operator()(auto&& val, const auto& subscriber, + const observer_state& state) const { + auto key = state.key_selector(utils::as_const(val)); + auto [itr, inserted] = state.state->key_to_subject.try_emplace(key); + + if (inserted) + subscriber.on_next(grouped_observable_group_by{ + key, group_by_on_subscribe{itr->second, state.state}}); + + const auto& subject_sub = itr->second.get_subscriber(); + if (subject_sub.is_subscribed()) + subject_sub.on_next( + state.value_selector(std::forward(val))); + } + }; + + struct on_error { + void operator()(const std::exception_ptr& err, const auto& subscriber, + const observer_state& state) const { + state.state->broadcast([&err](const auto& sub) { sub.on_error(err); }, + subscriber); + } + }; + + struct on_completed { + void operator()(const auto& subscriber, const observer_state& state) const { + state.state->broadcast([](const auto& sub) { sub.on_completed(); }, + subscriber); + } + }; + + public: + template + auto operator()(TSub&& subscriber) const { + auto state = std::make_shared(comparator); + + state->on_subscribe(subscriber.get_subscription()); + + return create_subscriber_with_state( + state->get_source_lifetime(), on_next{}, on_error{}, on_completed{}, + std::forward(subscriber), + observer_state{state, key_selector, value_selector}); + } +}; + +template KeySelector, std::invocable ValueSelector, + std::strict_weak_order KeyComparator> +auto group_by_impl(auto&& observable, KeySelector&& key_selector, + ValueSelector&& value_selector, KeyComparator&& comparator) { + using Res = grouped_observable_group_by< + TKey, utils::decayed_invoke_result_t>; + using Lifter = group_by_lift_impl, + std::decay_t, + std::decay_t>; + + return std::forward(observable) + .template lift(Lifter{std::forward(key_selector), + std::forward(value_selector), + std::forward(comparator)}); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/last.hpp b/symmetri/gui/rpp/rpp/operators/last.hpp new file mode 100644 index 0000000..483a8ca --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/last.hpp @@ -0,0 +1,68 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // take_last +#include // constraint::subscriber +#include // not_enough_emissions +#include // forwarding_on_error + +IMPLEMENTATION_FILE(last_tag); + +namespace rpp::details { +template +struct last_state : public take_last_state { + explicit last_state() : take_last_state{1} {} +}; + +/** + * Functor of last() operator for on_next events. + */ +using last_on_next = take_last_on_next; + +/** + * Functor of last() operator for on_completed event. + */ +struct last_on_completed { + template + void operator()(const constraint::subscriber auto& subscriber, + const last_state& state) const { + auto&& last_value = state.items.at(0); + if (!last_value.has_value()) { + subscriber.on_error(std::make_exception_ptr(utils::not_enough_emissions{ + "last() operator expects at least one emission from observable " + "before completion"})); + return; + } + + subscriber.on_next(std::move(last_value.value())); + subscriber.on_completed(); + } +}; + +template +struct last_impl { + public: + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + + return create_subscriber_with_dynamic_state( + std::move(subscription), last_on_next{}, utils::forwarding_on_error{}, + last_on_completed{}, std::forward(subscriber), + last_state{}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/lift.hpp b/symmetri/gui/rpp/rpp/operators/lift.hpp new file mode 100644 index 0000000..0e03689 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/lift.hpp @@ -0,0 +1,131 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include // create_subscriber_with_state +#include // own forwarding +#include // create observable +#include + +#include "rpp/observables/specific_observable.hpp" +#include "rpp/subscribers/constraints.hpp" + +IMPLEMENTATION_FILE(lift_tag); + +namespace rpp::details { +template +struct lift_action_by_callbacks { + RPP_NO_UNIQUE_ADDRESS OnNext on_next; + RPP_NO_UNIQUE_ADDRESS OnError on_error; + RPP_NO_UNIQUE_ADDRESS OnCompleted on_completed; + + template + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + return create_subscriber_with_state(std::move(subscription), on_next, + on_error, on_completed, + std::forward(subscriber)); + } +}; + +template OperatorFn> +using subscriber_type_of_list_fn = utils::extract_subscriber_type_t< + utils::decayed_invoke_result_t>>; + +/** + * \brief Functor of "lift" operator for on_subscribe overload function. + * \details Each observable has an on_subscribe function and observable is + * activated (pub-sub channel is established) after on_subscribe is called. The + * on_subscribe is called when the observable is subscribed by a subscriber + * + * \param _this is the current observable. + * \param op is the functor that provides the + * "operator()(subscriber_of_new_type) -> subscriber_of_old_type". + */ +template OperatorFn, + typename... ChildLiftArgs> +struct lift_on_subscribe + : public lift_on_subscribe< + NewType, OperatorFn, + lift_on_subscribe, + ChildLiftArgs...>> {}; + +template OperatorFn, + typename TOnSubscribe> +struct lift_on_subscribe { + using T = subscriber_type_of_list_fn; + RPP_NO_UNIQUE_ADDRESS specific_observable _this; + RPP_NO_UNIQUE_ADDRESS OperatorFn op; + + template TSub> + void operator()(TSub&& subscriber) const { + _this.subscribe(op(std::forward(subscriber))); + } +}; + +template OperatorFn, + typename ObservableValue, typename... ChildLiftArgs> +auto lift_impl_internal( + OperatorFn&& op, + specific_observable>&& + _this) { + return observable::create< + NewType, + lift_on_subscribe, ChildLiftArgs...>>( + {std::move(_this), std::forward(op)}); +} + +template OperatorFn, + typename ObservableValue, typename... ChildLiftArgs> +auto lift_impl_internal( + OperatorFn&& op, + const specific_observable< + ObservableValue, lift_on_subscribe>& + _this) { + return observable::create< + NewType, + lift_on_subscribe, ChildLiftArgs...>>( + {_this, std::forward(op)}); +} + +template OperatorFn, + typename ObservableValue, typename OnSubscribe> +auto lift_impl_internal( + OperatorFn&& op, + specific_observable&& _this) { + return observable::create< + NewType, + lift_on_subscribe, OnSubscribe>>( + {std::move(_this), std::forward(op)}); +} + +template OperatorFn, + typename ObservableValue, typename OnSubscribe> +auto lift_impl_internal( + OperatorFn&& op, + const specific_observable& _this) { + return observable::create< + NewType, + lift_on_subscribe, OnSubscribe>>( + {_this, std::forward(op)}); +} + +template OperatorFn, + typename TObs> +auto lift_impl(OperatorFn&& op, TObs&& _this) { + return lift_impl_internal(std::forward(op), + std::forward(_this)); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/map.hpp b/symmetri/gui/rpp/rpp/operators/map.hpp new file mode 100644 index 0000000..7243a96 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/map.hpp @@ -0,0 +1,48 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include // own forwarding +#include // required due to operator uses lift +#include // constraint::subscriber_of_type +#include // decayed_invoke_result_t +#include // as_counst +#include + +IMPLEMENTATION_FILE(map_tag); + +namespace rpp::details { +template Callable> +struct map_impl_on_next { + RPP_NO_UNIQUE_ADDRESS Callable callable; + + template > + TSub> + void operator()(TVal&& value, const TSub& subscriber) const { + subscriber.on_next(callable(utils::as_const(std::forward(value)))); + } +}; + +template Callable> +struct map_impl { + RPP_NO_UNIQUE_ADDRESS map_impl_on_next on_next; + + template + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + return create_subscriber_with_state( + std::move(subscription), on_next, utils::forwarding_on_error{}, + utils::forwarding_on_completed{}, std::forward(subscriber)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/merge.hpp b/symmetri/gui/rpp/rpp/operators/merge.hpp new file mode 100644 index 0000000..23927c7 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/merge.hpp @@ -0,0 +1,99 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include // early_unsubscribe +#include // make_serialized_subscriber +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include +#include // just +#include // constraint::subscriber +#include // forwarding_on_next + +IMPLEMENTATION_FILE(merge_tag); + +namespace rpp::details { +struct merge_state : early_unsubscribe_state { + using early_unsubscribe_state::early_unsubscribe_state; + + std::atomic_size_t count_of_on_completed_needed{}; +}; + +using merge_forwarding_on_next = utils::forwarding_on_next; +using merge_on_error = early_unsubscribe_on_error; + +struct merge_on_completed { + void operator()(const constraint::subscriber auto& sub, + const std::shared_ptr& state) const { + if (state->count_of_on_completed_needed.fetch_sub( + 1, std::memory_order::acq_rel) == 1) + sub.on_completed(); + } +}; + +struct merge_on_next { + template + void operator()(const TObs& new_observable, + const constraint::subscriber auto& sub, + const std::shared_ptr& state) const { + using ValueType = utils::extract_observable_type_t; + + state->count_of_on_completed_needed.fetch_add(1, + std::memory_order::relaxed); + + new_observable.subscribe(create_subscriber_with_state( + state->children_subscriptions.make_child(), merge_forwarding_on_next{}, + merge_on_error{}, merge_on_completed{}, sub, state)); + } +}; + +struct merge_state_with_serialized_mutex : merge_state { + using merge_state::merge_state; + + std::mutex mutex{}; +}; + +template +struct merge_impl { + using ValueType = utils::extract_observable_type_t; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = std::make_shared( + in_subscriber.get_subscription()); + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->mutex}); + + state->count_of_on_completed_needed.fetch_add(1, + std::memory_order::relaxed); + + auto subscription = state->children_subscriptions.make_child(); + return create_subscriber_with_state( + std::move(subscription), merge_on_next{}, merge_on_error{}, + merge_on_completed{}, std::move(subscriber), std::move(state)); + } +}; + +template ... TObservables> +auto merge_with_impl(TObservables&&... observables) { + return source::just(rpp::schedulers::immediate{}, + std::forward(observables).as_dynamic()...) + .merge(); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/multicast.hpp b/symmetri/gui/rpp/rpp/operators/multicast.hpp new file mode 100644 index 0000000..363ce0d --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/multicast.hpp @@ -0,0 +1,28 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(multicast_tag); + +namespace rpp::details { +template TObs, + subjects::constraint::subject_of_type TSubject> +auto multicast_impl(TObs&& observable, TSubject&& subject) { + return connectable_observable, + std::decay_t>{ + std::forward(observable), std::forward(subject)}; +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/observe_on.hpp b/symmetri/gui/rpp/rpp/operators/observe_on.hpp new file mode 100644 index 0000000..85f381a --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/observe_on.hpp @@ -0,0 +1,18 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +IMPLEMENTATION_FILE(observe_on_tag); + +// we just need delay to completed this one +#include diff --git a/symmetri/gui/rpp/rpp/operators/on_error_resume_next.hpp b/symmetri/gui/rpp/rpp/operators/on_error_resume_next.hpp new file mode 100644 index 0000000..d6db1e4 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/on_error_resume_next.hpp @@ -0,0 +1,57 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include // create_subscriber_with_state +#include // own forwarduing +#include // required due to operator uses lift +#include // constraint::subscriber_of_type +#include + +IMPLEMENTATION_FILE(on_error_resume_next_tag); + +namespace rpp::details { +/** + * Functor (type-erasure) of "on_error_resume_next" for on_error operator. + */ +struct on_error_resume_next_on_error { + template + void operator()(const std::exception_ptr& err, const auto& subscriber, + const ResumeCallable& resume_callable) const { + // Subscribe to next_observable + resume_callable(err).subscribe(subscriber); + } +}; + +/** + * \brief Functor of OperatorFn for "on_error_resume_next" operator (used by + * "lift"). + */ +template +struct on_error_resume_next_impl { + RPP_NO_UNIQUE_ADDRESS ResumeCallable resume_callable; + + template TSub> + auto operator()(TSub&& downstream_subscriber) const { + // Child subscription is for keeping the downstream subscriber's + // subscription alive when upstream sends on_error event. + auto subscription = downstream_subscriber.get_subscription().make_child(); + + return create_subscriber_with_state( + std::move(subscription), rpp::utils::forwarding_on_next{}, + on_error_resume_next_on_error{}, rpp::utils::forwarding_on_completed{}, + std::forward(downstream_subscriber), resume_callable); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/publish.hpp b/symmetri/gui/rpp/rpp/operators/publish.hpp new file mode 100644 index 0000000..4e48f46 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/publish.hpp @@ -0,0 +1,26 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(publish_tag); + +namespace rpp::details { +template TObs> +auto publish_impl(TObs&& observable) { + return std::forward(observable) + .multicast(rpp::subjects::publish_subject{}); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/reduce.hpp b/symmetri/gui/rpp/rpp/operators/reduce.hpp new file mode 100644 index 0000000..a1b8cd3 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/reduce.hpp @@ -0,0 +1,181 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // constraint::subscriber +#include // forwarding_on_error +#include // utils::as_const + +IMPLEMENTATION_FILE(reduce_tag); + +namespace rpp::details { +template SelectorFn = std::identity> +struct reduce_state { + mutable Seed seed; + RPP_NO_UNIQUE_ADDRESS AccumulatorFn accumulator; + RPP_NO_UNIQUE_ADDRESS SelectorFn selector{}; +}; + +struct reduce_on_next { + template + void operator()( + auto&& value, const constraint::subscriber auto&, + const reduce_state& state) const { + state.seed = state.accumulator(std::move(state.seed), + std::forward(value)); + } +}; + +struct reduce_on_completed { + template + void operator()( + const constraint::subscriber auto& sub, + const reduce_state& state) const { + try { + sub.on_next(state.selector(std::move(state.seed))); + } catch (...) { + sub.on_error(std::current_exception()); + return; + } + sub.on_completed(); + } +}; + +template AccumulatorFn, + std::invocable ResultSelectorFn> +struct reduce_impl { + Seed initial_value; + RPP_NO_UNIQUE_ADDRESS AccumulatorFn accumulator; + RPP_NO_UNIQUE_ADDRESS ResultSelectorFn selector; + + template > + TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), reduce_on_next{}, utils::forwarding_on_error{}, + reduce_on_completed{}, std::forward(subscriber), + reduce_state{ + initial_value, accumulator, selector}); + } +}; + +template +auto average_impl(TObs&& observable) { + using Type = utils::extract_observable_type_t>; + using Pair = std::pair, int32_t>; + return std::forward(observable) + .reduce( + Pair{}, + [](Pair&& seed, auto&& val) { + if (seed.first) + seed.first.value() += std::forward(val); + else + seed.first = std::forward(val); + ++seed.second; + return std::move(seed); + }, + [](Pair&& seed) { + if (!seed.first) + throw utils::not_enough_emissions{ + "`average` operator requires at least one emission to " + "calculate average"}; + + return static_cast( + std::move(seed.first).value()) / + seed.second; + }); +} + +template +auto sum_impl(TObs&& observable) { + using Type = utils::extract_observable_type_t>; + return std::forward(observable) + .reduce( + std::optional{}, + [](std::optional&& seed, auto&& val) { + if (!seed) + seed = std::forward(val); + else + seed.value() += std::forward(val); + return std::move(seed); + }, + [](std::optional&& seed) { + if (!seed) + throw utils::not_enough_emissions{ + "`sum` operator requires at least one emission to calculate " + "sum"}; + + return std::move(seed.value()); + }); +} + +template +auto count_impl(TObs&& observable) { + return std::forward(observable) + .reduce(size_t{}, [](size_t seed, auto&&) { return ++seed; }); +} + +template +auto min_impl(TObs&& observable, Comparator&& comparator) { + using Type = utils::extract_observable_type_t>; + return std::forward(observable) + .reduce( + std::optional{}, + [comparator](std::optional&& seed, auto&& val) { + if (!seed || comparator(utils::as_const(val), seed.value())) + seed = std::forward(val); + return std::move(seed); + }, + [](std::optional&& seed) { + if (!seed) + throw utils::not_enough_emissions{ + "`min` operator requires at least one emission to calculate " + "min"}; + + return std::move(seed.value()); + }); +} + +template +auto max_impl(TObs&& observable, Comparator&& comparator) { + using Type = utils::extract_observable_type_t>; + return std::forward(observable) + .reduce( + std::optional{}, + [comparator](std::optional&& seed, auto&& val) { + if (!seed || comparator(seed.value(), utils::as_const(val))) + seed = std::forward(val); + return std::move(seed); + }, + [](std::optional&& seed) { + if (!seed) + throw utils::not_enough_emissions{ + "`max` operator requires at least one emission to calculate " + "min"}; + + return std::move(seed.value()); + }); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/ref_count.hpp b/symmetri/gui/rpp/rpp/operators/ref_count.hpp new file mode 100644 index 0000000..823f73a --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/ref_count.hpp @@ -0,0 +1,77 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // create observable +#include // constraint::subscriber_of_type + +IMPLEMENTATION_FILE(ref_count_tag); + +namespace rpp::details { +struct ref_count_state_t { + bool on_subscribe() { + std::lock_guard lock{m_mutex}; + if (++m_count_of_active_subs != 1) return false; + + m_sub = composite_subscription{}; + return true; + } + + void on_unsubscribe() { + std::lock_guard lock{m_mutex}; + if (--m_count_of_active_subs == 0) m_sub.unsubscribe(); + } + + const composite_subscription& get_subscription() const { return m_sub; } + + private: + size_t m_count_of_active_subs{}; + composite_subscription m_sub = composite_subscription::empty(); + std::mutex m_mutex{}; +}; + +template TObs> +struct ref_count_on_subscribe { + TObs observable; + std::shared_ptr state = + std::make_shared(); + + template TSub> + void operator()(TSub&& subscriber) const { + const bool need_to_connect = state->on_subscribe(); + + subscriber.get_subscription().add([state = std::weak_ptr{state}] { + if (const auto locked = state.lock()) locked->on_unsubscribe(); + }); + + auto sub = subscriber.get_subscription(); + observable.subscribe(create_subscriber_with_state( + std::move(sub), utils::forwarding_on_next{}, + utils::forwarding_on_error{}, utils::forwarding_on_completed{}, + std::forward(subscriber), + // capture state to be sure that state is alive while ANY subscriber is + // alive + state)); + if (need_to_connect) observable.connect(state->get_subscription()); + } +}; + +template TObs> +auto ref_count_impl(TObs&& observable) { + return source::create(ref_count_on_subscribe>{ + std::forward(observable)}); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/repeat.hpp b/symmetri/gui/rpp/rpp/operators/repeat.hpp new file mode 100644 index 0000000..c7e7565 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/repeat.hpp @@ -0,0 +1,118 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // create observable +#include // constraint::subscriber +#include // forwarding_on_next + +IMPLEMENTATION_FILE(repeat_tag); + +namespace rpp::details { +template +class repeat_on_completed { + struct state_t { + state_t(const SpecificObservable& observable, Predicate&& predicate) + : observable{observable}, predicate{std::move(predicate)} {} + + RPP_NO_UNIQUE_ADDRESS SpecificObservable observable; + RPP_NO_UNIQUE_ADDRESS Predicate predicate; + }; + + public: + repeat_on_completed(const SpecificObservable& observable, + Predicate&& predicate) + : m_state{std::make_shared(observable, std::move(predicate))} {} + + void operator()(const auto& sub) const { + if (sub.is_subscribed()) { + if (m_state->predicate()) + subscribe_subscriber_for_repeat(sub); + else + sub.on_completed(); + } + } + + private: + void subscribe_subscriber_for_repeat( + const constraint::subscriber auto& subscriber) const { + m_state->observable.subscribe(create_subscriber_with_state( + subscriber.get_subscription().make_child(), utils::forwarding_on_next{}, + utils::forwarding_on_error{}, *this, subscriber)); + } + + std::shared_ptr m_state; +}; + +struct counted_repeat_predicate { + counted_repeat_predicate(size_t count) : m_count{count} {} + + bool operator()() { return m_count && m_count--; } + + private: + size_t m_count{}; +}; + +template TObs, typename CreatePredicateFn> +struct repeat_on_subscribe { + repeat_on_subscribe(TObs&& observable, CreatePredicateFn&& create_predicate) + : m_observable{std::move(observable)}, + m_create_predicate{std::move(create_predicate)} {} + + repeat_on_subscribe(const TObs& observable, + CreatePredicateFn&& create_predicate) + : m_observable{observable}, + m_create_predicate{std::move(create_predicate)} {} + + template TSub> + void operator()(const TSub& subscriber) const { + repeat_on_completed, + std::invoke_result_t>{ + m_observable, m_create_predicate()}(subscriber); + } + + private: + RPP_NO_UNIQUE_ADDRESS TObs m_observable; + RPP_NO_UNIQUE_ADDRESS CreatePredicateFn m_create_predicate; +}; + +template TObs, typename CreatePredicateFn> +auto create_repeat_on_subscribe(TObs&& observable, + CreatePredicateFn&& create_predicate) { + return source::create( + repeat_on_subscribe, + std::decay_t>( + std::forward(observable), + std::forward(create_predicate))); +} + +template TObs> +auto repeat_impl(TObs&& observable, size_t count) { + return create_repeat_on_subscribe( + std::forward(observable), + [count] { return counted_repeat_predicate{count}; }); +} + +template TObs> +auto repeat_impl(TObs&& observable) { + return create_repeat_on_subscribe(std::forward(observable), + [] { return [] { return true; }; }); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/sample.hpp b/symmetri/gui/rpp/rpp/operators/sample.hpp new file mode 100644 index 0000000..fb9528d --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/sample.hpp @@ -0,0 +1,99 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include // required due to operator uses lift +#include +#include + +IMPLEMENTATION_FILE(sample_tag); + +namespace rpp::details { +template +struct sample_state : early_unsubscribe_state { + using early_unsubscribe_state::early_unsubscribe_state; + + std::mutex value_mutex{}; + std::optional value{}; +}; + +template +struct sample_state_with_serialized_spinlock : sample_state { + using sample_state::sample_state; + + utils::spinlock spinlock{}; +}; + +struct sample_on_next { + template + void operator()( + Value&& value, const auto&, + const std::shared_ptr>>& state) const { + std::lock_guard lock{state->value_mutex}; + state->value.emplace(std::forward(value)); + } +}; + +using sample_on_error = early_unsubscribe_on_error; + +struct sample_on_completed { + void operator()(const auto& subscriber, const auto& state) const { + state->children_subscriptions.unsubscribe(); + + { + std::lock_guard lock{state->value_mutex}; + if (state->value.has_value()) + subscriber.on_next(std::move(state->value.value())); + } + subscriber.on_completed(); + } +}; + +template +struct sample_with_time_impl { + schedulers::duration period; + TScheduler scheduler; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = std::make_shared>( + in_subscriber.get_subscription()); + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->spinlock}); + + scheduler.create_worker(state->children_subscriptions) + .schedule(period, + [period = period, subscriber = subscriber, + state]() -> rpp::schedulers::optional_duration { + std::optional extracted{}; + { + std::lock_guard lock{state->value_mutex}; + std::swap(extracted, state->value); + } + if (extracted.has_value()) + subscriber.on_next(std::move(extracted.value())); + return period; + }); + + return create_subscriber_with_state( + state->children_subscriptions, sample_on_next{}, sample_on_error{}, + sample_on_completed{}, std::move(subscriber), std::move(state)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/scan.hpp b/symmetri/gui/rpp/rpp/operators/scan.hpp new file mode 100644 index 0000000..8effdce --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/scan.hpp @@ -0,0 +1,52 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // RPP_NO_UNIQUE_ADDRESS +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // reduce to re-use +#include // constraint::subscriber +#include // forwarding_on_error +#include // utils::as_const + +IMPLEMENTATION_FILE(scan_tag); + +namespace rpp::details { +struct scan_on_next : private reduce_on_next { + template + void operator()(auto&& value, const constraint::subscriber auto& sub, + const reduce_state& state) const { + reduce_on_next::operator()(std::forward(value), sub, + state); + sub.on_next(utils::as_const(state.seed)); + } +}; + +template AccumulatorFn> +struct scan_impl { + Result initial_value; + RPP_NO_UNIQUE_ADDRESS AccumulatorFn accumulator; + + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), scan_on_next{}, utils::forwarding_on_error{}, + utils::forwarding_on_completed{}, std::forward(subscriber), + reduce_state{initial_value, accumulator}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/skip.hpp b/symmetri/gui/rpp/rpp/operators/skip.hpp new file mode 100644 index 0000000..f9d1a7d --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/skip.hpp @@ -0,0 +1,51 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include // own forwarding +#include // required due to operator uses lift +#include // constraint::subscriber +#include // forwarding_on_error + +IMPLEMENTATION_FILE(skip_tag); + +namespace rpp::details { +struct skip_state { + mutable size_t count; +}; + +struct skip_on_next { + void operator()(auto&& value, const constraint::subscriber auto& subscriber, + const skip_state& state) const { + if (state.count == 0) + subscriber.on_next(std::forward(value)); + else + --state.count; + } +}; + +template +struct skip_impl { + size_t count; + + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), skip_on_next{}, utils::forwarding_on_error{}, + utils::forwarding_on_completed{}, std::forward(subscriber), + skip_state{count}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/start_with.hpp b/symmetri/gui/rpp/rpp/operators/start_with.hpp new file mode 100644 index 0000000..7790785 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/start_with.hpp @@ -0,0 +1,39 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(start_with_tag); + +namespace rpp::details { +template TObservable, + constraint::observable_of_type... TObservables> +auto start_with_impl(TObservable&& observable, + TObservables&&... observables_to_start_with) { + return concat_with_impl( + std::forward(observables_to_start_with)..., + std::forward(observable)); +} + +template ... TTypes, + constraint::observable_of_type TObservable> +auto start_with_impl(TObservable&& observable, TTypes&&... vals_to_start_with) { + return start_with_impl( + std::forward(observable), + rpp::source::just( + std::forward(vals_to_start_with)...)); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/subscribe_on.hpp b/symmetri/gui/rpp/rpp/operators/subscribe_on.hpp new file mode 100644 index 0000000..7e17d06 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/subscribe_on.hpp @@ -0,0 +1,38 @@ + +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(subscribe_on_tag); + +namespace rpp::details { +template TObs, + schedulers::constraint::scheduler TScheduler> +auto subscribe_on_impl(TObs&& obs, const TScheduler& scheduler) { + return source::create( + [obs = std::forward(obs), + scheduler] TSub>( + TSub&& subscriber) { + auto worker = scheduler.create_worker(subscriber.get_subscription()); + worker.schedule( + [obs, subscriber = std::forward( + subscriber)]() mutable -> schedulers::optional_duration { + obs.subscribe(std::move(subscriber)); + return {}; + }); + }); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/switch_map.hpp b/symmetri/gui/rpp/rpp/operators/switch_map.hpp new file mode 100644 index 0000000..4704b62 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/switch_map.hpp @@ -0,0 +1,27 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(switch_map_tag); + +namespace rpp::details { +template Callable> +auto switch_map_impl(auto &&observable, Callable &&callable) { + return std::forward(observable) + .map(std::forward(callable)) + .switch_on_next(); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/switch_on_next.hpp b/symmetri/gui/rpp/rpp/operators/switch_on_next.hpp new file mode 100644 index 0000000..af50942 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/switch_on_next.hpp @@ -0,0 +1,107 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include // required due to operator uses lift +#include +#include +#include +#include + +IMPLEMENTATION_FILE(switch_on_next_tag); + +namespace rpp::details { +struct switch_on_next_state : public merge_state { + using merge_state::merge_state; + + composite_subscription current_inner_observable = + rpp::composite_subscription::empty(); +}; + +struct switch_on_next_on_completed_inner { + void operator()(const constraint::subscriber auto& sub, + const std::shared_ptr& state) const { + // 1 because decrement happens in composite_subscription_callback + if (state->count_of_on_completed_needed.load(std::memory_order::acquire) == + 1) + sub.on_completed(); + } +}; + +using switch_on_next_on_next_inner = merge_forwarding_on_next; +using switch_on_next_on_error = merge_on_error; + +struct switch_on_next_on_next { + template + void operator()(const TObs& new_observable, + const constraint::subscriber auto& sub, + const std::shared_ptr& state) const { + using ValueType = utils::extract_observable_type_t; + + state->current_inner_observable.unsubscribe(); + state->current_inner_observable = + state->children_subscriptions.make_child(); + state->current_inner_observable.add([state = std::weak_ptr{state}] { + if (const auto locked = state.lock()) + locked->count_of_on_completed_needed.fetch_sub( + 1, std::memory_order::relaxed); + }); + + state->count_of_on_completed_needed.fetch_add(1, + std::memory_order::relaxed); + + new_observable.subscribe(create_subscriber_with_state( + state->current_inner_observable, switch_on_next_on_next_inner{}, + switch_on_next_on_error{}, switch_on_next_on_completed_inner{}, sub, + state)); + } +}; + +using switch_on_next_on_completed_outer = merge_on_completed; + +struct switch_on_next_state_with_serialized_spinlock : switch_on_next_state { + using switch_on_next_state::switch_on_next_state; + + // we can use spinlock there because 99.9% of time only one ever thread would + // send values from on_next (only one active observable), but we have small + // probability to get error from main observable immediately + utils::spinlock spinlock{}; +}; + +template +struct switch_on_next_impl { + using ValueType = utils::extract_observable_type_t; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = + std::make_shared( + in_subscriber.get_subscription()); + + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->spinlock}); + + state->count_of_on_completed_needed.fetch_add(1, + std::memory_order::relaxed); + + auto subscription = state->children_subscriptions.make_child(); + return create_subscriber_with_state( + std::move(subscription), switch_on_next_on_next{}, + switch_on_next_on_error{}, switch_on_next_on_completed_outer{}, + std::move(subscriber), std::move(state)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/take.hpp b/symmetri/gui/rpp/rpp/operators/take.hpp new file mode 100644 index 0000000..dbb2900 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/take.hpp @@ -0,0 +1,53 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include +#include // required due to operator uses lift +#include +#include + +IMPLEMENTATION_FILE(take_tag); + +namespace rpp::details { +struct take_state { + mutable size_t count; +}; + +struct take_on_next { + void operator()(auto&& value, const constraint::subscriber auto& subscriber, + const take_state& state) const { + if (state.count > 0) { + --state.count; + subscriber.on_next(std::forward(value)); + } + + if (state.count == 0) subscriber.on_completed(); + } +}; + +template +struct take_impl { + size_t count; + + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), take_on_next{}, utils::forwarding_on_error{}, + utils::forwarding_on_completed{}, std::forward(subscriber), + take_state{count}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/take_last.hpp b/symmetri/gui/rpp/rpp/operators/take_last.hpp new file mode 100644 index 0000000..999b3e2 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/take_last.hpp @@ -0,0 +1,83 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include // create_subscriber_with_state +#include +#include // required due to operator uses lift +#include +#include +#include + +IMPLEMENTATION_FILE(take_last_tag); + +namespace rpp::details { +template +struct take_last_state { + explicit take_last_state(size_t count) : items(count) {} + + size_t get_next_position(size_t pos) const { + return ++pos >= items.size() ? 0 : pos; + } + + mutable std::vector> items; + mutable size_t current_end_position{}; +}; + +struct take_last_on_next { + template + void operator()(T&& v, const auto&, + const take_last_state& state) const { + // handle case "count==0" + if (state.items.empty()) return; + + state.items[state.current_end_position].emplace(std::forward(v)); + state.current_end_position = + state.get_next_position(state.current_end_position); + } +}; + +struct take_last_on_completed { + template + void operator()(const auto& subscriber, + const take_last_state& state) const { + if (!state.items.empty()) { + size_t cur_pos = state.current_end_position; + + do { + if (auto&& value = state.items[cur_pos]) + subscriber.on_next(std::move(value.value())); + + cur_pos = state.get_next_position(cur_pos); + + } while (cur_pos != state.current_end_position); + } + subscriber.on_completed(); + } +}; + +template +struct take_last_impl { + size_t count; + + template TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), take_last_on_next{}, + utils::forwarding_on_error{}, take_last_on_completed{}, + std::forward(subscriber), take_last_state{count}); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/take_until.hpp b/symmetri/gui/rpp/rpp/operators/take_until.hpp new file mode 100644 index 0000000..83ed0ad --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/take_until.hpp @@ -0,0 +1,87 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include +#include // required due to operator uses lift +#include +#include +#include +#include + +IMPLEMENTATION_FILE(take_until_tag); + +namespace rpp::details { +using take_until_state = early_unsubscribe_state; + +using take_until_on_next = merge_forwarding_on_next; +using take_until_on_error = merge_on_error; +using take_until_on_completed = early_unsubscribe_on_completed; + +/** + * Functor (type-erasure) of throttler (trigger observable) for on_next + * operator. + */ +struct take_until_throttler_on_next { + void operator()(auto&&, const auto& subscriber, + const std::shared_ptr& state) const { + // Unsubscribe all sources due to we obtained "stop event" + state->children_subscriptions.unsubscribe(); + subscriber.on_completed(); + } +}; + +using take_until_throttler_on_error = take_until_on_error; +using take_until_throttler_on_completed = take_until_on_completed; + +struct take_until_state_with_serialized_spinlock : take_until_state { + using take_until_state::take_until_state; + + // we can use spinlock there because 99.9% of time only one ever thread would + // send values from on_next (main observable), but we have small probability + // to get error from "until observable" immediately + utils::spinlock spinlock{}; +}; +/** + * \brief "combine_latest" operator (an OperatorFn used by "lift"). + */ +template +struct take_until_impl { + using TriggerType = utils::extract_observable_type_t; + + TTriggerObservable m_until_observable; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = std::make_shared( + in_subscriber.get_subscription()); + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->spinlock}); + + // Subscribe to trigger observable + m_until_observable.subscribe(create_subscriber_with_state( + state->children_subscriptions.make_child(), + take_until_throttler_on_next{}, take_until_throttler_on_error{}, + take_until_throttler_on_completed{}, subscriber, state)); + + auto subscription = state->children_subscriptions.make_child(); + return create_subscriber_with_state( + std::move(subscription), take_until_on_next{}, take_until_on_error{}, + take_until_on_completed{}, std::move(subscriber), std::move(state)); + } +}; + +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/take_while.hpp b/symmetri/gui/rpp/rpp/operators/take_while.hpp new file mode 100644 index 0000000..908af65 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/take_while.hpp @@ -0,0 +1,47 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include // required due to operator uses lift +#include +#include + +IMPLEMENTATION_FILE(take_while_tag); + +namespace rpp::details { +template Predicate> +struct take_while_impl_on_next { + RPP_NO_UNIQUE_ADDRESS Predicate predicate; + + template TSub> + void operator()(TVal&& value, const TSub& subscriber) const { + if (predicate(utils::as_const(value))) + subscriber.on_next(std::forward(value)); + else + subscriber.on_completed(); + } +}; + +template Predicate> +struct take_while_impl { + RPP_NO_UNIQUE_ADDRESS take_while_impl_on_next on_next; + + template + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + return create_subscriber_with_state( + std::move(subscription), on_next, utils::forwarding_on_error{}, + utils::forwarding_on_completed{}, std::forward(subscriber)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/timeout.hpp b/symmetri/gui/rpp/rpp/operators/timeout.hpp new file mode 100644 index 0000000..ef41fcf --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/timeout.hpp @@ -0,0 +1,129 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include // required due to operator uses lift +#include +#include +#include +#include + +IMPLEMENTATION_FILE(timeout_tag); + +namespace rpp::details { +template +struct timeout_state : early_unsubscribe_state { + timeout_state(const FallbackObs& fallback_obs, + const composite_subscription& subscription_of_subscriber) + : early_unsubscribe_state(subscription_of_subscriber), + fallback_obs{fallback_obs} {} + + FallbackObs fallback_obs; + std::atomic last_emission_time{}; + + static constexpr schedulers::time_point s_timeout_reached = + schedulers::time_point::min(); +}; + +template +struct timeout_on_next { + template + void operator()( + Value&& v, const auto& subscriber, + const std::shared_ptr>& state) const { + if (state->last_emission_time.exchange(Worker::now(), + std::memory_order_acq_rel) != + timeout_state::s_timeout_reached) + subscriber.on_next(std::forward(v)); + } +}; + +using timeout_on_error = early_unsubscribe_on_error; +using timeout_on_completed = early_unsubscribe_on_completed; + +template +struct timeout_state_with_serialized_spinlock : timeout_state { + using timeout_state::timeout_state; + + // spinlock because most part of time there is only one thread would be active + utils::spinlock spinlock{}; +}; + +template FallbackObs, + schedulers::constraint::scheduler TScheduler> +struct timeout_impl { + schedulers::duration period; + FallbackObs fallback_obs; + TScheduler scheduler; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = + std::make_shared>( + fallback_obs, in_subscriber.get_subscription()); + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->spinlock}); + + const auto worker = scheduler.create_worker(state->children_subscriptions); + state->last_emission_time.store(worker.now(), std::memory_order_relaxed); + + const auto last_emission_time = + state->last_emission_time.load(std::memory_order_relaxed); + worker.schedule( + last_emission_time + period, + [period = period, prev_emission_time = last_emission_time, subscriber, + state]() mutable -> schedulers::optional_duration { + while (true) { + // last emission time still same value -> timeout reached, else -> + // prev_emission_time would be update to actual emission time + if (state->last_emission_time.compare_exchange_strong( + prev_emission_time, + timeout_state::s_timeout_reached, + std::memory_order_acq_rel)) + return time_is_out(state, subscriber); + + // if we still need to wait a bit more -> let's wait + if (const auto diff_to_schedule = + (prev_emission_time + period) - decltype(worker)::now(); + diff_to_schedule > rpp::schedulers::duration{0}) + return diff_to_schedule; + + // okay, we here because: + // 1) last_emission_time was not equal to prev_emission_time + // 2) last_emission_time + period before now -> we are still in + // timeout state 3) prev_emission_time updated to last_emission_time + // So we can return to begin + } + }); + + return create_subscriber_with_state( + state->children_subscriptions, + timeout_on_next{}, timeout_on_error{}, + timeout_on_completed{}, std::move(subscriber), std::move(state)); + } + + private: + static schedulers::optional_duration time_is_out(const auto& state, + const auto& subscriber) { + state->children_subscriptions.unsubscribe(); + state->fallback_obs.subscribe(subscriber); + return std::nullopt; + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/window.hpp b/symmetri/gui/rpp/rpp/operators/window.hpp new file mode 100644 index 0000000..6afe7e1 --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/window.hpp @@ -0,0 +1,98 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // create_subscriber_with_state +#include +#include +#include + +IMPLEMENTATION_FILE(window_tag); + +namespace rpp { +template +using windowed_observable = + decltype(std::declval>().get_observable()); +} + +namespace rpp::details { +template +struct window_state { + const size_t window_size{}; + mutable size_t items_in_current_window = window_size; + mutable subjects::publish_subject subject{}; +}; + +struct window_on_next { + template + void operator()(auto&& value, const auto& subscriber, + const window_state& state) const { + // need to send new subject due to NEW item appeared (we avoid sending new + // subjects if no any new items) + if (state.items_in_current_window == state.window_size) { + subscriber.on_next(state.subject.get_observable()); + state.items_in_current_window = 0; + } + + ++state.items_in_current_window; + state.subject.get_subscriber().on_next( + std::forward(value)); + + // cleanup current subject, but don't send due to wait for new value + if (state.items_in_current_window == state.window_size) { + state.subject.get_subscriber().on_completed(); + state.subject = rpp::subjects::publish_subject{}; + } + } +}; + +struct window_on_error { + template + void operator()(const std::exception_ptr& err, const auto& subscriber, + const window_state& state) const { + state.subject.get_subscriber().on_error(err); + subscriber.on_error(err); + } +}; + +struct window_on_completed { + template + void operator()(const auto& subscriber, + const window_state& state) const { + state.subject.get_subscriber().on_completed(); + subscriber.on_completed(); + } +}; + +template +struct window_lift_impl { + size_t window_size{}; + + template > TSub> + auto operator()(TSub&& subscriber) const { + auto subscription = subscriber.get_subscription(); + + // dynamic_state there to make shared_ptr for observer instead of making + // shared_ptr for state + return create_subscriber_with_dynamic_state( + std::move(subscription), window_on_next{}, window_on_error{}, + window_on_completed{}, std::forward(subscriber), + window_state{window_size}); + } +}; + +template TObs> +auto window_impl(TObs&& obs, size_t window_size) { + return std::forward(obs).template lift>( + window_lift_impl{window_size}); +} +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/with_latest_from.hpp b/symmetri/gui/rpp/rpp/operators/with_latest_from.hpp new file mode 100644 index 0000000..014604c --- /dev/null +++ b/symmetri/gui/rpp/rpp/operators/with_latest_from.hpp @@ -0,0 +1,145 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include +#include // create_subscriber_with_state +#include +#include +#include +#include +#include +#include +#include + +IMPLEMENTATION_FILE(with_latest_from_tag); + +namespace rpp::details { +template +struct with_latest_from_state : early_unsubscribe_state { + with_latest_from_state( + const TSelector& selector, + const composite_subscription& subscription_of_subscriber) + : early_unsubscribe_state{subscription_of_subscriber}, + selector(selector) {} + + // RPP_NO_UNIQUE_ADDRESS commented due to MSVC issue for base classes + /*RPP_NO_UNIQUE_ADDRESS*/ TSelector selector; + std::array mutexes{}; + std::tuple...> vals{}; +}; + +template +struct with_latest_from_on_next_inner { + void operator()(auto&& value, const constraint::subscriber auto&, + const auto& state) const { + std::lock_guard lock{state->mutexes[I]}; + std::get(state->vals) = std::forward(value); + } +}; + +using with_latest_from_on_error = merge_on_error; +using with_latest_from_on_completed_outer = early_unsubscribe_on_completed; + +template +void with_latest_from_subscribe(const auto& state_ptr, const TObs& observable, + const auto& subscriber) { + using Type = utils::extract_observable_type_t; + observable.subscribe(create_subscriber_with_state( + state_ptr->children_subscriptions.make_child(), + with_latest_from_on_next_inner{}, with_latest_from_on_error{}, + [](const auto&, const auto&) {}, subscriber, state_ptr)); +} + +template +void with_latest_from_subscribe_observables(std::index_sequence, + const auto& state_ptr, + const auto& subscriber, + const auto& observables_tuple) { + (with_latest_from_subscribe(state_ptr, std::get(observables_tuple), + subscriber), + ...); +} + +template +struct with_latest_from_on_next_outer { + template + void operator()(T&& v, const auto& sub, const auto& state) const { + using ResultType = + utils::decayed_invoke_result_t, + ValueTypes...>; + + auto result = std::apply( + [&](const auto&... current_cached_vals) -> std::optional { + auto lock = std::apply( + [](auto&... mutexes) { return std::scoped_lock{mutexes...}; }, + state->mutexes); + + if ((current_cached_vals.has_value() && ...)) + return state->selector( + utils::as_const(std::forward(v)), + utils::as_const(current_cached_vals.value())...); + return std::nullopt; + }, + state->vals); + + if (result.has_value()) sub.on_next(std::move(result.value())); + } +}; + +template +struct with_latest_from_state_with_serialized_spinlock + : public with_latest_from_state { + using with_latest_from_state::with_latest_from_state; + + // we can use spinlock there because 99.9% of time only one ever thread would + // send values from on_next (main observable), but we have small probability + // to get error from inner observables immediately + utils::spinlock spinlock{}; +}; + +template +struct with_latest_from_impl { + using ResultType = utils::decayed_invoke_result_t< + TSelector, Type, utils::extract_observable_type_t...>; + + RPP_NO_UNIQUE_ADDRESS TSelector selector; + RPP_NO_UNIQUE_ADDRESS std::tuple observables; + + template TSub> + auto operator()(TSub&& in_subscriber) const { + auto state = + std::make_shared...>>( + selector, in_subscriber.get_subscription()); + // change subscriber to serialized to avoid manual using of mutex + auto subscriber = make_serialized_subscriber( + std::forward(in_subscriber), + std::shared_ptr{state, &state->spinlock}); + + with_latest_from_subscribe_observables( + std::index_sequence_for{}, state, subscriber, + observables); + + auto sub = state->children_subscriptions.make_child(); + return create_subscriber_with_state( + std::move(sub), + with_latest_from_on_next_outer< + TSelector, utils::extract_observable_type_t...>{}, + with_latest_from_on_error{}, with_latest_from_on_completed_outer{}, + std::move(subscriber), std::move(state)); + } +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/rpp.hpp b/symmetri/gui/rpp/rpp/rpp.hpp new file mode 100644 index 0000000..06503fa --- /dev/null +++ b/symmetri/gui/rpp/rpp/rpp.hpp @@ -0,0 +1,19 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/schedulers.hpp b/symmetri/gui/rpp/rpp/schedulers.hpp new file mode 100644 index 0000000..5041cb5 --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers.hpp @@ -0,0 +1,22 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup schedulers Schedulers + * \brief Scheduler is the way to introduce multi-threading in your application + * via RPP \see https://reactivex.io/documentation/scheduler.html \ingroup rpp + */ + +#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/schedulers/constraints.hpp b/symmetri/gui/rpp/rpp/schedulers/constraints.hpp new file mode 100644 index 0000000..c339f3d --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/constraints.hpp @@ -0,0 +1,40 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +namespace rpp::schedulers::constraint { +// returns std::nullopt in case of don't need to re-schedule schedulable or some +// duration which will be added to "now" and re-scheduled +template +concept schedulable_fn = + std::invocable && + std::same_as, optional_duration>; + +template +concept inner_schedulable_fn = + std::invocable && std::same_as, void>; + +template +concept worker = std::is_base_of_v>; + +template +concept scheduler = + std::is_base_of_v> && + requires(const T t) { + { + t.create_worker(std::declval()) + } -> worker; + }; +} // namespace rpp::schedulers::constraint diff --git a/symmetri/gui/rpp/rpp/schedulers/details/queue_worker_state.hpp b/symmetri/gui/rpp/rpp/schedulers/details/queue_worker_state.hpp new file mode 100644 index 0000000..dca0b6a --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/details/queue_worker_state.hpp @@ -0,0 +1,133 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include +#include +#include // own forwarding +#include + +namespace rpp::schedulers::details { +template +class schedulable { + public: + schedulable(time_point time_point, size_t id, SchedulableFn&& fn) + : m_time_point{time_point}, m_id{id}, m_function{std::move(fn)} {} + + schedulable(const schedulable& other) = default; + schedulable(schedulable&& other) noexcept = default; + schedulable& operator=(const schedulable& other) = default; + schedulable& operator=(schedulable&& other) noexcept = default; + + bool operator<(const schedulable& other) const { + return std::tie(m_time_point, m_id) >= + std::tie(other.m_time_point, other.m_id); + } + + time_point get_time_point() const { return m_time_point; } + SchedulableFn&& extract_function() const { return std::move(m_function); } + + private: + time_point m_time_point; + size_t m_id; + mutable SchedulableFn m_function; +}; + +template +class queue_worker_state { + public: + queue_worker_state() = default; + queue_worker_state(const queue_worker_state&) = delete; + queue_worker_state(queue_worker_state&&) = delete; + + void emplace(time_point time_point, + constraint::inner_schedulable_fn auto&& fn) { + emplace_safe(time_point, std::forward(fn)); + m_cv.notify_one(); + } + + bool is_empty() const { + std::lock_guard lock{m_mutex}; + return m_queue.empty(); + } + + bool is_any_ready_schedulable() const { + std::lock_guard lock{m_mutex}; + return is_any_ready_schedulable_unsafe(); + } + + bool pop_if_ready(std::optional& out) { + std::lock_guard lock{m_mutex}; + if (!is_any_ready_schedulable_unsafe()) return false; + + out.emplace(std::move(m_queue.top().extract_function())); + m_queue.pop(); + return true; + } + + bool pop_with_wait(std::optional& out) { + while (m_subscription->is_subscribed()) { + std::unique_lock lock{m_mutex}; + + m_cv.wait(lock, [&] { + return !m_subscription->is_subscribed() || !m_queue.empty(); + }); + + if (m_queue.empty() || + !m_cv.wait_until(lock, m_queue.top().get_time_point(), [&] { + return !m_subscription->is_subscribed() || + is_any_ready_schedulable_unsafe(); + })) + continue; + + if (!m_subscription->is_subscribed()) return false; + + out.emplace(std::move(m_queue.top().extract_function())); + m_queue.pop(); + return true; + } + return false; + } + + bool is_subscribed() const { return m_subscription->is_subscribed(); } + + void unsubscribe() { m_subscription->unsubscribe(); } + + private: + void emplace_safe(time_point time_point, + constraint::inner_schedulable_fn auto&& fn) { + std::lock_guard lock{m_mutex}; + if (m_subscription->is_subscribed()) + m_queue.emplace(time_point, ++m_current_id, + std::forward(fn)); + } + + bool is_any_ready_schedulable_unsafe() const { + return !m_queue.empty() && + m_queue.top().get_time_point() <= clock_type::now(); + } + + private: + mutable std::mutex m_mutex{}; + std::condition_variable_any m_cv{}; + std::priority_queue> m_queue{}; + size_t m_current_id{}; + subscription_guard m_subscription = callback_subscription{[&] { + { + std::lock_guard lock{m_mutex}; + m_queue = std::priority_queue>{}; + } + m_cv.notify_one(); + }}; +}; +} // namespace rpp::schedulers::details diff --git a/symmetri/gui/rpp/rpp/schedulers/details/utils.hpp b/symmetri/gui/rpp/rpp/schedulers/details/utils.hpp new file mode 100644 index 0000000..f376bd1 --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/details/utils.hpp @@ -0,0 +1,47 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include + +namespace rpp::schedulers::details { +// keep old_timepoint to easily understand if we need to sleep (due to sleep is +// expensive enough even if time in the "past") +inline thread_local time_point s_last_sleep_timepoint{}; + +/** + * \brief Makes immediate-like scheduling for provided arguments + * \returns false in case of subscription unsubscribed or schedulable doesn't + *requested to re-schedule, true - in case of condition failed + **/ +bool immediate_scheduling_while_condition( + time_point& time_point, constraint::schedulable_fn auto&& schedulable, + const subscription_base& sub, const std::predicate auto& condition) { + while (condition()) { + if (!sub.is_subscribed()) return false; + + if (s_last_sleep_timepoint < time_point) { + std::this_thread::sleep_until(time_point); + s_last_sleep_timepoint = time_point; + + if (!sub.is_subscribed()) return false; + } + + if (const auto duration = schedulable()) + time_point = time_point + duration.value(); + else + return false; + } + + return true; +} +} // namespace rpp::schedulers::details diff --git a/symmetri/gui/rpp/rpp/schedulers/details/worker.hpp b/symmetri/gui/rpp/rpp/schedulers/details/worker.hpp new file mode 100644 index 0000000..5724d59 --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/details/worker.hpp @@ -0,0 +1,79 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include // schedulable_fn +#include // own forwarding +#include + +namespace rpp::schedulers { +template +concept worker_strategy = + std::copyable && + requires(const T t) { + t.defer_at(time_point{}, std::declval()); + { t.now() } -> std::same_as; + }; + +template +class schedulable_wrapper { + public: + template + schedulable_wrapper(const Strategy& strategy, time_point time_point, Fn&& fn) + : m_strategy{strategy}, + m_time_point{time_point}, + m_fn{std::forward(fn)} {} + + void operator()() { + if (!m_strategy.is_subscribed()) return; + + if (const auto duration = m_fn()) { + m_time_point = + std::max(m_strategy.now(), m_time_point + duration.value()); + + m_strategy.defer_at(m_time_point, std::move(*this)); + } + } + + private: + Strategy m_strategy; + time_point m_time_point; + std::function m_fn{}; +}; + +template +class worker final : public details::worker_tag { + public: + template + requires(!rpp::constraint::variadic_is_same_type, Args...>) + worker(Args&&... args) : m_strategy{std::forward(args)...} {} + + void schedule(constraint::schedulable_fn auto&& fn) const { + schedule(m_strategy.now(), std::forward(fn)); + } + + void schedule(duration delay, constraint::schedulable_fn auto&& fn) const { + schedule(m_strategy.now() + delay, std::forward(fn)); + } + + void schedule(time_point time_point, + constraint::schedulable_fn auto&& fn) const { + m_strategy.defer_at(time_point, std::forward(fn)); + } + + static time_point now() { return Strategy::now(); } + + private: + Strategy m_strategy; +}; +} // namespace rpp::schedulers diff --git a/symmetri/gui/rpp/rpp/schedulers/fwd.hpp b/symmetri/gui/rpp/rpp/schedulers/fwd.hpp new file mode 100644 index 0000000..105e95a --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/fwd.hpp @@ -0,0 +1,33 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::schedulers::details { +struct scheduler_tag {}; +struct worker_tag {}; +} // namespace rpp::schedulers::details + +namespace rpp::schedulers { +using clock_type = std::chrono::steady_clock; +using time_point = clock_type::time_point; +using duration = std::chrono::nanoseconds; +using optional_duration = std::optional; + +class immediate; +class trampoline; +using current_thread = trampoline; + +class new_thread; +class run_loop; +} // namespace rpp::schedulers diff --git a/symmetri/gui/rpp/rpp/schedulers/immediate_scheduler.hpp b/symmetri/gui/rpp/rpp/schedulers/immediate_scheduler.hpp new file mode 100644 index 0000000..c765930 --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/immediate_scheduler.hpp @@ -0,0 +1,48 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include // worker +#include // own forwarding +#include // lifetime + +namespace rpp::schedulers { +/** + * \brief immediately calls provided schedulable or waits for time_point (in the + * caller-thread) \ingroup schedulers + */ +class immediate final : public details::scheduler_tag { + public: + class worker_strategy { + public: + worker_strategy(const rpp::subscription_base& sub) : m_sub{sub} {} + + void defer_at(time_point time_point, + constraint::schedulable_fn auto&& fn) const { + details::immediate_scheduling_while_condition( + time_point, std::forward(fn), m_sub, + [] { return true; }); + } + + static time_point now() { return clock_type::now(); } + + private: + rpp::subscription_base m_sub; + }; + + static auto create_worker(const rpp::subscription_base& sub = {}) { + return worker{sub}; + } +}; +} // namespace rpp::schedulers diff --git a/symmetri/gui/rpp/rpp/schedulers/new_thread_scheduler.hpp b/symmetri/gui/rpp/rpp/schedulers/new_thread_scheduler.hpp new file mode 100644 index 0000000..2d81eb5 --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/new_thread_scheduler.hpp @@ -0,0 +1,126 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include // state +#include // worker +#include // own forwarding +#include // lifetime +#include // lifetime +#include + +namespace rpp::schedulers { +/** + * \brief scheduler which schedules execution of schedulables via queueing tasks + * to another thread with priority to time_point and order \warning Creates new + * thread for each "create_worker" call. Any scheduled task will be queued to + * created thread for execution with respect to time_point and number of task + * \ingroup schedulers + */ +class new_thread final : public details::scheduler_tag { + public: + class worker_strategy { + public: + using new_thread_schedulable = schedulable_wrapper; + + worker_strategy(const rpp::composite_subscription& sub) { + if (!sub.is_subscribed()) return; + + auto shared = std::make_shared(); + // init while it is alive as shared + shared->init_thread(sub); + m_state = shared; + } + + bool is_subscribed() const { + if (const auto locked = m_state.lock()) return locked->is_subscribed(); + return false; + } + + void defer_at(time_point time_point, + constraint::schedulable_fn auto&& fn) const { + defer_at(time_point, + new_thread_schedulable{*this, time_point, + std::forward(fn)}); + } + + void defer_at(time_point time_point, new_thread_schedulable&& fn) const { + if (auto locked = m_state.lock()) + locked->defer_at(time_point, std::move(fn)); + } + + static time_point now() { return clock_type::now(); } + + private: + class state : public std::enable_shared_from_this { + public: + state() = default; + state(const state&) = delete; + state(state&&) noexcept = delete; + + bool is_subscribed() const { return m_sub->is_subscribed(); } + + void defer_at(time_point time_point, new_thread_schedulable&& fn) { + if (m_sub->is_subscribed()) m_queue.emplace(time_point, std::move(fn)); + } + + void init_thread(const rpp::composite_subscription& sub) { + m_thread = std::thread{ + [state = shared_from_this()]() { state->data_thread(); }}; + const auto callback = + rpp::callback_subscription{[state = weak_from_this()] { + const auto locked = state.lock(); + if (!locked) return; + + locked->m_queue.unsubscribe(); + + if (locked->m_thread.joinable() && + locked->m_thread.get_id() != std::this_thread::get_id()) + locked->m_thread.join(); + else + locked->m_thread.detach(); + }}; + sub.add(callback); + m_sub.reset(callback); + } + + private: + void data_thread() { + std::optional fn{}; + while (m_queue.is_subscribed()) { + if (m_queue.pop_with_wait(fn)) { + (*fn)(); + fn.reset(); + } + } + + // clear + m_queue.unsubscribe(); + } + + details::queue_worker_state m_queue{}; + std::thread m_thread{}; + rpp::subscription_guard m_sub = rpp::subscription_base::empty(); + }; + + // original shared would alive in thread! + std::weak_ptr m_state{}; + }; + + static auto create_worker( + const rpp::composite_subscription& sub = composite_subscription{}) { + return worker{sub}; + } +}; +} // namespace rpp::schedulers diff --git a/symmetri/gui/rpp/rpp/schedulers/run_loop_scheduler.hpp b/symmetri/gui/rpp/rpp/schedulers/run_loop_scheduler.hpp new file mode 100644 index 0000000..fd609ce --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/run_loop_scheduler.hpp @@ -0,0 +1,121 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include // state +#include // worker +#include // own forwarding +#include // lifetime + +namespace rpp::schedulers { +/** + * \brief scheduler which schedules execution via queueing tasks, but execution + * of tasks should be manually dispatched \warning you need manually dispatch + * events for this scheduler in some thread. + * + * \par Example + * \snippet run_loop.cpp run_loop + * + * \ingroup schedulers + */ +class run_loop final : public details::scheduler_tag { + private: + class worker_strategy; + using run_loop_schedulable = schedulable_wrapper; + + class worker_strategy { + public: + worker_strategy( + std::weak_ptr> queue, + const composite_subscription& sub) + : m_queue{std::move(queue)}, m_sub{sub} {} + + bool is_subscribed() const { return m_sub.is_subscribed(); } + + void defer_at(time_point time_point, + constraint::schedulable_fn auto&& fn) const { + defer_at(time_point, + run_loop_schedulable{*this, time_point, + std::forward(fn)}); + } + + void defer_at(time_point time_point, run_loop_schedulable&& fn) const { + if (m_sub.is_subscribed()) + if (auto locked = m_queue.lock()) + locked->emplace(time_point, std::move(fn)); + } + + static time_point now() { return clock_type::now(); } + + private: + std::weak_ptr> m_queue{}; + composite_subscription m_sub{}; + }; + + class state { + public: + state(const composite_subscription& sub = composite_subscription{}) + : m_sub{sub} {} + + state(const state&) = delete; + state(state&&) noexcept = delete; + + ~state() { + m_queue.unsubscribe(); + m_sub.unsubscribe(); + } + + details::queue_worker_state& get_queue() { + return m_queue; + } + + const composite_subscription& get_subscription() const { return m_sub; } + + private: + rpp::composite_subscription m_sub; + details::queue_worker_state m_queue{}; + }; + + public: + run_loop(const composite_subscription& sub = composite_subscription{}) + : m_state(std::make_shared(sub)) {} + + auto create_worker( + const composite_subscription& sub = composite_subscription{}) const { + auto res = m_state->get_subscription().add(sub); + sub.add([weak = std::weak_ptr{m_state}, res] { + if (const auto sh = weak.lock()) sh->get_subscription().remove(res); + }); + return worker{ + std::shared_ptr>{ + m_state, &m_state->get_queue()}, + sub}; + } + + bool is_empty() const { return m_state->get_queue().is_empty(); } + + bool is_any_ready_schedulable() const { + return m_state->get_queue().is_any_ready_schedulable(); + } + + void dispatch_if_ready() const { + std::optional fn{}; + if (m_state->get_queue().pop_if_ready(fn)) (*fn)(); + } + + void dispatch() const { + std::optional fn{}; + if (m_state->get_queue().pop_with_wait(fn)) (*fn)(); + } + + private: + const std::shared_ptr m_state{}; +}; +} // namespace rpp::schedulers diff --git a/symmetri/gui/rpp/rpp/schedulers/trampoline_scheduler.hpp b/symmetri/gui/rpp/rpp/schedulers/trampoline_scheduler.hpp new file mode 100644 index 0000000..983cdff --- /dev/null +++ b/symmetri/gui/rpp/rpp/schedulers/trampoline_scheduler.hpp @@ -0,0 +1,173 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// TC Wang 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include // state +#include +#include // worker +#include // own forwarding +#include // lifetime +#include +#include + +namespace rpp::schedulers { +/** + * \brief Schedules execution of schedulables via queueing tasks to the caller + * thread with priority to time_point and order. \warning Caller thread is + * thread where "schedule" called. \warning For example, in case of merging + * values from two threads/observables and then using "trampoline", then it + * would be thread_local for each emission/thread, not one thread selected for + * all threads. + * + * \par Example + * \snippet trampoline.cpp trampoline + * + * \ingroup schedulers + */ +class trampoline final : public details::scheduler_tag { + class current_thread_schedulable; + class worker_strategy; + + using trampoline_schedulable = schedulable_wrapper; + + class worker_strategy { + public: + explicit worker_strategy(const rpp::composite_subscription& subscription) + : m_sub{subscription} {} + + bool is_subscribed() const { return m_sub.is_subscribed(); } + + void defer_at(time_point time_point, + constraint::schedulable_fn auto&& fn) const { + if (!m_sub.is_subscribed()) return; + + const bool someone_owns_queue = s_queue.has_value(); + + const auto drain_on_exit = + utils::finally_action(!someone_owns_queue ? &drain_queue : +[] {}); + + if (!someone_owns_queue) { + s_queue = std::priority_queue{}; + + if (!details::immediate_scheduling_while_condition( + time_point, fn, m_sub, []() { return s_queue->empty(); })) + return; + + // update time to make it more accurate due to we are going to push it + // to queue + time_point = std::max(now(), time_point); + } + + defer_at(time_point, + trampoline_schedulable{*this, time_point, + std::forward(fn)}); + } + + void defer_at(time_point time_point, trampoline_schedulable&& fn) const { + if (!m_sub.is_subscribed()) return; + + s_queue->emplace(time_point, std::move(fn), m_sub); + } + + static time_point now() { return clock_type::now(); } + + private: + rpp::composite_subscription m_sub; + }; + + static void drain_queue() { + if (!s_queue.has_value()) return; + + auto reset_at_final = utils::finally_action{[] { s_queue.reset(); }}; + std::optional function{}; + + while (!s_queue->empty()) { + const auto& top = s_queue->top(); + + wait_and_extract_executable_if_subscribed(top, function); + + // firstly we need to pop schedulable from queue due to execution of + // function can add new schedulable + s_queue->pop(); + + if (function) (*function)(); + + function.reset(); + } + } + + static void wait_and_extract_executable_if_subscribed( + const current_thread_schedulable& schedulable, + std::optional& out) { + if (!schedulable.is_subscribed()) return; + + // wait only if needed! + if (const auto requested_time = schedulable.get_time_point(); + details::s_last_sleep_timepoint < requested_time) { + std::this_thread::sleep_until(requested_time); + details::s_last_sleep_timepoint = requested_time; + + if (!schedulable.is_subscribed()) return; + } + + out.emplace(std::move(schedulable.extract_function())); + } + + class current_thread_schedulable + : public details::schedulable { + public: + current_thread_schedulable(time_point time_point, std::invocable auto&& fn, + rpp::composite_subscription subscription) + : schedulable(time_point, get_thread_local_id(), + std::forward(fn)), + m_subscription{std::move(subscription)} {} + + bool is_subscribed() const { return m_subscription.is_subscribed(); } + + private: + static size_t get_thread_local_id() { + static thread_local size_t s_id; + return s_id++; + } + + private: + rpp::composite_subscription m_subscription{}; + }; + + /** + * \brief Optional thread_local schedulable queue. If optional has value -> + * someone just owns thread. + */ + inline static thread_local std::optional< + std::priority_queue> + s_queue{}; + + public: + static utils::finally_action + own_queue_and_drain_finally_if_not_owned() { + const bool someone_owns_queue = s_queue.has_value(); + + if (!someone_owns_queue) + s_queue = std::priority_queue{}; + + return {!someone_owns_queue ? &drain_queue : +[] {}}; + } + + static auto create_worker( + const rpp::composite_subscription& sub = composite_subscription{}) { + return worker{sub}; + } +}; +} // namespace rpp::schedulers diff --git a/symmetri/gui/rpp/rpp/sources.hpp b/symmetri/gui/rpp/rpp/sources.hpp new file mode 100644 index 0000000..f8206da --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources.hpp @@ -0,0 +1,26 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup creational_operators Creational Operators + * \brief Creational operators are operators that create new observable + * \see https://reactivex.io/documentation/operators.html#creating + * \ingroup operators + */ + +#include +#include +#include +#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/sources/create.hpp b/symmetri/gui/rpp/rpp/sources/create.hpp new file mode 100644 index 0000000..9919a7e --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/create.hpp @@ -0,0 +1,81 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(create_tag); + +namespace rpp::observable { +/** + * \brief Creates rpp::specific_observable with passed action as OnSubscribe + * + * \marble create + { + operator "create: on_next(1), on_next(3), on_completed()": +--1--3--| + } + * + * \tparam Type manually specified type of value provided by this observable + * \param on_subscribe is action called after subscription on this observable + * \return rpp::specific_observable with passed action + * + * \par Examples: + * \snippet create.cpp create + * \snippet create.cpp create with capture + * \snippet create.cpp create type deduction + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/create.html + */ +template OnSubscribeFn> +auto create(OnSubscribeFn&& on_subscribe) + requires rpp::details::is_header_included +{ + using CreatedOnSubscribeFn = std::decay_t; + return specific_observable{ + std::forward(on_subscribe)}; +} + +/** + * \brief Creates rpp::specific_observable with passed action as OnSubscribe + * + * \warning this overloading deduce type of observable from passed function + argument + * + * \marble create + { + operator "create: on_next(1), on_next(3), on_completed()": +--1--3--| + } + * + * \param on_subscribe is action called after subscription on this observable + * \return rpp::specific_observable with passed action + * + * \par Examples: + * \snippet create.cpp create + * \snippet create.cpp create with capture + * \snippet create.cpp create type deduction + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/create.html + */ +template + requires constraint::on_subscribe_fn +auto create(OnSubscribeFn&& on_subscribe) + requires rpp::details::is_header_included +{ + return create(std::forward(on_subscribe)); +} +} // namespace rpp::observable diff --git a/symmetri/gui/rpp/rpp/sources/empty.hpp b/symmetri/gui/rpp/rpp/sources/empty.hpp new file mode 100644 index 0000000..d218485 --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/empty.hpp @@ -0,0 +1,40 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(empty_tag); + +namespace rpp::observable { +/** + * \brief Creates rpp::specific_observable that emits no items but terminates + normally + * + * \marble empty + { + operator "empty": +| + } + * + * \tparam Type type of value to specify observable + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/empty-never-throw.html + */ +template +auto empty() + requires rpp::details::is_header_included +{ + return create([](const auto& sub) { sub.on_completed(); }); +} +} // namespace rpp::observable diff --git a/symmetri/gui/rpp/rpp/sources/error.hpp b/symmetri/gui/rpp/rpp/sources/error.hpp new file mode 100644 index 0000000..3d3982c --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/error.hpp @@ -0,0 +1,41 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include + +IMPLEMENTATION_FILE(error_tag); + +namespace rpp::observable { +/** + * \brief Creates rpp::specific_observable that emits no items and terminates + with an error + * + * \marble error + { + operator "error": +# + } + * \tparam Type type of value to specify observable + * \param err exception ptr to be sent to subscriber + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/empty-never-throw.html + */ +template +auto error(const std::exception_ptr& err) + requires rpp::details::is_header_included +{ + return create([err](const auto& sub) { sub.on_error(err); }); +} +} // namespace rpp::observable diff --git a/symmetri/gui/rpp/rpp/sources/from.hpp b/symmetri/gui/rpp/rpp/sources/from.hpp new file mode 100644 index 0000000..8612366 --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/from.hpp @@ -0,0 +1,260 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +IMPLEMENTATION_FILE(just_tag); +IMPLEMENTATION_FILE(from_tag); + +namespace rpp::observable::details { +template +auto extract_iterable_from_packed(const T& v) -> const auto& { + return v; +} + +template +auto extract_iterable_from_packed(const std::shared_ptr& v) -> const auto& { + return *v; +} + +void iterate(const auto& iterable, + const schedulers::constraint::scheduler auto& scheduler, + constraint::subscriber auto&& subscriber) { + if constexpr (constraint::decayed_same_as) { + for (const auto& v : extract_iterable_from_packed(iterable)) { + if (subscriber.is_subscribed()) + subscriber.on_next(utils::as_const(v)); + else + return; + } + subscriber.on_completed(); + } else { + auto worker = scheduler.create_worker(subscriber.get_subscription()); + worker.schedule( + [iterable = iterable, + subscriber = std::forward(subscriber), + index = size_t{0}]() mutable -> schedulers::optional_duration { + try { + const auto& extracted_iterable = + extract_iterable_from_packed(iterable); + const auto end = std::cend(extracted_iterable); + auto itr = std::cbegin(extracted_iterable); + + std::advance(itr, static_cast(index)); + + if (itr != end) { + subscriber.on_next(utils::as_const(*itr)); + if (std::next(itr) != end) // it was not last + { + ++index; + return schedulers::duration{}; // re-schedule this + } + } + + subscriber.on_completed(); + } catch (...) { + subscriber.on_error(std::current_exception()); + } + return std::nullopt; + }); + } +} + +template +auto pack_to_container(Ts&&... items) { + if constexpr (memory_model == memory_model::use_stack) + return Container{std::forward(items)...}; + else + // raw new call to avoid extra copy/moves for items + return std::shared_ptr( + new Container{std::forward(items)...}); +} + +template +auto pack_variadic(Ts&&... items) { + return pack_to_container>( + std::forward(items)...); +} + +template +class iterate_impl { + public: + iterate_impl(const PackedIterable& iterable, const TScheduler& scheduler) + : m_iterable{iterable}, m_scheduler{scheduler} {} + + iterate_impl(PackedIterable&& iterable, const TScheduler& scheduler) + : m_iterable{std::move(iterable)}, m_scheduler{scheduler} {} + + template + void operator()(TSub&& subscriber) const { + details::iterate(m_iterable, m_scheduler, std::forward(subscriber)); + } + + private: + PackedIterable m_iterable; + RPP_NO_UNIQUE_ADDRESS TScheduler m_scheduler; +}; +} // namespace rpp::observable::details + +namespace rpp::observable { +/** + * \brief Creates rpp::specific_observable that emits a particular items and + completes + * + * \marble just + { + operator "just(1,2,3,5)": +-1-2-3-5-| + } + * + * \tparam memory_model rpp::memory_model startegy used to handle provided items + * \tparam Scheduler type of scheduler used for scheduling of submissions: next + item will be submitted to scheduler when previous one is executed + * \param item first value to be sent + * \param items rest values to be sent + * \return rpp::specific_observable with provided item + * + * \par Examples: + * \snippet just.cpp just + * \snippet just.cpp just memory model + * \snippet just.cpp just scheduler + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/just.html + */ +template +auto just(const schedulers::constraint::scheduler auto& scheduler, T&& item, + Ts&&... items) + requires(rpp::details::is_header_included && + (constraint::decayed_same_as && ...)) +{ + return create>(details::iterate_impl{ + details::pack_variadic>( + std::forward(item), std::forward(items)...), + scheduler}); +} + +/** + * \brief Creates rpp::specific_observable that emits a particular items and + completes + * \warning this overloading uses trampoline scheduler as default + * + * \marble just + { + operator "just(1,2,3,5)": +-1-2-3-5-| + } + * + * \tparam memory_model rpp::memory_model startegy used to handle provided items + * \param item first value to be sent + * \param items rest values to be sent + * \return rpp::specific_observable with provided item + * + * \par Examples: + * \snippet just.cpp just + * \snippet just.cpp just memory model + * \snippet just.cpp just scheduler + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/just.html + */ +template +auto just(T&& item, Ts&&... items) + requires(rpp::details::is_header_included && + (constraint::decayed_same_as && ...)) +{ + return just(schedulers::trampoline{}, std::forward(item), + std::forward(items)...); +} + +/** + * \brief Creates rpp::specific_observable that emits a items from provided + iterable + * + * \marble from_iterable + { + operator "from_iterable({1,2,3,5})": +-1-2-3-5-| + } + * + * \tparam memory_model rpp::memory_model strategy used to handle provided + iterable + * \param scheduler is scheduler used for scheduling of submissions: next item + will be submitted to scheduler when previous one is executed + * \param iterable container with values which will be flattened + * + * \par Examples: + * \snippet from.cpp from_iterable + * \snippet from.cpp from_iterable with model + * \snippet from.cpp from_iterable with scheduler + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/from.html + */ +template < + memory_model memory_model /* = memory_model::use_stack */, + schedulers::constraint::scheduler TScheduler /* = schedulers::trampoline */> +auto from_iterable(constraint::iterable auto&& iterable, + const TScheduler& scheduler /* = TScheduler{} */) + requires rpp::details::is_header_included +{ + using Container = std::decay_t; + return create>( + details::iterate_impl{details::pack_to_container( + std::forward(iterable)), + scheduler}); +} + +/** + * \brief Creates rpp::specific_observable that calls provided callable and + emits resulting value of this callable + * + * \marble from_callable + { + operator "from_callable: [](){return 42;}": +-(42)--| + } + * + * \tparam memory_model rpp::memory_model strategy used to handle callable + * + * \par Example + * \snippet from.cpp from_callable + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/from.html + */ +template +auto from_callable(std::invocable<> auto&& callable) + requires rpp::details::is_header_included +{ + auto obs = just(std::forward(callable)); + + if constexpr (std::same_as, + void>) + return std::move(obs).map([](const auto& fn) { + fn(); + return utils::none{}; + }); + else + return std::move(obs).map([](const auto& fn) { return fn(); }); +} +} // namespace rpp::observable diff --git a/symmetri/gui/rpp/rpp/sources/fwd.hpp b/symmetri/gui/rpp/rpp/sources/fwd.hpp new file mode 100644 index 0000000..b957368 --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/fwd.hpp @@ -0,0 +1,104 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace rpp::details { +struct create_tag; +struct empty_tag; +struct never_tag; +struct error_tag; +struct just_tag; +struct from_tag; +struct interval_tag; + +} // namespace rpp::details + +namespace rpp::observable { +//**************************** CREATE ****************// +template OnSubscribeFn> +auto create(OnSubscribeFn&& on_subscribe) + requires rpp::details::is_header_included; + +template >> + requires constraint::on_subscribe_fn +auto create(OnSubscribeFn&& on_subscribe) + requires rpp::details::is_header_included; + +//**************************** EMPTY *****************// +template +auto empty() + requires rpp::details::is_header_included; + +//**************************** NEVER *****************// +template +auto never() + requires rpp::details::is_header_included; + +//**************************** ERROR *****************// +template +auto error(const std::exception_ptr& err) + requires rpp::details::is_header_included; + +//************************** FROM ***********************// +template +auto just(const schedulers::constraint::scheduler auto& scheduler, T&& item, + Ts&&... items) + requires(rpp::details::is_header_included && + (constraint::decayed_same_as && ...)); + +template +auto just(T&& item, Ts&&... items) + requires(rpp::details::is_header_included && + (constraint::decayed_same_as && ...)); + +template +auto from_iterable(constraint::iterable auto&& iterable, + const TScheduler& scheduler = TScheduler{}) + requires rpp::details::is_header_included; + +template +auto from_callable(std::invocable<> auto&& callable) + requires rpp::details::is_header_included; + +//************************ INTERVAL *********************// +template +auto interval(schedulers::duration period, + const TScheduler& scheduler = TScheduler{}) + requires rpp::details::is_header_included; + +template +auto interval(schedulers::duration first_delay, schedulers::duration period, + const TScheduler& scheduler = TScheduler{}) + requires rpp::details::is_header_included; +} // namespace rpp::observable + +namespace rpp { +namespace source = observable; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/sources/interval.hpp b/symmetri/gui/rpp/rpp/sources/interval.hpp new file mode 100644 index 0000000..af0aa55 --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/interval.hpp @@ -0,0 +1,97 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include + +IMPLEMENTATION_FILE(interval_tag); + +namespace rpp::observable { +/** + * \brief Creates rpp::specific_observable which emits sequence of size_t every + provided time interval. + * + * \marble interval + { + operator "interval: 200": +--0--1--2--3--4--5--6--> + } + * + * \warn First emission also scheduled and delayed with same interval + * + * \param period period which would be used to delay emissions between each + other + * \param scheduler used for scheduling this periodic emissions + * \return rpp::specific_observable which emits values with provided + time_interval + * + * \par Examples: + * \snippet interval.cpp interval + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/interval.html + */ +template < + schedulers::constraint::scheduler TScheduler /*= schedulers::trampoline */> +auto interval(schedulers::duration period, + const TScheduler& scheduler /* = TScheduler{} */) + requires rpp::details::is_header_included +{ + return interval(period, period, scheduler); +} + +/** + * \brief Creates rpp::specific_observable which emits sequence of size_t every + provided time interval with first emission after provided delay + * + * \marble interval_init + { + operator "interval: 100, 200": +-0---1---2---3---4---5---6--> + } + * + * \param first_delay period which would be used to delay first emission + * \param period period which would be used to delay emissions between each + other + * \param scheduler used for scheduling this periodic emissions + * \return rpp::specific_observable which emits values with provided + time_interval + * + * \par Examples: + * \snippet interval.cpp interval_with_first + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/interval.html + */ +template < + schedulers::constraint::scheduler TScheduler /*= schedulers::trampoline*/> +auto interval(schedulers::duration first_delay, schedulers::duration period, + const TScheduler& scheduler /* = TScheduler{} */) + requires rpp::details::is_header_included +{ + return source::create( + [first_delay, period, scheduler](auto&& subscriber) { + auto worker = scheduler.create_worker(subscriber.get_subscription()); + worker.schedule( + first_delay, + [counter = size_t{}, + subscriber = std::forward(subscriber), + period]() mutable -> schedulers::optional_duration { + subscriber.on_next(counter++); + return period; + }); + }); +} +} // namespace rpp::observable diff --git a/symmetri/gui/rpp/rpp/sources/just.hpp b/symmetri/gui/rpp/rpp/sources/just.hpp new file mode 100644 index 0000000..a92451b --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/just.hpp @@ -0,0 +1,13 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include diff --git a/symmetri/gui/rpp/rpp/sources/never.hpp b/symmetri/gui/rpp/rpp/sources/never.hpp new file mode 100644 index 0000000..d2c2096 --- /dev/null +++ b/symmetri/gui/rpp/rpp/sources/never.hpp @@ -0,0 +1,39 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +IMPLEMENTATION_FILE(never_tag); + +namespace rpp::observable { +/** + * \brief Creates rpp::specific_observable that emits no items and does not + terminate + * + * \marble never + { + operator "never": +> + } + * \tparam Type type of value to specify observable + * + * \ingroup creational_operators + * \see https://reactivex.io/documentation/operators/empty-never-throw.html + */ +template +auto never() + requires rpp::details::is_header_included +{ + return create([](const auto&) {}); +} +} // namespace rpp::observable diff --git a/symmetri/gui/rpp/rpp/subjects.hpp b/symmetri/gui/rpp/rpp/subjects.hpp new file mode 100644 index 0000000..37c7e08 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects.hpp @@ -0,0 +1,21 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup subjects Subjects + * \brief Observable is the observable and observer at the same time. Uses as a + * bridge and for manual sending of values. \see + * https://reactivex.io/documentation/subject.html \ingroup rpp + */ + +#include +#include diff --git a/symmetri/gui/rpp/rpp/subjects/behavior_subject.hpp b/symmetri/gui/rpp/rpp/subjects/behavior_subject.hpp new file mode 100644 index 0000000..834f564 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects/behavior_subject.hpp @@ -0,0 +1,129 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include +#include + +namespace rpp::subjects::details { +template +class behavior_strategy { + public: + template TT, + rpp::constraint::decayed_same_as TSub> + behavior_strategy(TT&& v, TSub&& sub) + : m_state{std::make_shared(std::forward(v))}, + m_sub{std::forward(sub)} { + m_sub.add([state = std::weak_ptr{m_state}] { + if (const auto locked = state.lock()) locked->on_unsubscribe(); + }); + } + + void on_subscribe(const dynamic_subscriber& sub) const { + if (m_sub.is_subscribed()) sub.on_next(m_state->get_value()); + + m_state->on_subscribe(sub); + } + + auto get_subscriber() const { + return rpp::make_specific_subscriber( + m_sub, + [state = m_state](const T& v) { + state->set_value(v); + state->on_next(v); + }, + [state = m_state](const std::exception_ptr& err) { + state->on_error(err); + }, + [state = m_state]() { state->on_completed(); }); + } + + T get_value() const { return m_state->get_value(); } + + private: + class behavior_state : public subject_state { + public: + behavior_state(const T& v) : subject_state{}, value{v} {} + + behavior_state(T&& v) : subject_state{}, value{std::move(v)} {} + + T get_value() { + std::lock_guard lock{mutex}; + return value; + } + + void set_value(const T& v) { + std::lock_guard lock{mutex}; + value = v; + } + + private: + std::mutex mutex; + T value; + }; + + std::shared_ptr m_state; + composite_subscription m_sub{}; +}; +} // namespace rpp::subjects::details + +namespace rpp::subjects { +/** + * \brief Subject which multicasts values to observers subscribed on it and + * sends last emitted value (or initial value) on subscribe. It contains two + * parts: subscriber and observable at the same time. + * + * \details Each subscriber obtains only last/initial value + values which + * emitted after corresponding subscribe. on_error/on_completer/unsubscribe + * cached and provided to new subscribers if any + * + * \warning this subject is not synchronized/serialized! It means, that expected + * to call callbacks of subscriber in the serialized way to follow observable + * contract: "Observables must issue notifications to observers serially (not in + * parallel).". If you are not sure or need extra serialization, please, use + * serialized_subject. + * + * \tparam T value provided by this subject + * + * \ingroup subjects + * \see https://reactivex.io/documentation/subject.html + */ +template +class behavior_subject final + : public details::base_subject> { + public: + behavior_subject(const T& initial_value, const composite_subscription& sub) + : details::base_subject>{initial_value, + sub} {} + + behavior_subject(T&& initial_value, const composite_subscription& sub) + : details::base_subject>{ + std::move(initial_value), sub} {} + + behavior_subject(const T& initial_value, + composite_subscription&& sub = composite_subscription{}) + : details::base_subject>{ + initial_value, std::move(sub)} {} + + behavior_subject(T&& initial_value, + composite_subscription&& sub = composite_subscription{}) + : details::base_subject>{ + std::move(initial_value), std::move(sub)} {} + + T get_value() const { + return details::base_subject>::get_strategy() + .get_value(); + } +}; +} // namespace rpp::subjects diff --git a/symmetri/gui/rpp/rpp/subjects/constraints.hpp b/symmetri/gui/rpp/rpp/subjects/constraints.hpp new file mode 100644 index 0000000..8d0cd5a --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects/constraints.hpp @@ -0,0 +1,24 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include + +namespace rpp::subjects::constraint { +template +concept subject = std::derived_from, details::subject_tag>; + +template +concept subject_of_type = + subject && std::same_as, Type>; + +} // namespace rpp::subjects::constraint diff --git a/symmetri/gui/rpp/rpp/subjects/details/base_subject.hpp b/symmetri/gui/rpp/rpp/subjects/details/base_subject.hpp new file mode 100644 index 0000000..05f8288 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects/details/base_subject.hpp @@ -0,0 +1,40 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include + +namespace rpp::subjects::details { +struct subject_tag {}; + +template Strategy> +class base_subject : public subject_tag { + public: + auto get_subscriber() const { return m_strategy.get_subscriber(); } + + auto get_observable() const { + return source::create([strategy = this->m_strategy](const auto& sub) { + strategy.on_subscribe(sub); + }); + } + + protected: + base_subject(auto&&... args) + : m_strategy{std::forward(args)...} {} + + const Strategy& get_strategy() const { return m_strategy; } + + private: + Strategy m_strategy{}; +}; +} // namespace rpp::subjects::details diff --git a/symmetri/gui/rpp/rpp/subjects/details/subject_state.hpp b/symmetri/gui/rpp/rpp/subjects/details/subject_state.hpp new file mode 100644 index 0000000..2e0c18a --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects/details/subject_state.hpp @@ -0,0 +1,151 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rpp::subjects::details { +struct completed {}; +struct unsubscribed {}; + +template +class subject_state : public std::enable_shared_from_this> { + using subscriber = dynamic_subscriber; + using shared_subscribers = std::shared_ptr>; + using weak_subscribers = std::weak_ptr>; + using state_t = std::variant; + + public: + subject_state() = default; + subject_state(const subject_state&) = delete; + subject_state(subject_state&&) noexcept = delete; + + void on_subscribe(const subscriber& subscriber) { + std::unique_lock lock{m_mutex}; + + process_state( + m_state, + [&](const shared_subscribers& subs) { + auto new_subs = make_copy_of_subscribed_subs(subs->size() + 1, subs); + new_subs->push_back(subscriber); + m_state = new_subs; + m_weak_subscribers = new_subs; + + lock.unlock(); + + add_callback_on_unsubscribe(subscriber); + }, + [&](std::exception_ptr err) { + lock.unlock(); + subscriber.on_error(err); + }, + [&](completed) { + lock.unlock(); + subscriber.on_completed(); + }, + [&](unsubscribed) { + lock.unlock(); + subscriber.unsubscribe(); + }); + } + + void on_next(const T& v) { + if (auto subs = extract_subscribers_under_lock_if_there()) + rpp::utils::for_each(*subs, [&](const auto& sub) { sub.on_next(v); }); + } + + void on_error(const std::exception_ptr& err) { + if (auto subs = exchange_subscribers_under_lock_if_there(state_t{err})) + rpp::utils::for_each(*subs, [&](const auto& sub) { sub.on_error(err); }); + } + + void on_completed() { + if (auto subs = exchange_subscribers_under_lock_if_there(completed{})) + rpp::utils::for_each(*subs, + std::mem_fn(&dynamic_subscriber::on_completed)); + } + + void on_unsubscribe() { + if (auto subs = exchange_subscribers_under_lock_if_there(unsubscribed{})) + rpp::utils::for_each(*subs, + std::mem_fn(&dynamic_subscriber::unsubscribe)); + } + + private: + static void process_state(const state_t& state, const auto&... actions) { + std::visit(rpp::utils::overloaded{actions..., [](auto) {}}, state); + } + + static shared_subscribers make_copy_of_subscribed_subs( + size_t expected_size, shared_subscribers current_subs) { + auto subs = std::make_shared>>(); + subs->reserve(expected_size); + std::copy_if(current_subs->cbegin(), current_subs->cend(), + std::back_inserter(*subs), + std::mem_fn(&dynamic_subscriber::is_subscribed)); + return subs; + } + + void add_callback_on_unsubscribe(const dynamic_subscriber& subscriber) { + auto weak = this->weak_from_this(); + subscriber.get_subscription().add([weak] { + if (auto shared = weak.lock()) { + std::unique_lock lock{shared->m_mutex}; + process_state(shared->m_state, [&](const shared_subscribers& subs) { + auto new_size = std::max(subs->size(), size_t{1}) - 1; + shared->m_state = + shared->make_copy_of_subscribed_subs(new_size, subs); + }); + } + }); + } + + shared_subscribers extract_subscribers_under_lock_if_there() { + if (auto locked = m_weak_subscribers.lock()) return locked; + + std::unique_lock lock{m_mutex}; + + if (!std::holds_alternative(m_state)) return {}; + + auto subs = std::get(m_state); + m_weak_subscribers = subs; + + lock.unlock(); + return subs; + } + + shared_subscribers exchange_subscribers_under_lock_if_there( + state_t&& new_val) { + std::unique_lock lock{m_mutex}; + + if (!std::holds_alternative(m_state)) return {}; + + auto subs = std::get(m_state); + m_state = std::move(new_val); + + lock.unlock(); + return subs; + } + + private: + std::mutex m_mutex{}; + state_t m_state = std::make_shared>(); + weak_subscribers m_weak_subscribers = std::get(m_state); +}; +} // namespace rpp::subjects::details diff --git a/symmetri/gui/rpp/rpp/subjects/fwd.hpp b/symmetri/gui/rpp/rpp/subjects/fwd.hpp new file mode 100644 index 0000000..6967869 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects/fwd.hpp @@ -0,0 +1,36 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include + +namespace rpp::subjects::details { +struct subject_tag; + +template +concept subject_strategy = + requires(Strategy t) { + { t.get_subscriber() } -> rpp::constraint::subscriber; + t.on_subscribe(std::declval>()); + }; + +template Strategy> +class base_subject; +} // namespace rpp::subjects::details + +namespace rpp::subjects { +template +class publish_subject; + +template +class behavior_subject; +} // namespace rpp::subjects diff --git a/symmetri/gui/rpp/rpp/subjects/publish_subject.hpp b/symmetri/gui/rpp/rpp/subjects/publish_subject.hpp new file mode 100644 index 0000000..a362a50 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects/publish_subject.hpp @@ -0,0 +1,80 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include +#include +#include + +namespace rpp::subjects::details { +template +class publish_strategy { + public: + template TSub> + publish_strategy(TSub&& sub) : m_sub{std::forward(sub)} { + m_sub.add([state = std::weak_ptr{m_state}] { + if (const auto locked = state.lock()) locked->on_unsubscribe(); + }); + } + + void on_subscribe(const dynamic_subscriber& sub) const { + m_state->on_subscribe(sub); + } + + auto get_subscriber() const { + return rpp::make_specific_subscriber( + m_sub, [state = m_state](const T& v) { state->on_next(v); }, + [state = m_state](const std::exception_ptr& err) { + state->on_error(err); + }, + [state = m_state]() { state->on_completed(); }); + } + + private: + std::shared_ptr> m_state = + std::make_shared>(); + composite_subscription m_sub{}; +}; +} // namespace rpp::subjects::details + +namespace rpp::subjects { +/** + * \brief Subject which just multicasts values to observers subscribed on it. It + * contains two parts: subscriber and observable at the same time. + * + * \details Each subscriber obtains only values which emitted after + * corresponding subscribe. on_error/on_completer/unsubscribe cached and + * provided to new subscribers if any + * + * \warning this subject is not synchronized/serialized! It means, that expected + * to call callbacks of subscriber in the serialized way to follow observable + * contract: "Observables must issue notifications to observers serially (not in + * parallel).". If you are not sure or need extra serialization, please, use + * serialized_subject. + * + * \tparam T value provided by this subject + * + * \ingroup subjects + * \see https://reactivex.io/documentation/subject.html + */ +template +class publish_subject final + : public details::base_subject> { + public: + publish_subject(const composite_subscription& sub) + : details::base_subject>{sub} {} + + publish_subject(composite_subscription&& sub = composite_subscription{}) + : details::base_subject>{std::move(sub)} { + } +}; +} // namespace rpp::subjects diff --git a/symmetri/gui/rpp/rpp/subjects/type_traits.hpp b/symmetri/gui/rpp/rpp/subjects/type_traits.hpp new file mode 100644 index 0000000..8864d68 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subjects/type_traits.hpp @@ -0,0 +1,28 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::subjects::utils { +namespace details { +template +struct extract_subject_type { + template + static TT deduce(const subjects::details::base_subject&); + + using type = decltype(deduce(std::declval>())); +}; +} // namespace details + +template +using extract_subject_type_t = typename details::extract_subject_type::type; +} // namespace rpp::subjects::utils diff --git a/symmetri/gui/rpp/rpp/subscribers.hpp b/symmetri/gui/rpp/rpp/subscribers.hpp new file mode 100644 index 0000000..047839c --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscribers.hpp @@ -0,0 +1,14 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include diff --git a/symmetri/gui/rpp/rpp/subscribers/constraints.hpp b/symmetri/gui/rpp/rpp/subscribers/constraints.hpp new file mode 100644 index 0000000..a8f06cb --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscribers/constraints.hpp @@ -0,0 +1,44 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::constraint { +template +concept subscriber = + std::is_base_of_v> && + observer_callbacks_exists; +} + +namespace rpp::utils { +namespace details { +template +struct extract_subscriber_type { + template + static TT deduce(const rpp::details::typed_subscriber_tag&); + + using type = decltype(deduce(std::declval>())); +}; +} // namespace details + +template +using extract_subscriber_type_t = + typename details::extract_subscriber_type::type; +} // namespace rpp::utils + +namespace rpp::constraint { +template +concept subscriber_of_type = + subscriber && std::same_as, Type> && + observer_on_next_exists; +} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/subscribers/details/subscriber_base.hpp b/symmetri/gui/rpp/rpp/subscribers/details/subscriber_base.hpp new file mode 100644 index 0000000..afaf75e --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscribers/details/subscriber_base.hpp @@ -0,0 +1,53 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +/** + * \brief base implementation of subscriber with possibility to obtain + * observer's callbacks, query subscription state, unsubscribe and etc. Each + * observer's callback checks for actual subscription \tparam Type type of + * values expected by this subscriber + */ +class subscriber_base { + public: + subscriber_base( + composite_subscription&& subscription = composite_subscription{}) + : m_subscription{std::move(subscription)} {} + + subscriber_base(const composite_subscription& subscription) + : m_subscription{subscription} {} + + const composite_subscription& get_subscription() const { + return m_subscription; + } + + [[nodiscard]] bool is_subscribed() const { + return m_subscription.is_subscribed(); + } + + void unsubscribe() const { m_subscription.unsubscribe(); } + + protected: + void do_if_subscribed_and_unsubscribe(const auto& callable) const { + if (!is_subscribed()) return; + + subscription_guard guard{m_subscription}; + callable(); + } + + private: + composite_subscription m_subscription{}; +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/subscribers/dynamic_subscriber.hpp b/symmetri/gui/rpp/rpp/subscribers/dynamic_subscriber.hpp new file mode 100644 index 0000000..dec5641 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscribers/dynamic_subscriber.hpp @@ -0,0 +1,52 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp { +/** + * \brief subscriber which uses dynamic_observer to hide original callbacks + * \tparam T type of values expected by this subscriber + */ +template +class dynamic_subscriber final + : public specific_subscriber> { + public: + using specific_subscriber>::specific_subscriber; + + template TSub> + requires(!std::is_same_v, dynamic_subscriber>) + dynamic_subscriber(const TSub& subscriber) + : specific_subscriber>{ + subscriber.get_subscription(), subscriber.get_observer()} {} +}; + +template +dynamic_subscriber(TObs observer) + -> dynamic_subscriber>; + +template +dynamic_subscriber(composite_subscription, TObs observer) + -> dynamic_subscriber>; + +template +dynamic_subscriber(specific_subscriber) -> dynamic_subscriber; + +template +dynamic_subscriber(composite_subscription, OnNext, Args...) + -> dynamic_subscriber>>; + +template +dynamic_subscriber(OnNext, Args...) + -> dynamic_subscriber>>; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/subscribers/fwd.hpp b/symmetri/gui/rpp/rpp/subscribers/fwd.hpp new file mode 100644 index 0000000..2cb8627 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscribers/fwd.hpp @@ -0,0 +1,29 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp::details { +struct subscriber_tag {}; + +template +struct typed_subscriber_tag : public subscriber_tag {}; +} // namespace rpp::details + +namespace rpp { +template +class specific_subscriber; + +template +class dynamic_subscriber; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/subscribers/specific_subscriber.hpp b/symmetri/gui/rpp/rpp/subscribers/specific_subscriber.hpp new file mode 100644 index 0000000..2e596d1 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscribers/specific_subscriber.hpp @@ -0,0 +1,121 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace rpp { +/** + * \brief specific version of subscriber which stores type of observer used + * inside to prevent extra allocations \tparam Type type of values expected by + * this subscriber \tparam Observer observer which was wrapped by this + * subscriber + */ +template +class specific_subscriber : public details::typed_subscriber_tag, + public details::subscriber_base { + public: + template + specific_subscriber(Types&&... vals) + requires std::constructible_from + : subscriber_base{}, m_observer{std::forward(vals)...} {} + + template + specific_subscriber(composite_subscription sub, Types&&... vals) + requires std::constructible_from + : subscriber_base{std::move(sub)}, + m_observer{std::forward(vals)...} {} + + const Observer& get_observer() const { return m_observer; } + + void on_next(const Type& val) const { on_next_impl(val); } + + void on_next(Type&& val) const { on_next_impl(std::move(val)); } + + void on_error(const std::exception_ptr& err) const { + do_if_subscribed_and_unsubscribe( + [&err, this] { m_observer.on_error(err); }); + } + + void on_completed() const { + do_if_subscribed_and_unsubscribe([this] { m_observer.on_completed(); }); + } + + auto as_dynamic() const& { return dynamic_subscriber{*this}; } + auto as_dynamic() && { + return dynamic_subscriber{this->get_subscription(), + std::move(m_observer)}; + } + + private: + void on_next_impl(auto&& val) const { + if (!is_subscribed()) return; + + try { + m_observer.on_next(std::forward(val)); + } catch (...) { + on_error(std::current_exception()); + } + } + + RPP_NO_UNIQUE_ADDRESS Observer m_observer{}; +}; + +template +specific_subscriber(TObs observer) + -> specific_subscriber, TObs>; + +template +specific_subscriber(composite_subscription, TObs observer) + -> specific_subscriber, TObs>; + +template >> +specific_subscriber(composite_subscription, OnNext, Args...) + -> specific_subscriber< + Type, details::deduce_specific_observer_type_t>; + +template >> +specific_subscriber(OnNext, Args...) -> specific_subscriber< + Type, details::deduce_specific_observer_type_t>; + +/** + * \brief Creation of rpp::specific_subscriber with manual providing of type of + * subscriber. In case of ability to determine type of subscriber by function -> + * use constructor + */ +template +auto make_specific_subscriber(Args&&... args) -> specific_subscriber< + Type, details::deduce_specific_observer_type_t> { + return {std::forward(args)...}; +} + +template +auto make_specific_subscriber(composite_subscription sub, Args&&... args) + -> specific_subscriber< + Type, details::deduce_specific_observer_type_t> { + return {std::move(sub), std::forward(args)...}; +} + +template TObs, + typename... Args> +auto make_specific_subscriber(composite_subscription sub, Args&&... args) + -> specific_subscriber { + return {std::move(sub), std::forward(args)...}; +} +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/subscriptions/callback_subscription.hpp b/symmetri/gui/rpp/rpp/subscriptions/callback_subscription.hpp new file mode 100644 index 0000000..de46d61 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscriptions/callback_subscription.hpp @@ -0,0 +1,44 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include + +namespace rpp { +/** + * \brief Subscription which invoke callbable during unsubscribe + */ +class callback_subscription final : public subscription_base { + public: + template + callback_subscription(Fn&& fn) + : subscription_base{ + std::make_shared>>(std::forward(fn))} {} + + private: + template + class state final : public details::subscription_state { + public: + state(const Fn& fn) : m_fn{fn} {} + + state(Fn&& fn) : m_fn{std::move(fn)} {} + + protected: + void on_unsubscribe() override { m_fn(); } + + private: + Fn m_fn{}; + }; +}; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/subscriptions/composite_subscription.hpp b/symmetri/gui/rpp/rpp/subscriptions/composite_subscription.hpp new file mode 100644 index 0000000..e9bdb1e --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscriptions/composite_subscription.hpp @@ -0,0 +1,166 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "rpp/utils/utilities.hpp" + +namespace rpp { +/** + * \brief rpp::subscription_base with ability to add some dependent + * subscriptions as a part of this one: in case of initiation of unsubscribe of + * this subscription, then any dependent subscriptions will be unsubscribed too + */ +class composite_subscription final : public subscription_base { + public: + composite_subscription() : subscription_base{std::make_shared()} {} + + composite_subscription(const composite_subscription&) = default; + composite_subscription(composite_subscription&&) noexcept = default; + composite_subscription& operator=(const composite_subscription& other) = + default; + composite_subscription& operator=(composite_subscription&& other) noexcept = + default; + + /** + * \brief Add any other subscription to this as dependent + */ + template + std::weak_ptr add( + const TSub& sub = TSub{}) const { + if (static_cast(&sub) == + static_cast(this)) + return sub.get_state(); + + if (const auto pstate = std::static_pointer_cast(get_state())) + pstate->add(sub.get_state()); + else + sub.unsubscribe(); + return sub.get_state(); + } + + /** + * \brief Add callback/function subscription to this as dependent + */ + std::weak_ptr add( + const callback_subscription& sub) const { + return add(sub); + } + + composite_subscription make_child() const { + composite_subscription ret{}; + ret.add( + [weak_handle = add(ret), + state = std::weak_ptr{std::static_pointer_cast(get_state())}] { + // add cleanup + if (const auto locked_state = state.lock()) + if (const auto locked_handle = weak_handle.lock()) + locked_state->remove(locked_handle); + }); + return ret; + } + + void remove(const subscription_base& sub) const { + if (const auto pstate = std::static_pointer_cast(get_state())) + pstate->remove(sub.get_state()); + } + + void remove(const std::weak_ptr& sub) const { + if (const auto locked = sub.lock()) + if (const auto pstate = std::static_pointer_cast(get_state())) + pstate->remove(locked); + } + + bool is_empty() const { return !get_state(); } + + static composite_subscription empty() { + return composite_subscription{empty_tag{}}; + } + + private: + struct empty_tag {}; + + composite_subscription(const empty_tag&) + : subscription_base{std::shared_ptr{}} {} + + class state final : public details::subscription_state { + public: + state() = default; + + void add(std::shared_ptr sub) { + if (!sub || !sub->is_subscribed()) return; + + while (true) { + DepsState expected{DepsState::None}; + if (m_state.compare_exchange_strong(expected, DepsState::Edit, + std::memory_order::acq_rel)) { + m_deps.push_back(std::move(sub)); + + m_state.store(DepsState::None, std::memory_order::release); + return; + } + + if (expected == DepsState::Unsubscribed) { + sub->unsubscribe(); + return; + } + } + } + + void remove(const std::shared_ptr& sub) { + while (true) { + DepsState expected{DepsState::None}; + if (m_state.compare_exchange_strong(expected, DepsState::Edit, + std::memory_order::acq_rel)) { + std::erase(m_deps, sub); + + m_state.store(DepsState::None, std::memory_order::release); + return; + } + + if (expected == DepsState::Unsubscribed) return; + } + } + + private: + void on_unsubscribe() override { + while (true) { + DepsState expected{DepsState::None}; + if (m_state.compare_exchange_strong(expected, DepsState::Unsubscribed, + std::memory_order::acq_rel)) { + rpp::utils::for_each( + m_deps, std::mem_fn(&details::subscription_state::unsubscribe)); + m_deps.clear(); + return; + } + } + } + + private: + enum class DepsState : uint8_t { + None, //< default state + Edit, //< set it during adding new element into deps or removing. After + // success -> FallBack to None + Unsubscribed //< permanent state after unsubscribe + }; + + std::atomic m_state{DepsState::None}; + std::vector> m_deps{}; + }; +}; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/subscriptions/constraints.hpp b/symmetri/gui/rpp/rpp/subscriptions/constraints.hpp new file mode 100644 index 0000000..c9c1f2f --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscriptions/constraints.hpp @@ -0,0 +1,20 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include +#include + +namespace rpp::constraint { +template +concept subscription = std::derived_from || + decayed_same_as; +} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/subscriptions/details/subscription_state.hpp b/symmetri/gui/rpp/rpp/subscriptions/details/subscription_state.hpp new file mode 100644 index 0000000..414d15e --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscriptions/details/subscription_state.hpp @@ -0,0 +1,46 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +/** + * \brief Base implementation of subscription state used under-hood for + * rpp::subscription_base and its childs \details subscription_state uses + * atomic_bool to track current state of the subscription and where unsubscribe + * should be called or not. Used as base implementation for more complicated + * states + */ +class subscription_state { + public: + subscription_state() = default; + virtual ~subscription_state() = default; + + subscription_state(const subscription_state&) = delete; + subscription_state(subscription_state&&) noexcept = delete; + + [[nodiscard]] bool is_subscribed() const { return m_is_subscribed.load(); } + + void unsubscribe() { + if (m_is_subscribed.exchange(false)) on_unsubscribe(); + } + + protected: + /** + * \brief Derrived action on unsubscribe. Will be called only ONCE! + */ + virtual void on_unsubscribe() {} + + private: + std::atomic_bool m_is_subscribed{true}; +}; +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/subscriptions/fwd.hpp b/symmetri/gui/rpp/rpp/subscriptions/fwd.hpp new file mode 100644 index 0000000..f7027b1 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscriptions/fwd.hpp @@ -0,0 +1,18 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +namespace rpp { +class subscription_base; +class callback_subscription; +class composite_subscription; +class subscription_guard; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/subscriptions/subscription_base.hpp b/symmetri/gui/rpp/rpp/subscriptions/subscription_base.hpp new file mode 100644 index 0000000..f2769e8 --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscriptions/subscription_base.hpp @@ -0,0 +1,70 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include + +namespace rpp { +class composite_subscription; + +/** + * \brief Base subscription implementation used as base class/interface and core + * implementation for derrived subscriptions + */ +class subscription_base { + protected: + subscription_base(std::shared_ptr state) + : m_state{std::move(state)} {} + + const std::shared_ptr& get_state() const { + return m_state; + } + + friend rpp::composite_subscription; + + public: + subscription_base() + : m_state{std::make_shared()} {} + + subscription_base(const subscription_base&) = default; + subscription_base(subscription_base&&) noexcept = default; + subscription_base& operator=(const subscription_base& other) = default; + subscription_base& operator=(subscription_base&& other) noexcept = default; + + bool operator==(const subscription_base& rhs) const = default; + + virtual ~subscription_base() = default; + + static subscription_base empty() { return subscription_base{nullptr}; } + + /** + * \brief indicates current status of subscription + */ + [[nodiscard]] bool is_subscribed() const { + return m_state && m_state->is_subscribed(); + } + + /** + * \brief initiates unsubscription process (if subscribed) + */ + void unsubscribe() const { + if (m_state) { + m_state->unsubscribe(); + m_state.reset(); + } + } + + private: + mutable std::shared_ptr m_state{}; +}; + +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/subscriptions/subscription_guard.hpp b/symmetri/gui/rpp/rpp/subscriptions/subscription_guard.hpp new file mode 100644 index 0000000..04e210e --- /dev/null +++ b/symmetri/gui/rpp/rpp/subscriptions/subscription_guard.hpp @@ -0,0 +1,44 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp { +/** + * \brief guard over subscription to auto-unsubscribe during destructor + */ +class subscription_guard { + public: + subscription_guard(const subscription_base& sub) : m_sub{sub} {} + + subscription_guard(const subscription_guard&) = delete; + + subscription_guard& operator=(const subscription_guard& other) { + m_sub.unsubscribe(); + m_sub = other.m_sub; + return *this; + } + + void reset(const subscription_base& other) { + m_sub.unsubscribe(); + m_sub = other; + } + + ~subscription_guard() { m_sub.unsubscribe(); } + + const subscription_base* operator->() const { return &m_sub; } + const subscription_base& operator*() const { return m_sub; } + + private: + subscription_base m_sub; +}; +} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/utils/constraints.hpp b/symmetri/gui/rpp/rpp/utils/constraints.hpp new file mode 100644 index 0000000..e94b2fd --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/constraints.hpp @@ -0,0 +1,33 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include + +namespace rpp::constraint { +template +concept decayed_same_as = std::same_as, std::decay_t>; + +template +concept decayed_type = std::same_as, T>; + +template +concept variadic_is_same_type = sizeof +...(Types) == 1 && (decayed_same_as && ...); + +template +concept iterable = requires(R& rng) { + std::cbegin(rng); + std::cend(rng); + }; +} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/utils/exceptions.hpp b/symmetri/gui/rpp/rpp/utils/exceptions.hpp new file mode 100644 index 0000000..67beb10 --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/exceptions.hpp @@ -0,0 +1,23 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::utils { +struct not_enough_emissions : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +struct timeout : std::runtime_error { + using std::runtime_error::runtime_error; +}; +} // namespace rpp::utils diff --git a/symmetri/gui/rpp/rpp/utils/function_traits.hpp b/symmetri/gui/rpp/rpp/utils/function_traits.hpp new file mode 100644 index 0000000..a12480a --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/function_traits.hpp @@ -0,0 +1,74 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::utils { +template +struct is_callable_t : std::false_type {}; + +template +struct is_callable_t> + : std::true_type {}; + +template +struct is_callable_t : std::true_type {}; + +template +struct is_callable_t : std::true_type {}; + +template +struct is_callable_t : std::true_type {}; + +template +concept is_callable = is_callable_t::value; + +// Lambda +template +struct function_traits : function_traits {}; + +// Operator of lambda +template +struct function_traits + : function_traits {}; + +// Operator of lambda with mutable +template +struct function_traits : function_traits {}; + +// Classical global function no args +template +struct function_traits { + using result = R; +}; + +// Classical global function +template +struct function_traits { + using result = R; + using arguments = std::tuple; + + template + requires(sizeof...(Args) > i) + using argument = std::tuple_element_t; +}; + +template +using function_argument_t = typename function_traits::template argument; + +template +using decayed_function_argument_t = std::decay_t>; + +template +using decayed_invoke_result_t = std::decay_t>; + +} // namespace rpp::utils diff --git a/symmetri/gui/rpp/rpp/utils/functors.hpp b/symmetri/gui/rpp/rpp/utils/functors.hpp new file mode 100644 index 0000000..247e5a1 --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/functors.hpp @@ -0,0 +1,91 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include + +namespace rpp::utils { +template +struct empty_function_t { + void operator()(const Types&...) const noexcept {} +}; + +struct rethrow_error_t { + [[noreturn]] void operator()(const std::exception_ptr& err) const { + std::rethrow_exception(err); + } +}; + +template +auto make_forwarding_on_next(const Observer& obs) { + return [obs](auto&& v) { obs.on_next(std::forward(v)); }; +} + +template +auto make_forwarding_on_error(const Observer& obs) { + return [obs](const std::exception_ptr& err) { obs.on_error(err); }; +} + +template +auto make_forwarding_on_completed(const Observer& obs) { + return [obs]() { obs.on_completed(); }; +} + +struct pack_to_tuple { + auto operator()(auto&&... vals) const { + return std::make_tuple(std::forward(vals)...); + } +}; + +template +struct get { + template + auto operator()(Args&&... args) const { + return std::get(std::forward_as_tuple(std::forward(args)...)); + } +}; + +struct forwarding_on_next { + void operator()(auto&& v, const auto& sub, const auto&...) const { + sub.on_next(std::forward(v)); + } +}; + +struct forwarding_on_error { + void operator()(const std::exception_ptr& err, const auto& sub, + const auto&...) const { + sub.on_error(err); + } +}; + +struct forwarding_on_completed { + void operator()(const auto& sub, const auto&...) const { sub.on_completed(); } +}; + +struct forwarding_on_next_for_pointer { + void operator()(auto&& v, const auto& sub) const { + sub->on_next(std::forward(v)); + } +}; + +struct forwarding_on_error_for_pointer { + void operator()(const std::exception_ptr& err, const auto& sub) const { + sub->on_error(err); + } +}; + +struct forwarding_on_completed_for_pointer { + void operator()(const auto& sub) const { sub->on_completed(); } +}; +} // namespace rpp::utils diff --git a/symmetri/gui/rpp/rpp/utils/operator_declaration.hpp b/symmetri/gui/rpp/rpp/utils/operator_declaration.hpp new file mode 100644 index 0000000..b63b400 --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/operator_declaration.hpp @@ -0,0 +1,30 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rpp::details { +template +struct operator_declaration { + static std::false_type header_included(); +}; + +template +concept is_header_included = + decltype(operator_declaration::header_included())::value; + +#define IMPLEMENTATION_FILE(tag) \ + template \ + struct rpp::details::operator_declaration { \ + static std::true_type header_included(); \ + } +} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/utils/overloaded.hpp b/symmetri/gui/rpp/rpp/utils/overloaded.hpp new file mode 100644 index 0000000..1f90473 --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/overloaded.hpp @@ -0,0 +1,21 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +namespace rpp::utils { +template +struct overloaded : Ts... { + using Ts::operator()...; +}; + +template +overloaded(Ts...) -> overloaded; +} // namespace rpp::utils diff --git a/symmetri/gui/rpp/rpp/utils/spinlock.hpp b/symmetri/gui/rpp/rpp/utils/spinlock.hpp new file mode 100644 index 0000000..2daa189 --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/spinlock.hpp @@ -0,0 +1,36 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#pragma once + +#include +#include + +namespace rpp::utils { +class spinlock { + public: + spinlock() = default; + + void lock() { + while (m_lock_flag.exchange(true, std::memory_order_acq_rel)) { + for (uint8_t i = 0; m_lock_flag.load(std::memory_order_relaxed); ++i) { + if (i == 30u) { + std::this_thread::yield(); + i = 0; + } + } + } + } + + void unlock() { m_lock_flag.store(false, std::memory_order_release); } + + private: + std::atomic_bool m_lock_flag{false}; +}; +} // namespace rpp::utils diff --git a/symmetri/gui/rpp/rpp/utils/utilities.hpp b/symmetri/gui/rpp/rpp/utils/utilities.hpp new file mode 100644 index 0000000..52fd665 --- /dev/null +++ b/symmetri/gui/rpp/rpp/utils/utilities.hpp @@ -0,0 +1,124 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace rpp::utils { +template +constexpr std::add_const_t& as_const(const T& v) noexcept { + return v; +} + +template +constexpr T&& as_const(T&& v) noexcept + requires std::is_rvalue_reference_v +{ + return std::forward(v); +} + +#if defined(__cpp_lib_atomic_shared_ptr) && __cpp_lib_atomic_shared_ptr +template +using atomic_shared_ptr = std::atomic>; +#else +template +using atomic_shared_ptr = std::shared_ptr; +#endif + +// used as interpetation of "void" +struct none {}; + +template +using iterable_value_t = + std::iter_value_t()))>; + +template > Fn> +void for_each(Cont&& container, Fn&& fn) { + std::for_each(std::begin(container), std::end(container), + std::forward(fn)); +} + +template > Fn> +bool all_of(const Cont& container, const Fn& fn) { + return std::all_of(std::cbegin(container), std::cend(container), fn); +} + +// some objects can't be copy/moved-assigned, but can be copy/move constructed +// (like immutable lambdas), so, use trick with optional and emplace. +template +class copy_assignable_callable { + public: + copy_assignable_callable(const Callable& callable) : m_callable{callable} {} + + copy_assignable_callable(Callable&& callable) + : m_callable{std::move(callable)} {} + + ~copy_assignable_callable() = default; + + copy_assignable_callable(const copy_assignable_callable& other) + : m_callable{other.m_callable} {} + + copy_assignable_callable(copy_assignable_callable&& other) noexcept( + std::is_nothrow_move_constructible_v) + : m_callable{std::move(other.m_callable)} {} + + copy_assignable_callable& operator=(const copy_assignable_callable& other) { + if (this == &other) return *this; + if (other.m_callable.has_value()) + m_callable.emplace(other.m_callable.value()); + else + m_callable.reset(); + return *this; + } + + copy_assignable_callable& operator=( + copy_assignable_callable&& other) noexcept { + if (this == &other) return *this; + if (other.m_callable.has_value()) + m_callable.emplace(std::move(other.m_callable).value()); + else + m_callable.reset(); + return *this; + } + + decltype(auto) operator()(auto&&... args) { + return (*m_callable)(std::forward(args)...); + } + decltype(auto) operator()(auto&&... args) const { + return (*m_callable)(std::forward(args)...); + } + + private: + RPP_NO_UNIQUE_ADDRESS std::optional m_callable; +}; + +/** + * \brief Calls passed function during destruction + */ +template +class finally_action { + public: + finally_action(Fn&& fn) : m_fn{std::move(fn)} {} + + finally_action(const Fn& fn) : m_fn{fn} {} + + ~finally_action() noexcept { m_fn(); } + + private: + RPP_NO_UNIQUE_ADDRESS Fn m_fn; +}; +} // namespace rpp::utils From c9c99ef41ba8f2d953575a9e1649be83500e7944 Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Tue, 16 Jan 2024 22:24:51 +0100 Subject: [PATCH 021/142] flux... --- symmetri/gui/CMakeLists.txt | 4 +- symmetri/gui/draw_graph.cpp | 124 +++++++++++++++++++----------------- symmetri/gui/drawable.h | 33 ---------- symmetri/gui/graph.hpp | 19 +++--- symmetri/gui/initialize.hpp | 37 ----------- symmetri/gui/main.mm | 67 +++++++++++++------ symmetri/gui/marking.hpp | 21 ------ symmetri/gui/menu_bar.cpp | 67 +++++++++++++++++++ symmetri/gui/menu_bar.h | 7 ++ symmetri/gui/menu_bar.hpp | 60 ----------------- symmetri/gui/model.h | 79 +++++++++++++++++++++++ symmetri/gui/redux.cpp | 14 ---- symmetri/gui/redux.hpp | 63 ------------------ symmetri/gui/rxdispatch.cpp | 26 ++++++++ symmetri/gui/rxdispatch.h | 9 +++ symmetri/gui/rximgui.h | 10 +++ symmetri/gui/symbol.cpp | 71 --------------------- symmetri/gui/symbol.h | 114 --------------------------------- symmetri/gui/util.h | 25 ++++++++ symmetri/gui/view.hpp | 42 ------------ 20 files changed, 348 insertions(+), 544 deletions(-) delete mode 100644 symmetri/gui/drawable.h delete mode 100644 symmetri/gui/initialize.hpp delete mode 100644 symmetri/gui/marking.hpp create mode 100644 symmetri/gui/menu_bar.cpp create mode 100644 symmetri/gui/menu_bar.h delete mode 100644 symmetri/gui/menu_bar.hpp create mode 100644 symmetri/gui/model.h delete mode 100644 symmetri/gui/redux.cpp delete mode 100644 symmetri/gui/redux.hpp create mode 100644 symmetri/gui/rxdispatch.cpp create mode 100644 symmetri/gui/rxdispatch.h create mode 100644 symmetri/gui/rximgui.h delete mode 100644 symmetri/gui/symbol.cpp delete mode 100644 symmetri/gui/symbol.h create mode 100644 symmetri/gui/util.h delete mode 100644 symmetri/gui/view.hpp diff --git a/symmetri/gui/CMakeLists.txt b/symmetri/gui/CMakeLists.txt index cf04d72..b3045d0 100644 --- a/symmetri/gui/CMakeLists.txt +++ b/symmetri/gui/CMakeLists.txt @@ -24,9 +24,9 @@ include_directories( set(SOURCES main.mm - redux.cpp - symbol.cpp + menu_bar.cpp draw_graph.cpp + rxdispatch.cpp ) set(IMGUI_SOURCES diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index cb2338b..b7f4c04 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -7,11 +7,10 @@ #include #include "color.hpp" -#include "drawable.h" #include "graph.hpp" #include "imgui.h" #include "imgui_internal.h" -#include "redux.hpp" +#include "rxdispatch.h" #include "symmetri/colors.hpp" // State @@ -118,7 +117,7 @@ void draw_arc(const Arc& arc, const std::vector& nodes) { void draw_nodes(Node& node) { open_context_menu = false; - ImGui::PushID(node.id.key()); + ImGui::PushID(node.name.c_str()); ImVec2 node_rect_min = offset + node.Pos; // Display node contents first @@ -149,11 +148,11 @@ void draw_nodes(Node& node) { arc_selected = nullptr; } if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - MVC::push( - [Pos = &node.Pos, d = ImGui::GetIO().MouseDelta](Model&& m) mutable { - *Pos += d; - return m; - }); + rxdispatch::push([Pos = &node.Pos, + d = ImGui::GetIO().MouseDelta](model::Model& m) mutable { + *Pos += d; + return m; + }); } const int opacity = 255; auto select_color = @@ -161,7 +160,7 @@ void draw_nodes(Node& node) { : &node == node_hovered_in_list || &node == node_hovered_in_scene ? IM_COL32(0, 255, 0, opacity) : IM_COL32(100, 100, 100, opacity); - if (node.id.chr() == 'P') { + if (node.type == Node::Type::Place) { draw_list->AddCircleFilled(offset + Node::GetCenterPos(node.Pos, size), 0.5f * size.x, IM_COL32(135, 135, 135, opacity), -5); @@ -180,8 +179,9 @@ void draw_nodes(Node& node) { // Dummy data structure provided for the example. // Note that we storing links as indices (not ID) to make example code shorter. -template <> void draw(Graph& g) { + ImGui::Begin("test", NULL, ImGuiWindowFlags_NoTitleBar); + ImVec2 WindowSize = ImGui::GetWindowSize(); WindowSize.y -= 140.0f; // Draw a list of nodes on the left side @@ -196,13 +196,14 @@ void draw(Graph& g) { if (node != std::end(g.nodes)) { ImGui::Text("Name"); ImGui::SameLine(); - const auto id = std::string("##") + std::to_string(node->id.key()); + static int i = 0; + const auto id = std::string("##") + std::to_string(i++); ImGui::PushItemWidth(-1); ImGui::InputText(id.c_str(), node->name.data(), 30); ImGui::PopItemWidth(); static int priority = 1; static int multiplicity = 1; - if (node_selected->id.chr() == 'T') { + if (node_selected->type == Node::Type::Transition) { ImGui::Text("Priority"); ImGui::SameLine(); ImGui::InputInt("##", &priority); @@ -239,8 +240,8 @@ void draw(Graph& g) { ImGui::BeginChild("place_list", ImVec2(200, height_fraction * WindowSize.y)); for (auto idx : g.n_idx) { const auto& node = g.nodes[idx]; - if (node.id.chr() == 'P') { - ImGui::PushID(node.id.key()); + if (node.type == Node::Type::Place) { + ImGui::PushID(idx); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { node_selected = &node; arc_selected = nullptr; @@ -261,8 +262,8 @@ void draw(Graph& g) { ImVec2(200, height_fraction * WindowSize.y)); for (auto idx : g.n_idx) { const auto& node = g.nodes[idx]; - if (node.id.chr() == 'T') { - ImGui::PushID(node.id.key()); + if (node.type == Node::Type::Transition) { + ImGui::PushID(idx); if (ImGui::Selectable(node.name.c_str(), &node == node_selected)) { node_selected = &node; arc_selected = nullptr; @@ -338,28 +339,29 @@ void draw(Graph& g) { ImGui::Text("Node '%s'", node_selected->name.c_str()); ImGui::Separator(); if (ImGui::MenuItem("Delete")) { - MVC::push([node_selected = node_selected](Model&& m) { + rxdispatch::push([node_selected = node_selected](model::Model& m_ptr) { + auto& m = *m_ptr.data; const auto idx = std::distance( - m.graph->nodes.begin(), - std::find_if(m.graph->nodes.begin(), m.graph->nodes.end(), + m.graph.nodes.begin(), + std::find_if(m.graph.nodes.begin(), m.graph.nodes.end(), [=](const Node& n) { return node_selected == &n; })); const auto swap_idx = std::distance( - m.graph->n_idx.begin(), - std::find(m.graph->n_idx.begin(), m.graph->n_idx.end(), idx)); - std::swap(m.graph->n_idx[swap_idx], m.graph->n_idx.back()); - m.graph->n_idx.pop_back(); + m.graph.n_idx.begin(), + std::find(m.graph.n_idx.begin(), m.graph.n_idx.end(), idx)); + std::swap(m.graph.n_idx[swap_idx], m.graph.n_idx.back()); + m.graph.n_idx.pop_back(); // delete arcs related to this - for (auto iter = m.graph->a_idx.begin(); - iter != m.graph->a_idx.end();) { - const auto [color, from_to_idx] = m.graph->arcs[*iter]; + for (auto iter = m.graph.a_idx.begin(); + iter != m.graph.a_idx.end();) { + const auto [color, from_to_idx] = m.graph.arcs[*iter]; if (from_to_idx[0] == idx || from_to_idx[1] == idx) { - m.graph->a_idx.erase(iter); + m.graph.a_idx.erase(iter); } else { ++iter; } } - return m; + return m_ptr; }); node_selected = nullptr; // node_hovered_in_list = nullptr; @@ -372,7 +374,8 @@ void draw(Graph& g) { ImGui::Separator(); if (ImGui::MenuItem("Delete")) { - MVC::push([&, ptr = arc_selected](Model&& m) { + rxdispatch::push([&, ptr = arc_selected](model::Model& m_ptr) { + auto& m = *m_ptr.data; const auto idx = std::distance( g.arcs.begin(), std::find_if(g.arcs.begin(), g.arcs.end(), @@ -381,26 +384,28 @@ void draw(Graph& g) { g.a_idx.begin(), std::find(g.a_idx.begin(), g.a_idx.end(), idx)); std::swap(g.a_idx[swap_idx], g.a_idx.back()); g.a_idx.pop_back(); - return m; + return m_ptr; }); arc_selected = nullptr; } if (ImGui::BeginMenu("Change color")) { for (const auto& arc : symmetri::Color::getColors()) { if (ImGui::MenuItem(arc.second.c_str())) { - MVC::push([color = arc.first, ptr = arc_selected](Model&& m) { + rxdispatch::push([color = arc.first, + ptr = arc_selected](model::Model& m_ptr) { + auto& m = *m_ptr.data; const auto idx = std::distance( - m.graph->arcs.begin(), - std::find_if(m.graph->arcs.begin(), m.graph->arcs.end(), + m.graph.arcs.begin(), + std::find_if(m.graph.arcs.begin(), m.graph.arcs.end(), [=](const Arc& a) { return ptr == &a; })); const auto swap_idx = std::distance( - m.graph->a_idx.begin(), - std::find(m.graph->a_idx.begin(), m.graph->a_idx.end(), idx)); - std::swap(m.graph->a_idx[swap_idx], m.graph->a_idx.back()); - m.graph->a_idx.pop_back(); - m.graph->a_idx.push_back(m.graph->arcs.size()); - m.graph->arcs.push_back({color, ptr->from_to_pos_idx}); - return m; + m.graph.a_idx.begin(), + std::find(m.graph.a_idx.begin(), m.graph.a_idx.end(), idx)); + std::swap(m.graph.a_idx[swap_idx], m.graph.a_idx.back()); + m.graph.a_idx.pop_back(); + m.graph.a_idx.push_back(m.graph.arcs.size()); + m.graph.arcs.push_back({color, ptr->from_to_pos_idx}); + return m_ptr; }); arc_selected = nullptr; } @@ -410,19 +415,21 @@ void draw(Graph& g) { } else { ImVec2 scene_pos = ImGui::GetMousePosOnOpeningCurrentPopup() - offset; if (ImGui::MenuItem("Add place")) { - MVC::push([scene_pos](Model&& m) { - m.graph->n_idx.push_back(m.graph->nodes.size()); - m.graph->nodes.push_back( - Node{"New node", Symbol('P', m.graph->nodes.size()), scene_pos}); - return m; + rxdispatch::push([scene_pos](model::Model& m_ptr) { + auto& m = *m_ptr.data; + m.graph.n_idx.push_back(m.graph.nodes.size()); + m.graph.nodes.push_back( + Node{"New node", Node::Type::Place, scene_pos}); + return m_ptr; }); } if (ImGui::MenuItem("Add transition")) { - MVC::push([scene_pos](Model&& m) { - m.graph->n_idx.push_back(m.graph->nodes.size()); - m.graph->nodes.push_back( - Node{"New node", Symbol('T', m.graph->nodes.size()), scene_pos}); - return m; + rxdispatch::push([scene_pos](model::Model& m_ptr) { + auto& m = *m_ptr.data; + m.graph.n_idx.push_back(m.graph.nodes.size()); + m.graph.nodes.push_back( + Node{"New node", Node::Type::Transition, scene_pos}); + return m_ptr; }); } if (ImGui::BeginMenu("Add arc")) { @@ -432,17 +439,18 @@ void draw(Graph& g) { node_hovered_in_scene = &from; for (const auto& to_idx : g.n_idx) { const auto& to = g.nodes[to_idx]; - if (from.id.chr() != to.id.chr() && - ImGui::BeginMenu(to.name.c_str())) { + if (from.type != to.type && ImGui::BeginMenu(to.name.c_str())) { node_hovered_in_scene = &to; for (const auto& color : symmetri::Color::getColors()) { if (ImGui::MenuItem(color.second.c_str())) { - MVC::push([arc = Arc{color.first, {from_idx, to_idx}}]( - Model&& m) { - m.graph->a_idx.push_back(m.graph->arcs.size()); - m.graph->arcs.push_back(arc); - return m; - }); + rxdispatch::push( + [arc = Arc{color.first, {from_idx, to_idx}}]( + model::Model& m_ptr) { + auto& m = *m_ptr.data; + m.graph.a_idx.push_back(m.graph.arcs.size()); + m.graph.arcs.push_back(arc); + return m_ptr; + }); node_hovered_in_scene = nullptr; } } diff --git a/symmetri/gui/drawable.h b/symmetri/gui/drawable.h deleted file mode 100644 index 11df582..0000000 --- a/symmetri/gui/drawable.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -/** @file drawable.h */ - -#include -#include - -template -void draw(T &) { - std::cout << "joe" << std::endl; -} - -class Drawable { - private: - struct concept_t { - virtual ~concept_t() = default; - virtual void draw_() = 0; - }; - template - struct model final : concept_t { - model(drawable_t &&x) : drawable_(std::move(x)) {} - void draw_() override { return draw(drawable_); } - drawable_t drawable_; - }; - - public: - template - Drawable(drawable_t drawable) - : self_(std::make_shared>(std::move(drawable))) {} - friend void draw(Drawable &drawable) { return drawable.self_->draw_(); } - - std::shared_ptr self_; -}; diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index b91273a..70fa5b6 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -17,21 +17,19 @@ #include #include -#include #include "imgui.h" -#include "symbol.h" template size_t toIndex(const std::vector& m, const std::function& s) { - auto ptr = std::find_if(m.begin(), m.end(), s); - return ptr->id.index(); + return std::distance(m.begin(), std::find_if(m.begin(), m.end(), s)); } struct Node { std::string name; - Symbol id; + enum Type { Place, Transition }; + Type type = Place; ImVec2 Pos; static ImVec2 GetCenterPos(const ImVec2& pos, const ImVec2& size) { return ImVec2(pos.x + size.x * 0.5f, pos.y + size.y * 0.5f); @@ -83,12 +81,11 @@ inline std::shared_ptr createGraph(const symmetri::Net net) { if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; }) == std::end(nodes)) { - const auto place_key = Symbol('P', nodes.size()); ogdf_nodes.push_back(G.newNode()); auto current_node = ogdf_nodes.back(); GA.label(current_node) = s.first; GA.shape(current_node) = ogdf::Shape::Ellipse; - nodes.push_back({s.first, place_key}); + nodes.push_back({s.first, Node::Type::Place}); } } @@ -96,20 +93,18 @@ inline std::shared_ptr createGraph(const symmetri::Net net) { if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; }) == std::end(nodes)) { - const auto place_key = Symbol('P', nodes.size()); ogdf_nodes.push_back(G.newNode()); auto current_node = ogdf_nodes.back(); GA.label(current_node) = s.first; GA.shape(current_node) = ogdf::Shape::Ellipse; - nodes.push_back({s.first, place_key}); + nodes.push_back({s.first, Node::Type::Place}); } } - const auto transition_key = Symbol('T', nodes.size()); ogdf_nodes.push_back(G.newNode()); auto current_node = ogdf_nodes.back(); GA.label(current_node) = t; - nodes.push_back({t, transition_key}); + nodes.push_back({t, Node::Type::Transition}); } // make the graph @@ -156,3 +151,5 @@ inline std::shared_ptr createGraph(const symmetri::Net net) { return std::make_shared( Graph{std::move(arcs), std::move(nodes), std::move(v), std::move(w)}); } + +void draw(Graph&); diff --git a/symmetri/gui/initialize.hpp b/symmetri/gui/initialize.hpp deleted file mode 100644 index adc6bdc..0000000 --- a/symmetri/gui/initialize.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include - -#include "graph.hpp" -#include "marking.hpp" -#include "menu_bar.hpp" -#include "view.hpp" -inline Model initializeModel(Model &&m) { - m.working_dir = "/users/thomashorstink/Projects/symmetri/nets"; - // m.active_file = "/users/thomashorstink/Projects/symmetri/nets/n1.pnml"; - m.active_file = - "/Users/thomashorstink/Projects/Symmetri/examples/combinations/" - "DualProcessWorker.pnml"; - symmetri::Net net; - symmetri::Marking marking; - symmetri::PriorityTable pt; - const std::filesystem::path pn_file = m.active_file.value(); - if (pn_file.extension() == std::string(".pnml")) { - std::tie(net, marking) = symmetri::readPnml({pn_file}); - } else { - std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); - } - - m.graph = std::make_shared(); - m.graph->reset(*createGraph(net)); - - // create a file browser instance - ImGui::FileBrowser fileDialog; - - // (optional) set browser properties - fileDialog.SetTitle("title"); - fileDialog.SetTypeFilters({".pnml", ".grml"}); - fileDialog.SetPwd(m.working_dir); - m.statics.push_back(fileDialog); - return m; -} diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 2318749..c2e4055 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -1,11 +1,15 @@ #include +#include #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_metal.h" -#include "initialize.hpp" #include "rpp/rpp.hpp" - +#include "rxdispatch.h" +#include "rximgui.h" +using namespace rximgui; +#include "model.h" +#include "util.h" #define GLFW_INCLUDE_NONE #define GLFW_EXPOSE_NATIVE_COCOA #include @@ -18,8 +22,6 @@ static void glfw_error_callback(int error, const char *description) { int main(int, char **) { // Setup Dear ImGui context - MVC::push(&initializeModel); - IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO &io = ImGui::GetIO(); @@ -58,16 +60,41 @@ int main(int, char **) { MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor new]; float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; - // Main loop - while (!glfwWindowShouldClose(window)) { - while (auto v = MVC::dequeue()) { - if (v.has_value()) { - MVC::update(v.value()); - // dispatcher.on_next(v.value()); - } - // should happen on a different thread? - } + // Flux starts here + + auto reducers = rpp::source::create(&rxdispatch::dequeue) + .subscribe_on(rpp::schedulers::new_thread{}); + + auto models = reducers + .scan(model::initializeModel(), + [=](model::Model &&m, model::Reducer f) { + try { + auto r = f(m); + r.data->timestamp = std::chrono::steady_clock::now(); + return r; + } catch (const std::exception &e) { + std::cerr << e.what() << std::endl; + return std::move(m); + } + }) + .sample_with_time(std::chrono::milliseconds{200}, rl); + + auto view_models = models + .filter([=](const model::Model &m) { + return m.data->timestamp <= std::chrono::steady_clock::now(); + }) + .start_with(model::initializeModel()) + .map([](const model::Model &m) { return model::ViewModel{m}; }); + + auto draw_frames = frames.with_latest_from(rxu::take_at<1>(), view_models); + + auto subscription = draw_frames.tap([](const model::ViewModel &vm) { draw(vm); }).subscribe(); + + // and loop + + // Main loop + while (!glfwWindowShouldClose(window) && subscription.is_subscribed()) { @autoreleasepool { glfwPollEvents(); @@ -87,21 +114,25 @@ int main(int, char **) { renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - [renderEncoder pushDebugGroup:@"ImGui demo"]; + // [renderEncoder pushDebugGroup:@"ImGui demo"]; ???? // Start the Dear ImGui frame ImGui_ImplMetal_NewFrame(renderPassDescriptor); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); - MVC::render(); + while (rl.is_any_ready_schedulable()) rl.dispatch(); + + sendframe(); + + while (rl.is_any_ready_schedulable()) rl.dispatch(); // Rendering ImGui::Render(); - ImGui::EndFrame(); // <-- Added + // ImGui::EndFrame(); // <-- Added, can be deleted? ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), commandBuffer, renderEncoder); - [renderEncoder popDebugGroup]; + // [renderEncoder popDebugGroup]; ???? [renderEncoder endEncoding]; [commandBuffer presentDrawable:drawable]; @@ -109,6 +140,7 @@ int main(int, char **) { } } + subscription.unsubscribe(); // Cleanup ImGui_ImplMetal_Shutdown(); ImGui_ImplGlfw_Shutdown(); @@ -116,6 +148,5 @@ int main(int, char **) { glfwDestroyWindow(window); glfwTerminate(); - return 0; } diff --git a/symmetri/gui/marking.hpp b/symmetri/gui/marking.hpp deleted file mode 100644 index 4c5a7e2..0000000 --- a/symmetri/gui/marking.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include - -#include -#include - -#include "imgui.h" -#include "redux.hpp" - -template <> -void draw(symmetri::Marking& marking) { - if (ImGui::BeginListBox("##marking")) { - for (const auto& [place, color] : marking) { - std::stringstream s; - s << "(" << place << "," << symmetri::Color::toString(color) << ")"; - if (ImGui::Selectable(s.str().c_str(), false)) { - } - } - ImGui::EndListBox(); - } -} diff --git a/symmetri/gui/menu_bar.cpp b/symmetri/gui/menu_bar.cpp new file mode 100644 index 0000000..adbf29f --- /dev/null +++ b/symmetri/gui/menu_bar.cpp @@ -0,0 +1,67 @@ +#include "menu_bar.h" + +#include "model.h" +#include "rxdispatch.h" + +model::Reducer updateActiveFile(const std::filesystem::path &file) { + return [=](model::Model &m_ptr) { + auto &m = *m_ptr.data; + m.active_file = file; + symmetri::Net net; + symmetri::Marking marking; + symmetri::PriorityTable pt; + const std::filesystem::path pn_file = m.active_file.value(); + if (pn_file.extension() == std::string(".pnml")) { + std::tie(net, marking) = symmetri::readPnml({pn_file}); + } else { + std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); + } + m.graph.reset(*createGraph(net)); + // auto g = createGraph(net); + // m.graph = *g; + // m.arcs = g->arcs; + // m.nodes = g->nodes; + // m.a_idx = g->a_idx; + // m.n_idx = g->n_idx; + return m_ptr; + }; +} + +void draw_menu_bar(ImGui::FileBrowser &fileDialog) { + fileDialog.Display(); + if (fileDialog.HasSelected()) { + rxdispatch::push(updateActiveFile(fileDialog.GetSelected())); + fileDialog.ClearSelected(); + } + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("New")) { + } + if (ImGui::MenuItem("Open")) { + fileDialog.Open(); + } + // Exit... + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Edit")) { + //... + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Window")) { + //... + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Help")) { + //... + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); + } + ImGui::SetNextWindowSize( + ImVec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y)); + ImGui::SetNextWindowPos(ImVec2(0, 20)); // fix this +} diff --git a/symmetri/gui/menu_bar.h b/symmetri/gui/menu_bar.h new file mode 100644 index 0000000..d66882b --- /dev/null +++ b/symmetri/gui/menu_bar.h @@ -0,0 +1,7 @@ +#pragma once +// clang-format off +#include "imgui.h" +// clang-format on +#include "imfilebrowser.h" + +void draw_menu_bar(ImGui::FileBrowser &fileDialog); diff --git a/symmetri/gui/menu_bar.hpp b/symmetri/gui/menu_bar.hpp deleted file mode 100644 index ae50bac..0000000 --- a/symmetri/gui/menu_bar.hpp +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include - -#include "graph.hpp" -#include "imfilebrowser.h" -#include "redux.hpp" -#include "symmetri/parsers.h" - -Reducer updateActiveFile(const std::filesystem::path &file) { - return [=](Model &&m) { - m.active_file = file; - auto [net, marking] = symmetri::readPnml({file}); - m.graph->reset(*createGraph(net)); - return m; - }; -} - -template <> -void draw(ImGui::FileBrowser &fileDialog) { - fileDialog.Display(); - if (fileDialog.HasSelected()) { - MVC::push(updateActiveFile(fileDialog.GetSelected())); - fileDialog.ClearSelected(); - } - if (ImGui::BeginMainMenuBar()) { - // silly but ok. - MVC::push([menu_height = ImGui::GetWindowSize().y](Model &&m) { - m.menu_height = menu_height; - return m; - }); - - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New")) { - } - if (ImGui::MenuItem("Open")) { - fileDialog.Open(); - } - // Exit... - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Edit")) { - //... - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Window")) { - //... - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Help")) { - //... - ImGui::EndMenu(); - } - - ImGui::EndMainMenuBar(); - } -} diff --git a/symmetri/gui/model.h b/symmetri/gui/model.h new file mode 100644 index 0000000..fd1f957 --- /dev/null +++ b/symmetri/gui/model.h @@ -0,0 +1,79 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "graph.hpp" +#include "imgui.h" +#include "menu_bar.h" +#include "symmetri/parsers.h" + +namespace model { + +struct Model { + struct shared { + std::chrono::steady_clock::time_point timestamp; + std::filesystem::path working_dir; + std::optional active_file; + int menu_height = 20; + std::vector arcs = {}; + std::vector nodes = {}; + std::vector a_idx = {}; + std::vector n_idx = {}; + Graph graph; + ImGui::FileBrowser file_dialog; + }; + std::shared_ptr data = std::make_shared(); +}; + +struct ViewModel { + ViewModel() {} + explicit ViewModel(const Model& m) : m(m) { auto& model = *m.data; } + Model m; + struct shared {}; + std::shared_ptr data = std::make_shared(); +}; + +using Reducer = std::function; +inline auto noop = Reducer([](Model& m) { return std::move(m); }); + +inline Model initializeModel() { + Model m_ptr; + auto& m = *m_ptr.data; + m.working_dir = "/users/thomashorstink/Projects/symmetri/nets"; + m.active_file = + "/Users/thomashorstink/Projects/Symmetri/examples/combinations/" + "DualProcessWorker.pnml"; + symmetri::Net net; + symmetri::Marking marking; + symmetri::PriorityTable pt; + const std::filesystem::path pn_file = m.active_file.value(); + if (pn_file.extension() == std::string(".pnml")) { + std::tie(net, marking) = symmetri::readPnml({pn_file}); + } else { + std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); + } + auto g = createGraph(net); + m.graph = *g; + m.arcs = g->arcs; + m.nodes = g->nodes; + m.a_idx = g->a_idx; + m.n_idx = g->n_idx; + + m.file_dialog.SetTitle("title"); + m.file_dialog.SetTypeFilters({".pnml", ".grml"}); + m.file_dialog.SetPwd(m.working_dir); + return m_ptr; +} + +inline void draw(const ViewModel& vm) { + auto& m = *vm.m.data; + draw_menu_bar(m.file_dialog); + + draw(m.graph); + ImGui::End(); +} + +} // namespace model diff --git a/symmetri/gui/redux.cpp b/symmetri/gui/redux.cpp deleted file mode 100644 index 11d2524..0000000 --- a/symmetri/gui/redux.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "redux.hpp" - -#include "graph.hpp" -void draw(Model& m) { - for (auto& drawable : m.statics) { - draw(drawable); - } - ImGui::SetNextWindowSize( - ImVec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y)); - ImGui::SetNextWindowPos(ImVec2(0, m.menu_height)); - ImGui::Begin("test", NULL, ImGuiWindowFlags_NoTitleBar); - draw(*m.graph); - ImGui::End(); -} diff --git a/symmetri/gui/redux.hpp b/symmetri/gui/redux.hpp deleted file mode 100644 index e71be29..0000000 --- a/symmetri/gui/redux.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "blockingconcurrentqueue.h" -#include "drawable.h" -#include "graph.hpp" -#include "imgui.h" - -using View = std::vector; -using History = std::vector; - -inline void commit(History& x) { - assert(!x.empty()); - x.push_back(x.back()); -} -inline void undo(History& x) { - assert(!x.empty()); - x.pop_back(); -} -inline View& current(History& x) { - assert(!x.empty()); - return x.back(); -} - -struct ViewModel {}; - -struct Model { - std::shared_ptr graph; - std::filesystem::path working_dir; - std::optional active_file; - int menu_height = 0; - View statics; -}; - -void draw(Model& m); - -using Reducer = std::function; - -class MVC { - private: - inline static moodycamel::BlockingConcurrentQueue reducers{10}; - - public: - inline static Model model; - inline static void render() { draw(model); }; - inline static void update(const Reducer& f) { model = f(model); } - inline static void push(Reducer&& f) { - reducers.enqueue(std::forward(f)); - } - inline static std::optional dequeue() { - static Reducer r; - return reducers.try_dequeue(r) ? std::optional(r) : std::nullopt; - } -}; diff --git a/symmetri/gui/rxdispatch.cpp b/symmetri/gui/rxdispatch.cpp new file mode 100644 index 0000000..fa171ab --- /dev/null +++ b/symmetri/gui/rxdispatch.cpp @@ -0,0 +1,26 @@ +#include "rxdispatch.h" + +#include "blockingconcurrentqueue.h" +namespace rxdispatch { + +static moodycamel::BlockingConcurrentQueue reducer_queue{10}; + +void push(model::Reducer&& r) { + reducer_queue.enqueue(std::forward(r)); +} + +void dequeue(const rpp::dynamic_subscriber& sub) { + model::Reducer f; + sub.get_subscription().add([&]() { + std::cout << "x-"; // x is notation for unsubscribed + rxdispatch::reducer_queue.enqueue(model::noop); + }); + + while (sub.is_subscribed()) { + while (sub.is_subscribed() && rxdispatch::reducer_queue.wait_dequeue_timed( + f, std::chrono::milliseconds(100))) { + sub.on_next(f); + } + } +}; +} // namespace rxdispatch diff --git a/symmetri/gui/rxdispatch.h b/symmetri/gui/rxdispatch.h new file mode 100644 index 0000000..4b59688 --- /dev/null +++ b/symmetri/gui/rxdispatch.h @@ -0,0 +1,9 @@ +#pragma once + +#include "model.h" +#include "rpp/rpp.hpp" +namespace rxdispatch { + +void push(model::Reducer&& r); +void dequeue(const rpp::dynamic_subscriber& sub); +} // namespace rxdispatch diff --git a/symmetri/gui/rximgui.h b/symmetri/gui/rximgui.h new file mode 100644 index 0000000..69ac02e --- /dev/null +++ b/symmetri/gui/rximgui.h @@ -0,0 +1,10 @@ +#pragma once + +namespace rximgui { +rpp::subjects::publish_subject framebus; +auto frameout = framebus.get_subscriber(); +auto sendframe = []() { frameout.on_next(1); }; +auto frames = framebus.get_observable(); +inline rpp::schedulers::run_loop rl{}; + +} // namespace rximgui diff --git a/symmetri/gui/symbol.cpp b/symmetri/gui/symbol.cpp deleted file mode 100644 index 3d803cf..0000000 --- a/symmetri/gui/symbol.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* ---------------------------------------------------------------------------- - - * GTSAM Copyright 2010, Georgia Tech Research Corporation, - * Atlanta, Georgia 30332-0415 - * All Rights Reserved - * Authors: Frank Dellaert, et al. (see THANKS for the full author list) - - * See LICENSE for the license information - - * -------------------------------------------------------------------------- */ - -/** - * @file Symbol.cpp - * @date June 9, 2012 - * @author: Frank Dellaert - * @author: Richard Roberts - */ - -#include "symbol.h" - -#include - -#include -#include -#include -#include - -static const size_t keyBits = sizeof(Key) * 8; -static const size_t chrBits = sizeof(unsigned char) * 8; -static const size_t indexBits = keyBits - chrBits; -static const Key chrMask = - Key(UCHAR_MAX) - << indexBits; // For some reason, std::numeric_limits::max() - // fails -static const Key indexMask = ~chrMask; - -Symbol::Symbol(Key key) - : c_((unsigned char)((key & chrMask) >> indexBits)), j_(key & indexMask) {} - -Key Symbol::key() const { - if (j_ > indexMask) { - std::stringstream msg; - msg << "Symbol index is too large, j=" << j_ << ", indexMask=" << indexMask; - throw std::invalid_argument(msg.str()); - } - Key key = (Key(c_) << indexBits) | j_; - return key; -} - -void Symbol::print(const std::string& s) const { - std::cout << s << (std::string)(*this) << std::endl; -} - -bool Symbol::equals(const Symbol& expected, double tol) const { - return (*this) == expected; -} - -Symbol::operator std::string() const { - char buffer[10]; - snprintf(buffer, 10, "%c%llu", c_, static_cast(j_)); - return std::string(buffer); -} - -static Symbol make(Key key) { return Symbol(key); } - -std::function Symbol::ChrTest(unsigned char c) { - auto equals = [](unsigned char s, unsigned char c) { return s == c; }; - return std::bind( - equals, std::bind(&Symbol::chr, std::bind(make, std::placeholders::_1)), - c); -} diff --git a/symmetri/gui/symbol.h b/symmetri/gui/symbol.h deleted file mode 100644 index 94a28f3..0000000 --- a/symmetri/gui/symbol.h +++ /dev/null @@ -1,114 +0,0 @@ -/* ---------------------------------------------------------------------------- - - * GTSAM Copyright 2010, Georgia Tech Research Corporation, - * Atlanta, Georgia 30332-0415 - * All Rights Reserved - * Authors: Frank Dellaert, et al. (see THANKS for the full author list) - - * See LICENSE for the license information - - * -------------------------------------------------------------------------- */ - -#pragma once - -#include -#include -#include - -using Key = std::uint64_t; -class Symbol { - protected: - unsigned char c_; - std::uint64_t j_; - - public: - Symbol() : c_(0), j_(0) {} - - Symbol(const Symbol& key) : c_(key.c_), j_(key.j_) {} - - Symbol(unsigned char c, std::uint64_t j) : c_(c), j_(j) {} - - Symbol(Key key); - - Key key() const; - - operator Key() const { return key(); } - - void print(const std::string& s = "") const; - - bool equals(const Symbol& expected, double tol = 0.0) const; - - unsigned char chr() const { return c_; } - - std::uint64_t index() const { return j_; } - - operator std::string() const; - - std::string string() const { return std::string(*this); } - - bool operator<(const Symbol& comp) const { - return c_ < comp.c_ || (comp.c_ == c_ && j_ < comp.j_); - } - - bool operator==(const Symbol& comp) const { - return comp.c_ == c_ && comp.j_ == j_; - } - - bool operator==(Key comp) const { return comp == (Key)(*this); } - - bool operator!=(const Symbol& comp) const { - return comp.c_ != c_ || comp.j_ != j_; - } - - bool operator!=(Key comp) const { return comp != (Key)(*this); } - - static std::function ChrTest(unsigned char c); - - friend std::ostream& operator<<(std::ostream&, const Symbol&); -}; - -inline Key symbol(unsigned char c, std::uint64_t j) { - return (Key)Symbol(c, j); -} - -inline unsigned char symbolChr(Key key) { return Symbol(key).chr(); } - -inline std::uint64_t symbolIndex(Key key) { return Symbol(key).index(); } - -namespace symbol_shorthand { -inline Key A(std::uint64_t j) { return Symbol('a', j); } -inline Key B(std::uint64_t j) { return Symbol('b', j); } -inline Key C(std::uint64_t j) { return Symbol('c', j); } -inline Key D(std::uint64_t j) { return Symbol('d', j); } -inline Key E(std::uint64_t j) { return Symbol('e', j); } -inline Key F(std::uint64_t j) { return Symbol('f', j); } -inline Key G(std::uint64_t j) { return Symbol('g', j); } -inline Key H(std::uint64_t j) { return Symbol('h', j); } -inline Key I(std::uint64_t j) { return Symbol('i', j); } -inline Key J(std::uint64_t j) { return Symbol('j', j); } -inline Key K(std::uint64_t j) { return Symbol('k', j); } -inline Key L(std::uint64_t j) { return Symbol('l', j); } -inline Key M(std::uint64_t j) { return Symbol('m', j); } -inline Key N(std::uint64_t j) { return Symbol('n', j); } -inline Key O(std::uint64_t j) { return Symbol('o', j); } -inline Key P(std::uint64_t j) { return Symbol('p', j); } -inline Key Q(std::uint64_t j) { return Symbol('q', j); } -inline Key R(std::uint64_t j) { return Symbol('r', j); } -inline Key S(std::uint64_t j) { return Symbol('s', j); } -inline Key T(std::uint64_t j) { return Symbol('t', j); } -inline Key U(std::uint64_t j) { return Symbol('u', j); } -inline Key V(std::uint64_t j) { return Symbol('v', j); } -inline Key W(std::uint64_t j) { return Symbol('w', j); } -inline Key X(std::uint64_t j) { return Symbol('x', j); } -inline Key Y(std::uint64_t j) { return Symbol('y', j); } -inline Key Z(std::uint64_t j) { return Symbol('z', j); } -} // namespace symbol_shorthand - -class SymbolGenerator { - const unsigned char c_; - - public: - constexpr SymbolGenerator(const unsigned char c) : c_(c) {} - Symbol operator()(const std::uint64_t j) const { return Symbol(c_, j); } - constexpr unsigned char chr() const { return c_; } -}; diff --git a/symmetri/gui/util.h b/symmetri/gui/util.h new file mode 100644 index 0000000..5cc786e --- /dev/null +++ b/symmetri/gui/util.h @@ -0,0 +1,25 @@ +#pragma once +#include +namespace rxu { + +namespace detail { +template +using decay_t = typename std::decay::type; + +template +struct take_at { + template + auto operator()(const ParamN&... pn) const -> + typename std::tuple_element...>>::type { + return std::get(std::tie(pn...)); + } +}; + +} // namespace detail + +template +inline auto take_at() -> detail::take_at { + return detail::take_at(); +} + +} // namespace rxu diff --git a/symmetri/gui/view.hpp b/symmetri/gui/view.hpp deleted file mode 100644 index 907ddce..0000000 --- a/symmetri/gui/view.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include - -#include -#include - -#include "imgui.h" - -using Mutation = - std::pair>, - std::vector>>; - -static void showMutation(const Mutation& mutation) { - std::stringstream s; - for (const auto& input : mutation.first) { - s << "(" << input.first << "," << symmetri::Color::toString(input.second) - << ")"; - } - s << " -> "; - for (const auto& output : mutation.second) { - s << "(" << output.first << "," << symmetri::Color::toString(output.second) - << ")"; - } - if (ImGui::BeginItemTooltip()) { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::TextUnformatted(s.str().c_str()); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } -} - -template <> -void draw(symmetri::Net& net) { - if (ImGui::BeginListBox("##bla")) { - for (const auto& [t, p] : net) { - if (ImGui::Selectable(t.c_str(), false)) { - } - showMutation(p); - } - ImGui::EndListBox(); - } -} From 9d7d5bda887518c09399216d01bf92adc94dac8c Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Fri, 19 Jan 2024 15:41:33 +0100 Subject: [PATCH 022/142] no more auto layout --- symmetri/gui/CMakeLists.txt | 9 ++--- symmetri/gui/draw_graph.cpp | 2 +- symmetri/gui/graph.hpp | 69 ++++++++----------------------------- symmetri/gui/main.mm | 2 +- symmetri/gui/menu_bar.cpp | 8 +++-- symmetri/gui/model.h | 5 ++- symmetri/gui/rxdispatch.cpp | 2 +- 7 files changed, 30 insertions(+), 67 deletions(-) diff --git a/symmetri/gui/CMakeLists.txt b/symmetri/gui/CMakeLists.txt index b3045d0..44ab428 100644 --- a/symmetri/gui/CMakeLists.txt +++ b/symmetri/gui/CMakeLists.txt @@ -19,7 +19,6 @@ include_directories( ../externals gui extensions - /usr/local/include ) set(SOURCES @@ -27,6 +26,7 @@ set(SOURCES menu_bar.cpp draw_graph.cpp rxdispatch.cpp + position_parsers.cpp ) set(IMGUI_SOURCES @@ -37,11 +37,6 @@ set(IMGUI_SOURCES imgui/imgui_tables.cpp imgui/backends/imgui_impl_glfw.cpp imgui/backends/imgui_impl_metal.mm - - # extensions/crude_json.cpp - # extensions/imgui_node_editor_api.cpp - # extensions/imgui_node_editor.cpp - # extensions/imgui_canvas.cpp ) if(APPLE) @@ -65,4 +60,4 @@ if(APPLE) endif(APPLE) add_executable(Farbart ${SOURCES} ${IMGUI_SOURCES}) -target_link_libraries(Farbart symmetri OGDF ${EXTRA_LIBS}) +target_link_libraries(Farbart symmetri ${EXTRA_LIBS}) diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index b7f4c04..2877540 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -104,7 +104,7 @@ void draw_arc(const Arc& arc, const std::vector& nodes) { const auto theta = std::atan2(d.y, d.x) - M_PI_2; const float h = draw_list->_Data->FontSize * 1.00f; const float r = h * 0.40f * 1; - const ImVec2 center = p1 + d / 2.f; + const ImVec2 center = p1 + d * 0.6f; const auto a_sin = std::sin(theta); const auto a_cos = std::cos(theta); const auto a = ImRotate(ImVec2(+0.000f, +1.f) * r, a_cos, a_sin); diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index 70fa5b6..99f6db5 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -1,25 +1,14 @@ #pragma once #include // fmodf -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include +#include +#include +#include #include "imgui.h" - template size_t toIndex(const std::vector& m, const std::function& s) { @@ -36,6 +25,10 @@ struct Node { } }; +inline ImVec2 toImVec2(const std::pair& p) { + return ImVec2(p.first, p.second); +} + struct Arc { symmetri::Token color; std::array from_to_pos_idx; @@ -67,25 +60,19 @@ struct Graph { } }; -inline std::shared_ptr createGraph(const symmetri::Net net) { +inline std::shared_ptr createGraph( + const symmetri::Net net, + const std::map>& positions) { std::vector nodes; std::vector arcs; - ogdf::Graph G; - ogdf::GraphAttributes GA(G, ogdf::GraphAttributes::nodeGraphics | - ogdf::GraphAttributes::nodeLabel | - ogdf::GraphAttributes::edgeGraphics); - std::vector ogdf_places, ogdf_transitions, ogdf_nodes; for (const auto& [t, io] : net) { for (const auto& s : io.first) { if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; }) == std::end(nodes)) { - ogdf_nodes.push_back(G.newNode()); - auto current_node = ogdf_nodes.back(); - GA.label(current_node) = s.first; - GA.shape(current_node) = ogdf::Shape::Ellipse; - nodes.push_back({s.first, Node::Type::Place}); + nodes.push_back( + {s.first, Node::Type::Place, toImVec2(positions.at(s.first))}); } } @@ -93,18 +80,12 @@ inline std::shared_ptr createGraph(const symmetri::Net net) { if (std::find_if(nodes.begin(), nodes.end(), [&](const auto& n) { return s.first == n.name; }) == std::end(nodes)) { - ogdf_nodes.push_back(G.newNode()); - auto current_node = ogdf_nodes.back(); - GA.label(current_node) = s.first; - GA.shape(current_node) = ogdf::Shape::Ellipse; - nodes.push_back({s.first, Node::Type::Place}); + nodes.push_back( + {s.first, Node::Type::Place, toImVec2(positions.at(s.first))}); } } - ogdf_nodes.push_back(G.newNode()); - auto current_node = ogdf_nodes.back(); - GA.label(current_node) = t; - nodes.push_back({t, Node::Type::Transition}); + nodes.push_back({t, Node::Type::Transition, toImVec2(positions.at(t))}); } // make the graph @@ -115,36 +96,16 @@ inline std::shared_ptr createGraph(const symmetri::Net net) { for (const auto& s : io.first) { const auto place_idx = toIndex( nodes, [=](const Node& n) { return s.first == n.name; }); - G.newEdge(ogdf_nodes[place_idx], ogdf_nodes[transition_idx]); arcs.push_back({s.second, {place_idx, transition_idx}}); } for (const auto& s : io.second) { const auto place_idx = toIndex( nodes, [=](const Node& n) { return s.first == n.name; }); - G.newEdge(ogdf_nodes[transition_idx], ogdf_nodes[place_idx]); arcs.push_back({s.second, {transition_idx, place_idx}}); } } - { - using namespace ogdf; - SugiyamaLayout SL; - SL.setRanking(new OptimalRanking); - SL.setCrossMin(new MedianHeuristic); - - OptimalHierarchyLayout* ohl = new OptimalHierarchyLayout; - SL.setLayout(ohl); - - SL.call(GA); - GA.rotateLeft90(); - GA.translateToNonNeg(); - } - - for (size_t i = 0; i < ogdf_nodes.size(); i++) { - nodes[i].Pos = ImVec2(GA.x(ogdf_nodes[i]), 2 * GA.y(ogdf_nodes[i])); - } - std::vector v(arcs.size()), w(nodes.size()); std::iota(v.begin(), v.end(), 0); std::iota(w.begin(), w.end(), 0); diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index c2e4055..0d8da2f 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -74,7 +74,7 @@ int main(int, char **) { r.data->timestamp = std::chrono::steady_clock::now(); return r; } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; + printf("%s", e.what()); return std::move(m); } }) diff --git a/symmetri/gui/menu_bar.cpp b/symmetri/gui/menu_bar.cpp index adbf29f..89797ef 100644 --- a/symmetri/gui/menu_bar.cpp +++ b/symmetri/gui/menu_bar.cpp @@ -1,8 +1,8 @@ #include "menu_bar.h" #include "model.h" +#include "position_parsers.h" #include "rxdispatch.h" - model::Reducer updateActiveFile(const std::filesystem::path &file) { return [=](model::Model &m_ptr) { auto &m = *m_ptr.data; @@ -10,13 +10,17 @@ model::Reducer updateActiveFile(const std::filesystem::path &file) { symmetri::Net net; symmetri::Marking marking; symmetri::PriorityTable pt; + std::map> positions; + const std::filesystem::path pn_file = m.active_file.value(); if (pn_file.extension() == std::string(".pnml")) { std::tie(net, marking) = symmetri::readPnml({pn_file}); + positions = farbart::readPnmlPositions({pn_file}); + } else { std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); } - m.graph.reset(*createGraph(net)); + m.graph.reset(*createGraph(net, positions)); // auto g = createGraph(net); // m.graph = *g; // m.arcs = g->arcs; diff --git a/symmetri/gui/model.h b/symmetri/gui/model.h index fd1f957..4741541 100644 --- a/symmetri/gui/model.h +++ b/symmetri/gui/model.h @@ -8,6 +8,7 @@ #include "graph.hpp" #include "imgui.h" #include "menu_bar.h" +#include "position_parsers.h" #include "symmetri/parsers.h" namespace model { @@ -49,13 +50,15 @@ inline Model initializeModel() { symmetri::Net net; symmetri::Marking marking; symmetri::PriorityTable pt; + std::map> positions; const std::filesystem::path pn_file = m.active_file.value(); if (pn_file.extension() == std::string(".pnml")) { std::tie(net, marking) = symmetri::readPnml({pn_file}); + positions = farbart::readPnmlPositions({pn_file}); } else { std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); } - auto g = createGraph(net); + auto g = createGraph(net, positions); m.graph = *g; m.arcs = g->arcs; m.nodes = g->nodes; diff --git a/symmetri/gui/rxdispatch.cpp b/symmetri/gui/rxdispatch.cpp index fa171ab..58089b0 100644 --- a/symmetri/gui/rxdispatch.cpp +++ b/symmetri/gui/rxdispatch.cpp @@ -12,7 +12,7 @@ void push(model::Reducer&& r) { void dequeue(const rpp::dynamic_subscriber& sub) { model::Reducer f; sub.get_subscription().add([&]() { - std::cout << "x-"; // x is notation for unsubscribed + printf("%s", "x-"); // x is notation for unsubscribed rxdispatch::reducer_queue.enqueue(model::noop); }); From e2daeacf20a8fbad65fbf701107696ac8591fa1b Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Fri, 19 Jan 2024 16:19:58 +0100 Subject: [PATCH 023/142] pretty --- symmetri/gui/main.mm | 2 +- symmetri/gui/rxdispatch.cpp | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 0d8da2f..0c3253c 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -139,7 +139,7 @@ int main(int, char **) { [commandBuffer commit]; } } - + rxdispatch::push(std::move(model::noop)); subscription.unsubscribe(); // Cleanup ImGui_ImplMetal_Shutdown(); diff --git a/symmetri/gui/rxdispatch.cpp b/symmetri/gui/rxdispatch.cpp index 58089b0..6b47593 100644 --- a/symmetri/gui/rxdispatch.cpp +++ b/symmetri/gui/rxdispatch.cpp @@ -12,15 +12,12 @@ void push(model::Reducer&& r) { void dequeue(const rpp::dynamic_subscriber& sub) { model::Reducer f; sub.get_subscription().add([&]() { - printf("%s", "x-"); // x is notation for unsubscribed - rxdispatch::reducer_queue.enqueue(model::noop); + // cleanup? }); - while (sub.is_subscribed()) { - while (sub.is_subscribed() && rxdispatch::reducer_queue.wait_dequeue_timed( - f, std::chrono::milliseconds(100))) { - sub.on_next(f); - } + while (sub.is_subscribed() && + rxdispatch::reducer_queue.wait_dequeue_timed(f, -1)) { + sub.on_next(f); } }; } // namespace rxdispatch From bc596a6f61f3ab9358df17269d68fc643675461b Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Fri, 19 Jan 2024 16:22:57 +0100 Subject: [PATCH 024/142] pretty --- symmetri/gui/main.mm | 7 +++-- symmetri/gui/position_parsers.cpp | 44 +++++++++++++++++++++++++++++++ symmetri/gui/position_parsers.h | 16 +++++++++++ symmetri/gui/rxdispatch.cpp | 2 +- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 symmetri/gui/position_parsers.cpp create mode 100644 symmetri/gui/position_parsers.h diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 0c3253c..6667605 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -139,8 +139,11 @@ int main(int, char **) { [commandBuffer commit]; } } - rxdispatch::push(std::move(model::noop)); - subscription.unsubscribe(); + rxdispatch::push([&](model::Model &m) { + subscription.unsubscribe(); + return m; + }); + // Cleanup ImGui_ImplMetal_Shutdown(); ImGui_ImplGlfw_Shutdown(); diff --git a/symmetri/gui/position_parsers.cpp b/symmetri/gui/position_parsers.cpp new file mode 100644 index 0000000..d15109c --- /dev/null +++ b/symmetri/gui/position_parsers.cpp @@ -0,0 +1,44 @@ +#include "position_parsers.h" + +#include + +#include "tinyxml2/tinyxml2.h" +using namespace tinyxml2; + +namespace farbart { + +std::map> readPnmlPositions( + const std::set &files) { + std::map> positions; + + for (auto file : files) { + XMLDocument net; + net.LoadFile(file.c_str()); + XMLElement *levelElement = net.FirstChildElement("pnml") + ->FirstChildElement("net") + ->FirstChildElement("page"); + + // loop places. + for (XMLElement *child = levelElement->FirstChildElement("place"); + child != NULL; child = child->NextSiblingElement("place")) { + auto place_id = child->Attribute("id"); + auto position = + child->FirstChildElement("graphics")->FirstChildElement("position"); + positions[place_id] = {std::stof(position->Attribute("x")), + std::stof(position->Attribute("y"))}; + } + + // loop transitions + for (XMLElement *child = levelElement->FirstChildElement("transition"); + child != NULL; child = child->NextSiblingElement("transition")) { + auto transition_id = child->Attribute("id"); + auto position = + child->FirstChildElement("graphics")->FirstChildElement("position"); + positions[transition_id] = {std::stof(position->Attribute("x")), + std::stof(position->Attribute("y"))}; + } + } + + return positions; +} +} // namespace farbart diff --git a/symmetri/gui/position_parsers.h b/symmetri/gui/position_parsers.h new file mode 100644 index 0000000..d34d33d --- /dev/null +++ b/symmetri/gui/position_parsers.h @@ -0,0 +1,16 @@ +#pragma once + +/** @file parsers.h */ + +#include +#include +#include + +namespace farbart { + +// std::map> readGrmlPositions( +// const std::set &files); +std::map> readPnmlPositions( + const std::set &files); + +} // namespace farbart diff --git a/symmetri/gui/rxdispatch.cpp b/symmetri/gui/rxdispatch.cpp index 6b47593..20d9dd8 100644 --- a/symmetri/gui/rxdispatch.cpp +++ b/symmetri/gui/rxdispatch.cpp @@ -3,7 +3,7 @@ #include "blockingconcurrentqueue.h" namespace rxdispatch { -static moodycamel::BlockingConcurrentQueue reducer_queue{10}; +moodycamel::BlockingConcurrentQueue reducer_queue{10}; void push(model::Reducer&& r) { reducer_queue.enqueue(std::forward(r)); From 0792233cd6b45d2d4f0d538d94ad602e4cf77a5a Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Tue, 30 Jan 2024 20:58:17 +0100 Subject: [PATCH 025/142] pretty --- symmetri/gui/draw_graph.cpp | 71 +++++++++++++--------------- symmetri/gui/graph.hpp | 3 +- symmetri/gui/main.mm | 2 +- symmetri/gui/menu_bar.cpp | 11 ++--- symmetri/gui/model.h | 24 +++++----- symmetri/gui/position_parsers.cpp | 2 - symmetri/gui/write_graph_to_disk.hpp | 56 ++++++++++++++++++++++ symmetri/pnml_parser.cpp | 1 + 8 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 symmetri/gui/write_graph_to_disk.hpp diff --git a/symmetri/gui/draw_graph.cpp b/symmetri/gui/draw_graph.cpp index 2877540..e5fa4d2 100644 --- a/symmetri/gui/draw_graph.cpp +++ b/symmetri/gui/draw_graph.cpp @@ -115,7 +115,7 @@ void draw_arc(const Arc& arc, const std::vector& nodes) { arc_hovered_in_scene == &arc ? 3.0f : 2.0f); }; -void draw_nodes(Node& node) { +void draw_nodes(const Node& node, size_t idx) { open_context_menu = false; ImGui::PushID(node.name.c_str()); ImVec2 node_rect_min = offset + node.Pos; @@ -148,11 +148,11 @@ void draw_nodes(Node& node) { arc_selected = nullptr; } if (node_moving_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - rxdispatch::push([Pos = &node.Pos, - d = ImGui::GetIO().MouseDelta](model::Model& m) mutable { - *Pos += d; - return m; - }); + rxdispatch::push( + [idx, d = ImGui::GetIO().MouseDelta](model::Model& m) mutable { + m.data->graph.nodes[idx].Pos += d; + return m; + }); } const int opacity = 255; auto select_color = @@ -179,7 +179,8 @@ void draw_nodes(Node& node) { // Dummy data structure provided for the example. // Note that we storing links as indices (not ID) to make example code shorter. -void draw(Graph& g) { +void draw(const Graph& g, const std::vector& n_idx, + const std::vector& a_idx) { ImGui::Begin("test", NULL, ImGuiWindowFlags_NoTitleBar); ImVec2 WindowSize = ImGui::GetWindowSize(); @@ -199,7 +200,8 @@ void draw(Graph& g) { static int i = 0; const auto id = std::string("##") + std::to_string(i++); ImGui::PushItemWidth(-1); - ImGui::InputText(id.c_str(), node->name.data(), 30); + ImGui::Text("%s", node->name.c_str()); + // ImGui::InputText(id.c_str(), node->name.data(), 30); ImGui::PopItemWidth(); static int priority = 1; static int multiplicity = 1; @@ -238,7 +240,7 @@ void draw(Graph& g) { ImGui::Separator(); constexpr float height_fraction = 0.8 / 2.0; ImGui::BeginChild("place_list", ImVec2(200, height_fraction * WindowSize.y)); - for (auto idx : g.n_idx) { + for (auto idx : n_idx) { const auto& node = g.nodes[idx]; if (node.type == Node::Type::Place) { ImGui::PushID(idx); @@ -260,7 +262,7 @@ void draw(Graph& g) { ImGui::Separator(); ImGui::BeginChild("transition_list", ImVec2(200, height_fraction * WindowSize.y)); - for (auto idx : g.n_idx) { + for (auto idx : n_idx) { const auto& node = g.nodes[idx]; if (node.type == Node::Type::Transition) { ImGui::PushID(idx); @@ -311,8 +313,8 @@ void draw(Graph& g) { } // draw places & transitions - for (auto idx : g.n_idx) { - draw_nodes(g.nodes[idx]); + for (auto idx : n_idx) { + draw_nodes(g.nodes[idx], idx); } // Open context menu @@ -339,12 +341,9 @@ void draw(Graph& g) { ImGui::Text("Node '%s'", node_selected->name.c_str()); ImGui::Separator(); if (ImGui::MenuItem("Delete")) { - rxdispatch::push([node_selected = node_selected](model::Model& m_ptr) { + rxdispatch::push([idx = GetIndexFromRef(g.nodes, *node_selected)]( + model::Model& m_ptr) { auto& m = *m_ptr.data; - const auto idx = std::distance( - m.graph.nodes.begin(), - std::find_if(m.graph.nodes.begin(), m.graph.nodes.end(), - [=](const Node& n) { return node_selected == &n; })); const auto swap_idx = std::distance( m.graph.n_idx.begin(), std::find(m.graph.n_idx.begin(), m.graph.n_idx.end(), idx)); @@ -364,50 +363,44 @@ void draw(Graph& g) { return m_ptr; }); node_selected = nullptr; - // node_hovered_in_list = nullptr; - // node_hovered_in_scene = nullptr; - // arc_selected = nullptr; - // arc_hovered_in_scene = nullptr; } } else if (arc_selected) { ImGui::Text("Arc"); ImGui::Separator(); if (ImGui::MenuItem("Delete")) { - rxdispatch::push([&, ptr = arc_selected](model::Model& m_ptr) { + rxdispatch::push([&, idx = GetIndexFromRef(g.arcs, *arc_selected)]( + model::Model& m_ptr) { auto& m = *m_ptr.data; - const auto idx = std::distance( - g.arcs.begin(), - std::find_if(g.arcs.begin(), g.arcs.end(), - [=](const Arc& a) { return ptr == &a; })); const auto swap_idx = std::distance( - g.a_idx.begin(), std::find(g.a_idx.begin(), g.a_idx.end(), idx)); - std::swap(g.a_idx[swap_idx], g.a_idx.back()); - g.a_idx.pop_back(); + m.graph.a_idx.begin(), + std::find(m.graph.a_idx.begin(), m.graph.a_idx.end(), idx)); + std::swap(m.graph.a_idx[swap_idx], m.graph.a_idx.back()); + m.graph.a_idx.pop_back(); return m_ptr; }); arc_selected = nullptr; + arc_hovered_in_scene = nullptr; } if (ImGui::BeginMenu("Change color")) { for (const auto& arc : symmetri::Color::getColors()) { if (ImGui::MenuItem(arc.second.c_str())) { rxdispatch::push([color = arc.first, - ptr = arc_selected](model::Model& m_ptr) { + idx = GetIndexFromRef(g.arcs, *arc_selected)]( + model::Model& m_ptr) { auto& m = *m_ptr.data; - const auto idx = std::distance( - m.graph.arcs.begin(), - std::find_if(m.graph.arcs.begin(), m.graph.arcs.end(), - [=](const Arc& a) { return ptr == &a; })); const auto swap_idx = std::distance( m.graph.a_idx.begin(), std::find(m.graph.a_idx.begin(), m.graph.a_idx.end(), idx)); std::swap(m.graph.a_idx[swap_idx], m.graph.a_idx.back()); m.graph.a_idx.pop_back(); m.graph.a_idx.push_back(m.graph.arcs.size()); - m.graph.arcs.push_back({color, ptr->from_to_pos_idx}); + m.graph.arcs.push_back( + {color, m.graph.arcs[idx].from_to_pos_idx}); return m_ptr; }); arc_selected = nullptr; + arc_hovered_in_scene = nullptr; } } ImGui::EndMenu(); @@ -419,7 +412,7 @@ void draw(Graph& g) { auto& m = *m_ptr.data; m.graph.n_idx.push_back(m.graph.nodes.size()); m.graph.nodes.push_back( - Node{"New node", Node::Type::Place, scene_pos}); + Node{"NewPlace", Node::Type::Place, scene_pos}); return m_ptr; }); } @@ -428,16 +421,16 @@ void draw(Graph& g) { auto& m = *m_ptr.data; m.graph.n_idx.push_back(m.graph.nodes.size()); m.graph.nodes.push_back( - Node{"New node", Node::Type::Transition, scene_pos}); + Node{"NewTransition", Node::Type::Transition, scene_pos}); return m_ptr; }); } if (ImGui::BeginMenu("Add arc")) { - for (const auto& from_idx : g.n_idx) { + for (const auto& from_idx : n_idx) { const auto& from = g.nodes[from_idx]; if (ImGui::BeginMenu(from.name.c_str())) { node_hovered_in_scene = &from; - for (const auto& to_idx : g.n_idx) { + for (const auto& to_idx : n_idx) { const auto& to = g.nodes[to_idx]; if (from.type != to.type && ImGui::BeginMenu(to.name.c_str())) { node_hovered_in_scene = &to; diff --git a/symmetri/gui/graph.hpp b/symmetri/gui/graph.hpp index 99f6db5..dde1f5d 100644 --- a/symmetri/gui/graph.hpp +++ b/symmetri/gui/graph.hpp @@ -113,4 +113,5 @@ inline std::shared_ptr createGraph( Graph{std::move(arcs), std::move(nodes), std::move(v), std::move(w)}); } -void draw(Graph&); +void draw(const Graph&, const std::vector& n_idx, + const std::vector& a_idx); diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 6667605..5a74f0b 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -7,6 +7,7 @@ #include "rpp/rpp.hpp" #include "rxdispatch.h" #include "rximgui.h" +#include "write_graph_to_disk.hpp" using namespace rximgui; #include "model.h" #include "util.h" @@ -62,7 +63,6 @@ int main(int, char **) { float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; // Flux starts here - auto reducers = rpp::source::create(&rxdispatch::dequeue) .subscribe_on(rpp::schedulers::new_thread{}); diff --git a/symmetri/gui/menu_bar.cpp b/symmetri/gui/menu_bar.cpp index 89797ef..39d6b7b 100644 --- a/symmetri/gui/menu_bar.cpp +++ b/symmetri/gui/menu_bar.cpp @@ -3,6 +3,7 @@ #include "model.h" #include "position_parsers.h" #include "rxdispatch.h" +#include "write_graph_to_disk.hpp" model::Reducer updateActiveFile(const std::filesystem::path &file) { return [=](model::Model &m_ptr) { auto &m = *m_ptr.data; @@ -21,12 +22,6 @@ model::Reducer updateActiveFile(const std::filesystem::path &file) { std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); } m.graph.reset(*createGraph(net, positions)); - // auto g = createGraph(net); - // m.graph = *g; - // m.arcs = g->arcs; - // m.nodes = g->nodes; - // m.a_idx = g->a_idx; - // m.n_idx = g->n_idx; return m_ptr; }; } @@ -44,6 +39,10 @@ void draw_menu_bar(ImGui::FileBrowser &fileDialog) { if (ImGui::MenuItem("Open")) { fileDialog.Open(); } + if (ImGui::MenuItem("Save")) { + rxdispatch::push(farbart::writeToDisk(std::filesystem::path( + "/Users/thomashorstink/Projects/Symmetri/nets/test9001.pnml"))); + } // Exit... ImGui::EndMenu(); } diff --git a/symmetri/gui/model.h b/symmetri/gui/model.h index 4741541..efc48e2 100644 --- a/symmetri/gui/model.h +++ b/symmetri/gui/model.h @@ -19,10 +19,6 @@ struct Model { std::filesystem::path working_dir; std::optional active_file; int menu_height = 20; - std::vector arcs = {}; - std::vector nodes = {}; - std::vector a_idx = {}; - std::vector n_idx = {}; Graph graph; ImGui::FileBrowser file_dialog; }; @@ -31,9 +27,16 @@ struct Model { struct ViewModel { ViewModel() {} - explicit ViewModel(const Model& m) : m(m) { auto& model = *m.data; } + explicit ViewModel(const Model& m) : m(m) { + auto& model = *m.data; + data->a_idx = model.graph.a_idx; + data->n_idx = model.graph.n_idx; + } Model m; - struct shared {}; + struct shared { + std::vector a_idx = {}; + std::vector n_idx = {}; + }; std::shared_ptr data = std::make_shared(); }; @@ -58,12 +61,7 @@ inline Model initializeModel() { } else { std::tie(net, marking, pt) = symmetri::readGrml({pn_file}); } - auto g = createGraph(net, positions); - m.graph = *g; - m.arcs = g->arcs; - m.nodes = g->nodes; - m.a_idx = g->a_idx; - m.n_idx = g->n_idx; + m.graph = *createGraph(net, positions); m.file_dialog.SetTitle("title"); m.file_dialog.SetTypeFilters({".pnml", ".grml"}); @@ -75,7 +73,7 @@ inline void draw(const ViewModel& vm) { auto& m = *vm.m.data; draw_menu_bar(m.file_dialog); - draw(m.graph); + draw(m.graph, vm.data->n_idx, vm.data->a_idx); ImGui::End(); } diff --git a/symmetri/gui/position_parsers.cpp b/symmetri/gui/position_parsers.cpp index d15109c..f4286e6 100644 --- a/symmetri/gui/position_parsers.cpp +++ b/symmetri/gui/position_parsers.cpp @@ -1,7 +1,5 @@ #include "position_parsers.h" -#include - #include "tinyxml2/tinyxml2.h" using namespace tinyxml2; diff --git a/symmetri/gui/write_graph_to_disk.hpp b/symmetri/gui/write_graph_to_disk.hpp new file mode 100644 index 0000000..ca631ae --- /dev/null +++ b/symmetri/gui/write_graph_to_disk.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include "model.h" +#include "tinyxml2/tinyxml2.h" + +namespace farbart { +inline model::Reducer writeToDisk(const std::filesystem::path& path) { + return [=](model::Model& m_ptr) { + auto& m = *m_ptr.data; + const auto& graph = m.graph; + using namespace tinyxml2; + XMLDocument doc; + doc.SaveFile(path.c_str()); + XMLElement* pnml = doc.NewElement("pnml"); + pnml->SetAttribute("xmlns", + "http://www.pnml.org/version-2009/grammar/pnml"); + doc.InsertFirstChild(pnml); + auto net = pnml->InsertFirstChild(doc.NewElement("net")); + auto root = net->InsertFirstChild(doc.NewElement("page")); + const auto& nodes = graph.nodes; + for (auto idx : graph.n_idx) { + const auto& node = nodes[idx]; + XMLElement* child = doc.NewElement( + node.type == Node::Type::Place ? "place" : "transition"); + XMLElement* s_child = doc.NewElement("graphics"); + XMLElement* s_s_child = doc.NewElement("position"); + child->SetAttribute("id", node.name.c_str()); + s_s_child->SetAttribute("x", node.Pos.x); + s_s_child->SetAttribute("y", node.Pos.y); + s_child->InsertEndChild(s_s_child); + child->InsertEndChild(s_child); + root->InsertEndChild(child); + } + const auto& arcs = graph.arcs; + + for (auto idx : graph.a_idx) { + const auto& arc = arcs[idx]; + XMLElement* child = doc.NewElement("arc"); + child->SetAttribute("source", nodes[arc.from_to_pos_idx[0]].name.c_str()); + child->SetAttribute("target", nodes[arc.from_to_pos_idx[1]].name.c_str()); + child->SetAttribute("color", + symmetri::Color::toString(arc.color).c_str()); + + root->InsertEndChild(child); + } + + doc.SaveFile(path.c_str()); + + m.working_dir = path.parent_path(); + m.active_file = path; + return m_ptr; + }; +} +} // namespace farbart diff --git a/symmetri/pnml_parser.cpp b/symmetri/pnml_parser.cpp index f7595ef..e2b1466 100644 --- a/symmetri/pnml_parser.cpp +++ b/symmetri/pnml_parser.cpp @@ -1,3 +1,4 @@ +#include #include #include #include From 53792e79a455179b8c8a2166aed20c31fea793cf Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Wed, 7 Feb 2024 20:52:46 +0100 Subject: [PATCH 026/142] color mystery solved --- symmetri/grml_parser.cpp | 8 ++++---- symmetri/gui/main.mm | 16 ++++++---------- symmetri/gui/menu_bar.cpp | 1 - symmetri/gui/menu_bar.h | 2 +- symmetri/gui/model.h | 1 - symmetri/gui/rxdispatch.cpp | 16 ++++++++++------ symmetri/pnml_parser.cpp | 8 ++++---- 7 files changed, 25 insertions(+), 27 deletions(-) diff --git a/symmetri/grml_parser.cpp b/symmetri/grml_parser.cpp index 3c656de..58529b5 100644 --- a/symmetri/grml_parser.cpp +++ b/symmetri/grml_parser.cpp @@ -88,10 +88,10 @@ std::tuple readGrml( ->GetText()); for (int i = 0; i < multiplicity; i++) { + const auto arc_color = + NULL == color ? Color::Success : Color::registerToken(color); if (places.find(source_id) != places.end()) { // if the source is a place, tokens are consumed. - const auto arc_color = - NULL == color ? Color::Success : Color::registerToken(color); if (state_net.find(target_id) != state_net.end()) { state_net.find(target_id)->second.first.push_back( {source_id, arc_color}); @@ -102,9 +102,9 @@ std::tuple readGrml( // if the destination is a place, tokens are produced. if (state_net.find(source_id) != state_net.end()) { state_net.find(source_id)->second.second.push_back( - {target_id, Color::Success}); + {target_id, arc_color}); } else { - state_net.insert({source_id, {{}, {{target_id, Color::Success}}}}); + state_net.insert({source_id, {{}, {{target_id, arc_color}}}}); } } else { throw std::runtime_error(std::string( diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 5a74f0b..6ff04a5 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -62,11 +62,14 @@ int main(int, char **) { float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; + // start event + rxdispatch::push([](model::Model &m) { return m; }); + // Flux starts here auto reducers = rpp::source::create(&rxdispatch::dequeue) .subscribe_on(rpp::schedulers::new_thread{}); - auto models = reducers + auto models = reducers.observe_on(rximgui::rl) .scan(model::initializeModel(), [=](model::Model &&m, model::Reducer f) { try { @@ -84,15 +87,12 @@ int main(int, char **) { .filter([=](const model::Model &m) { return m.data->timestamp <= std::chrono::steady_clock::now(); }) - .start_with(model::initializeModel()) .map([](const model::Model &m) { return model::ViewModel{m}; }); auto draw_frames = frames.with_latest_from(rxu::take_at<1>(), view_models); auto subscription = draw_frames.tap([](const model::ViewModel &vm) { draw(vm); }).subscribe(); - // and loop - // Main loop while (!glfwWindowShouldClose(window) && subscription.is_subscribed()) { @autoreleasepool { @@ -129,7 +129,6 @@ int main(int, char **) { // Rendering ImGui::Render(); - // ImGui::EndFrame(); // <-- Added, can be deleted? ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), commandBuffer, renderEncoder); // [renderEncoder popDebugGroup]; ???? @@ -139,17 +138,14 @@ int main(int, char **) { [commandBuffer commit]; } } - rxdispatch::push([&](model::Model &m) { - subscription.unsubscribe(); - return m; - }); + subscription.unsubscribe(); - // Cleanup ImGui_ImplMetal_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); + return 0; } diff --git a/symmetri/gui/menu_bar.cpp b/symmetri/gui/menu_bar.cpp index 39d6b7b..783755d 100644 --- a/symmetri/gui/menu_bar.cpp +++ b/symmetri/gui/menu_bar.cpp @@ -1,6 +1,5 @@ #include "menu_bar.h" -#include "model.h" #include "position_parsers.h" #include "rxdispatch.h" #include "write_graph_to_disk.hpp" diff --git a/symmetri/gui/menu_bar.h b/symmetri/gui/menu_bar.h index d66882b..33e3392 100644 --- a/symmetri/gui/menu_bar.h +++ b/symmetri/gui/menu_bar.h @@ -4,4 +4,4 @@ // clang-format on #include "imfilebrowser.h" -void draw_menu_bar(ImGui::FileBrowser &fileDialog); +void draw_menu_bar(ImGui::FileBrowser& fileDialog); diff --git a/symmetri/gui/model.h b/symmetri/gui/model.h index efc48e2..bd13afe 100644 --- a/symmetri/gui/model.h +++ b/symmetri/gui/model.h @@ -72,7 +72,6 @@ inline Model initializeModel() { inline void draw(const ViewModel& vm) { auto& m = *vm.m.data; draw_menu_bar(m.file_dialog); - draw(m.graph, vm.data->n_idx, vm.data->a_idx); ImGui::End(); } diff --git a/symmetri/gui/rxdispatch.cpp b/symmetri/gui/rxdispatch.cpp index 20d9dd8..4a94e3a 100644 --- a/symmetri/gui/rxdispatch.cpp +++ b/symmetri/gui/rxdispatch.cpp @@ -1,9 +1,11 @@ #include "rxdispatch.h" +#include + #include "blockingconcurrentqueue.h" namespace rxdispatch { -moodycamel::BlockingConcurrentQueue reducer_queue{10}; +static moodycamel::BlockingConcurrentQueue reducer_queue{10}; void push(model::Reducer&& r) { reducer_queue.enqueue(std::forward(r)); @@ -11,13 +13,15 @@ void push(model::Reducer&& r) { void dequeue(const rpp::dynamic_subscriber& sub) { model::Reducer f; - sub.get_subscription().add([&]() { - // cleanup? + sub.get_subscription().add([&]() { // exit stuff }); - while (sub.is_subscribed() && - rxdispatch::reducer_queue.wait_dequeue_timed(f, -1)) { - sub.on_next(f); + while (sub.is_subscribed()) { + if (rxdispatch::reducer_queue.wait_dequeue_timed( + f, std::chrono::milliseconds(16)) && + sub.is_subscribed()) { + sub.on_next(f); + } } }; } // namespace rxdispatch diff --git a/symmetri/pnml_parser.cpp b/symmetri/pnml_parser.cpp index e2b1466..c573028 100644 --- a/symmetri/pnml_parser.cpp +++ b/symmetri/pnml_parser.cpp @@ -62,10 +62,10 @@ std::tuple readPnml(const std::set &files) { ->GetText()); for (int i = 0; i < multiplicity; i++) { + const auto arc_color = + NULL == color ? Color::Success : Color::registerToken(color); if (places.find(source_id) != places.end()) { // if the source is a place, tokens are consumed. - const auto arc_color = - NULL == color ? Color::Success : Color::registerToken(color); if (state_net.find(target_id) != state_net.end()) { state_net.find(target_id)->second.first.push_back( {source_id, arc_color}); @@ -76,9 +76,9 @@ std::tuple readPnml(const std::set &files) { // if the destination is a place, tokens are produced. if (state_net.find(source_id) != state_net.end()) { state_net.find(source_id)->second.second.push_back( - {target_id, Color::Success}); + {target_id, arc_color}); } else { - state_net.insert({source_id, {{}, {{target_id, Color::Success}}}}); + state_net.insert({source_id, {{}, {{target_id, arc_color}}}}); } } else { const auto arc_id = child->Attribute("id"); From 62b8bd2ec9c3487136cbb13ac66e6004cb5a079b Mon Sep 17 00:00:00 2001 From: Thomas Horstink Date: Sat, 11 May 2024 15:27:05 +0200 Subject: [PATCH 027/142] rpp++ v2 --- symmetri/gui/main.mm | 57 +-- symmetri/gui/rpp/CMakeLists.txt | 34 +- symmetri/gui/rpp/rpp/defs.hpp | 2 +- symmetri/gui/rpp/rpp/fwd.hpp | 9 +- symmetri/gui/rpp/rpp/memory_model.hpp | 25 +- symmetri/gui/rpp/rpp/observables.hpp | 16 +- .../rpp/observables/blocking_observable.hpp | 120 +++--- .../observables/connectable_observable.hpp | 207 +++++---- .../gui/rpp/rpp/observables/constraints.hpp | 42 -- .../observables/details/member_overload.hpp | 20 - .../rpp/observables/dynamic_observable.hpp | 152 +++---- symmetri/gui/rpp/rpp/observables/fwd.hpp | 206 ++++++--- .../rpp/observables/grouped_observable.hpp | 30 +- .../rpp/observables/interface_observable.hpp | 122 ------ .../rpp/observables/specific_observable.hpp | 129 ------ symmetri/gui/rpp/rpp/observers.hpp | 20 +- .../gui/rpp/rpp/observers/constraints.hpp | 57 --- .../rpp/rpp/observers/dynamic_observer.hpp | 247 +++++------ symmetri/gui/rpp/rpp/observers/fwd.hpp | 249 ++++++++++- .../rpp/rpp/observers/specific_observer.hpp | 129 ------ .../gui/rpp/rpp/observers/state_observer.hpp | 96 ----- symmetri/gui/rpp/rpp/operators.hpp | 119 +++--- symmetri/gui/rpp/rpp/operators/buffer.hpp | 139 +++--- .../gui/rpp/rpp/operators/combine_latest.hpp | 284 +++++++------ symmetri/gui/rpp/rpp/operators/concat.hpp | 316 +++++++++----- symmetri/gui/rpp/rpp/operators/debounce.hpp | 289 ++++++++----- symmetri/gui/rpp/rpp/operators/delay.hpp | 319 ++++++++------ .../operators/details/early_unsubscribe.hpp | 42 -- .../details/serialized_subscriber.hpp | 75 ---- .../details/subscriber_with_state.hpp | 49 --- .../rpp/operators/distinct_until_changed.hpp | 139 ++++-- symmetri/gui/rpp/rpp/operators/do.hpp | 49 --- symmetri/gui/rpp/rpp/operators/filter.hpp | 105 +++-- symmetri/gui/rpp/rpp/operators/first.hpp | 102 +++-- symmetri/gui/rpp/rpp/operators/flat_map.hpp | 70 +++- symmetri/gui/rpp/rpp/operators/fwd.hpp | 305 ++++++++++++-- symmetri/gui/rpp/rpp/operators/fwd/buffer.hpp | 93 ---- .../rpp/rpp/operators/fwd/combine_latest.hpp | 183 -------- symmetri/gui/rpp/rpp/operators/fwd/concat.hpp | 158 ------- .../gui/rpp/rpp/operators/fwd/debounce.hpp | 88 ---- symmetri/gui/rpp/rpp/operators/fwd/delay.hpp | 102 ----- .../operators/fwd/distinct_until_changed.hpp | 85 ---- symmetri/gui/rpp/rpp/operators/fwd/do.hpp | 220 ---------- symmetri/gui/rpp/rpp/operators/fwd/filter.hpp | 78 ---- symmetri/gui/rpp/rpp/operators/fwd/first.hpp | 84 ---- .../gui/rpp/rpp/operators/fwd/flat_map.hpp | 77 ---- .../gui/rpp/rpp/operators/fwd/group_by.hpp | 106 ----- symmetri/gui/rpp/rpp/operators/fwd/last.hpp | 82 ---- symmetri/gui/rpp/rpp/operators/fwd/lift.hpp | 210 ---------- symmetri/gui/rpp/rpp/operators/fwd/map.hpp | 86 ---- symmetri/gui/rpp/rpp/operators/fwd/merge.hpp | 159 ------- .../gui/rpp/rpp/operators/fwd/multicast.hpp | 63 --- .../gui/rpp/rpp/operators/fwd/observe_on.hpp | 79 ---- .../operators/fwd/on_error_resume_next.hpp | 107 ----- .../gui/rpp/rpp/operators/fwd/publish.hpp | 58 --- symmetri/gui/rpp/rpp/operators/fwd/reduce.hpp | 326 -------------- .../gui/rpp/rpp/operators/fwd/ref_count.hpp | 56 --- symmetri/gui/rpp/rpp/operators/fwd/repeat.hpp | 120 ------ symmetri/gui/rpp/rpp/operators/fwd/sample.hpp | 88 ---- symmetri/gui/rpp/rpp/operators/fwd/scan.hpp | 95 ----- symmetri/gui/rpp/rpp/operators/fwd/skip.hpp | 76 ---- .../gui/rpp/rpp/operators/fwd/start_with.hpp | 128 ------ .../gui/rpp/rpp/operators/fwd/subscribe.hpp | 125 ------ .../rpp/rpp/operators/fwd/subscribe_on.hpp | 62 --- .../gui/rpp/rpp/operators/fwd/switch_map.hpp | 72 ---- .../rpp/rpp/operators/fwd/switch_on_next.hpp | 86 ---- symmetri/gui/rpp/rpp/operators/fwd/take.hpp | 83 ---- .../gui/rpp/rpp/operators/fwd/take_last.hpp | 76 ---- .../gui/rpp/rpp/operators/fwd/take_until.hpp | 105 ----- .../gui/rpp/rpp/operators/fwd/take_while.hpp | 78 ---- .../gui/rpp/rpp/operators/fwd/timeout.hpp | 123 ------ symmetri/gui/rpp/rpp/operators/fwd/window.hpp | 87 ---- .../rpp/operators/fwd/with_latest_from.hpp | 150 ------- symmetri/gui/rpp/rpp/operators/group_by.hpp | 348 +++++++++------ symmetri/gui/rpp/rpp/operators/last.hpp | 120 ++++-- symmetri/gui/rpp/rpp/operators/lift.hpp | 131 ------ symmetri/gui/rpp/rpp/operators/map.hpp | 116 +++-- symmetri/gui/rpp/rpp/operators/merge.hpp | 316 +++++++++++--- symmetri/gui/rpp/rpp/operators/multicast.hpp | 96 ++++- symmetri/gui/rpp/rpp/operators/observe_on.hpp | 48 ++- .../rpp/operators/on_error_resume_next.hpp | 138 ++++-- symmetri/gui/rpp/rpp/operators/publish.hpp | 44 +- symmetri/gui/rpp/rpp/operators/reduce.hpp | 315 +++++++------- symmetri/gui/rpp/rpp/operators/ref_count.hpp | 96 ++--- symmetri/gui/rpp/rpp/operators/repeat.hpp | 172 ++++---- symmetri/gui/rpp/rpp/operators/sample.hpp | 99 ----- symmetri/gui/rpp/rpp/operators/scan.hpp | 222 ++++++++-- symmetri/gui/rpp/rpp/operators/skip.hpp | 102 +++-- symmetri/gui/rpp/rpp/operators/start_with.hpp | 207 +++++++-- .../gui/rpp/rpp/operators/subscribe_on.hpp | 100 +++-- symmetri/gui/rpp/rpp/operators/switch_map.hpp | 27 -- .../gui/rpp/rpp/operators/switch_on_next.hpp | 209 +++++---- symmetri/gui/rpp/rpp/operators/take.hpp | 103 +++-- symmetri/gui/rpp/rpp/operators/take_last.hpp | 141 ++++--- symmetri/gui/rpp/rpp/operators/take_until.hpp | 191 ++++++--- symmetri/gui/rpp/rpp/operators/take_while.hpp | 111 +++-- symmetri/gui/rpp/rpp/operators/timeout.hpp | 334 ++++++++++----- symmetri/gui/rpp/rpp/operators/window.hpp | 181 +++++--- .../rpp/rpp/operators/with_latest_from.hpp | 373 +++++++++++------ symmetri/gui/rpp/rpp/rpp.hpp | 5 +- symmetri/gui/rpp/rpp/schedulers.hpp | 20 +- .../gui/rpp/rpp/schedulers/constraints.hpp | 40 -- .../schedulers/details/queue_worker_state.hpp | 133 ------ .../gui/rpp/rpp/schedulers/details/utils.hpp | 137 +++++- .../gui/rpp/rpp/schedulers/details/worker.hpp | 101 ++--- symmetri/gui/rpp/rpp/schedulers/fwd.hpp | 203 ++++++++- .../rpp/schedulers/immediate_scheduler.hpp | 48 --- .../rpp/schedulers/new_thread_scheduler.hpp | 126 ------ .../rpp/rpp/schedulers/run_loop_scheduler.hpp | 121 ------ .../rpp/schedulers/trampoline_scheduler.hpp | 173 -------- symmetri/gui/rpp/rpp/sources.hpp | 15 +- symmetri/gui/rpp/rpp/sources/create.hpp | 125 +++--- symmetri/gui/rpp/rpp/sources/empty.hpp | 54 ++- symmetri/gui/rpp/rpp/sources/error.hpp | 71 ++-- symmetri/gui/rpp/rpp/sources/from.hpp | 396 +++++++++--------- symmetri/gui/rpp/rpp/sources/fwd.hpp | 164 ++++---- symmetri/gui/rpp/rpp/sources/interval.hpp | 173 ++++---- symmetri/gui/rpp/rpp/sources/never.hpp | 53 ++- symmetri/gui/rpp/rpp/subjects.hpp | 3 +- .../gui/rpp/rpp/subjects/behavior_subject.hpp | 182 ++++---- symmetri/gui/rpp/rpp/subjects/constraints.hpp | 24 -- .../rpp/rpp/subjects/details/base_subject.hpp | 40 -- .../rpp/subjects/details/subject_state.hpp | 193 +++++---- symmetri/gui/rpp/rpp/subjects/fwd.hpp | 54 ++- .../gui/rpp/rpp/subjects/publish_subject.hpp | 113 +++-- symmetri/gui/rpp/rpp/subjects/type_traits.hpp | 28 -- symmetri/gui/rpp/rpp/subscribers.hpp | 14 - .../gui/rpp/rpp/subscribers/constraints.hpp | 44 -- .../subscribers/details/subscriber_base.hpp | 53 --- .../rpp/subscribers/dynamic_subscriber.hpp | 52 --- symmetri/gui/rpp/rpp/subscribers/fwd.hpp | 29 -- .../rpp/subscribers/specific_subscriber.hpp | 121 ------ .../subscriptions/callback_subscription.hpp | 44 -- .../subscriptions/composite_subscription.hpp | 166 -------- .../gui/rpp/rpp/subscriptions/constraints.hpp | 20 - .../details/subscription_state.hpp | 46 -- symmetri/gui/rpp/rpp/subscriptions/fwd.hpp | 18 - .../rpp/subscriptions/subscription_base.hpp | 70 ---- .../rpp/subscriptions/subscription_guard.hpp | 44 -- symmetri/gui/rpp/rpp/utils/constraints.hpp | 34 +- symmetri/gui/rpp/rpp/utils/exceptions.hpp | 10 +- .../gui/rpp/rpp/utils/function_traits.hpp | 35 +- symmetri/gui/rpp/rpp/utils/functors.hpp | 86 ++-- .../rpp/rpp/utils/operator_declaration.hpp | 30 -- symmetri/gui/rpp/rpp/utils/overloaded.hpp | 21 - symmetri/gui/rpp/rpp/utils/spinlock.hpp | 36 -- symmetri/gui/rpp/rpp/utils/utilities.hpp | 124 ------ symmetri/gui/rxdispatch.cpp | 23 +- symmetri/gui/rxdispatch.h | 4 +- symmetri/gui/rximgui.h | 2 +- 150 files changed, 6376 insertions(+), 10310 deletions(-) delete mode 100644 symmetri/gui/rpp/rpp/observables/constraints.hpp delete mode 100644 symmetri/gui/rpp/rpp/observables/details/member_overload.hpp delete mode 100644 symmetri/gui/rpp/rpp/observables/interface_observable.hpp delete mode 100644 symmetri/gui/rpp/rpp/observables/specific_observable.hpp delete mode 100644 symmetri/gui/rpp/rpp/observers/constraints.hpp delete mode 100644 symmetri/gui/rpp/rpp/observers/specific_observer.hpp delete mode 100644 symmetri/gui/rpp/rpp/observers/state_observer.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/do.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/buffer.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/combine_latest.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/concat.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/debounce.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/delay.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/distinct_until_changed.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/do.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/filter.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/first.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/flat_map.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/group_by.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/last.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/lift.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/map.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/merge.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/multicast.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/observe_on.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/on_error_resume_next.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/publish.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/reduce.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/ref_count.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/repeat.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/sample.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/scan.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/skip.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/start_with.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/subscribe.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/subscribe_on.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/switch_map.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/switch_on_next.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take_last.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take_until.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/take_while.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/timeout.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/window.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/fwd/with_latest_from.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/lift.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/sample.hpp delete mode 100644 symmetri/gui/rpp/rpp/operators/switch_map.hpp delete mode 100644 symmetri/gui/rpp/rpp/schedulers/constraints.hpp delete mode 100644 symmetri/gui/rpp/rpp/schedulers/details/queue_worker_state.hpp delete mode 100644 symmetri/gui/rpp/rpp/schedulers/immediate_scheduler.hpp delete mode 100644 symmetri/gui/rpp/rpp/schedulers/new_thread_scheduler.hpp delete mode 100644 symmetri/gui/rpp/rpp/schedulers/run_loop_scheduler.hpp delete mode 100644 symmetri/gui/rpp/rpp/schedulers/trampoline_scheduler.hpp delete mode 100644 symmetri/gui/rpp/rpp/subjects/constraints.hpp delete mode 100644 symmetri/gui/rpp/rpp/subjects/details/base_subject.hpp delete mode 100644 symmetri/gui/rpp/rpp/subjects/type_traits.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscribers.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscribers/constraints.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscribers/details/subscriber_base.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscribers/dynamic_subscriber.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscribers/fwd.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscribers/specific_subscriber.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscriptions/callback_subscription.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscriptions/composite_subscription.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscriptions/constraints.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscriptions/details/subscription_state.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscriptions/fwd.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscriptions/subscription_base.hpp delete mode 100644 symmetri/gui/rpp/rpp/subscriptions/subscription_guard.hpp delete mode 100644 symmetri/gui/rpp/rpp/utils/operator_declaration.hpp delete mode 100644 symmetri/gui/rpp/rpp/utils/overloaded.hpp delete mode 100644 symmetri/gui/rpp/rpp/utils/spinlock.hpp delete mode 100644 symmetri/gui/rpp/rpp/utils/utilities.hpp diff --git a/symmetri/gui/main.mm b/symmetri/gui/main.mm index 6ff04a5..3b715b5 100644 --- a/symmetri/gui/main.mm +++ b/symmetri/gui/main.mm @@ -66,35 +66,36 @@ int main(int, char **) { rxdispatch::push([](model::Model &m) { return m; }); // Flux starts here - auto reducers = rpp::source::create(&rxdispatch::dequeue) - .subscribe_on(rpp::schedulers::new_thread{}); - - auto models = reducers.observe_on(rximgui::rl) - .scan(model::initializeModel(), - [=](model::Model &&m, model::Reducer f) { - try { - auto r = f(m); - r.data->timestamp = std::chrono::steady_clock::now(); - return r; - } catch (const std::exception &e) { - printf("%s", e.what()); - return std::move(m); - } - }) - .sample_with_time(std::chrono::milliseconds{200}, rl); - - auto view_models = models - .filter([=](const model::Model &m) { - return m.data->timestamp <= std::chrono::steady_clock::now(); - }) - .map([](const model::Model &m) { return model::ViewModel{m}; }); - - auto draw_frames = frames.with_latest_from(rxu::take_at<1>(), view_models); - - auto subscription = draw_frames.tap([](const model::ViewModel &vm) { draw(vm); }).subscribe(); + auto reducers = rpp::source::create(&rxdispatch::dequeue) | + rpp::operators::subscribe_on(rpp::schedulers::new_thread{}); + + auto models = + reducers | rpp::operators::subscribe_on(rximgui::rl) | + rpp::operators::scan(model::initializeModel(), [=](model::Model &&m, model::Reducer f) { + try { + auto r = f(m); + r.data->timestamp = std::chrono::steady_clock::now(); + return r; + } catch (const std::exception &e) { + printf("%s", e.what()); + return std::move(m); + } + }); + // | + // rpp::operators::sample_with_time(std::chrono::milliseconds{200}, rl); + + auto view_models = models | rpp::operators::filter([=](const model::Model &m) { + return m.data->timestamp <= std::chrono::steady_clock::now(); + }) | + rpp::operators::map([](const model::Model &m) { return model::ViewModel{m}; }); + + auto draw_frames = frames | rpp::operators::with_latest_from(rxu::take_at<1>(), view_models); + + draw_frames | rpp::operators::tap([](const model::ViewModel &vm) { draw(vm); }) | + rpp::operators::subscribe(); // Main loop - while (!glfwWindowShouldClose(window) && subscription.is_subscribed()) { + while (!glfwWindowShouldClose(window)) { @autoreleasepool { glfwPollEvents(); @@ -138,7 +139,7 @@ int main(int, char **) { [commandBuffer commit]; } } - subscription.unsubscribe(); + rxdispatch::unsubscribe(); ImGui_ImplMetal_Shutdown(); ImGui_ImplGlfw_Shutdown(); diff --git a/symmetri/gui/rpp/CMakeLists.txt b/symmetri/gui/rpp/CMakeLists.txt index ab7a043..68d9708 100644 --- a/symmetri/gui/rpp/CMakeLists.txt +++ b/symmetri/gui/rpp/CMakeLists.txt @@ -1,6 +1,6 @@ # ReactivePlusPlus library # -# Copyright Aleksey Loginov 2022 - present. +# Copyright Aleksey Loginov 2023 - present. # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # https://www.boost.org/LICENSE_1_0.txt) @@ -8,34 +8,4 @@ # Project home: https://github.com/victimsnino/ReactivePlusPlus # -file(GLOB_RECURSE FILES "*.hpp") - -if(${CMAKE_VERSION} VERSION_LESS "3.19.0") - add_library(rpp INTERFACE) -else() - add_library(rpp INTERFACE ${FILES}) -endif() - -add_library(RPP::rpp ALIAS rpp) - -target_include_directories(rpp ${RPP_WARNING_GUARD} - INTERFACE - "$" - # "$" -) - -target_link_libraries(rpp INTERFACE Threads::Threads) -target_compile_features(rpp INTERFACE cxx_std_20) - -if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") - target_compile_options(rpp INTERFACE -fsized-deallocation) -endif() - -foreach(FILE ${FILES}) - get_filename_component(PARENT_DIR "${FILE}" PATH) - file(RELATIVE_PATH REL_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rpp" ${PARENT_DIR}) - set(REL_PARENT_DIR "Header Files\\${REL_PARENT_DIR}") - - string(REPLACE "/" "\\" GROUP ${REL_PARENT_DIR}) - source_group("${GROUP}" FILES "${FILE}") -endforeach() +rpp_add_library(rpp) diff --git a/symmetri/gui/rpp/rpp/defs.hpp b/symmetri/gui/rpp/rpp/defs.hpp index ed313e0..8353930 100644 --- a/symmetri/gui/rpp/rpp/defs.hpp +++ b/symmetri/gui/rpp/rpp/defs.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) diff --git a/symmetri/gui/rpp/rpp/fwd.hpp b/symmetri/gui/rpp/rpp/fwd.hpp index 81c69ee..03a2f3c 100644 --- a/symmetri/gui/rpp/rpp/fwd.hpp +++ b/symmetri/gui/rpp/rpp/fwd.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,15 +11,14 @@ #pragma once /** - * \defgroup rpp RPP - * \brief Rpp is Reactive extension for C++20 + * @defgroup rpp RPP + * @brief ReactivePlusPlus (RPP) is Reactive extension for C++20 */ +#include #include #include #include #include #include #include -#include -#include diff --git a/symmetri/gui/rpp/rpp/memory_model.hpp b/symmetri/gui/rpp/rpp/memory_model.hpp index d02dc55..4d079ba 100644 --- a/symmetri/gui/rpp/rpp/memory_model.hpp +++ b/symmetri/gui/rpp/rpp/memory_model.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -10,11 +10,18 @@ #pragma once -namespace rpp { -enum class memory_model { - // copy and move everywhere when needed - use_stack, - // make shared_ptr once and avoid any future copies/moves - use_shared, -}; -} // namespace rpp +#include + +namespace rpp::memory_model { +// copy and move everywhere when needed +struct use_stack {}; + +// make shared_ptr once and avoid any future copies/moves +struct use_shared {}; +} // namespace rpp::memory_model + +namespace rpp::constraint { +template +concept memory_model = std::same_as || + std::same_as; +} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/observables.hpp b/symmetri/gui/rpp/rpp/observables.hpp index 98f4971..ff9d9b7 100644 --- a/symmetri/gui/rpp/rpp/observables.hpp +++ b/symmetri/gui/rpp/rpp/observables.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,13 +11,17 @@ #pragma once /** - * \defgroup observables Observables - * \brief Observable is the source of any Reactive Stream. Observable provides - * ability to subscribe observer on some events. \see - * https://reactivex.io/documentation/observable.html \ingroup rpp + * @defgroup observables Observables + * @brief Observable is the source of any Reactive Stream. + * @details Observable provides ability to subscribe observer on stream of + * events. After subscription observable would emit values to observer. + * @see https://reactivex.io/documentation/observable.html + * @ingroup rpp */ #include #include #include -#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/observables/blocking_observable.hpp b/symmetri/gui/rpp/rpp/observables/blocking_observable.hpp index 4b3cb92..2d3e8ca 100644 --- a/symmetri/gui/rpp/rpp/observables/blocking_observable.hpp +++ b/symmetri/gui/rpp/rpp/observables/blocking_observable.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -9,67 +9,81 @@ #pragma once -#include -#include // RPP_EMPTY_BASES -#include // OriginalObservable type -#include // overload operators -#include // wrap subscribers -#include // create_subscriber_with_state -#include // forwarding of member_overaloads -#include // create subscriber +#include +#include +#include +#include +#include -namespace rpp { -/** - * \brief blocking alternative of observable: provides interface where each - * function do blocking subscribe on original observable (waits till - * on_completed and provides value) \tparam Type type of values emitted by this - * observable \tparam OriginalObservable original observable wrapped by this - * observable \ingroup observables - */ -template OriginalObservable> -class RPP_EMPTY_BASES blocking_observable final - : public details::member_overload< - Type, blocking_observable, - details::subscribe_tag> { +namespace rpp::details::observables { +class blocking_disposble final : public base_disposable { public: - blocking_observable(const OriginalObservable& original) - : m_original{original} {} - - blocking_observable(OriginalObservable&& original) - : m_original{std::move(original)} {} + void wait() { + std::unique_lock lock{m_mutex}; + m_cv.wait(lock, [this] { return m_completed; }); + } - friend struct details::member_overload< - Type, blocking_observable, - details::subscribe_tag>; + void base_dispose_impl(interface_disposable::Mode) noexcept override { + { + std::lock_guard lock{m_mutex}; + m_completed = true; + } + m_cv.notify_all(); + } private: - template TSub> - void subscribe_impl(TSub&& subscriber) const noexcept { - if (!subscriber.is_subscribed()) return; + std::mutex m_mutex{}; + std::condition_variable m_cv{}; + bool m_completed{}; +}; + +template Strategy> +class blocking_strategy { + public: + using value_type = Type; + using expected_disposable_strategy = + typename rpp::details::observables::deduce_disposable_strategy_t< + Strategy>::template add<1>; - std::promise is_success{}; - const auto future = is_success.get_future(); - auto sub = subscriber.get_subscription(); - m_original.subscribe(create_subscriber_with_state( - std::move(sub), utils::forwarding_on_next{}, - [&](const std::exception_ptr& err, const auto& sub) { - sub.on_error(err); - is_success.set_value(false); - }, - [&](const auto& sub) { - sub.on_completed(); - is_success.set_value(true); - }, - std::forward(subscriber))); - future.wait(); + blocking_strategy(observable&& observable) + : m_original{std::move(observable)} {} + + blocking_strategy(const observable& observable) + : m_original{observable} {} + + template ObserverStrategy> + void subscribe(observer&& obs) const { + auto d = disposable_wrapper_impl::make(); + obs.set_upstream(d); + m_original.subscribe(std::move(obs)); + + if (!d.is_disposed()) + if (const auto locked = d.lock()) locked->wait(); } private: - OriginalObservable m_original; + RPP_NO_UNIQUE_ADDRESS observable m_original; }; +} // namespace rpp::details::observables -template -blocking_observable(const TObs&) - -> blocking_observable, TObs>; +namespace rpp { +/** + * @brief Extension over rpp::observable with set of blocking operators - it + * waits till completion of underlying observable + * + * @par Example: + * @snippet as_blocking.cpp as_blocking + * + * @ingroup observables + */ +template Strategy> +class blocking_observable + : public observable< + Type, details::observables::blocking_strategy> { + public: + using observable>::observable; +}; } // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/connectable_observable.hpp b/symmetri/gui/rpp/rpp/observables/connectable_observable.hpp index 7275ce7..2cbca87 100644 --- a/symmetri/gui/rpp/rpp/observables/connectable_observable.hpp +++ b/symmetri/gui/rpp/rpp/observables/connectable_observable.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -9,101 +9,164 @@ #pragma once -#include #include -#include // RPP_EMPTY_BASES -#include // OriginalObservable type -#include // create_subscriber_with_state -#include // include forwarding for member_overload -#include // type of subject used -#include // deduce observable type by subject type -#include // lifetime +#include +#include +#include + +namespace rpp::details { +template +struct ref_count_on_subscribe_t; + +template +struct ref_count_on_subscribe_t< + rpp::connectable_observable> { + rpp::connectable_observable original_observable; + + struct state_t { + std::mutex mutex{}; + disposable_wrapper_impl disposable = + disposable_wrapper_impl::empty(); + }; + + std::shared_ptr m_state = std::make_shared(); + + using value_type = rpp::utils::extract_observable_type_t; + using expected_disposable_strategy = + typename rpp::details::observables::deduce_disposable_strategy_t< + rpp::connectable_observable>::template add<1>; + + template Strategy> + void subscribe(observer&& obs) const { + auto [disposable, upstream] = on_subscribe(); + + obs.set_upstream(disposable); + original_observable.subscribe(std::move(obs)); + if (!upstream.is_disposed()) + original_observable.connect(std::move(upstream)); + } + + private: + std::pair + on_subscribe() const { + std::unique_lock lock(m_state->mutex); + if (!m_state->disposable.is_disposed()) + return {m_state->disposable.lock()->add_ref(), + composite_disposable_wrapper::empty()}; + + m_state->disposable = + disposable_wrapper_impl::make(); + return {m_state->disposable.lock()->add_ref(), m_state->disposable}; + } +}; +} // namespace rpp::details namespace rpp { /** - * \brief connectable alternative of observable: extends interface with extra - * functionality. Common subscription will subscribe on underlying subject, but - * connect/ref_count will initiate subscription on original observable \tparam - * Type type of values emitted by this observable \tparam OriginalObservable - * original observable wrapped by this observable \see - * https://reactivex.io/documentation/operators/publish.html \ingroup - * observables + * @brief Extension over raw observable with ability to be manually connected at + * any time or ref_counting (sharing same observable between multiple observers) + * + * @ingroup observables */ -template Subject, - constraint::observable_of_type OriginalObservable> -class RPP_EMPTY_BASES connectable_observable - : public decltype(std::declval().get_observable()), - public details::member_overload< - Type, connectable_observable, - details::ref_count_tag> { +template +class connectable_observable final + : public decltype(std::declval().get_observable()) { using base = decltype(std::declval().get_observable()); public: + static_assert(rpp::constraint::subject); + connectable_observable(const OriginalObservable& original_observable, const Subject& subject = Subject{}) : base{subject.get_observable()}, m_original_observable{original_observable}, - m_state{std::make_shared(subject)} {} + m_subject{subject} {} connectable_observable(OriginalObservable&& original_observable, const Subject& subject = Subject{}) : base{subject.get_observable()}, m_original_observable{std::move(original_observable)}, - m_state{std::make_shared(subject)} {} - - composite_subscription connect(const composite_subscription& subscription = - composite_subscription{}) const { - auto subscriber = m_state->subject.get_subscriber(); - const auto& subscriber_subscription = subscriber.get_subscription(); - - { - std::lock_guard lock(m_state->mutex); - - if (!m_state->sub.is_empty()) return subscription; - - subscriber_subscription.add(subscription); - m_state->sub = subscription; - } - - subscription.add([state = std::weak_ptr{m_state}] { - if (const auto locked = state.lock()) { - auto current_sub = composite_subscription::empty(); - { - std::lock_guard lock(locked->mutex); - std::swap(current_sub, locked->sub); - } - current_sub.unsubscribe(); - locked->subject.get_subscriber().get_subscription().remove(current_sub); - } - }); - - m_original_observable.subscribe(create_subscriber_with_state( - m_state->sub, utils::forwarding_on_next{}, utils::forwarding_on_error{}, - utils::forwarding_on_completed{}, subscriber.get_observer(), - // capture state to be sure that state is alive while ANY subscriber is - // alive - m_state)); - - return subscription; + m_subject{subject} {} + + /** + * @brief Connects to underlying observable right-now making it hot-observable + * + * @par Example: + * @snippet connect.cpp connect + * + */ + rpp::disposable_wrapper connect( + rpp::composite_disposable_wrapper wrapper = + composite_disposable_wrapper::make()) const { + std::unique_lock lock(m_state->mutex); + + if (m_subject.get_disposable().is_disposed()) + return rpp::disposable_wrapper::empty(); + + if (!m_state->disposable.is_disposed()) return m_state->disposable; + + m_state->disposable = wrapper; + lock.unlock(); + + m_original_observable.subscribe(wrapper, m_subject.get_observer()); + return wrapper; + } + + /** + * @brief Forces rpp::connectable_observable to behave like common observable + * @details Connects rpp::connectable_observable on the first subscription and + * unsubscribes on last unsubscription + * + * @par Example + * @snippet ref_count.cpp ref_count + * + * @ingroup connectable_operators + * @see https://reactivex.io/documentation/operators/refcount.html + */ + auto ref_count() const { + return rpp::observable< + rpp::utils::extract_observable_type_t, + details::ref_count_on_subscribe_t< + connectable_observable>>{*this}; + } + + template + Op> + auto operator|(Op&& op) const& { + return std::forward(op)(*this); + } + + template < + rpp::constraint::operator_observable_transform + Op> + auto operator|(Op&& op) && { + return std::forward(op)(std::move(*this)); + } + + template + decltype(std::declval() | std::declval()) operator|( + Op&& op) const& { + return static_cast(*this) | std::forward(op); + } + + template + decltype(std::declval() | std::declval()) operator|(Op&& op) && { + return static_cast(*this) | std::forward(op); } private: OriginalObservable m_original_observable; - struct state_t { - state_t(const Subject& subj) : subject{subj} {} + Subject m_subject; - Subject subject; + struct state_t { std::mutex mutex{}; - composite_subscription sub = composite_subscription::empty(); + rpp::composite_disposable_wrapper disposable = + composite_disposable_wrapper::empty(); }; - std::shared_ptr m_state{}; + std::shared_ptr m_state = std::make_shared(); }; - -template -connectable_observable(const OriginalObservable&, const Subject&) - -> connectable_observable, - Subject, OriginalObservable>; } // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/constraints.hpp b/symmetri/gui/rpp/rpp/observables/constraints.hpp deleted file mode 100644 index 0e7a39c..0000000 --- a/symmetri/gui/rpp/rpp/observables/constraints.hpp +++ /dev/null @@ -1,42 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include -#include - -namespace rpp::constraint { -template -concept observable = - std::derived_from, details::observable_tag>; -} - -namespace rpp::utils { -namespace details { -template -struct extract_observable_type { - template - static TT deduce(const rpp::details::typed_observable_tag&); - - using type = decltype(deduce(std::declval>())); -}; -} // namespace details - -template -using extract_observable_type_t = - typename details::extract_observable_type::type; -} // namespace rpp::utils - -namespace rpp::constraint { -template -concept observable_of_type = - observable && std::same_as, Type>; -} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/observables/details/member_overload.hpp b/symmetri/gui/rpp/rpp/observables/details/member_overload.hpp deleted file mode 100644 index 51d4fb8..0000000 --- a/symmetri/gui/rpp/rpp/observables/details/member_overload.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include -#include - -namespace rpp::details { -template -struct member_overload; -} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp b/symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp index a9315f6..05e9824 100644 --- a/symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp +++ b/symmetri/gui/rpp/rpp/observables/dynamic_observable.hpp @@ -1,120 +1,90 @@ -// ReactivePlusPlus library +// ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus +// Copyright Aleksey Loginov 2023 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) // +// Project home: https://github.com/victimsnino/ReactivePlusPlus #pragma once #include -#include -#include // base -#include // for header include - -IMPLEMENTATION_FILE(dynamic_observable_tag); - -namespace rpp::details { -template -class dynamic_observable_state { +#include +#include +#include +#include + +namespace rpp::details::observables { +template +void forwarding_subscribe(const void* const ptr, dynamic_observer&& obs) { + static_cast(ptr)->subscribe(std::move(obs)); +} + +template +class dynamic_strategy final { public: - template TObs> - dynamic_observable_state(TObs&& obs) - : m_impl{ - std::make_shared>>( - std::forward(obs))} {} - - template TOnSub> - requires( - !constraint::decayed_same_as>) - dynamic_observable_state(TOnSub&& on_sub) - : m_impl{std::make_shared>>>( - std::forward(on_sub))} {} - - dynamic_observable_state(const dynamic_observable_state& other) = default; - dynamic_observable_state(dynamic_observable_state&& other) noexcept = default; - dynamic_observable_state& operator=(const dynamic_observable_state& other) = - default; - dynamic_observable_state& operator=( - dynamic_observable_state&& other) noexcept = default; - - composite_subscription operator()( - const dynamic_subscriber& subscriber) const { - return (*m_impl)(subscriber); + using value_type = Type; + + template Strategy> + requires(!rpp::constraint::decayed_same_as>) + explicit dynamic_strategy(observable&& obs) + : m_forwarder{std::make_shared>( + std::move(obs))}, + m_vtable{vtable::template create>()} {} + + template Strategy> + requires(!rpp::constraint::decayed_same_as>) + explicit dynamic_strategy(const observable& obs) + : m_forwarder{std::make_shared>(obs)}, + m_vtable{vtable::template create>()} {} + + template ObserverStrategy> + void subscribe(observer&& observer) const { + m_vtable->subscribe(m_forwarder.get(), std::move(observer).as_dynamic()); } private: - struct interface_dynamic_observable_state_impl { - virtual ~interface_dynamic_observable_state_impl() = default; - - virtual composite_subscription operator()( - const dynamic_subscriber& subscriber) const = 0; - }; - - template - class dynamic_observable_state_impl final - : public interface_dynamic_observable_state_impl { - public: - dynamic_observable_state_impl(TObs&& observable) - : m_observable{std::move(observable)} {} + struct vtable { + void (*subscribe)(const void*, dynamic_observer&&){}; - dynamic_observable_state_impl(const TObs& observable) - : m_observable{observable} {} - - composite_subscription operator()( - const dynamic_subscriber& subscriber) const override { - return m_observable.subscribe(subscriber); + template + static const vtable* create() noexcept { + static vtable s_res{.subscribe = forwarding_subscribe}; + return &s_res; } - - private: - RPP_NO_UNIQUE_ADDRESS TObs m_observable{}; }; - std::shared_ptr m_impl{}; + private: + std::shared_ptr m_forwarder; + const vtable* m_vtable; }; -} // namespace rpp::details +} // namespace rpp::details::observables namespace rpp { /** - * \brief Type-less observable (or partially untyped) that has the notion of - * Type but hides the notion of on_subscribe for C++ compiler. + * @brief Type-erased version of the `rpp::observable`. Any observable can be + * converted to dynamic_observable via `rpp::observable::as_dynamic` member + * function. + * @details To provide type-erasure it uses `std::shared_ptr`. As a result it + * has worse performance. * - * \details This is a C++ technique called type-erasure. Multiple instances of - * the observable that may have different upstream graphs are considered - * homogeneous. i.e. They can be stored in the same container, e.g. std::vector. - * As a result, it uses heap to store on_subscribe and hide its type. + * @tparam Type of value this obsevalbe can provide * - * \param Type is the value type. Observable of type means this source could - * emit a sequence of items of that "Type". \ingroup observables + * @ingroup observables */ template class dynamic_observable - : public specific_observable> { + : public observable> { + using base = observable>; + public: - using base = - specific_observable>; using base::base; - explicit dynamic_observable( - constraint::on_subscribe_fn auto&& on_subscribe) - : base{std::forward(on_subscribe)} {} + dynamic_observable(base&& b) : base{std::move(b)} {} - template TObs> - requires(!std::is_same_v, dynamic_observable>) - dynamic_observable(TObs&& observable) - : base{std::forward(observable)} {} + dynamic_observable(const base& b) : base{b} {} }; - -template -dynamic_observable(TObs obs) - -> dynamic_observable>; - -template -dynamic_observable(OnSub on_subscribe) -> dynamic_observable< - utils::extract_subscriber_type_t>>; } // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/fwd.hpp b/symmetri/gui/rpp/rpp/observables/fwd.hpp index f34af68..76c5d8a 100644 --- a/symmetri/gui/rpp/rpp/observables/fwd.hpp +++ b/symmetri/gui/rpp/rpp/observables/fwd.hpp @@ -1,75 +1,167 @@ -// ReactivePlusPlus library +// ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus +// Copyright Aleksey Loginov 2023 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) // +// Project home: https://github.com/victimsnino/ReactivePlusPlus #pragma once -#include // on_subscribe_fn -#include // decayed type - -namespace rpp::details { -struct observable_tag {}; - -template -struct typed_observable_tag : public details::observable_tag {}; -struct dynamic_observable_tag; -} // namespace rpp::details +#include +#include +#include +#include namespace rpp::constraint { -template -concept on_subscribe_fn = - std::invocable, dynamic_subscriber>; +template +concept observable_strategy = requires( + const S& strategy, rpp::details::observers::fake_observer&& observer) { + { + strategy.subscribe(std::move(observer)) + } -> std::same_as; + typename S::value_type; + }; } // namespace rpp::constraint +namespace rpp::details::observables { +template +class dynamic_strategy; + +template Strategy> +class blocking_strategy; + +template +struct fake_strategy { + using value_type = Type; + + static void subscribe(const auto&) {} +}; +} // namespace rpp::details::observables + namespace rpp { +template +class observable_chain_strategy; -/** - * \brief Type-full observable (or typed) that has the notion of Type and - * upstream observables for C++ compiler. e.g. observable> is different from observable>. - * - * \details This is a C++ technique about de-virtualization. To achieve - * polymorphic behavior, we could either go for function virtualization or - * function overload. However, virtualization is more expensive than function - * overload in both compile time and runtime. Therefore, we go for function - * overload. Actually, we use more advanced functor paradigm for better - * performance. As a result it has better performance comparing to - * rpp::dynamic_observable. Use it if possible. But it has worse usability due - * to OnSubscribeFn template parameter. - * - * \param Type is the value type. Observable of type means this source could - * emit a sequence of items of that "Type". \param OnSubscribeFn is the - * on_subscribe functor that is called when a subscriber subscribes to this - * observable. specific_observable stores OnSubscribeFn as member variable, so, - * it is stored on stack (instead of allocating it on heap). \ingroup - * observables - */ template OnSubscribeFn> -class specific_observable; - -/** - * \brief Type-less observable (or partially untyped) that has the notion of - * Type but hides the notion of on_subscribe for C++ compiler. - * - * \details This is a C++ technique called type-erasure. Multiple instances of - * the observable that may have different upstream graphs are considered - * homogeneous. i.e. They can be stored in the same container, e.g. std::vector. - * As a result, it uses heap to store on_subscribe and hide its type. - * - * \param Type is the value type. Observable of type means this source could - * emit a sequence of items of that "Type". \ingroup observables - */ + constraint::observable_strategy Strategy> +class observable; + template class dynamic_observable; +template Strategy> +class blocking_observable; + template OnSubscribeFn> + constraint::observable_strategy Strategy> class grouped_observable; } // namespace rpp + +namespace rpp::constraint { +template +concept observable = rpp::utils::is_base_of_v; +} // namespace rpp::constraint + +namespace rpp { +template +class connectable_observable; +} // namespace rpp + +namespace rpp::utils { +template +using extract_observable_type_t = + typename rpp::utils::extract_base_type_params_t< + T, rpp::observable>::template type_at_index_t<0>; +} // namespace rpp::utils + +namespace rpp::constraint { +template +concept observable_of_type = + observable && + std::same_as, std::decay_t>; + +template +concept operator_observable_transform = requires(const Op& op, TObs obs) { + { + op(static_cast(obs)) + } -> rpp::constraint::observable; + }; + +template +concept operator_base = + requires(const Op& op) { + typename std::decay_t::template operator_traits; + } && + details::observables::constraint::disposable_strategy< + details::observables::deduce_updated_disposable_strategy< + std::decay_t, typename observable_chain_strategy< + details::observables::fake_strategy>:: + expected_disposable_strategy>>; + +template +concept operator_subscribe = + operator_base && + requires(const Op& op, + rpp::details::observers::fake_observer::template operator_traits::result_type>&& observer, + const observable_chain_strategy< + details::observables::fake_strategy>& chain) { + { op.subscribe(std::move(observer), chain) }; + }; + +template +concept operator_lift = + operator_base && + requires(const Op& op, + rpp::details::observers::fake_observer::template operator_traits::result_type>&& observer) { + { + op.template lift(std::move(observer)) + } -> rpp::constraint::observer_of_type; + }; + +template +concept operator_lift_with_disposable_strategy = + operator_base && + requires(const Op& op, + rpp::details::observers::fake_observer::template operator_traits::result_type>&& observer) { + { + op.template lift_with_disposable_strategy( + std::move(observer)) + } -> rpp::constraint::observer_of_type; + }; + +template +concept operator_chain = + operator_base, Type> && + requires { + typename std::decay_t::template operator_traits::result_type; + } && + (operator_subscribe, Type> || + operator_lift, Type> || + operator_lift_with_disposable_strategy, Type, + DisposableStrategy>); + +template +concept observables_of_same_type = + rpp::constraint::observable && + (rpp::constraint::observable && ...) && + (std::same_as, + rpp::utils::extract_observable_type_t> && + ...); +} // namespace rpp::constraint + +#define RPP_CHECK_IF_TRAIT_ASSERTS_SATISFIED(Op, Type) \ + /* operator_traits can be instantiated if all inner static_asserts are \ + * fine*/ \ + if constexpr (requires { \ + { \ + typename std::decay_t::template operator_traits< \ + Type>{} \ + }; \ + }) diff --git a/symmetri/gui/rpp/rpp/observables/grouped_observable.hpp b/symmetri/gui/rpp/rpp/observables/grouped_observable.hpp index 9007143..1354b59 100644 --- a/symmetri/gui/rpp/rpp/observables/grouped_observable.hpp +++ b/symmetri/gui/rpp/rpp/observables/grouped_observable.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -9,20 +9,30 @@ #pragma once -#include +#include namespace rpp { + +/** + * @brief Extension over rpp::observable for some "subset" of values from + * original observable grouped by some key. It has `get_key()` member function. + * Used in `group_by` operator to represent grouped observable + * + * @tparam KeyType is type of key + * @tparam Type of value this obsevalbe can provide + * @tparam Strategy is observable strategy + * + * @ingroup observables + */ template OnSubscribeFn> -class grouped_observable final - : public specific_observable { + constraint::observable_strategy Strategy> +class grouped_observable final : public observable { public: - grouped_observable(KeyType key, const OnSubscribeFn& on_subscribe) - : specific_observable{on_subscribe}, - m_key{std::move(key)} {} + grouped_observable(KeyType key, const Strategy& strategy) + : observable{strategy}, m_key{std::move(key)} {} - grouped_observable(KeyType key, OnSubscribeFn&& on_subscribe) - : specific_observable{std::move(on_subscribe)}, + grouped_observable(KeyType key, Strategy&& strategy) + : observable{std::move(strategy)}, m_key{std::move(key)} {} const KeyType& get_key() const { return m_key; } diff --git a/symmetri/gui/rpp/rpp/observables/interface_observable.hpp b/symmetri/gui/rpp/rpp/observables/interface_observable.hpp deleted file mode 100644 index 245fc4f..0000000 --- a/symmetri/gui/rpp/rpp/observables/interface_observable.hpp +++ /dev/null @@ -1,122 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include // RPP_EMPTY_BASES -#include // as_blocking -#include // own constraints -#include // own forwarding -#include // forwarding of member_overaloads -#include // decayed_invoke_result_t -#include - -namespace rpp::details { -template -concept op_fn = - constraint::observable>; -} // namespace rpp::details - -namespace rpp { -/** - * \brief Base part of observable. Mostly used to provide some interface - * functions used by all observables \tparam Type type provided by this - * observable \tparam SpecificObservable final type of observable inherited from - * this observable to successfully copy/move it - */ -template -struct RPP_EMPTY_BASES interface_observable - : public details::typed_observable_tag, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload, - details::member_overload { - public: - /** - * \brief The apply function to observable which returns observable of another - * type \tparam OperatorFn type of function which applies to this observable - * \return new specific_observable of NewType - * \ingroup operators - * - */ - template OperatorFn> - auto op(OperatorFn&& fn) const& { - return fn(CastThis()); - } - - template OperatorFn> - auto op(OperatorFn&& fn) && { - return fn(MoveThis()); - } - - /** - * \brief Converts existing observable to rpp::blocking_observable which has - * another interface and abilities - */ - auto as_blocking() const& { return blocking_observable{CastThis()}; } - - auto as_blocking() && { return blocking_observable{MoveThis()}; } - - private: - const SpecificObservable& CastThis() const { - return *static_cast(this); - } - - SpecificObservable&& MoveThis() { - return std::move(*static_cast(this)); - } -}; -} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observables/specific_observable.hpp b/symmetri/gui/rpp/rpp/observables/specific_observable.hpp deleted file mode 100644 index fd466c8..0000000 --- a/symmetri/gui/rpp/rpp/observables/specific_observable.hpp +++ /dev/null @@ -1,129 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include -#include -#include // base_class -#include -#include -#include // for header include -#include // copy_assignable_callable -#include - -namespace rpp { -/** - * \brief Type-full observable (or typed) that has the notion of Type and - * upstream observables for C++ compiler. e.g. observable> is different from observable>. - * - * \details This is a C++ technique about de-virtualization. To achieve - * polymorphic behavior, we could either go for function virtualization or - * function overload. However, virtualization is more expensive than function - * overload in both compile time and runtime. Therefore, we go for function - * overload. Actually, we use more advanced functor paradigm for better - * performance. As a result it has better performance comparing to - * rpp::dynamic_observable. Use it if possible. But it has worse usability due - * to OnSubscribeFn template parameter. - * - * \param Type is the value type. Observable of type means this source could - * emit a sequence of items of that "Type". \param OnSubscribeFn is the - * on_subscribe functor that is called when a subscriber subscribes to this - * observable. specific_observable stores OnSubscribeFn as member variable, so, - * it is stored on stack (instead of allocating it on heap). \ingroup - * observables - */ -template OnSubscribeFn> -class specific_observable - : public interface_observable> { - public: - specific_observable(OnSubscribeFn&& on_subscribe) - : m_on_subscribe{std::move(on_subscribe)} {} - - specific_observable(const OnSubscribeFn& on_subscribe) - : m_on_subscribe{on_subscribe} {} - - specific_observable(const specific_observable& other) = default; - specific_observable(specific_observable&& other) noexcept = default; - specific_observable& operator=(const specific_observable& other) = default; - specific_observable& operator=(specific_observable&& other) noexcept = - default; - /** - * \brief Converts rpp::specific_observable to rpp::dynamic_observable via - * type-erasure mechanism. - */ - template - [[nodiscard]] auto as_dynamic() const& - requires details::is_header_included - { - return rpp::dynamic_observable{*this}; - } - template - [[nodiscard]] auto as_dynamic() && - requires details::is_header_included - { - return rpp::dynamic_observable{std::move(*this)}; - } - - friend struct details::member_overload< - Type, specific_observable, details::subscribe_tag>; - - private: - // used by rpp::details::member_overload, rpp::details::subscribe_tag>; - template TSub> - composite_subscription subscribe_impl(const TSub& subscriber) const { - if (subscriber.is_subscribed()) actual_subscribe(subscriber); - - return subscriber.get_subscription(); - } - - template TSub> - void actual_subscribe(const TSub& subscriber) const { - // take ownership over current thread as early as possible to delay all next - // "current_thread" schedulings. For example, scheduling of emissions from - // "just" to delay it till whole chain is subscribed and ready to listened - // emissions For example, if we have - // rpp::source::just(rpp::schedulers::current_thread{}, - // 1,2).combine_latest(rpp::source::just(rpp::schedulers::current_thread{}, - // 1,2)) - // - // then we expect to see emissions like (1,1) (2,1) (2,2) instead of (2,1) - // (2,2). TO do it we need to "take ownership" over queue to prevent ANY - // immediate schedulings from ANY next subscriptions - const auto drain_on_exit_if_needed = - schedulers::current_thread::own_queue_and_drain_finally_if_not_owned(); - try { - m_on_subscribe(subscriber); - } catch (...) { - if (subscriber.is_subscribed()) - subscriber.on_error(std::current_exception()); - else - throw; - } - } - - private: - /** - * \brief The on_subscribe functor, which has the operator()(const auto& - * subscriber) overload function. - */ - RPP_NO_UNIQUE_ADDRESS OnSubscribeFn m_on_subscribe; -}; - -template -specific_observable(OnSub on_subscribe) -> specific_observable< - utils::extract_subscriber_type_t>, OnSub>; -} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observers.hpp b/symmetri/gui/rpp/rpp/observers.hpp index 4066ac2..aa6fb6d 100644 --- a/symmetri/gui/rpp/rpp/observers.hpp +++ b/symmetri/gui/rpp/rpp/observers.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,10 +11,20 @@ #pragma once /** - * \defgroup observers Observers - * \brief Observer subscribe on observable and obtains values provided by - * observable. \ingroup rpp + * @defgroup observers Observers + * @brief Observer subscribes on Observable and obtains values provided by + * Observable. + * @details Observer is kind of wrapper over 3 core functions: + * - on_next(T) - callback with new emission provided by observable + * - on_error(err) - failure termination callback with reason of failure of + * observable (why observable can't continue processing) + * - on_completed() - succeed termination callback - observable is done, no any + * future emissions from this + * @see https://reactivex.io/documentation/observable.html + * @ingroup rpp */ #include -#include +#include +#include +#include diff --git a/symmetri/gui/rpp/rpp/observers/constraints.hpp b/symmetri/gui/rpp/rpp/observers/constraints.hpp deleted file mode 100644 index 4e51463..0000000 --- a/symmetri/gui/rpp/rpp/observers/constraints.hpp +++ /dev/null @@ -1,57 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include -#include - -namespace rpp::constraint { -template -concept observer_callbacks_exists = - requires(const T t) { - // t.on_next(...); - t.on_error(std::declval()); - t.on_completed(); - }; - -template -concept observer = std::is_base_of_v> && - observer_callbacks_exists; -template -concept decayed_observer = observer && decayed_type; -} // namespace rpp::constraint - -namespace rpp::utils { -namespace details { -template -struct extract_observer_type { - template - static TT deduce(const rpp::details::typed_observer_tag&); - - using type = decltype(deduce(std::declval>())); -}; - -} // namespace details -template -using extract_observer_type_t = - typename details::extract_observer_type::type; -} // namespace rpp::utils - -namespace rpp::constraint { -template -concept observer_on_next_exists = - requires(const T t) { t.on_next(std::declval()); }; - -template -concept observer_of_type = - observer && std::same_as, Type> && - observer_on_next_exists; -} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp b/symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp index 712e6b6..738d10d 100644 --- a/symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp +++ b/symmetri/gui/rpp/rpp/observers/dynamic_observer.hpp @@ -1,158 +1,133 @@ -// ReactivePlusPlus library +// ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus +// Copyright Aleksey Loginov 2023 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) // +// Project home: https://github.com/victimsnino/ReactivePlusPlus #pragma once #include -#include -#include // wrapping constructor -#include // base -#include // extract function args -#include // default arguments - -namespace rpp::details { -template -struct dynamic_observer_state_base { - virtual ~dynamic_observer_state_base() = default; - - virtual void on_next(const T& v) const = 0; - virtual void on_next(T&& v) const = 0; - virtual void on_error(const std::exception_ptr& err) const = 0; - virtual void on_completed() const = 0; +#include +#include +#include +#include + +namespace rpp::details::observers { +template +struct member_ptr_caller_impl; + +template +struct member_ptr_caller_impl { + static R call(void* data, Args... args) noexcept(NoExcept) { + return (static_cast(data)->*F)(static_cast(args)...); + } }; -template TObserver> -class dynamic_observer_state final : public dynamic_observer_state_base { - public: - template - requires std::constructible_from - dynamic_observer_state(Args&&... args) - : m_observer{std::forward(args)...} {} - - void on_next(const T& v) const override { m_observer.on_next(v); } - void on_next(T&& v) const override { m_observer.on_next(std::move(v)); } - void on_error(const std::exception_ptr& err) const override { - m_observer.on_error(err); +template +struct member_ptr_caller_impl { + static R call(const void* data, Args... args) noexcept(NoExcept) { + return (static_cast(data)->*F)(static_cast(args)...); } - void on_completed() const override { m_observer.on_completed(); } - - private: - RPP_NO_UNIQUE_ADDRESS TObserver m_observer; }; -template TObserver, - typename... Args> -std::shared_ptr> make_dynamic_observer_state( - Args&&... args) - requires std::constructible_from, Args...> -{ - return std::make_shared>>( - std::forward(args)...); -} - -template -std::shared_ptr> -make_dynamic_observer_state_from_fns(Args&&... args) { - return make_dynamic_observer_state< - T, details::state_observer...>>( - std::forward(args)...); -} - -template -class dynamic_state_observer - : public state_observer>> { - using base = state_observer>>; +template +using member_ptr_caller = member_ptr_caller_impl; +template +class dynamic_strategy final { public: - template - requires(constraint::decayed_same_as && ...) - dynamic_state_observer( - std::invocable auto&& on_next, - std::invocable auto&& on_error, - std::invocable auto&& on_completed, TStates&&... states) - : base{utils::forwarding_on_next_for_pointer{}, - utils::forwarding_on_error_for_pointer{}, - utils::forwarding_on_completed_for_pointer{}, - make_dynamic_observer_state_from_fns( - std::forward(on_next), - std::forward(on_error), - std::forward(on_completed), - std::forward(states)...)} {} - - dynamic_state_observer( - std::shared_ptr> state) - : base{utils::forwarding_on_next_for_pointer{}, - utils::forwarding_on_error_for_pointer{}, - utils::forwarding_on_completed_for_pointer{}, std::move(state)} {} - - template TObserver> - requires(!std::is_same_v, - dynamic_state_observer>) - dynamic_state_observer(TObserver&& obs) - : dynamic_state_observer{ - details::make_dynamic_observer_state>( - std::forward(obs))} {} + template Strategy> + requires(!rpp::constraint::decayed_same_as>) + explicit dynamic_strategy(observer&& obs) + : m_forwarder{std::make_shared>(std::move(obs))}, + m_vtable{vtable::template create>()} {} + + void set_upstream(const disposable_wrapper& d) noexcept { + m_vtable->set_upstream(m_forwarder.get(), d); + } + + bool is_disposed() const noexcept { + return m_vtable->is_disposed(m_forwarder.get()); + } + + void on_next(const Type& v) const noexcept { + m_vtable->on_next_lvalue(m_forwarder.get(), v); + } + + void on_next(Type&& v) const noexcept { + m_vtable->on_next_rvalue(m_forwarder.get(), std::move(v)); + } + + void on_error(const std::exception_ptr& err) const noexcept { + m_vtable->on_error(m_forwarder.get(), err); + } + + void on_completed() const noexcept { + m_vtable->on_completed(m_forwarder.get()); + } + + private: + struct vtable { + void (*on_next_lvalue)(const void*, const Type&){}; + void (*on_next_rvalue)(const void*, Type&&){}; + void (*on_error)(const void*, const std::exception_ptr&){}; + void (*on_completed)(const void*){}; + + void (*set_upstream)(void*, const disposable_wrapper&){}; + bool (*is_disposed)(const void*){}; + + template + static const vtable* create() noexcept { + static vtable s_res{ + .on_next_lvalue = + &member_ptr_caller( + &Strategy::on_next)>::call, + .on_next_rvalue = + &member_ptr_caller( + &Strategy::on_next)>::call, + .on_error = &member_ptr_caller<&Strategy::on_error>::call, + .on_completed = &member_ptr_caller<&Strategy::on_completed>::call, + .set_upstream = &member_ptr_caller<&Strategy::set_upstream>::call, + .is_disposed = &member_ptr_caller<&Strategy::is_disposed>::call, + }; + return &s_res; + } + }; + + private: + std::shared_ptr m_forwarder; + const vtable* m_vtable; }; -} // namespace rpp::details +} // namespace rpp::details::observers namespace rpp { /** - * \brief Dynamic (type-erased) version of observer (comparing to - * specific_observer) \details It uses type-erasure mechanism to hide types of - * OnNext, OnError and OnCompleted callbacks. But it has higher cost in the - * terms of performance due to usage of heap. Use it only when you need to store - * observer as member variable or make copy of original subscriber. In other - * cases prefer using "auto" to avoid converting to dynamic_observer \tparam T - * is type of value handled by this observer \ingroup observers + * @brief Type-erased version of the `rpp::observer`. Any observer can be + * converted to dynamic_observer via `rpp::observer::as_dynamic` member + * function. + * @details To provide type-erasure it uses `std::shared_ptr`. As a result it + * has worse performance, but it is **ONLY** way to copy observer. + * + * @tparam Type of value this observer can handle + * + * @ingroup observers */ -template -class dynamic_observer final : public details::dynamic_state_observer { +template +class dynamic_observer final + : public observer> { + using base = observer>; + public: - template OnNext = utils::empty_function_t, - constraint::on_error_fn OnError = utils::rethrow_error_t, - constraint::on_completed_fn OnCompleted = utils::empty_function_t<>> - dynamic_observer(OnNext&& on_next = {}, OnError&& on_error = {}, - OnCompleted&& on_completed = {}) - : details::dynamic_state_observer{ - std::forward(on_next), std::forward(on_error), - std::forward(on_completed)} {} - - dynamic_observer(constraint::on_next_fn auto&& on_next, - constraint::on_completed_fn auto&& on_completed) - : details::dynamic_state_observer{ - std::forward(on_next), utils::rethrow_error_t{}, - std::forward(on_completed)} {} - - template TObserver> - requires(!std::is_same_v, dynamic_observer>) - dynamic_observer(TObserver&& obs) - : details::dynamic_state_observer{std::forward(obs)} {} - - /** - * \brief Do nothing for rpp::dynamic_observer. Created only for unification - * of interfaces with rpp::specific_observer - */ - const dynamic_observer& as_dynamic() const { return *this; } -}; + using base::base; -template -dynamic_observer(TObserver) - -> dynamic_observer>; + dynamic_observer(base&& b) : base{std::move(b)} {} -template -dynamic_observer(OnNext, Args...) - -> dynamic_observer>; + dynamic_observer(const base& b) : base{b} {} +}; } // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observers/fwd.hpp b/symmetri/gui/rpp/rpp/observers/fwd.hpp index 11d5dc2..b7ecd2e 100644 --- a/symmetri/gui/rpp/rpp/observers/fwd.hpp +++ b/symmetri/gui/rpp/rpp/observers/fwd.hpp @@ -10,34 +10,245 @@ #pragma once +#include +#include +#include #include +#include +#include +#include -namespace rpp::details { -struct observer_tag {}; +namespace rpp::constraint { +template +concept observer_strategy_base = + requires(const S& const_strategy, S& strategy, + const rpp::disposable_wrapper& disposable) { + // const_strategy.on_next(v); + // const_strategy.on_next(std::move(mv)); + const_strategy.on_error(std::exception_ptr{}); + const_strategy.on_completed(); -template -struct typed_observer_tag : public observer_tag {}; -} // namespace rpp::details + strategy.set_upstream(disposable); + { strategy.is_disposed() } -> std::same_as; + }; -namespace rpp::constraint { -template -concept on_next_fn = std::invocable, Type>; -template -concept on_error_fn = std::invocable, std::exception_ptr>; -template -concept on_completed_fn = std::invocable>; +/** + * @brief Concept to define strategy to override observer behavior. Strategy + * must be able to handle all observer's callbacks: + * on_next/on_error/on_completed + * + * @tparam S is Strategy + * @tparam Type is type of value observer would obtain + * + * @ingroup observers + */ +template +concept observer_strategy = + observer_strategy_base && + requires(const S& const_strategy, const Type& v, Type& mv) { + const_strategy.on_next(v); + const_strategy.on_next(std::move(mv)); + }; } // namespace rpp::constraint +namespace rpp::details::observers { +template +class dynamic_strategy; + +template OnNext, + std::invocable OnError, + std::invocable<> OnCompleted> +struct lambda_strategy; +} // namespace rpp::details::observers + +namespace rpp::details { +template +struct with_disposable_strategy { + using preferred_disposable_strategy = Strategy; + + with_disposable_strategy() = delete; + + static void on_next(const auto&) noexcept; + static void on_error(const std::exception_ptr&) noexcept; + static void on_completed() noexcept; + + static void set_upstream(const disposable_wrapper&) noexcept; + static bool is_disposed() noexcept; +}; +} // namespace rpp::details + namespace rpp { +template Strategy> +class observer; + +template Strategy, + rpp::details::observers::constraint::disposable_strategy + DisposableStrategy = + rpp::details::observers::external_disposable_strategy> +using observer_with_disposable = observer< + Type, rpp::details::with_disposable_strategy>; + template class dynamic_observer; -template OnNext, - constraint::on_error_fn OnError, - constraint::on_completed_fn OnCompleted> -class specific_observer; +/** + * @brief Observer specialized with passed callbacks. Most easiesest way to + * construct observer "on the fly" via lambdas and etc. + * + * @tparam Type of value this observer can handle + * @tparam OnNext is type of callback to handle on_next(const Type&) and + * on_next(Type&&) + * @tparam OnError is type of callback to handle on_error(const + * std::exception_ptr&) + * @tparam OnCompleted is type of callback to handle on_completed() + * + * @ingroup observers + */ +template OnNext, + std::invocable OnError, + std::invocable<> OnCompleted> +using lambda_observer = + observer>; + +template OnNext, + std::invocable OnError, + std::invocable<> OnCompleted> +using lambda_observer_with_disposable = + observer_with_disposable>; + +/** + * @brief Constructs observer specialized with passed callbacks. Most easiesest + * way to construct observer "on the fly" via lambdas and etc. + * + * @tparam Type of value this observer can handle + * @param on_next is callback to handle on_next(const Type&) and on_next(Type&&) + * @param on_error is callback to handle on_error(const std::exception_ptr&) + * @param on_completed is callback to handle on_completed() + * + * @ingroup observers + */ +template OnNext, + std::invocable OnError = + rpp::utils::rethrow_error_t, + std::invocable<> OnCompleted = rpp::utils::empty_function_t<>> +auto make_lambda_observer(OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) + -> lambda_observer, std::decay_t, + std::decay_t>; -template -using specific_observer_with_decayed_args = - rpp::specific_observer...>; +/** + * @brief Constructs observer specialized with passed callbacks. Most easiesest + * way to construct observer "on the fly" via lambdas and etc. + * + * @tparam Type of value this observer can handle + * @param d is disposable to attach to resulting observer + * @param on_next is callback to handle on_next(const Type&) and on_next(Type&&) + * @param on_error is callback to handle on_error(const std::exception_ptr&) + * @param on_completed is callback to handle on_completed() + * + * @ingroup observers + */ +template OnNext, + std::invocable OnError = + rpp::utils::rethrow_error_t, + std::invocable<> OnCompleted = rpp::utils::empty_function_t<>> +auto make_lambda_observer(const rpp::composite_disposable_wrapper& d, + OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) + -> lambda_observer_with_disposable, + std::decay_t, + std::decay_t>; + +/** + * @brief Constructs observer specialized with passed callbacks. Most easiesest + * way to construct observer "on the fly" via lambdas and etc. + * + * @param on_next is callback to handle on_next(const Type&) and on_next(Type&&) + * @param on_error is callback to handle on_error(const std::exception_ptr&) + * @param on_completed is callback to handle on_completed() + * @tparam Type of value this observer can handle (deduced from OnNext callback) + * + * @ingroup observers + */ +template OnError = + rpp::utils::rethrow_error_t, + std::invocable<> OnCompleted = rpp::utils::empty_function_t<>, + constraint::decayed_type Type = + rpp::utils::decayed_function_argument_t> + requires std::invocable +auto make_lambda_observer(OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) { + return make_lambda_observer(std::forward(on_next), + std::forward(on_error), + std::forward(on_completed)); +} + +/** + * @brief Constructs observer specialized with passed callbacks. Most easiesest + * way to construct observer "on the fly" via lambdas and etc. + * + * @param d is disposable to attach to resulting observer + * @param on_next is callback to handle on_next(const Type&) and on_next(Type&&) + * @param on_error is callback to handle on_error(const std::exception_ptr&) + * @param on_completed is callback to handle on_completed() + * @tparam Type of value this observer can handle (deduced from OnNext callback) + * + * @ingroup observers + */ + +template OnError = + rpp::utils::rethrow_error_t, + std::invocable<> OnCompleted = rpp::utils::empty_function_t<>, + constraint::decayed_type Type = + rpp::utils::decayed_function_argument_t> + requires std::invocable +auto make_lambda_observer(const rpp::composite_disposable_wrapper& d, + OnNext&& on_next, OnError&& on_error = {}, + OnCompleted&& on_completed = {}) { + return make_lambda_observer(d, std::forward(on_next), + std::forward(on_error), + std::forward(on_completed)); +} } // namespace rpp + +namespace rpp::details::observers { +struct fake_strategy { + using preferred_disposable_strategy = + rpp::details::observers::none_disposable_strategy; + + static void on_next(const auto&) noexcept {} + + static void on_error(const std::exception_ptr&) noexcept {} + + static void on_completed() noexcept {} + + static void set_upstream(const disposable_wrapper&) noexcept {} + + static bool is_disposed() noexcept { return true; } +}; + +template +using fake_observer = rpp::observer; +} // namespace rpp::details::observers + +namespace rpp::utils { +template +using extract_observer_type_t = typename rpp::utils::extract_base_type_params_t< + T, rpp::observer>::template type_at_index_t<0>; +} // namespace rpp::utils + +namespace rpp::constraint { +template +concept observer = rpp::utils::is_base_of_v; + +template +concept observer_of_type = + observer> && + std::same_as>, Type>; +} // namespace rpp::constraint diff --git a/symmetri/gui/rpp/rpp/observers/specific_observer.hpp b/symmetri/gui/rpp/rpp/observers/specific_observer.hpp deleted file mode 100644 index cfdb480..0000000 --- a/symmetri/gui/rpp/rpp/observers/specific_observer.hpp +++ /dev/null @@ -1,129 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include // base class -#include // extract function args -#include // default arguments - -namespace rpp { -/** - * \brief Observer specified with specific template types of callbacks to avoid - * extra heap usage. - * - * \details It has better performance comparing to rpp::dynamic_observer due to - * using stack instead of heap as long as possible. It is default return type of - * all operators - * - * \tparam T is type of value handled by this observer - * \tparam OnNext type of on_next callback - * \tparam OnError type of on_error callback - * \tparam OnCompleted type of on_completed callback - * \ingroup observers - */ -template OnNext = utils::empty_function_t, - constraint::on_error_fn OnError = utils::rethrow_error_t, - constraint::on_completed_fn OnCompleted = utils::empty_function_t<>> -class specific_observer - : public details::state_observer { - using base = details::state_observer; - - using base::base; - - public: - template < - constraint::on_next_fn TOnNext = utils::empty_function_t, - constraint::on_error_fn TOnError = utils::rethrow_error_t, - constraint::on_completed_fn TOnCompleted = utils::empty_function_t<>> - specific_observer(TOnNext&& on_next = {}, TOnError&& on_error = {}, - TOnCompleted&& on_completed = {}) - : base{std::forward(on_next), std::forward(on_error), - std::forward(on_completed)} {} - - specific_observer(constraint::on_next_fn auto&& on_next, - constraint::on_completed_fn auto&& on_completed) - : base{std::forward(on_next), utils::rethrow_error_t{}, - std::forward(on_completed)} {} - - /** - * \brief Converting current rpp::specific_observer to rpp::dynamic_observer - * alternative with erasing of type (and using heap) \return converted - * rpp::dynamic_observer - */ - [[nodiscard]] auto as_dynamic() const& { return dynamic_observer{*this}; } - [[nodiscard]] auto as_dynamic() && { - return dynamic_observer{std::move(*this)}; - } -}; - -template -specific_observer(OnNext) - -> specific_observer, OnNext>; - -template -specific_observer(OnNext, OnError, Args...) - -> specific_observer, OnNext, - OnError, Args...>; - -template -specific_observer(OnNext, OnCompleted) - -> specific_observer, OnNext, - utils::rethrow_error_t, OnCompleted>; - -/** - * \brief Create specific_observer with manually specified Type. In case of type - * can be deduced from argument of OnNext use direct constructor of - * rpp::specific_observer \tparam Type manually specific type of observer - */ -template -auto make_specific_observer() -> specific_observer_with_decayed_args { - return {}; -} - -template OnNext> -auto make_specific_observer(OnNext&& on_next) - -> specific_observer_with_decayed_args { - return {std::forward(on_next)}; -} - -template OnNext, - constraint::on_completed_fn OnCompleted> -auto make_specific_observer(OnNext&& on_next, OnCompleted&& on_completed) - -> specific_observer_with_decayed_args { - return {std::forward(on_next), - std::forward(on_completed)}; -} - -template OnNext, - constraint::on_error_fn OnError> -auto make_specific_observer(OnNext&& on_next, OnError&& on_error) - -> specific_observer_with_decayed_args { - return {std::forward(on_next), std::forward(on_error)}; -} - -template OnNext, - constraint::on_error_fn OnError, - constraint::on_completed_fn OnCompleted> -auto make_specific_observer(OnNext&& on_next, OnError&& on_error, - OnCompleted&& on_completed) - -> specific_observer_with_decayed_args { - return {std::forward(on_next), std::forward(on_error), - std::forward(on_completed)}; -} - -namespace details { -template -using deduce_specific_observer_type_t = - decltype(make_specific_observer(std::declval()...)); -} // namespace details -} // namespace rpp diff --git a/symmetri/gui/rpp/rpp/observers/state_observer.hpp b/symmetri/gui/rpp/rpp/observers/state_observer.hpp deleted file mode 100644 index 0cb94db..0000000 --- a/symmetri/gui/rpp/rpp/observers/state_observer.hpp +++ /dev/null @@ -1,96 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include -#include -#include -#include // extract argument type -#include - -namespace rpp::details { -/** - * \brief Special type of specific_observer which has some state which this - * observer stores and pass to each callback. Actually it is base class for all - * observers. - */ -template - requires(std::invocable && - std::invocable && - std::invocable) -class state_observer : public details::typed_observer_tag { - public: - template - requires(constraint::decayed_same_as && ...) - state_observer(std::invocable auto&& on_next, - std::invocable auto&& on_error, - std::invocable auto&& on_completed, - TStates&&... states) - : m_on_next{std::forward(on_next)}, - m_on_err{std::forward(on_error)}, - m_on_completed{std::forward(on_completed)}, - m_state{std::forward(states)...} {} - - /** - * \brief Observable calls this methods to notify observer about new value. - * - * \note obtains value by const-reference to original object. - */ - void on_next(const T& v) const { - std::apply([&v, this](const States&... states) { m_on_next(v, states...); }, - m_state); - } - - /** - * \brief Observable calls this methods to notify observer about new value. - * - * \note obtains value by rvalue-reference to original object - */ - void on_next(T&& v) const { - std::apply( - [&v, this](const States&... states) { - m_on_next(std::move(v), states...); - }, - m_state); - } - - /** - * \brief Observable calls this method to notify observer about some error - * during generation next data. \warning Obtaining this call means no any - * further on_next or on_completed calls \param err details of error - */ - void on_error(const std::exception_ptr& err) const { - std::apply( - [&err, this](const States&... states) { m_on_err(err, states...); }, - m_state); - } - - /** - * \brief Observable calls this method to notify observer about finish of - * work. \warning Obtaining this call means no any further on_next calls - */ - void on_completed() const { std::apply(m_on_completed, m_state); } - - private: - RPP_NO_UNIQUE_ADDRESS OnNext m_on_next; - RPP_NO_UNIQUE_ADDRESS OnError m_on_err; - RPP_NO_UNIQUE_ADDRESS OnCompleted m_on_completed; - - RPP_NO_UNIQUE_ADDRESS std::tuple m_state; -}; - -template -state_observer(TOnNext, Args...) - -> state_observer>, - TOnNext, Args...>; - -} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators.hpp b/symmetri/gui/rpp/rpp/operators.hpp index 3bfa251..945c997 100644 --- a/symmetri/gui/rpp/rpp/operators.hpp +++ b/symmetri/gui/rpp/rpp/operators.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,18 +11,26 @@ #pragma once /** - * \defgroup operators Operators - * \brief Operators is way to modify observables and extend with some extra - * custom logic \see https://reactivex.io/documentation/operators.html \ingroup - * rpp + * @defgroup operators Operators + * @brief Operators is way to modify observables and extend with some extra + * custom logic + * @details By default observable emits some values with some underlying logic. + * For example, it iterates over vector and emits values. But operators provide + * ability to make such an stream a bit more complex, for example, emit only + * SOME values, transform to string and etc. As a result, it would be ANOTHER + * stream of ANOTHER values, but more suitable for some specific case. + * @see https://reactivex.io/documentation/operators.html + * @ingroup rpp */ +#include + /** - * \defgroup transforming_operators Transforming Operators - * \brief Transforming operators are operators that transform items provided by - * observable \see - * https://reactivex.io/documentation/operators.html#transforming \ingroup - * operators + * @defgroup transforming_operators Transforming Operators + * @brief Transforming operators are operators that transform items provided by + * observable + * @see https://reactivex.io/documentation/operators.html#transforming + * @ingroup operators */ #include @@ -30,83 +38,78 @@ #include #include #include +#include #include +#include /** - * \defgroup filtering_operators Filtering Operators - * \brief Filtering operators are operators that emit only part of items that - * satisfies some condition \see - * https://reactivex.io/documentation/operators.html#filtering \ingroup - * operators + * @defgroup filtering_operators Filtering Operators + * @brief Filtering operators are operators that emit only part of items that + * satisfies some condition + * @see https://reactivex.io/documentation/operators.html#filtering + * @ingroup operators */ #include +#include #include #include #include #include -#include #include #include #include +#include /** - * \defgroup conditional_operators Conditional Operators - * \brief Conditional operators are operators that emit items based on some - * condition including condition of items from other observables \see - * https://reactivex.io/documentation/operators.html#conditional \ingroup - * operators + * @defgroup conditional_operators Conditional Operators + * @brief Conditional operators are operators that emit items based on some + * condition including condition of items from other observables + * @see https://reactivex.io/documentation/operators.html#conditional + * @ingroup operators */ #include #include /** - * \defgroup combining_operators Combining Operators - * \brief Combining operators are operators that combines emissions of multiple - * observables into same observable by some rule \see - * https://reactivex.io/documentation/operators.html#combining \ingroup - * operators + * @defgroup combining_operators Combining Operators + * @brief Combining operators are operators that combines emissions of multiple + * observables into same observable by some rule + * @see https://reactivex.io/documentation/operators.html#combining + * @ingroup operators */ #include #include #include -#include #include #include +#include /** - * \defgroup aggregate_operators Aggregate Operators - * \brief Aggregate operators are operators on the entire sequence of items - * provided by observable \see - * https://reactivex.io/documentation/operators.html#mathematical \ingroup - * operators - */ - -#include -#include - -/** - * \defgroup utility_operators Utility Operators - * \brief Utility operators are operators that provide some extra functionality - * without changing of original values, but changing of behaviour \see - * https://reactivex.io/documentation/operators.html#utility \ingroup operators + * @defgroup utility_operators Utility Operators + * @brief Utility operators are operators that provide some extra functionality + * without changing of original values, but changing of behaviour + * @see https://reactivex.io/documentation/operators.html#utility + * @ingroup operators */ +#include #include -#include +#include #include #include #include +#include #include /** - * \defgroup connectable_operators Connectable Operators - * \brief Connectable operators are operators that provide extra functionality - * for multicasting of controlling of subscription \see - * https://reactivex.io/documentation/operators.html#connectable \ingroup - * operators + * @defgroup connectable_operators Connectable Operators + * @brief Connectable operators are operators that provide extra functionality + * for multicasting of controlling of subscription + * @see https://reactivex.io/documentation/operators.html#connectable + * @ingroup operators */ #include @@ -114,10 +117,22 @@ #include /** - * \defgroup error_handling_operators Error handling Operators - * \brief Error handling operators Operators that help to recover from error - * notifications from an Observable. \see - * https://reactivex.io/documentation/operators.html#error \ingroup operators + * @defgroup aggregate_operators Aggregate Operators + * @brief Aggregate operators are operators that operate on the entire sequence + * of items emitted by an Observable + * @see https://reactivex.io/documentation/operators.html#mathematical + * @ingroup operators + */ + +#include +#include + +/** + * @defgroup error_handling_operators Error Handling Operators + * @brief Operators that help to recover from error notifications from an + * Observable + * @see https://reactivex.io/documentation/operators.html#error + * @ingroup operators */ #include diff --git a/symmetri/gui/rpp/rpp/operators/buffer.hpp b/symmetri/gui/rpp/rpp/operators/buffer.hpp index 9d5758f..18e9b8e 100644 --- a/symmetri/gui/rpp/rpp/operators/buffer.hpp +++ b/symmetri/gui/rpp/rpp/operators/buffer.hpp @@ -1,7 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. -// TC Wang 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,76 +10,94 @@ #pragma once -#include -#include // create_subscriber_with_dynamic_state -#include // own forwarding -#include // required due to operator uses lift -#include // subscriber_of_type -#include // forwarding_on_error - -IMPLEMENTATION_FILE(buffer_tag); - -namespace rpp::details { -/// A non-copyable class that provides a copyable on_next for the subscriber and -/// allows copies of on_next(s) to share the same states. -template -struct buffer_state { - /// \param count Number of items being bundled. Note when count == 0, we'll - /// treat the behavior like when count == 1. - explicit buffer_state(size_t count) : max(std::max(size_t{1}, count)) { - clear_and_reserve_buckets(); - } +#include +#include +#include +#include - buffer_state(const buffer_state& other) = delete; - buffer_state(buffer_state&&) noexcept = default; - buffer_state& operator=(const buffer_state&) = delete; - buffer_state& operator=(buffer_state&&) noexcept = default; +namespace rpp::operators::details { +template +class buffer_observer_strategy { + using container = rpp::utils::extract_observer_type_t; + using value_type = typename container::value_type; + static_assert(std::same_as>); - void clear_and_reserve_buckets() const { - buckets.clear(); - buckets.reserve(max); - } + public: + using preferred_disposable_strategy = + rpp::details::observers::none_disposable_strategy; - const size_t max; - mutable buffer_bundle_type buckets; -}; + buffer_observer_strategy(TObserver&& observer, size_t count) + : m_observer{std::move(observer)} { + m_bucket.reserve(std::max(size_t{1}, count)); + } -struct buffer_on_next { - template - void operator()(auto&& value, const auto& subscriber, - const buffer_state& state) const { - state.buckets.push_back(std::forward(value)); - if (state.buckets.size() == state.max) { - subscriber.on_next(std::move(state.buckets)); - state.clear_and_reserve_buckets(); + template + void on_next(T&& v) const { + m_bucket.push_back(std::forward(v)); + if (m_bucket.size() == m_bucket.capacity()) { + m_observer.on_next(std::move(m_bucket)); + m_bucket.clear(); } } -}; -struct buffer_on_completed { - template - void operator()(const auto& subscriber, - const buffer_state& state) const { - if (!state.buckets.empty()) subscriber.on_next(std::move(state.buckets)); - subscriber.on_completed(); + void on_error(const std::exception_ptr& err) const { + m_observer.on_error(err); + } + + void on_completed() const { + if (!m_bucket.empty()) m_observer.on_next(std::move(m_bucket)); + m_observer.on_completed(); } + + void set_upstream(const disposable_wrapper& d) { m_observer.set_upstream(d); } + + bool is_disposed() const { return m_observer.is_disposed(); } + + private: + RPP_NO_UNIQUE_ADDRESS TObserver m_observer; + mutable std::vector m_bucket; }; -template -struct buffer_impl { - const size_t count; +struct buffer_t : lift_operator { + using lift_operator::lift_operator; - template > TSub> - auto operator()(TSub&& subscriber) const { - auto subscription = subscriber.get_subscription(); + template + struct operator_traits { + using result_type = std::vector; - // dynamic_state there to make shared_ptr for observer instead of making - // shared_ptr for state - return create_subscriber_with_dynamic_state( - std::move(subscription), buffer_on_next{}, utils::forwarding_on_error{}, - buffer_on_completed{}, std::forward(subscriber), - buffer_state{count}); - } + template TObserver> + using observer_strategy = buffer_observer_strategy; + }; + + template + using updated_disposable_strategy = Prev; }; +} // namespace rpp::operators::details -} // namespace rpp::details +namespace rpp::operators { +/** + * @brief Periodically gather emissions emitted by an original Observable into + bundles and emit these bundles rather than emitting + * the items one at a time + * + * @marble buffer + { + source observable : +-1-2-3-| + operator "buffer(2)" : +---{1,2}-{3}-| + } + * + * @details The resulting bundle is `std::vector` of requested size. + Actually it is similar to `window()` operator, but it emits vectors instead of + observables. + * + * @param count number of items being bundled. + * @warning #include + * + * @par Example: + * @snippet buffer.cpp buffer + * + * @ingroup transforming_operators + * @see https://reactivex.io/documentation/operators/buffer.html + */ +inline auto buffer(size_t count) { return details::buffer_t{count}; } +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/combine_latest.hpp b/symmetri/gui/rpp/rpp/operators/combine_latest.hpp index dc60ff9..692c92b 100644 --- a/symmetri/gui/rpp/rpp/operators/combine_latest.hpp +++ b/symmetri/gui/rpp/rpp/operators/combine_latest.hpp @@ -1,7 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. -// TC Wang 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,160 +10,159 @@ #pragma once -#include -#include // RPP_NO_UNIQUE_ADDRESS -#include // create_subscriber_with_state -#include // own forwarding -#include // required due to operator uses lift -#include // merge_state -#include // constraint::subscriber_of_type -#include // spinlock +#include +#include +#include +#include -IMPLEMENTATION_FILE(combine_latest_tag); +namespace rpp::operators::details { +template +class combine_latest_disposable final + : public combining_disposable { + public: + explicit combine_latest_disposable(Observer&& observer, + const TSelector& selector) + : combining_disposable(std::move(observer)), + m_selector(selector) {} -namespace rpp::details { -/** - * \brief The state that caches the values from all the observables and - * dispatches latest caches to the observer. Note the emission is only sent to - * the observer when all the observables at least emits once. - */ -template -struct combine_latest_state : public merge_state { - explicit combine_latest_state( - const TCombiner& combiner, - const composite_subscription& subscription_of_subscriber) - : merge_state(subscription_of_subscriber), combiner(combiner) {} + const auto& get_selector() const { return m_selector; } + + auto& get_values() { return m_values; } - // don't use NO_UNIQUE_ADDRESS there due to issue in MSVC base class becomes - // invalid - /*NO_UNIQUE_ADDRESS*/ TCombiner combiner; - std::mutex values_mutex{}; - std::tuple...> values{}; + private: + rpp::utils::tuple...> m_values{}; + + RPP_NO_UNIQUE_ADDRESS TSelector m_selector; }; -template -struct combine_latest_on_next { - template - void operator()( - auto&& value, const auto& subscriber, - const std::shared_ptr>& state) - const { +template +struct combine_latest_observer_strategy final + : public combining_observer_strategy< + combine_latest_disposable> { + using combining_observer_strategy< + combine_latest_disposable>::disposable; + + template + void on_next(T&& v) const { // mutex need to be locked during changing of values, generating new values // and sending of new values due to we can't update value while we are // sending old one - std::scoped_lock lock{state->values_mutex}; - std::get(state->values) = std::forward(value); + const auto observer = disposable->get_observer_under_lock(); + disposable->get_values().template get().emplace(std::forward(v)); - std::apply( - [&](const auto&... cached_values) { - if ((cached_values.has_value() && ...)) - subscriber.on_next(state->combiner(cached_values.value()...)); - }, - state->values); + disposable->get_values().apply(&apply_impl, + disposable, observer); } -}; - -using combine_latest_on_error = merge_on_error; -using combine_latest_on_completed = merge_on_completed; - -template -struct combine_latest_state_with_serialized_spinlock - : combine_latest_state { - using combine_latest_state::combine_latest_state; - - // we can use spinlock there because 99.9% of time only one ever thread would - // send values from on_next serialized (due to values_mutex), but we have - // small probability to get error from another observable immediately - utils::spinlock spinlock{}; -}; - -/** - * \brief "combine_latest" operator (an OperatorFn used by "lift"). - */ -template -struct combine_latest_impl { - RPP_NO_UNIQUE_ADDRESS TCombiner m_combiner; - RPP_NO_UNIQUE_ADDRESS std::tuple m_other_observables; private: - static constexpr size_t s_index_of_source_type = 0; - - /** - * \brief Templated helper function for subscribing to variadic 'other' - * observables. - * - * \param subscriber is the downstream subscriber. - * \param state manages the cache of emission from the observables and - * coordinate dispatching. - */ - template - void subscribe_other_observables( - std::index_sequence, - // Used in compile time for variadic expansion - const auto& subscriber, - const std::shared_ptr...>>& state) - const { - // +1 because the first element in tuple is the current observable, and you - // want to subscribe to the 'other' observables. (Use variadic expansion to - // iterate the observables) - (subscribe_observable(std::get(m_other_observables), subscriber, - state), - ...); - } - - template - static void subscribe_observable( - const TObservable& observable, const auto& subscriber, - const std::shared_ptr...>>& state) { - using ValueType = utils::extract_observable_type_t; - observable.subscribe( - create_inner_subscriber(subscriber, state)); - } - - template - static auto create_inner_subscriber( - auto&& subscriber, - std::shared_ptr...>> - state) { - auto subscription = state->children_subscriptions.make_child(); - return create_subscriber_with_state( - std::move(subscription), combine_latest_on_next{}, - combine_latest_on_error{}, combine_latest_on_completed{}, - std::forward(subscriber), std::move(state)); + template + static void apply_impl( + const TDisposable& disposable, + const rpp::utils::pointer_under_lock& observer, + const std::optional&... vals) { + if ((vals.has_value() && ...)) + observer->on_next(disposable->get_selector()(vals.value()...)); } +}; - public: - using DownstreamType = utils::decayed_invoke_result_t< - TCombiner, Type, utils::extract_observable_type_t...>; - - template TSub> - auto operator()(TSub&& in_subscriber) const { - auto state = std::make_shared...>>( - m_combiner, in_subscriber.get_subscription()); - // change subscriber to serialized to avoid manual using of mutex - auto subscriber = make_serialized_subscriber( - std::forward(in_subscriber), - std::shared_ptr{state, &state->spinlock}); - - state->count_of_on_completed_needed.store(sizeof...(TOtherObservable) + 1, - std::memory_order::relaxed); +template +struct combine_latest_t + : public combining_operator_t {}; +} // namespace rpp::operators::details - // Subscribe to other observables and redirect on_next event to state - subscribe_other_observables(std::index_sequence_for{}, - subscriber, state); +namespace rpp::operators { +/** + * @brief Combines latest emissions from observables with emission from current + observable when any observable sends new value via applying selector + * + * @marble combine_latest_custom_selector + { + source observable : +------1 -2 -- + -3 -| source other_observable : +-5-6-7- -- -8 + -- -| operator "combine_latest: x,y =>std::pair{x,y}" : + +------{1,5}-{2,7}-{2,8}-{3,8}-| + } + * + * @details Actually this operator subscribes on all of theses observables and + emits new combined value when any of them emits new emission (and each + observable emit values at least one to be able to provide combined value) + * + * @par Performance notes: + * - 1 heap allocation for disposable + * - each value from any observable copied/moved to internal storage + * - mutex acquired every time value obtained + * + * @param selector is applied to current emission of current observable and + latests emissions from observables + * @param observables are observables whose emissions would be combined with + current observable + * @warning #include + * + * @par Examples + * @snippet combine_latest.cpp combine_latest custom selector + * + * @ingroup combining_operators + * @see https://reactivex.io/documentation/operators/combinelatest.html + */ +template + requires(!rpp::constraint::observable && + (!utils::is_not_template_callable || + std::invocable, + utils::extract_observable_type_t...>)) +auto combine_latest(TSelector&& selector, TObservable&& observable, + TObservables&&... observables) { + return details::combine_latest_t, + std::decay_t, + std::decay_t...>{ + rpp::utils::tuple{std::forward(observable), + std::forward(observables)...}, + std::forward(selector)}; +} - // Redirect values from this observable to the state for value composition - return create_inner_subscriber( - std::move(subscriber), std::move(state)); - } -}; -} // namespace rpp::details +/** + * @brief Combines latest emissions from observables with emission from current + observable when any observable sends new value via making tuple + * + * @marble combine_latest + { + source observable : +------1 -2 -- -3 -| + source other_observable : +-5-6-7- -- -8 -- -| + operator "combine_latest: make_tuple" : + +------{1,5}-{2,7}-{2,8}-{3,8}-| + } + * + * @details Actually this operator subscribes on all of theses observables and + emits new combined value when any of them emits new emission (and each + observable emit values at least one to be able to provide combined value) + * + * @warning Selector is just packing values to tuple in this case + * + * @par Performance notes: + * - 1 heap allocation for disposable + * - each value from any observable copied/moved to internal storage + * - mutex acquired every time value obtained + * + * @param observables are observables whose emissions would be combined when any + observable sends new value + * @warning #include + * + * @par Examples + * @snippet combine_latest.cpp combine_latest + * + * @ingroup combining_operators + * @see https://reactivex.io/documentation/operators/combinelatest.html + */ +template +auto combine_latest(TObservable&& observable, TObservables&&... observables) { + return combine_latest(rpp::utils::pack_to_tuple{}, + std::forward(observable), + std::forward(observables)...); +} +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/concat.hpp b/symmetri/gui/rpp/rpp/operators/concat.hpp index c945ab2..bc047dc 100644 --- a/symmetri/gui/rpp/rpp/operators/concat.hpp +++ b/symmetri/gui/rpp/rpp/operators/concat.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -10,131 +10,231 @@ #pragma once -#include -#include +#include #include -#include // dynamic_observable -#include // create_subscriber_with_state -#include // own forwarding -#include // required due to operator uses lift -#include // merge_forwarding_on_next/merge_on_error -#include -#include -#include // constraint::subscriber_of_type -#include // composite_subscription -#include -#include - -IMPLEMENTATION_FILE(concat_tag); - -namespace rpp::details { -template -struct concat_state : public early_unsubscribe_state { - concat_state(const composite_subscription& subscription_of_subscriber) - : early_unsubscribe_state{subscription_of_subscriber}, - source_subscription{children_subscriptions.make_child()} {} - - composite_subscription source_subscription; - std::mutex queue_mutex{}; - std::queue> observables_to_subscribe{}; - std::atomic_bool inner_subscribed{}; +#include +#include +#include +#include + +namespace rpp::operators::details { +template +struct concat_inner_observer_strategy; + +enum ConcatStage : uint8_t { + None = 0, + Draining = 1, + CompletedWhileDraining = 2, + Processing = 3, }; -using concat_on_next_inner = merge_forwarding_on_next; -using concat_on_error = merge_on_error; - -template -struct concat_on_next_outer { - template - void operator()(TObs&& new_observable, const TSub& sub, - const std::shared_ptr>& state) const { - if (state->inner_subscribed.exchange(true, std::memory_order::acq_rel)) { - std::lock_guard lock{state->queue_mutex}; - if (state->inner_subscribed.exchange(true, std::memory_order::relaxed)) { - state->observables_to_subscribe.push( - std::forward(new_observable).as_dynamic()); +template +class concat_state_t final : public rpp::refcount_disposable { + public: + concat_state_t(TObserver&& observer) : m_observer{std::move(observer)} {} + + rpp::utils::pointer_under_lock get_observer() { + return m_observer; + } + rpp::utils::pointer_under_lock> get_queue() { + return m_queue; + } + + std::atomic& stage() { return m_stage; } + + void drain(rpp::composite_disposable_wrapper refcounted) { + while (!is_disposed()) { + const auto observable = get_observable(); + if (!observable) { + stage().store(ConcatStage::None, std::memory_order::relaxed); + refcounted.dispose(); + if (is_disposed()) get_observer()->on_completed(); return; } + + if (handle_observable_impl(observable.value(), refcounted)) return; } - subscribe_inner_subscriber(new_observable, sub, state); + } + + void handle_observable( + const rpp::constraint::decayed_same_as auto& observable, + rpp::composite_disposable_wrapper refcounted) { + if (handle_observable_impl(observable, refcounted)) return; + + drain(refcounted); } private: - static void subscribe_inner_subscriber( - const auto& observable, const constraint::subscriber auto& subscriber, - const std::shared_ptr>& state) { - observable.subscribe(create_subscriber_with_state( - state->children_subscriptions.make_child(), concat_on_next_inner{}, - concat_on_error{}, - [](const constraint::subscriber auto& sub, - const std::shared_ptr>& state) { - { - std::unique_lock lock{state->queue_mutex}; - if (!state->observables_to_subscribe.empty()) { - auto res = std::move(state->observables_to_subscribe.front()); - state->observables_to_subscribe.pop(); - lock.unlock(); - subscribe_inner_subscriber(res, sub, state); - return; - } - if (state->source_subscription.is_subscribed()) { - state->inner_subscribed.store(false, std::memory_order::relaxed); - return; - } - } - sub.on_completed(); - }, - subscriber, state)); + bool handle_observable_impl( + const rpp::constraint::decayed_same_as auto& observable, + rpp::composite_disposable_wrapper refcounted) { + stage().store(ConcatStage::Draining, std::memory_order::relaxed); + refcounted.clear(); + observable.subscribe(concat_inner_observer_strategy{ + disposable_wrapper_impl{wrapper_from_this()}.lock(), + std::move(refcounted)}); + + ConcatStage current = ConcatStage::Draining; + return stage().compare_exchange_strong(current, ConcatStage::Processing, + std::memory_order::seq_cst); } + + private: + std::optional get_observable() { + auto queue = get_queue(); + if (queue->empty()) return std::nullopt; + auto observable = queue->front(); + queue->pop(); + return observable; + } + + private: + rpp::utils::value_with_mutex m_observer; + rpp::utils::value_with_mutex> m_queue; + std::atomic m_stage{}; }; -template -struct concat_on_completed { - void operator()(const constraint::subscriber auto& sub, - const std::shared_ptr>& state) const { - std::unique_lock lock{state->queue_mutex}; - if (!state->inner_subscribed.load(std::memory_order::relaxed)) - sub.on_completed(); +template +struct concat_observer_strategy_base { + concat_observer_strategy_base( + std::shared_ptr> state, + rpp::composite_disposable_wrapper refcounted) + : state{std::move(state)}, refcounted{std::move(refcounted)} {} + + concat_observer_strategy_base( + std::shared_ptr> state) + : concat_observer_strategy_base{state, state->add_ref()} {} + + std::shared_ptr> state; + rpp::composite_disposable_wrapper refcounted; + + void on_error(const std::exception_ptr& err) const { + state->get_observer()->on_error(err); + state->dispose(); } + + void set_upstream(const disposable_wrapper& d) const { refcounted.add(d); } + + bool is_disposed() const { return refcounted.is_disposed(); } }; -template -struct concat_state_with_serialized_spinlock : concat_state { - using concat_state::concat_state; +template +struct concat_inner_observer_strategy + : public concat_observer_strategy_base { + using base = concat_observer_strategy_base; + + using base::concat_observer_strategy_base; + + template + void on_next(T&& v) const { + base::state->get_observer()->on_next(std::forward(v)); + } + + void on_completed() const { + ConcatStage current{ConcatStage::Draining}; + if (base::state->stage().compare_exchange_strong( + current, ConcatStage::CompletedWhileDraining, + std::memory_order::seq_cst)) + return; - // we can use spinlock there because 99.9% of time only one ever thread would - // send values from on_next (only one active observable), but we have small - // probability to get error from main observable immediately - utils::spinlock spinlock{}; + assert(current == ConcatStage::Processing); + + base::state->drain(base::refcounted); + } }; -template -struct concat_impl { - using ValueType = utils::extract_observable_type_t; - - template TSub> - auto operator()(TSub&& in_subscriber) const { - auto state = - std::make_shared>( - in_subscriber.get_subscription()); - - // change subscriber to serialized to avoid manual using of mutex - auto subscriber = make_serialized_subscriber( - std::forward(in_subscriber), - std::shared_ptr{state, &state->spinlock}); - - return create_subscriber_with_state( - state->source_subscription, concat_on_next_outer{}, - concat_on_error{}, concat_on_completed{}, - std::move(subscriber), std::move(state)); +template +struct concat_observer_strategy + : public concat_observer_strategy_base { + using base = concat_observer_strategy_base; + using preferred_disposable_strategy = + rpp::details::observers::none_disposable_strategy; + + concat_observer_strategy(TObserver&& observer) + : base{init_state(std::move(observer))} {} + + template + void on_next(T&& v) const { + ConcatStage current = ConcatStage::None; + if (base::state->stage().compare_exchange_strong( + current, ConcatStage::Draining, std::memory_order::seq_cst)) + base::state->handle_observable(std::forward(v), + base::state->add_ref()); + else + base::state->get_queue()->push(std::forward(v)); + } + + void on_completed() const { + base::refcounted.dispose(); + if (base::state->is_disposed()) base::state->get_observer()->on_completed(); + } + + private: + static std::shared_ptr> init_state( + TObserver&& observer) { + const auto d = + disposable_wrapper_impl>::make( + std::move(observer)); + auto ptr = d.lock(); + ptr->get_observer()->set_upstream(d.as_weak()); + return ptr; } }; -template ... TObservables> -auto concat_with_impl(TObservables&&... observables) { - return source::just(rpp::schedulers::immediate{}, - std::forward(observables).as_dynamic()...) - .concat(); -} -} // namespace rpp::details +struct concat_t : lift_operator { + using lift_operator::lift_operator; + + template + struct operator_traits { + static_assert(rpp::constraint::observable, "T is not observable"); + + using result_type = rpp::utils::extract_observable_type_t; + + template TObserver> + using observer_strategy = concat_observer_strategy; + }; + + template + using updated_disposable_strategy = + rpp::details::observables::fixed_disposable_strategy_selector<1>; +}; +} // namespace rpp::operators::details + +namespace rpp::operators { +/** + * @brief Make observable which would merge emissions from underlying + observables but without overlapping (current observable completes THEN next + started to emit its values) + * + * @marble concat + { + source observable : + { + +--1-2-3-| + .....+4--6-| + } + operator "concat" : +--1-2-3-4--6-| + } + * + * @details Actually it subscribes on first observable from emissions. When + first observable completes, then it subscribes on second observable from + emissions and etc... + * + * @tparam MemoryModel rpp::memory_model strategy used to handle provided + observables + * + * @warning #include + * + * @par Example + * @snippet concat.cpp concat_as_operator + * + * @ingroup creational_operators + * @see https://reactivex.io/documentation/operators/concat.html + */ +inline auto concat() { return details::concat_t{}; } +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/debounce.hpp b/symmetri/gui/rpp/rpp/operators/debounce.hpp index d6c2a44..fda8472 100644 --- a/symmetri/gui/rpp/rpp/operators/debounce.hpp +++ b/symmetri/gui/rpp/rpp/operators/debounce.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -10,49 +10,100 @@ #pragma once -#include -#include -#include -#include -#include // required due to operator uses lift -#include -#include -#include -#include - -IMPLEMENTATION_FILE(debounce_tag); - -namespace rpp::details { -template -class debounce_state : public early_unsubscribe_state { +#include +#include +#include +#include + +namespace rpp::operators::details { +template +class debounce_disposable; + +template +struct debounce_disposable_wrapper { + std::shared_ptr> + disposable{}; + + bool is_disposed() const { return disposable->is_disposed(); } + + void on_error(const std::exception_ptr& err) const { + disposable->get_observer_under_lock()->on_error(err); + } +}; + +template +class debounce_disposable final + : public rpp::composite_disposable_impl, + public rpp::details::enable_wrapper_from_this< + debounce_disposable> { + using T = rpp::utils::extract_observer_type_t; + public: - debounce_state(schedulers::duration period, const Scheduler& scheduler, - const composite_subscription& subscription_of_subscriber) - : early_unsubscribe_state(subscription_of_subscriber), - m_period{period}, - m_worker{scheduler.create_worker(children_subscriptions)} {} + debounce_disposable(Observer&& in_observer, Worker&& in_worker, + rpp::schedulers::duration period) + : m_observer(std::move(in_observer)), + m_worker{std::move(in_worker)}, + m_period{period} { + if constexpr (!Worker::is_none_disposable) { + if (auto d = m_worker.get_disposable(); !d.is_disposed()) + rpp::composite_disposable_impl::add(std::move(d)); + } + } - std::optional emplace_safe(auto&& v) { + template + void emplace_safe(TT&& v) { std::lock_guard lock{m_mutex}; - m_value_to_be_emitted.emplace(std::forward(v)); + m_value_to_be_emitted.emplace(std::forward(v)); const bool need_to_scheduled = - !m_time_when_value_should_be_emitted.has_value() || - !m_value_to_be_emitted.has_value(); + !m_time_when_value_should_be_emitted.has_value(); m_time_when_value_should_be_emitted = m_worker.now() + m_period; - return need_to_scheduled ? m_time_when_value_should_be_emitted - : std::optional{}; + if (need_to_scheduled) { + schedule(); + } + } + + std::optional extract_value() { + std::lock_guard lock{m_mutex}; + return std::exchange(m_value_to_be_emitted, std::optional{}); + } + + rpp::utils::pointer_under_lock get_observer_under_lock() { + return m_observer; } - std::variant + private: + void schedule() { + m_worker.schedule( + m_time_when_value_should_be_emitted.value(), + [](const debounce_disposable_wrapper& + handler) -> schedulers::optional_delay_to { + auto value_or_duration = handler.disposable->extract_value_or_time(); + if (auto* timepoint = + std::get_if(&value_or_duration)) + return schedulers::optional_delay_to{*timepoint}; + + if (auto* value = std::get_if(&value_or_duration)) + handler.disposable->get_observer_under_lock()->on_next( + std::move(*value)); + + return std::nullopt; + }, + debounce_disposable_wrapper{ + this->wrapper_from_this().lock()}); + } + + std::variant extract_value_or_time() { std::lock_guard lock{m_mutex}; if (!m_time_when_value_should_be_emitted.has_value() || !m_value_to_be_emitted.has_value()) return std::monostate{}; - const auto now = m_worker.now(); - if (m_time_when_value_should_be_emitted > now) - return m_time_when_value_should_be_emitted.value() - now; + if (m_time_when_value_should_be_emitted > m_worker.now()) + return m_time_when_value_should_be_emitted.value(); m_time_when_value_should_be_emitted.reset(); auto v = std::move(m_value_to_be_emitted).value(); @@ -60,100 +111,116 @@ class debounce_state : public early_unsubscribe_state { return v; } - std::optional extract_value() { - std::lock_guard lock{m_mutex}; - std::optional res{}; - m_value_to_be_emitted.swap(res); - return res; - } - - using Worker = decltype(std::declval().create_worker( - std::declval())); - const Worker& get_worker() const { return m_worker; } + rpp::utils::value_with_mutex m_observer; + RPP_NO_UNIQUE_ADDRESS Worker m_worker; + rpp::schedulers::duration m_period; - private: - schedulers::duration m_period; - Worker m_worker; std::mutex m_mutex{}; std::optional m_time_when_value_should_be_emitted{}; std::optional m_value_to_be_emitted{}; }; -struct debounce_on_next { - template - void operator()(Value&& v, const auto& state_ptr) const { - if (const auto time_to_schedule = - state_ptr->emplace_safe(std::forward(v))) { - state_ptr->get_worker().schedule( - time_to_schedule.value(), - [state_ptr]() mutable -> schedulers::optional_duration { - auto value_or_duration = state_ptr->extract_value_or_time(); - if (auto* duration = - std::get_if(&value_or_duration)) - return *duration; - - if (auto* value = - std::get_if>(&value_or_duration)) - state_ptr->subscriber.on_next(std::move(*value)); - - return std::nullopt; - }); - } - } -}; +template +struct debounce_observer_strategy { + using preferred_disposable_strategy = + rpp::details::observers::none_disposable_strategy; + + std::shared_ptr> + disposable{}; -struct debounce_on_error { - void operator()(const std::exception_ptr& err, const auto& state) const { - state->children_subscriptions.unsubscribe(); - state->subscriber.on_error(err); + void set_upstream(const rpp::disposable_wrapper& d) const { + disposable->add(d); } -}; -struct debounce_on_completed { - void operator()(const auto& state_ptr) const { - state_ptr->children_subscriptions.unsubscribe(); + bool is_disposed() const { return disposable->is_disposed(); } - if (auto v = state_ptr->extract_value()) - state_ptr->subscriber.on_next(std::move(v.value())); + template + void on_next(T&& v) const { + disposable->emplace_safe(std::forward(v)); + } - state_ptr->subscriber.on_completed(); + void on_error(const std::exception_ptr& err) const noexcept { + disposable->dispose(); + disposable->get_observer_under_lock()->on_error(err); } -}; -template -struct debounce_state_with_serialized_spinlock : debounce_state { - debounce_state_with_serialized_spinlock(auto&& sub, - schedulers::duration period, - const Scheduler& scheduler) - : debounce_state{std::move(period), scheduler, - sub.get_subscription()}, - subscriber(make_serialized_subscriber(std::forward(sub), - std::ref(spinlock))) {} - - // spinlock because most part of time there is only one thread would be active - utils::spinlock spinlock{}; - - using InnerSub = decltype(make_serialized_subscriber( - std::declval(), - std::declval>())); - InnerSub subscriber; + void on_completed() const noexcept { + disposable->dispose(); + const auto value = disposable->extract_value(); + const auto observer = disposable->get_observer_under_lock(); + if (value) observer->on_next(std::move(value).value()); + observer->on_completed(); + } }; -template -struct debounce_impl { - schedulers::duration period; - TScheduler scheduler; - - template TSub> - auto operator()(TSub&& in_subscriber) const { - auto state = std::make_shared>>( - std::forward(in_subscriber), period, scheduler); - - return create_subscriber_with_state( - state->children_subscriptions, debounce_on_next{}, debounce_on_error{}, - debounce_on_completed{}, std::move(state)); +template +struct debounce_t { + template + struct operator_traits { + using result_type = T; + }; + + template + using updated_disposable_strategy = + rpp::details::observables::fixed_disposable_strategy_selector<1>; + + rpp::schedulers::duration duration; + RPP_NO_UNIQUE_ADDRESS Scheduler scheduler; + + template + auto lift_with_disposable_strategy(Observer&& observer) const { + using worker_t = rpp::schedulers::utils::get_worker_t; + using container = typename DisposableStrategy::template add< + worker_t::is_none_disposable ? 0 : 1>::disposable_container; + + const auto disposable = disposable_wrapper_impl< + debounce_disposable, worker_t, container>>:: + make(std::forward(observer), scheduler.create_worker(), + duration); + auto ptr = disposable.lock(); + ptr->get_observer_under_lock()->set_upstream(disposable.as_weak()); + return rpp::observer, + worker_t, container>>{ + std::move(ptr)}; } }; -} // namespace rpp::details +} // namespace rpp::operators::details + +namespace rpp::operators { +/** + * @brief Only emit emission if specified period of time has passed without any + other emission. On each new emission timer reset. + * + * @marble debounce + { + source observable : +--1-2-----3---| + operator "debounce(4)" : +--------2-----3| + } + * + * @details Actually this operator resets time of last emission, schedules + action to send this emission after specified period if no any new emissions + till this moment. + * + * @param period is duration of time should be passed since emission from + original observable without any new emissions to emit this emission. + * @param scheduler is scheduler used to run timer for debounce + + * @warning #include + * + * @par Example + * @snippet debounce.cpp debounce + * + * @ingroup utility_operators + * @see https://reactivex.io/documentation/operators/debounce.html + */ +template +auto debounce(rpp::schedulers::duration period, Scheduler&& scheduler) { + return details::debounce_t>{ + period, std::forward(scheduler)}; +} +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/delay.hpp b/symmetri/gui/rpp/rpp/operators/delay.hpp index f8a800d..f3ea847 100644 --- a/symmetri/gui/rpp/rpp/operators/delay.hpp +++ b/symmetri/gui/rpp/rpp/operators/delay.hpp @@ -1,7 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. -// TC Wang 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,158 +10,220 @@ #pragma once -#include // RPP_NO_UNIQUE_ADDRESS -#include // create_subscriber_with_state -#include // own forwarding -#include // required due to operator uses lift -#include // constraint::subscriber_of_type -#include -#include - -IMPLEMENTATION_FILE(delay_tag); - -namespace rpp::details { -struct completion {}; - -template -class queue_based_worker final - : public std::enable_shared_from_this< - queue_based_worker> { - public: - queue_based_worker(schedulers::duration delay, Worker&& worker, - const Subscriber& subscriber) - : m_delay{delay}, m_worker{std::move(worker)}, m_subscriber{subscriber} {} - - queue_based_worker(schedulers::duration delay, Worker&& worker, - Subscriber&& subscriber) - : m_delay{delay}, - m_worker{std::move(worker)}, - m_subscriber{std::move(subscriber)} {} - - struct on_next { - void operator()( - auto&& value, - const std::shared_ptr>& state) - const { - state->emplace(std::forward(value)); - } - }; +#include +#include +#include +#include +#include +#include - struct on_error { - void operator()( - const std::exception_ptr& err, - const std::shared_ptr>& state) - const { - state->emplace(err); - } - }; +namespace rpp::operators::details { +template +struct emission { + template + emission(TT&& item, schedulers::time_point time) + : value{std::forward(item)}, time_point{time} {} - struct on_completed { - void operator()( - const std::shared_ptr>& state) - const { - state->emplace(completion{}); + std::variant value{}; + rpp::schedulers::time_point time_point{}; +}; + +template +struct delay_disposable final + : public rpp::composite_disposable_impl { + using T = rpp::utils::extract_observer_type_t; + + delay_disposable(Observer&& in_observer, Worker&& in_worker, + rpp::schedulers::duration delay) + : observer(std::move(in_observer)), + worker{std::move(in_worker)}, + delay{delay} { + if constexpr (!Worker::is_none_disposable) { + if (auto d = worker.get_disposable(); !d.is_disposed()) + rpp::composite_disposable_impl::add(std::move(d)); } - }; + } + + Observer observer; + RPP_NO_UNIQUE_ADDRESS Worker worker; + rpp::schedulers::duration delay; + + std::mutex mutex{}; + std::queue> queue; + bool is_active{}; +}; + +template +struct delay_disposable_wrapper { + std::shared_ptr> disposable{}; + + bool is_disposed() const { return disposable->is_disposed(); } + + void on_error(const std::exception_ptr& err) const { + disposable->observer.on_error(err); + } +}; + +template +struct delay_observer_strategy { + std::shared_ptr> disposable{}; + + void set_upstream(const rpp::disposable_wrapper& d) const { + disposable->add(d); + } + + bool is_disposed() const { return disposable->is_disposed(); } + + template + void on_next(T&& v) const { + emplace(std::forward(v)); + } + + void on_error(const std::exception_ptr& err) const noexcept { emplace(err); } + + void on_completed() const noexcept { emplace(rpp::utils::none{}); } private: template - void emplace(TT&& item) { - if (const auto timepoint = emplace_safe(std::forward(item))) { - m_worker.schedule( - timepoint.value(), - [state = - this->shared_from_this()]() -> schedulers::optional_duration { - return state->drain_queue(); - }); + void emplace(TT&& value) const { + if (const auto tp = emplace_safe(std::forward(value))) { + disposable->worker.schedule( + tp.value(), + [](const delay_disposable_wrapper& + wrapper) { return drain_queue(wrapper.disposable); }, + delay_disposable_wrapper{disposable}); } } template - std::optional emplace_safe(TT&& item) { - std::lock_guard lock{m_mutex}; - const auto delay = std::is_same_v> - ? schedulers::duration{0} - : m_delay; - m_queue.emplace(++m_current_id, m_worker.now() + delay, - std::forward(item)); - if (!m_active && m_queue.size() == 1) { - m_active = true; - return m_queue.top().time; + std::optional emplace_safe(TT&& item) const { + std::lock_guard lock{disposable->mutex}; + if constexpr (ClearOnError && + rpp::constraint::decayed_same_as) { + disposable->queue = + std::queue>>{}; + disposable->observer.on_error(std::forward(item)); + return std::nullopt; + } else { + const auto tp = disposable->worker.now() + disposable->delay; + disposable->queue.emplace(std::forward(item), tp); + if (!disposable->is_active) { + disposable->is_active = true; + return tp; + } + return std::nullopt; } - return {}; } - schedulers::optional_duration drain_queue() { + static schedulers::optional_delay_to drain_queue( + const std::shared_ptr>& + disposable) { while (true) { - std::unique_lock lock{m_mutex}; - if (m_queue.empty()) { - m_active = false; - return {}; + std::unique_lock lock{disposable->mutex}; + if (disposable->queue.empty()) { + disposable->is_active = false; + return std::nullopt; } - auto& top = m_queue.top(); - const auto now = m_worker.now(); - if (top.time > now) return top.time - now; + auto& top = disposable->queue.front(); + if (top.time_point > disposable->worker.now()) + return schedulers::optional_delay_to{top.time_point}; - auto item = std::move(top.item); - m_queue.pop(); + auto item = std::move(top.value); + disposable->queue.pop(); lock.unlock(); std::visit( - utils::overloaded{[&](T&& v) { m_subscriber.on_next(std::move(v)); }, - [&](const std::exception_ptr& err) { - m_subscriber.on_error(err); - }, - [&](completion) { m_subscriber.on_completed(); }}, + rpp::utils::overloaded{ + [&](rpp::utils::extract_observer_type_t&& v) { + disposable->observer.on_next(std::move(v)); + }, + [&](const std::exception_ptr& err) { + disposable->observer.on_error(err); + }, + [&](rpp::utils::none) { disposable->observer.on_completed(); }}, std::move(item)); } } +}; - private: - struct emission { - template - emission(size_t id, schedulers::time_point time, TT&& item) - : id{id}, time{std::move(time)}, item{std::forward(item)} {} - - size_t id{}; - schedulers::time_point time{}; - std::variant item{}; - - bool operator<(const emission& other) const { - return std::tie(time, id) >= std::tie(other.time, other.id); - } +template +struct delay_t { + template + struct operator_traits { + using result_type = T; }; - schedulers::duration m_delay; - Worker m_worker; - Subscriber m_subscriber; - std::mutex m_mutex{}; - size_t m_current_id{}; - std::priority_queue m_queue{}; - bool m_active{}; -}; - -template -struct delay_impl { - RPP_NO_UNIQUE_ADDRESS TScheduler scheduler; - schedulers::duration delay; - - template TSub> - auto operator()(TSub&& subscriber) const { - auto worker = scheduler.create_worker(subscriber.get_subscription()); - auto subscription = subscriber.get_subscription().make_child(); - - using state_t = queue_based_worker, - std::decay_t>; - auto state = std::make_shared(delay, std::move(worker), - std::forward(subscriber)); - - return create_subscriber_with_state( - std::move(subscription), typename state_t::on_next{}, - typename state_t::on_error{}, typename state_t::on_completed{}, - std::move(state)); + template + using updated_disposable_strategy = + rpp::details::observables::fixed_disposable_strategy_selector<1>; + + rpp::schedulers::duration duration; + RPP_NO_UNIQUE_ADDRESS Scheduler scheduler; + + template + auto lift_with_disposable_strategy(Observer&& observer) const { + using worker_t = rpp::schedulers::utils::get_worker_t; + using container = typename DisposableStrategy::template add< + worker_t::is_none_disposable ? 0 : 1>::disposable_container; + + const auto disposable = disposable_wrapper_impl< + delay_disposable, worker_t, + container>>::make(std::forward(observer), + scheduler.create_worker(), duration); + auto ptr = disposable.lock(); + ptr->observer.set_upstream(disposable.as_weak()); + return rpp::observer< + Type, delay_observer_strategy, worker_t, + container, ClearOnError>>{std::move(ptr)}; } }; -} // namespace rpp::details +} // namespace rpp::operators::details + +namespace rpp::operators { +/** + * @brief Shift the emissions from an Observable forward in time by a particular + amount. + * @details The delay operator modifies its source Observable by pausing for a + particular increment of time (that you specify) before emitting each of the + source Observable’s items. This has the effect of shifting the entire sequence + of items emitted by the Observable forward in time by that specified increment. + * + * @marble delay + { + source observable : +-1-2-3-# + operator "delay:(--)" : +---1-2-3-# + } + * + * @details Actually this operator just schedules emissions via provided + scheduler with provided delay_duration. + * @warning on_error/on_completed invoking also would be delayed as any other + emissions, so, WHOLE observable would be shifter. If you want to obtain + `on_error` immediately, use `observe_on` instead. + * + * @param delay_duration is the delay duration for emitting items. Delay + duration should be able to cast to rpp::schedulers::duration. + * @param scheduler provides the threading model for delay. e.g. With a new + thread scheduler, the observer sees the values in a new thread after a delay + duration to the subscription. + * @warning #include + * + * @par Examples + * @snippet delay.cpp delay + * + * @ingroup utility_operators + * @see https://reactivex.io/documentation/operators/delay.html + */ +template +auto delay(rpp::schedulers::duration delay_duration, Scheduler&& scheduler) { + return details::delay_t, false>{ + delay_duration, std::forward(scheduler)}; +} +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp b/symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp deleted file mode 100644 index 21fa1e2..0000000 --- a/symmetri/gui/rpp/rpp/operators/details/early_unsubscribe.hpp +++ /dev/null @@ -1,42 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus - -#pragma once - -#include -#include - -namespace rpp::details { -struct early_unsubscribe_state { - early_unsubscribe_state( - const composite_subscription& subscription_of_subscriber) - : children_subscriptions(subscription_of_subscriber.make_child()) {} - - // use this subscription as source for any child subscription that should be - // early unsubscribed - composite_subscription children_subscriptions; -}; - -struct early_unsubscribe_on_error { - void operator()(const std::exception_ptr& err, - const constraint::subscriber auto& sub, - const std::shared_ptr& state) const { - state->children_subscriptions.unsubscribe(); - sub.on_error(err); - } -}; - -struct early_unsubscribe_on_completed { - void operator()(const constraint::subscriber auto& sub, - const std::shared_ptr& state) const { - state->children_subscriptions.unsubscribe(); - sub.on_completed(); - } -}; -} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp b/symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp deleted file mode 100644 index aa40a39..0000000 --- a/symmetri/gui/rpp/rpp/operators/details/serialized_subscriber.hpp +++ /dev/null @@ -1,75 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus - -#pragma once - -#include -#include -#include // create_subscriber_with_state -#include -#include - -namespace rpp::details { -template -auto lock(const std::shared_ptr& ptr) { - return std::lock_guard{*ptr}; -} - -template -auto lock(const std::reference_wrapper& ref) { - return std::lock_guard{ref.get()}; -} - -struct forwarding_on_next_under_lock { - template - void operator()(T&& v, const auto& subscriber, const auto& primitive) const { - auto lock_guard = lock(primitive); - subscriber.on_next(std::forward(v)); - } -}; - -struct forwarding_on_error_under_lock { - void operator()(const std::exception_ptr& err, const auto& subscriber, - const auto& primitive) const { - auto lock_guard = lock(primitive); - subscriber.on_error(err); - } -}; - -struct forwarding_on_completed_under_lock { - void operator()(const auto& subscriber, const auto& primitive) const { - auto lock_guard = lock(primitive); - subscriber.on_completed(); - } -}; - -template -auto make_serialized_subscriber( - TSub&& subscriber, - const std::shared_ptr& primitive) { - auto sub = subscriber.get_subscription(); - return create_subscriber_with_state< - utils::extract_subscriber_type_t>>( - std::move(sub), forwarding_on_next_under_lock{}, - forwarding_on_error_under_lock{}, forwarding_on_completed_under_lock{}, - std::forward(subscriber), primitive); -} - -template -auto make_serialized_subscriber( - TSub&& subscriber, - std::reference_wrapper primitive) { - auto sub = subscriber.get_subscription(); - return create_subscriber_with_state< - utils::extract_subscriber_type_t>>( - std::move(sub), forwarding_on_next_under_lock{}, - forwarding_on_error_under_lock{}, forwarding_on_completed_under_lock{}, - std::forward(subscriber), primitive); -} -} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp b/symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp deleted file mode 100644 index 4db71e6..0000000 --- a/symmetri/gui/rpp/rpp/operators/details/subscriber_with_state.hpp +++ /dev/null @@ -1,49 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus - -#pragma once - -#include -#include -#include -#include - -namespace rpp::details { -template ...> OnNext, - std::invocable...> OnError, - std::invocable...> OnCompleted> -auto create_subscriber_with_state(composite_subscription sub, OnNext&& on_next, - OnError&& on_error, - OnCompleted&& on_completed, - States&&... states) { - using TObs = - state_observer, std::decay_t, - std::decay_t, std::decay_t...>; - return make_specific_subscriber( - std::move(sub), std::forward(on_next), - std::forward(on_error), std::forward(on_completed), - std::forward(states)...); -} - -template ...> OnNext, - std::invocable...> OnError, - std::invocable...> OnCompleted> -auto create_subscriber_with_dynamic_state(composite_subscription sub, - OnNext&& on_next, OnError&& on_error, - OnCompleted&& on_completed, - States&&... states) { - using TObs = dynamic_state_observer...>; - return make_specific_subscriber( - std::move(sub), std::forward(on_next), - std::forward(on_error), std::forward(on_completed), - std::forward(states)...); -} -} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp b/symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp index 217ddfc..e8b1f9a 100644 --- a/symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp +++ b/symmetri/gui/rpp/rpp/operators/distinct_until_changed.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -10,56 +10,103 @@ #pragma once -#include -#include // RPP_NO_UNIQUE_ADDRESS -#include // create_subscriber_with_dynamic_state -#include // own forwarding -#include // required due to operator uses lift -#include // constraint::subscriber_of_type -#include // forwarding_on_error/forwarding_on_completed -#include // as_const - -IMPLEMENTATION_FILE(distinct_until_changed_tag); - -namespace rpp::details { -template EqualityFn> -struct distinct_until_changed_state { - RPP_NO_UNIQUE_ADDRESS EqualityFn equality_comparator; +#include +#include +#include +#include + +namespace rpp::operators::details { +template +struct distinct_until_changed_observer_strategy { + using preferred_disposable_strategy = + rpp::details::observers::none_disposable_strategy; + + RPP_NO_UNIQUE_ADDRESS TObserver observer; + RPP_NO_UNIQUE_ADDRESS EqualityFn comparator; mutable std::optional last_value{}; -}; -struct distinct_until_changed_on_next { - template EqualityFn> - void operator()( - auto&& new_value, const constraint::subscriber auto& sub, - const distinct_until_changed_state& state) const { - if (state.last_value.has_value() && - state.equality_comparator(utils::as_const(state.last_value.value()), - utils::as_const(new_value))) + template + void on_next(T&& v) const { + if (last_value.has_value() && + comparator(utils::as_const(last_value.value()), + rpp::utils::as_const(v))) return; - state.last_value.emplace(new_value); - sub.on_next(std::forward(new_value)); + last_value.emplace(std::forward(v)); + observer.on_next(utils::as_const(last_value.value())); } + + void on_error(const std::exception_ptr& err) const { observer.on_error(err); } + + void on_completed() const { observer.on_completed(); } + + void set_upstream(const disposable_wrapper& d) { observer.set_upstream(d); } + + bool is_disposed() const { return observer.is_disposed(); } }; -template EqualityFn> -struct distinct_until_changed_impl { - RPP_NO_UNIQUE_ADDRESS EqualityFn equality_comparator; - - template TSub> - auto operator()(TSub&& subscriber) const { - auto subscription = subscriber.get_subscription(); - // dynamic_state there to make shared_ptr for observer instead of making - // shared_ptr for state - return create_subscriber_with_dynamic_state( - std::move(subscription), distinct_until_changed_on_next{}, - utils::forwarding_on_error{}, utils::forwarding_on_completed{}, - std::forward(subscriber), - distinct_until_changed_state{equality_comparator}); - } +template +struct distinct_until_changed_t + : public operators::details::lift_operator< + distinct_until_changed_t, EqualityFn> { + using operators::details::lift_operator, + EqualityFn>::lift_operator; + + template + struct operator_traits { + static_assert(rpp::constraint::invocable_r_v, + "EqualityFn is not invocable with T and T returning bool"); + + using result_type = T; + + template TObserver> + using observer_strategy = + distinct_until_changed_observer_strategy; + }; + + template + using updated_disposable_strategy = Prev; }; -} // namespace rpp::details +} // namespace rpp::operators::details + +namespace rpp::operators { +/** + * @brief Suppress consecutive duplicates of emissions from original observable + * + * @marble distinct_until_changed + { + source observable : +--1-1-2-2-3-2-1-| + operator "distinct_until_changed" : +--1---2---3-2-1-| + } + * + * @details Actually this operator has `std::optional` with last item and checks + everytime where new emission is same or not. + * + * @par Performance notes: + * - No any heap allocations at all + * - std::optional to keep last value + * - passing last and emitted value to predicate + * + * @param equality_fn optional equality comparator function + * @warning #include + * + * @par Example + * @snippet distinct_until_changed.cpp distinct_until_changed + * @snippet distinct_until_changed.cpp distinct_until_changed_with_comparator + * + * @ingroup filtering_operators + * @see https://reactivex.io/documentation/operators/distinct.html + */ +template + requires( + !utils::is_not_template_callable || + std::same_as< + bool, std::invoke_result_t>) +auto distinct_until_changed(EqualityFn&& equality_fn) { + return details::distinct_until_changed_t>{ + std::forward(equality_fn)}; +} +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/do.hpp b/symmetri/gui/rpp/rpp/operators/do.hpp deleted file mode 100644 index 13e3373..0000000 --- a/symmetri/gui/rpp/rpp/operators/do.hpp +++ /dev/null @@ -1,49 +0,0 @@ -// ReactivePlusPlus library -// -// Copyright Aleksey Loginov 2022 - present. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) -// -// Project home: https://github.com/victimsnino/ReactivePlusPlus -// - -#pragma once - -#include // create_subscriber_with_state -#include // own forwarding -#include // required due to operator uses lift -#include // constraint::subscriber_of_type -#include // utils::as_const - -IMPLEMENTATION_FILE(do_tag); - -namespace rpp::details { -template TObs> -struct do_impl { - TObs observer; - - template TSub> - auto operator()(TSub&& subscriber) const { - auto subscription = subscriber.get_subscription(); - - return create_subscriber_with_state( - std::move(subscription), - [](auto&& value, const TSub& subscriber, const TObs& do_observer) { - do_observer.on_next(utils::as_const(value)); - subscriber.on_next(std::forward(value)); - }, - [](const std::exception_ptr& err, const TSub& subscriber, - const TObs& do_observer) { - do_observer.on_error(err); - subscriber.on_error(err); - }, - [](const TSub& subscriber, const TObs& do_observer) { - do_observer.on_completed(); - subscriber.on_completed(); - }, - std::forward(subscriber), observer); - } -}; -} // namespace rpp::details diff --git a/symmetri/gui/rpp/rpp/operators/filter.hpp b/symmetri/gui/rpp/rpp/operators/filter.hpp index c938d94..6eee21a 100644 --- a/symmetri/gui/rpp/rpp/operators/filter.hpp +++ b/symmetri/gui/rpp/rpp/operators/filter.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -10,37 +10,88 @@ #pragma once -#include // RPP_NO_UNIQUE_ADDRESS -#include // own forwarding -#include // required due to operator uses lift -#include // constraint::subscriber_of_type -#include // utils::as_const -#include +#include +#include +#include +#include -IMPLEMENTATION_FILE(filter_tag); +namespace rpp::operators::details { +template +struct filter_observer_strategy { + using preferred_disposable_strategy = + rpp::details::observers::none_disposable_strategy; -namespace rpp::details { -template Predicate> -struct filter_impl_on_next { - RPP_NO_UNIQUE_ADDRESS Predicate predicate; + RPP_NO_UNIQUE_ADDRESS TObserver observer; + RPP_NO_UNIQUE_ADDRESS Fn fn; - template TSub> - void operator()(TVal&& value, const TSub& subscriber) const { - if (predicate(utils::as_const(value))) - subscriber.on_next(std::forward(value)); + template + void on_next(T&& v) const { + if (fn(rpp::utils::as_const(v))) observer.on_next(std::forward(v)); } + + void on_error(const std::exception_ptr& err) const { observer.on_error(err); } + + void on_completed() const { observer.on_completed(); } + + void set_upstream(const disposable_wrapper& d) { observer.set_upstream(d); } + + bool is_disposed() const { return observer.is_disposed(); } }; -template Predicate> -struct filter_impl { - RPP_NO_UNIQUE_ADDRESS filter_impl_on_next on_next; +template +struct filter_t : lift_operator, Fn> { + using lift_operator, Fn>::lift_operator; - template - auto operator()(TSub&& subscriber) const { - auto subscription = subscriber.get_subscription(); - return create_subscriber_with_state( - std::move(subscription), on_next, utils::forwarding_on_error{}, - utils::forwarding_on_completed{}, std::forward(subscriber)); - } + template + struct operator_traits { + static_assert(std::is_invocable_r_v, + "Fn is not invocable with T returning bool"); + + using result_type = T; + + template TObserver> + using observer_strategy = filter_observer_strategy; + }; + + template + using updated_disposable_strategy = Prev; }; -} // namespace rpp::details +} // namespace rpp::operators::details + +namespace rpp::operators { +/** +* @brief Emit only those items from an Observable that satisfies a provided +predicate +* +* @marble filter +{ + source observable : +--1-2-3-4-| + operator "filter: x=>x%2==0" : +----2---4-| +} +* +* @details Actually this operator just checks if predicate returns true, then +forwards emission +* +* @par Performance notes: +* - No any heap allocations at all +* - No any copies/moves of emissions, just passing by const& to predicate and +then forwarding +* +* @param predicate is predicate used to check emitted items. true -> items +satisfies condition, false -> not +* @warning #include +* +* @par Example: +* @snippet filter.cpp Filter +* +* @ingroup filtering_operators +* @see https://reactivex.io/documentation/operators/filter.html +*/ +template + requires(!utils::is_not_template_callable || + std::same_as< + bool, std::invoke_result_t>) +auto filter(Fn&& predicate) { + return details::filter_t>{std::forward(predicate)}; +} +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/first.hpp b/symmetri/gui/rpp/rpp/operators/first.hpp index 309adb9..0185e3b 100644 --- a/symmetri/gui/rpp/rpp/operators/first.hpp +++ b/symmetri/gui/rpp/rpp/operators/first.hpp @@ -1,7 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. -// TC Wang 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -11,44 +10,81 @@ #pragma once -#include // create_subscriber_with_state -#include // own forwarding -#include // required due to operator uses lift -#include // take_state -#include // constraint::subscriber -#include // not_enough_emissions -#include // forwarding_on_error +#include +#include +#include -IMPLEMENTATION_FILE(first_tag); +namespace rpp::operators::details { +template +struct first_observer_strategy { + using preferred_disposable_strategy = + rpp::details::observers::none_disposable_strategy; -namespace rpp::details { -struct first_state : take_state { - first_state() : take_state{1} {} -}; + RPP_NO_UNIQUE_ADDRESS TObserver observer; -using first_on_next = take_on_next; + template + void on_next(T&& v) const { + observer.on_next(std::forward(v)); + observer.on_completed(); + } -struct first_on_completed { - void operator()(const constraint::subscriber auto& subscriber, - const first_state&) const { - subscriber.on_error(std::make_exception_ptr(utils::not_enough_emissions{ + void on_completed() const { + observer.on_error(std::make_exception_ptr(utils::not_enough_emissions{ "first() operator expects at least one emission from observable before " "completion"})); } + + void on_error(const std::exception_ptr& err) const { observer.on_error(err); } + + void set_upstream(const disposable_wrapper& d) { observer.set_upstream(d); } + + bool is_disposed() const { return observer.is_disposed(); } }; -template -struct first_impl { - public: - template TSub> - auto operator()(TSub&& subscriber) const { - auto subscription = subscriber.get_subscription(); - - // dynamic_state there to make shared_ptr for observer instead of making - // shared_ptr for state - return create_subscriber_with_dynamic_state( - std::move(subscription), first_on_next{}, utils::forwarding_on_error{}, - first_on_completed{}, std::forward(subscriber), first_state{}); - } +struct first_t : lift_operator { + using lift_operator::lift_operator; + + template + struct operator_traits { + using result_type = T; + + template TObserver> + using observer_strategy = first_observer_strategy; + }; + + template + using updated_disposable_strategy = Prev; }; -} // namespace rpp::details +} // namespace rpp::operators::details + +namespace rpp::operators { +/** + * @brief Emit only the first item. + * + * @marble first + { + source observable : +--1--2--3--| + operator "first" : +--1| + } + * + * @details Actually this operator is `take(1)` with exception during + `on_completed` if no any emision happens. So, it just forwards first obtained + emission and emits on_completed immediately + * @throws rpp::utils::not_enough_emissions in case of on_completed obtained + without any emissions + * + * @par Performance notes: + * - No any heap allocations + * - No any copies/moves just forwarding of emission + * + * @warning #include + * + * @par Example: + * @snippet first.cpp first + * @snippet first.cpp first_empty + * + * @ingroup filtering_operators + * @see https://reactivex.io/documentation/operators/first.html + */ +inline auto first() { return details::first_t{}; } +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/flat_map.hpp b/symmetri/gui/rpp/rpp/operators/flat_map.hpp index b10d513..4d1a132 100644 --- a/symmetri/gui/rpp/rpp/operators/flat_map.hpp +++ b/symmetri/gui/rpp/rpp/operators/flat_map.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -10,17 +10,67 @@ #pragma once -#include +#include #include #include -IMPLEMENTATION_FILE(flat_map_tag); +namespace rpp::operators::details { +template +struct flat_map_t { + RPP_NO_UNIQUE_ADDRESS Fn m_fn; -namespace rpp::details { -template Callable> -auto flat_map_impl(auto&& observable, Callable&& callable) { - return std::forward(observable) - .map(std::forward(callable)) - .merge(); + template + requires(std::invocable< + Fn, rpp::utils::extract_observable_type_t> && + rpp::constraint::observable>>) + auto operator()(TObservable&& observable) const& { + return std::forward(observable) | rpp::ops::map(m_fn) | + rpp::ops::merge(); + } + + template + requires(std::invocable< + Fn, rpp::utils::extract_observable_type_t> && + rpp::constraint::observable>>) + auto operator()(TObservable&& observable) && { + return std::forward(observable) | + rpp::ops::map(std::move(m_fn)) | rpp::ops::merge(); + } +}; + +} // namespace rpp::operators::details + +namespace rpp::operators { + +/** + * @brief Transform the items emitted by an Observable into Observables, then + flatten the emissions from those into a single Observable + * + * @marble flat_map + { + source observable : +--1--2--3--| + operator "flat_map: x=>just(x,x+1)" : +--12-23-34-| + } + * + * @details Actually it makes `map(callable)` and then `merge`. + * @details Note that flat_map merges the emissions of these Observables, so + that they may interleave. + * + * @param callable function that returns an observable for each item emitted by + the source observable. + * @warning #include + * + * @ingroup transforming_operators + * @see https://reactivex.io/documentation/operators/flatmap.html + */ +template + requires(!utils::is_not_template_callable || + rpp::constraint::observable< + std::invoke_result_t>) +auto flat_map(Fn&& callable) { + return details::flat_map_t>{std::forward(callable)}; } -} // namespace rpp::details + +} // namespace rpp::operators diff --git a/symmetri/gui/rpp/rpp/operators/fwd.hpp b/symmetri/gui/rpp/rpp/operators/fwd.hpp index 556bd6d..753f582 100644 --- a/symmetri/gui/rpp/rpp/operators/fwd.hpp +++ b/symmetri/gui/rpp/rpp/operators/fwd.hpp @@ -1,6 +1,6 @@ // ReactivePlusPlus library // -// Copyright Aleksey Loginov 2022 - present. +// Copyright Aleksey Loginov 2023 - present. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -10,40 +10,269 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include + +namespace rpp::operators { +auto as_blocking(); + +auto buffer(size_t count); + +auto concat(); + +template + requires(!rpp::constraint::observable && + (!utils::is_not_template_callable || + std::invocable, + utils::extract_observable_type_t...>)) +auto combine_latest(TSelector&& selector, TObservable&& observable, + TObservables&&... observables); + +template +auto combine_latest(TObservable&& observable, TObservables&&... observables); + +template +auto debounce(rpp::schedulers::duration period, Scheduler&& scheduler); + +template +auto delay(rpp::schedulers::duration delay_duration, Scheduler&& scheduler); + +auto distinct(); + +template + requires( + !utils::is_not_template_callable || + std::same_as< + bool, std::invoke_result_t>) +auto distinct_until_changed(EqualityFn&& equality_fn = {}); + +auto first(); + +template + requires(!utils::is_not_template_callable || + std::same_as< + bool, std::invoke_result_t>) +auto filter(Fn&& predicate); + +template LastFn> +auto finally(LastFn&& lastFn); + +template + requires(!utils::is_not_template_callable || + rpp::constraint::observable< + std::invoke_result_t>) +auto flat_map(Fn&& callable); + +template + requires( + (!utils::is_not_template_callable || + !std::same_as>) && + (!utils::is_not_template_callable || + !std::same_as>) && + (!utils::is_not_template_callable || + std::strict_weak_order)) +auto group_by(KeySelector&& key_selector, ValueSelector&& value_selector = {}, + KeyComparator&& comparator = {}); + +auto last(); + +template + requires(!utils::is_not_template_callable || + !std::same_as< + void, std::invoke_result_t>) +auto map(Fn&& callable); + +template +auto multicast(Subject&& subject); + +template