From 83d51d3e83736afe308daba54380ebce9bd8a19e Mon Sep 17 00:00:00 2001 From: Ilusha <63184036+IlyaShurupov@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:15:22 +0300 Subject: [PATCH] ini --- .clang-format | 4 +- CMakeLists.txt | 1 + Connection/private/bindings/Network.cpp | 93 +++--- .../{private/bindings => public}/Network.hpp | 10 +- Externals/imgui | 2 +- Graphics/private/bindings/Canvas.cpp | 70 +++-- Graphics/private/bindings/DebugGUI.cpp | 5 +- Graphics/private/bindings/Window.cpp | 29 +- Graphics/private/bindings/WindowContext.hpp | 28 +- Graphics/public/Graphics.hpp | 22 +- Graphics/public/Window.hpp | 3 +- LibraryViewer/CMakeLists.txt | 4 +- LibraryViewer/public/GUI.hpp | 143 +++++---- Math/public/Rect.hpp | 6 +- Widgets/CMakeLists.txt | 25 ++ Widgets/examples/Entry.cpp | 46 +++ Widgets/examples/ExampleGUI.hpp | 281 ++++++++++++++++++ Widgets/examples/Font.ttf | Bin 0 -> 42624 bytes {Graphics => Widgets}/private/Animations.cpp | 0 Widgets/private/WidgetConfig.cpp | 9 + {Graphics => Widgets}/public/Animations.hpp | 6 +- Widgets/public/ButtonWidget.hpp | 61 ++++ Widgets/public/LabelWidget.hpp | 34 +++ Widgets/public/ScrollableWidget.hpp | 218 ++++++++++++++ Widgets/public/SplitViewWidget.hpp | 79 +++++ Widgets/public/TextInputWidget.hpp | 72 +++++ Widgets/public/WidgetBase.hpp | 64 ++++ Widgets/public/WidgetConfig.hpp | 70 +++++ Widgets/public/Widgets.hpp | 8 + Widgets/tests/Tests.cpp | 1 + 30 files changed, 1186 insertions(+), 208 deletions(-) rename Connection/{private/bindings => public}/Network.hpp (58%) create mode 100644 Widgets/CMakeLists.txt create mode 100644 Widgets/examples/Entry.cpp create mode 100644 Widgets/examples/ExampleGUI.hpp create mode 100644 Widgets/examples/Font.ttf rename {Graphics => Widgets}/private/Animations.cpp (100%) create mode 100644 Widgets/private/WidgetConfig.cpp rename {Graphics => Widgets}/public/Animations.hpp (92%) create mode 100644 Widgets/public/ButtonWidget.hpp create mode 100644 Widgets/public/LabelWidget.hpp create mode 100644 Widgets/public/ScrollableWidget.hpp create mode 100644 Widgets/public/SplitViewWidget.hpp create mode 100644 Widgets/public/TextInputWidget.hpp create mode 100644 Widgets/public/WidgetBase.hpp create mode 100644 Widgets/public/WidgetConfig.hpp create mode 100644 Widgets/public/Widgets.hpp create mode 100644 Widgets/tests/Tests.cpp diff --git a/.clang-format b/.clang-format index e5e9a2b..7f8e865 100644 --- a/.clang-format +++ b/.clang-format @@ -76,7 +76,7 @@ BreakBeforeTernaryOperators: true BreakConstructorInitializers: AfterColon BreakInheritanceList: BeforeColon BreakStringLiterals: true -ColumnLimit: 400 +ColumnLimit: 120 CommentPragmas: "^ IWYU pragma:" CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false @@ -157,7 +157,7 @@ RemoveBracesLLVM: false RequiresClausePosition: OwnLine SeparateDefinitionBlocks: Leave ShortNamespaceLines: 1 -SortIncludes: CaseSensitive +SortIncludes: Never SortJavaStaticImport: Before SortUsingDeclarations: true SpaceAfterCStyleCast: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 872ad95..c41b819 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,4 +26,5 @@ add_subdirectory(Graphics) add_subdirectory(RayTracer) add_subdirectory(DataAnalysis) add_subdirectory(Objects) +add_subdirectory(Widgets) add_subdirectory(LibraryViewer) diff --git a/Connection/private/bindings/Network.cpp b/Connection/private/bindings/Network.cpp index 8cdcc92..1d5a33f 100644 --- a/Connection/private/bindings/Network.cpp +++ b/Connection/private/bindings/Network.cpp @@ -42,10 +42,7 @@ public: Server::Server() { mContext = new ServerContext(); } -Server::~Server() { - delete mContext; - assert(0); -} +Server::~Server() { delete mContext; } void Server::start(ualni port) { mContext->socket.open(asio::ip::tcp::v4()); @@ -61,29 +58,23 @@ Server::Socket Server::accept() { return clientSocket; } -int1* Server::read(Socket client) { - short messageSize; - if (asio::read(*(ServerContext::Socket*) client, asio::buffer(&messageSize, 2)) == -1) { - std::cerr << "Failed to read from socket" << std::endl; - ((ServerContext::Socket*) client)->close(); - return nullptr; +bool Server::read(Socket client, int1* message, halni messageSize) { + try { + std::size_t bytesRead = asio::read(*(ServerContext::Socket*) client, asio::buffer(message, messageSize)); + return bytesRead == messageSize; + } catch (const std::exception& e) { + std::cerr << "Error during read: " << e.what() << "\n"; + return false; } - auto message = new char[messageSize + 1]; - message[messageSize] = '\0'; - if (asio::read(*(ServerContext::Socket*) client, asio::buffer(message, messageSize)) == -1) { - std::cerr << "Failed to read from socket" << std::endl; - memcpy(message, "Client wanna say something but i cant read", 100); - } - return message; } -void Server::write(Socket client, const char* message) { - auto messageSize = (short) strlen(message); - if (asio::write(*(ServerContext::Socket*) client, asio::buffer(&messageSize, 2)) == -1) { - std::cerr << "Failed to write to socket" << std::endl; - } - if (asio::write(*(ServerContext::Socket*) client, asio::buffer(message, messageSize)) == -1) { - std::cerr << "Failed to write to socket" << std::endl; +bool Server::write(Socket client, const int1* message, halni messageSize) { + try { + std::size_t bytesWritten = asio::write(*(ServerContext::Socket*) client, asio::buffer(message, messageSize)); + return bytesWritten == messageSize; + } catch (const std::exception& e) { + std::cerr << "Error during write: " << e.what() << "\n"; + return false; } } @@ -91,37 +82,35 @@ void Server::write(Socket client, const char* message) { Client::Client() { mContext = new ClientContext(); } -Client::~Client() { - delete mContext; - assert(0); +Client::~Client() { delete mContext; } + +bool Client::connect(const char* IP, ualni PORT) { + try { + asio::ip::tcp::endpoint endpoint(asio::ip::address::from_string(IP), PORT); + mContext->socket.connect(endpoint); + return true; + } catch (const std::exception& e) { + std::cerr << "Error during read: " << e.what() << "\n"; + return false; + } } -void Client::connect(const char* IP, ualni PORT) { - asio::ip::tcp::endpoint endpoint(asio::ip::address::from_string(IP), PORT); - mContext->socket.connect(endpoint); +bool Client::read(int1* buff, halni size) { + try { + std::size_t bytesRead = asio::read(mContext->socket, asio::buffer(buff, size)); + return bytesRead == size; + } catch (const std::exception& e) { + std::cerr << "Error during read: " << e.what() << "\n"; + return false; + } } -char* Client::read() { - short messageSize; - if (asio::read(mContext->socket, asio::buffer(&messageSize, 2)) == -1) { - std::cerr << "Failed to read from socket" << std::endl; - return nullptr; - } - auto message = new char[messageSize + 1]; - message[messageSize] = '\0'; - if (asio::read(mContext->socket, asio::buffer(message, messageSize)) == -1) { - std::cerr << "Failed to read from socket" << std::endl; - memcpy(message, "Cant wanna say something but i cant read", 100); - } - return message; -} - -void Client::write(const char* message) { - auto messageSize = (short) strlen(message); - if (asio::write(mContext->socket, asio::buffer(&messageSize, 2)) == -1) { - std::cerr << "Failed to write to socket" << std::endl; - } - if (asio::write(mContext->socket, asio::buffer(message, messageSize)) == -1) { - std::cerr << "Failed to write to socket" << std::endl; +bool Client::write(const int1* message, halni messageSize) { + try { + std::size_t bytesWritten = asio::write(mContext->socket, asio::buffer(message, messageSize)); + return bytesWritten == messageSize; + } catch (const std::exception& e) { + std::cerr << "Error during write: " << e.what() << "\n"; + return false; } } diff --git a/Connection/private/bindings/Network.hpp b/Connection/public/Network.hpp similarity index 58% rename from Connection/private/bindings/Network.hpp rename to Connection/public/Network.hpp index 9258c49..604e0b2 100644 --- a/Connection/private/bindings/Network.hpp +++ b/Connection/public/Network.hpp @@ -18,8 +18,8 @@ namespace tp { void start(ualni port); Socket accept(); - static int1* read(Socket client); - static void write(Socket client, const int1* message); + bool read(Socket client, int1* message, halni size); + bool write(Socket client, const int1* message, halni size); }; class Client { @@ -29,8 +29,8 @@ namespace tp { Client(); ~Client(); - void connect(const int1* IP, ualni PORT); - int1* read(); - void write(const int1* message); + bool connect(const int1* IP, ualni PORT); + bool read(int1* buff, halni size); + bool write(const int1* message, halni size); }; } \ No newline at end of file diff --git a/Externals/imgui b/Externals/imgui index 4144d7d..1109de3 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 4144d7d772a800f08693b8b13f7c81f1f22a73c4 +Subproject commit 1109de38277fd2d14d4dca4c1cb8d4a2c4ff0f95 diff --git a/Graphics/private/bindings/Canvas.cpp b/Graphics/private/bindings/Canvas.cpp index 0320a2f..9a56e61 100644 --- a/Graphics/private/bindings/Canvas.cpp +++ b/Graphics/private/bindings/Canvas.cpp @@ -24,25 +24,21 @@ Graphics::Canvas::Canvas() { mContext = new Context(); } Graphics::Canvas::~Canvas() { delete mContext; } -void Graphics::Canvas::init() { - mContext->vg = nvgCreateGL3(NVG_ANTIALIAS | NVG_STENCIL_STROKES); - +void Graphics::Canvas::init() { + mContext->vg = nvgCreateGL3(NVG_ANTIALIAS | NVG_STENCIL_STROKES); + if (nvgCreateFont(mContext->vg, "default", "Font.ttf") == -1) { - // TODO + // TODO } } -void Graphics::Canvas::deinit() { - nvgDeleteGL3(mContext->vg); -} +void Graphics::Canvas::deinit() { nvgDeleteGL3(mContext->vg); } -RectF Graphics::Canvas::getAvaliableArea() { - return { (halnf) 0, (halnf) 0, mWidth, mHeight }; -} +RectF Graphics::Canvas::getAvaliableArea() { return { (halnf) 0, (halnf) 0, mWidth, mHeight }; } void Graphics::Canvas::rect(const RectF& rec, const RGBA& col, halnf round) { nvgBeginPath(mContext->vg); - + if (round == 0) { nvgRect(mContext->vg, rec.x, rec.y, rec.z, rec.w); } else { @@ -53,49 +49,69 @@ void Graphics::Canvas::rect(const RectF& rec, const RGBA& col, halnf round) { nvgFill(mContext->vg); } -void Graphics::Canvas::text(const String& string, const RectF& aRec, halnf size, Align align, halnf marging, const RGBA& col) { +void Graphics::Canvas::pushClamp(const RectF& rec) { + RectF intersection = rec; + if (mScissors.size()) { + mScissors.last().calcIntersection(rec, intersection); + } + nvgScissor(mContext->vg, intersection.x, intersection.y, intersection.z, intersection.w); + mScissors.append(rec); +} + +void Graphics::Canvas::popClamp() { + DEBUG_ASSERT(mScissors.size()); + mIsClamping = false; + mScissors.pop(); + if (mScissors.size()) { + const auto& rec = mScissors.last(); + nvgScissor(mContext->vg, rec.x, rec.y, rec.z, rec.w); + } else { + nvgResetScissor(mContext->vg); + } +} + +void Graphics::Canvas::text( + const String& string, const RectF& aRec, halnf size, Align align, halnf marging, const RGBA& col +) { RectF rec = { aRec.x + marging, aRec.y + marging, aRec.z - marging * 2, aRec.w - marging * 2 }; - nvgScissor(mContext->vg, rec.x, rec.y, rec.z, rec.w); + pushClamp(rec); nvgFontSize(mContext->vg, size); nvgFontFace(mContext->vg, "default"); - nvgFillColor(mContext->vg, { col.r, col.g, col.b, col.a } ); + nvgFillColor(mContext->vg, { col.r, col.g, col.b, col.a }); float centerX = rec.x; float centerY = rec.y; int alignNVG = 0; - if (((int1*)&align)[1] == 0x00) { // center x + if (((int1*) &align)[1] == 0x00) { // center x alignNVG |= NVG_ALIGN_CENTER; centerX += rec.z * 0.5f; - } else if (((int1*)&align)[1] == 0x01) { // left x + } else if (((int1*) &align)[1] == 0x01) { // left x alignNVG |= NVG_ALIGN_LEFT; - } else if (((int1*)&align)[1] == 0x02) { // right x + } else if (((int1*) &align)[1] == 0x02) { // right x alignNVG |= NVG_ALIGN_RIGHT; centerX += rec.z; } - if (((int1*)&align)[0] == 0x00) { // center y + if (((int1*) &align)[0] == 0x00) { // center y alignNVG |= NVG_ALIGN_MIDDLE; centerY += rec.w * 0.5f; - } else if (((int1*)&align)[0] == 0x01) { // top y + } else if (((int1*) &align)[0] == 0x01) { // top y alignNVG |= NVG_ALIGN_TOP; centerY += rec.w; - } else if (((int1*)&align)[0] == 0x02) { // bottom y + } else if (((int1*) &align)[0] == 0x02) { // bottom y alignNVG |= NVG_ALIGN_BOTTOM; } nvgTextAlign(mContext->vg, alignNVG); nvgText(mContext->vg, centerX, centerY, string.read(), nullptr); - nvgResetScissor(mContext->vg); + + popClamp(); } -void Graphics::Canvas::proc() { - nvgBeginFrame(mContext->vg, mWidth, mHeight, 1.0); -} +void Graphics::Canvas::proc() { nvgBeginFrame(mContext->vg, mWidth, mHeight, 1.0); } -void Graphics::Canvas::draw() { - nvgEndFrame(mContext->vg); -} +void Graphics::Canvas::draw() { nvgEndFrame(mContext->vg); } diff --git a/Graphics/private/bindings/DebugGUI.cpp b/Graphics/private/bindings/DebugGUI.cpp index 2435a84..376d79b 100644 --- a/Graphics/private/bindings/DebugGUI.cpp +++ b/Graphics/private/bindings/DebugGUI.cpp @@ -4,9 +4,9 @@ // -------- Debug UI -------- // #include -#include #include #include +#include namespace tp { class Graphics::GUI::Context { @@ -33,6 +33,9 @@ void Graphics::GUI::init(Window* window) { // ImGui_ImplGlfw_GetBackendData(); ImGui_ImplOpenGL3_Init("#version 330"); + + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontFromFileTTF("Font.ttf", 20.f); } void Graphics::GUI::deinit() { diff --git a/Graphics/private/bindings/Window.cpp b/Graphics/private/bindings/Window.cpp index 2f23ca7..5fbd168 100644 --- a/Graphics/private/bindings/Window.cpp +++ b/Graphics/private/bindings/Window.cpp @@ -47,8 +47,7 @@ void Graphics::GL::deinit() { glDeleteVertexArrays(1, &mContext->vao); } -void Graphics::GL::proc() { -} +void Graphics::GL::proc() {} void Graphics::GL::draw() { glClearColor(0.f, 0.f, 0.f, 0.f); @@ -102,7 +101,7 @@ Window::Window(int width, int height, const char* title) { } mContext->inputManager.init(mContext->window); - + mGraphics.init(this); mEvents.mContext = mContext; @@ -149,7 +148,7 @@ bool Window::shouldClose() const { return glfwWindowShouldClose(mContext->window void Window::processEvents() { mContext->inputManager.isEvent = false; glfwPollEvents(); - + int w, h; glfwGetWindowSize(mContext->window, &w, &h); @@ -176,22 +175,14 @@ auto Window::getContext() -> Context* { return mContext; } Graphics::Canvas& Window::getCanvas() { return mGraphics.mCanvas; } const Window::Events& Window::getEvents() { return mEvents; } -const Vec2F& Window::Events::getPos() const { - return mContext->inputManager.getPos(); -} +const Vec2F& Window::Events::getPos() const { return mContext->inputManager.getPos(); } -bool Window::Events::isPressed() const { - return mContext->inputManager.isPressed(); -} +bool Window::Events::isPressed() const { return mContext->inputManager.isPressed(); } -bool Window::Events::isDown() const { - return mContext->inputManager.isDown(); -} +bool Window::Events::isReleased() const { return mContext->inputManager.isReleased(); } -halnf Window::Events::getScrollY() const { - return mContext->inputManager.getScrollY(); -} +bool Window::Events::isDown() const { return mContext->inputManager.isDown(); } -bool Window::Events::isEvent() const { - return mContext->inputManager.isEvents(); -} +halnf Window::Events::getScrollY() const { return mContext->inputManager.getScrollY(); } + +bool Window::Events::isEvent() const { return mContext->inputManager.isEvents(); } diff --git a/Graphics/private/bindings/WindowContext.hpp b/Graphics/private/bindings/WindowContext.hpp index 77623ae..9aa780d 100644 --- a/Graphics/private/bindings/WindowContext.hpp +++ b/Graphics/private/bindings/WindowContext.hpp @@ -11,8 +11,8 @@ namespace tp { class InputManager { public: InputManager() = default; - - void init(GLFWwindow* aWindow) { + + void init(GLFWwindow* aWindow) { window = aWindow; // Set up key and mouse button callbacks @@ -24,25 +24,17 @@ namespace tp { const Vec2F& getPos() const { return mousePos; } - bool isKeyPressed(int key) const { - return keyStates[key] == GLFW_PRESS; - } + bool isKeyPressed(int key) const { return keyStates[key] == GLFW_PRESS; } - bool isKeyDown(int key) const { - return keyStates[key] == GLFW_REPEAT || keyStates[key] == GLFW_PRESS; - } + bool isKeyDown(int key) const { return keyStates[key] == GLFW_REPEAT || keyStates[key] == GLFW_PRESS; } - bool isPressed() const { - return mouseButtonStates[GLFW_MOUSE_BUTTON_LEFT] == GLFW_PRESS; - } + bool isPressed() const { return mouseButtonStates[GLFW_MOUSE_BUTTON_LEFT] == GLFW_PRESS; } - bool isDown() const { - return mouseButtonStates[GLFW_MOUSE_BUTTON_LEFT] == GLFW_REPEAT || mouseButtonStates[GLFW_MOUSE_BUTTON_LEFT] == GLFW_PRESS; - } + bool isReleased() const { return mouseButtonStates[GLFW_MOUSE_BUTTON_LEFT] == GLFW_RELEASE; } - bool isEvents() const { - return isEvent; - } + bool isDown() const { return mouseButtonStates[GLFW_MOUSE_BUTTON_LEFT] == GLFW_REPEAT || mouseButtonStates[GLFW_MOUSE_BUTTON_LEFT] == GLFW_PRESS; } + + bool isEvents() const { return isEvent; } double getScrollY() const { return scrollY; } @@ -105,7 +97,7 @@ namespace tp { bool isEvent = false; double scrollX = 0.0; - double scrollY = 0.0; + double scrollY = 0.0; GLFWwindow* window; Vec2F mousePos; Vec2F mousePosPrev; diff --git a/Graphics/public/Graphics.hpp b/Graphics/public/Graphics.hpp index 18d3f16..ff7bea3 100644 --- a/Graphics/public/Graphics.hpp +++ b/Graphics/public/Graphics.hpp @@ -69,19 +69,21 @@ namespace tp { void draw(); public: - enum Align : int2 { - CC = 0x0000, - CT = 0x0001, - CB = 0x0002, - LC = 0x0100, - LT = 0x0101, - LB = 0x0102, - RC = 0x0200, - RT = 0x0201, + enum Align : int2 { + CC = 0x0000, + CT = 0x0001, + CB = 0x0002, + LC = 0x0100, + LT = 0x0101, + LB = 0x0102, + RC = 0x0200, + RT = 0x0201, RB = 0x0202, }; RectF getAvaliableArea(); + void pushClamp(const RectF& rec); + void popClamp(); void rect(const RectF& rec, const RGBA& col, halnf round = 0); void text(const String&, const RectF&, halnf size, Align, halnf marging, const RGBA&); @@ -90,6 +92,8 @@ namespace tp { private: halnf mWidth = 600; halnf mHeight = 600; + Buffer mScissors; + bool mIsClamping = false; }; private: diff --git a/Graphics/public/Window.hpp b/Graphics/public/Window.hpp index 477c95f..eddd121 100644 --- a/Graphics/public/Window.hpp +++ b/Graphics/public/Window.hpp @@ -21,9 +21,10 @@ namespace tp { struct Events { Vec2F mPointer; - + const Vec2F& getPos() const; bool isPressed() const; + bool isReleased() const; bool isDown() const; halnf getScrollY() const; bool isEvent() const; diff --git a/LibraryViewer/CMakeLists.txt b/LibraryViewer/CMakeLists.txt index b5c64c4..1bb13fe 100644 --- a/LibraryViewer/CMakeLists.txt +++ b/LibraryViewer/CMakeLists.txt @@ -2,7 +2,7 @@ project(LibraryViewer) ### ---------------------- Externals --------------------- ### set(BINDINGS_INCLUDE ../Externals/glfw/include ${GLEW_INCLUDE_DIR} ${PORTAUDIO_INCLUDE_DIR}) -set(BINDINGS_LIBS glfw Imgui Nanovg ${GLEW_LIB} ${ALSA_LIBRARY} ${PORTAUDIO_LIB}) +set(BINDINGS_LIBS glfw ${GLEW_LIB} ${ALSA_LIBRARY} ${PORTAUDIO_LIB}) ### ---------------------- Static Library --------------------- ### file(GLOB SOURCES "./private/*.cpp" "./private/*/*.cpp") @@ -11,7 +11,7 @@ file(GLOB HEADERS "./public/*.hpp" "./public/*/*.hpp" "./applications/*.hpp") add_library(${PROJECT_NAME} STATIC ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} PUBLIC ./public/ ${BINDINGS_INCLUDE} ./ext/) -target_link_libraries(${PROJECT_NAME} PUBLIC Graphics Connection) +target_link_libraries(${PROJECT_NAME} PUBLIC Graphics Connection Widgets) target_link_libraries(${PROJECT_NAME} PUBLIC ${BINDINGS_LIBS}) file(COPY "library" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/") diff --git a/LibraryViewer/public/GUI.hpp b/LibraryViewer/public/GUI.hpp index 7da83da..ee825dc 100644 --- a/LibraryViewer/public/GUI.hpp +++ b/LibraryViewer/public/GUI.hpp @@ -2,17 +2,12 @@ #include "Library.hpp" #include "Player.hpp" -#include "Rect.hpp" -#include "Animations.hpp" -#include "imgui.h" +#include "Widgets.hpp" -template -concept DrawableConcept = true; - -template requires(DrawableConcept) +template class ResizerWidget { public: - ResizerWidget() {} + ResizerWidget() = default; void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& area) { if (!areaParent.isOverlap(area)) { @@ -23,7 +18,7 @@ public: hover = area.isInside(events.getPos()); if (events.isPressed() && hover) { - resizing = true; + resizing = true; } else if (!events.isDown()) { resizing = false; } @@ -43,11 +38,11 @@ public: } } - void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { + void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { if (!areaParent.isOverlap(area)) return; - + if (hover || resizing) { - canvas.rect(area, { 0.23f, 0.23f, 0.23f, 1.f}, 0.f); + canvas.rect(area, { 0.23f, 0.23f, 0.23f, 1.f }, 0.f); } else { canvas.rect(area, { 0.04f, 0.04f, 0.04f, 1.f }, 0.f); } @@ -61,7 +56,8 @@ public: tp::halnf value = 0; }; -template requires(DrawableConcept) +template + requires(DrawableConcept) class TrackInfoWidget { struct SortType { tp::String text; @@ -70,31 +66,36 @@ class TrackInfoWidget { }; public: - TrackInfoWidget(const Track* track = nullptr) : mTrack(track) { - items.append( { "Date Added" } ); - items.append( { "Date Last Played" } ); + TrackInfoWidget(const Track* track = nullptr) : + mTrack(track) { + items.append({ "Date Added" }); + items.append({ "Date Last Played" }); } void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& area) { if (!mTrack) return; if (!areaParent.isOverlap(area)) return; - } - void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { + void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { if (!areaParent.isOverlap(area)) return; - - ImGui::SetNextWindowPos( { area.x, area.y } ); - ImGui::SetNextWindowSize( { area.z, area.w } ); + + ImGui::SetNextWindowPos({ area.x, area.y }); + ImGui::SetNextWindowSize({ area.z, area.w }); RenderUI(); - canvas.rect(area, { 0.13f, 0.13f, 0.13f, 1.f}, 4.f); + canvas.rect(area, { 0.13f, 0.13f, 0.13f, 1.f }, 4.f); if (!mTrack) return; } void RenderUI() { - ImGui::Begin("InfoWindow", 0, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoResize); + ImGui::Begin( + "InfoWindow", + 0, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoResize + ); if (mTrack) { @@ -146,20 +147,20 @@ public: if (ImGui::Checkbox("Loved only", &filterLoved)) { isSongFilterChanged |= true; } - + if (ImGui::SliderInt("Existing only", &filterExisting, 0, 3)) { isSongFilterChanged |= true; } isSongFilterChanged |= songFilter.Draw("Song Filter"); - + sortFilter.Draw("Sorting Type"); for (int i = 0; i < items.size(); ++i) { if (!sortFilter.PassFilter(items[i].text.read())) continue; ImGui::PushID(i); - + if (ImGui::Button("Inc")) { items[i].inc = true; } @@ -189,8 +190,8 @@ public: int filterExisting = 0; // all existing no-existing }; - -template requires(DrawableConcept) +template + requires(DrawableConcept) class ScrollBarWidget { public: ScrollBarWidget() = default; @@ -214,17 +215,17 @@ public: scrollInertia = -scrollInertia + offset; } } - + if (tp::abs(scrollInertia) > 0.1f) { auto offset = scrollInertia * mScrollFactor; mPositionFraction += offset; mPositionFraction = tp::clamp(mPositionFraction, 0.f, 1.f - mSizeFraction); scrollInertia *= 0.f; - return; + return; } if (events.isPressed() && area.isInside(events.getPos())) { - mIsScrolling = true; + mIsScrolling = true; } else if (!events.isDown()) { mIsScrolling = false; } @@ -238,10 +239,10 @@ public: mPositionFraction = tp::clamp(mPositionFraction, 0.f, 1.f - mSizeFraction); } - void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { + void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { if (mSizeFraction > 1.f) return; if (!areaParent.isOverlap(area)) return; - + auto minSize = 10.f; tp::RGBA col = tp::RGBA{ 0.25f, 0.25f, 0.25f, 1.f }; @@ -250,10 +251,10 @@ public: minSize = 20.f; } - canvas.rect(area, { 0.15f, 0.15f, 0.15f, 1.f }, 4.f); + canvas.rect(area, { 0.15f, 0.15f, 0.15f, 1.f }, 4.f); auto sliderSize = tp::clamp(area.w * mSizeFraction, minSize, area.w); auto diffSize = sliderSize - area.w * mSizeFraction; - canvas.rect({ area.x, area.y + (area.w - diffSize) * mPositionFraction, area.z, sliderSize }, col, 4.f); + canvas.rect({ area.x, area.y + (area.w - diffSize) * mPositionFraction, area.z, sliderSize }, col, 4.f); } public: @@ -264,11 +265,12 @@ public: tp::halnf mPositionFraction = 0.f; }; - -template requires(DrawableConcept) +template + requires(DrawableConcept) class TrackWidget { public: - TrackWidget(const Track* track = nullptr) : mTrack(track) { + TrackWidget(const Track* track = nullptr) : + mTrack(track) { col.mColor.setAnimTime(0); col.mColor.setNoTransition({ 0.15f, 0.15f, 0.15f, 0.f }); }; @@ -277,31 +279,33 @@ public: if (!mTrack) return; if (!areaParent.isOverlap(area)) return; if (area.isInside(events.getPos())) { - col.set( { 0.15f, 0.15f, 0.15f, 1.f } ); + col.set({ 0.15f, 0.15f, 0.15f, 1.f }); insideDebug = true; } else { - col.set( { 0.15f, 0.15f, 0.15f, 0.f } ); + col.set({ 0.15f, 0.15f, 0.15f, 0.f }); insideDebug = false; } } - void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { + void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { if (!mTrack) return; if (!areaParent.isOverlap(area)) return; - canvas.rect(area, col.get(), 4.f); + canvas.rect(area, col.get(), 4.f); const tp::RectF imageArea = { area.x + marging, area.y + marging, area.w - marging * 2, area.w - marging * 2 }; - canvas.rect(imageArea, { 0.25f, 0.25f, 0.25f, 1.f }, 4.f); + canvas.rect(imageArea, { 0.25f, 0.25f, 0.25f, 1.f }, 4.f); - const tp::RectF textArea = { area.x + area.w + marging, area.y + marging, area.z - area.w - marging * 2, area.w - marging * 2 }; - // canvas.rect(textArea, { 0.25f, 0.25f, 0.25f, 1.f }, 4.f); + const tp::RectF textArea = { + area.x + area.w + marging, area.y + marging, area.z - area.w - marging * 2, area.w - marging * 2 + }; + // canvas.rect(textArea, { 0.25f, 0.25f, 0.25f, 1.f }, 4.f); const tp::RectF textAreaName = { textArea.x, textArea.y, textArea.z, textArea.w * 0.5f }; const tp::RectF textAreaAuthor = { textArea.x, textArea.y + textArea.w * 0.5f, textArea.z, textArea.w * 0.5f }; - - canvas.text(mTrack->mName.read(), textAreaName, 15.f, Canvas::LC, 4.f, { 0.9f, 0.9f, 0.9f, 1.f} ); - canvas.text(mTrack->mArtist.read(), textAreaAuthor, 12.f, Canvas::LC, 4.f, { 0.8f, 0.8f, 0.8f, 1.f} ); + + canvas.text(mTrack->mName.read(), textAreaName, 15.f, Canvas::LC, 4.f, { 0.9f, 0.9f, 0.9f, 1.f }); + canvas.text(mTrack->mArtist.read(), textAreaAuthor, 12.f, Canvas::LC, 4.f, { 0.8f, 0.8f, 0.8f, 1.f }); } public: @@ -311,10 +315,13 @@ public: const Track* mTrack; }; -template requires(DrawableConcept) +template + requires(DrawableConcept) class LibraryWidget { public: - LibraryWidget(Library* lib, Player* player) : mLibrary(lib), mPlayer(player) { + LibraryWidget(Library* lib, Player* player) : + mLibrary(lib), + mPlayer(player) { for (auto track : mLibrary->mTraks) { mTraks.append(TrackWidget(&track.data())); } @@ -333,14 +340,16 @@ public: areaCP = { aArea.x, aArea.y + aArea.w - controllPanelSize, aArea.z, controllPanelSize }; area = { aArea.x, aArea.y, mResizeWidget.value, aArea.w - controllPanelSize }; - areaInfo = { mResizeWidget.value + resizeSize, aArea.y, aArea.z - mResizeWidget.value - resizeSize, aArea.w - controllPanelSize } ; + areaInfo = { + mResizeWidget.value + resizeSize, aArea.y, aArea.z - mResizeWidget.value - resizeSize, aArea.w - controllPanelSize + }; areaResize = { mResizeWidget.value, aArea.y, resizeSize, areaInfo.w }; mScroll.mSizeFraction = (area.w - 10) / (mTraksFiltered.size() * trackSize); mScroll.mScrollFactor = 1.f / mTraksFiltered.size(); - mScroll.proc(events, area, { area.x + area.z - scrollSize - 3, area.y + 10, scrollSize - 3.f, area.w - 20 } ); - mResizeWidget.proc(events, area, areaResize ); + mScroll.proc(events, area, { area.x + area.z - scrollSize - 3, area.y + 10, scrollSize - 3.f, area.w - 20 }); + mResizeWidget.proc(events, area, areaResize); if (area.isInside(events.getPos())) { @@ -348,7 +357,8 @@ public: auto idx = 0; for (auto track : mTraksFiltered) { - auto trackArea = tp::RectF{ area.x + 10, area.y + 10 + idx * trackSize - offset, area.z - 20 - scrollSize, trackSize - 5 }; + auto trackArea = + tp::RectF{ area.x + 10, area.y + 10 + idx * trackSize - offset, area.z - 20 - scrollSize, trackSize - 5 }; track->proc(events, area, trackArea); @@ -364,32 +374,33 @@ public: debugPos = events.getPos(); } - void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& aArea) { + void draw(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& aArea) { + + canvas.rect(aArea, { 0.1f, 0.1f, 0.1f, 1.f }); - canvas.rect(aArea, { 0.1f, 0.1f, 0.1f, 1.f }); - tp::halnf offset = mScroll.mPositionFraction * mTraksFiltered.size() * trackSize; auto idx = 0; for (auto track : mTraksFiltered) { - auto const trackArea = tp::RectF{ area.x + 10, area.y + 10 + idx * trackSize - offset, area.z - 20 - scrollSize, trackSize - 5 }; + auto const trackArea = + tp::RectF{ area.x + 10, area.y + 10 + idx * trackSize - offset, area.z - 20 - scrollSize, trackSize - 5 }; if (track->mTrack == mCurrentTrack.mTrack) canvas.rect(trackArea, { 0.2f, 0.2f, 0.2f, 0.5f }, 5); - track->draw(canvas, area, trackArea); + track->draw(canvas, area, trackArea); idx++; } - mScroll.draw(canvas, area, { area.x + area.z - scrollSize - 3, area.y + 10, scrollSize - 3.f, area.w - 20 } ); - - // canvas.rect( areaInfo, { 0.04f, 0.04f, 0.04f, 1.f }); - mCurrentTrackInfo.draw(canvas, aArea, areaInfo ); + mScroll.draw(canvas, area, { area.x + area.z - scrollSize - 3, area.y + 10, scrollSize - 3.f, area.w - 20 }); - mResizeWidget.draw(canvas, aArea, areaResize ); + // canvas.rect( areaInfo, { 0.04f, 0.04f, 0.04f, 1.f }); + mCurrentTrackInfo.draw(canvas, aArea, areaInfo); + + mResizeWidget.draw(canvas, aArea, areaResize); drawCP(canvas, aArea, areaCP); } void drawCP(Canvas& canvas, const tp::RectF& areaParent, const tp::RectF& area) { - canvas.rect( area, { 0.04f, 0.04f, 0.04f, 1.f }); + canvas.rect(area, { 0.04f, 0.04f, 0.04f, 1.f }); mCurrentTrack.draw(canvas, area, area); } @@ -416,7 +427,7 @@ public: break; } - mTraksFiltered.append( &track.data() ); + mTraksFiltered.append(&track.data()); } mCurrentTrackInfo.isSongFilterChanged = false; diff --git a/Math/public/Rect.hpp b/Math/public/Rect.hpp index c1e5c72..8d90b91 100644 --- a/Math/public/Rect.hpp +++ b/Math/public/Rect.hpp @@ -76,13 +76,13 @@ namespace tp { return ret; } - void calcIntersection(Rect& in, Rect& out) const { + void calcIntersection(const Rect& in, Rect& out) const { if (isOverlap(in)) { out = *this; for (char i = 0; i < 2; i++) { - clamp(out.pos[i], in.pos[i], in.pos[i] + in.size[i]); + out.pos[i] = tp::clamp(out.pos[i], in.pos[i], in.pos[i] + in.size[i]); Type p2 = pos[i] + size[i]; - clamp(p2, in.pos[i], in.pos[i] + in.size[i]); + p2 = tp::clamp(p2, in.pos[i], in.pos[i] + in.size[i]); out.size[i] = p2 - out.pos[i]; } } else { diff --git a/Widgets/CMakeLists.txt b/Widgets/CMakeLists.txt new file mode 100644 index 0000000..af81eb1 --- /dev/null +++ b/Widgets/CMakeLists.txt @@ -0,0 +1,25 @@ +project(Widgets) + +### ---------------------- Static Library --------------------- ### +file(GLOB SOURCES "./private/*.cpp" "./private/*/*.cpp") +file(GLOB HEADERS "./public/*.hpp") + +add_library(${PROJECT_NAME} STATIC ${SOURCES} ${HEADERS}) +target_include_directories(${PROJECT_NAME} PUBLIC ./public/) +target_link_libraries(${PROJECT_NAME} PUBLIC Math Strings) + +### -------------------------- Tests -------------------------- ### +enable_testing() +file(GLOB TEST_SOURCES "./tests/*.cpp") +add_executable(${PROJECT_NAME}Tests ${TEST_SOURCES}) +target_link_libraries(${PROJECT_NAME}Tests ${PROJECT_NAME} Utils) +add_test(NAME ${PROJECT_NAME}Tests COMMAND ${PROJECT_NAME}Tests) + +### -------------------------- Applications -------------------------- ### + +add_executable(ExampleGui examples/Entry.cpp) + +target_link_libraries(ExampleGui ${PROJECT_NAME} Graphics Imgui Nanovg glfw ${GLEW_LIB}) +target_include_directories(ExampleGui PUBLIC ../Externals/glfw/include ${GLEW_INCLUDE_DIR}) + +file(COPY "examples/Font.ttf" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/") \ No newline at end of file diff --git a/Widgets/examples/Entry.cpp b/Widgets/examples/Entry.cpp new file mode 100644 index 0000000..820945c --- /dev/null +++ b/Widgets/examples/Entry.cpp @@ -0,0 +1,46 @@ + +#include "ExampleGUI.hpp" +#include "Window.hpp" + +using namespace tp; + +void runApp() { + auto window = tp::Window::createWindow(800, 600, "Window 1"); + + tp::ComplexWidget gui; + + if (window) { + while (!window->shouldClose()) { + window->processEvents(); + + auto area = window->getCanvas().getAvaliableArea(); + + gui.proc(window->getEvents(), {}, { area.x, area.y, area.z, area.w }); + gui.draw(window->getCanvas()); + + tp::sleep(20); + + window->draw(); + } + } + + tp::Window::destroyWindow(window); +} + +int main() { + + tp::ModuleManifest* deps[] = { &tp::gModuleWidgets, nullptr }; + tp::ModuleManifest binModule("Chat", nullptr, nullptr, deps); + + if (!binModule.initialize()) { + return 1; + } + + { + tp::GlobalGUIConfig config; + tp::gGlobalGUIConfig = &config; + runApp(); + } + + binModule.deinitialize(); +} diff --git a/Widgets/examples/ExampleGUI.hpp b/Widgets/examples/ExampleGUI.hpp new file mode 100644 index 0000000..defc63e --- /dev/null +++ b/Widgets/examples/ExampleGUI.hpp @@ -0,0 +1,281 @@ +#pragma once + +#include "Widgets.hpp" + +namespace tp { + + template + class UserWidget : public Widget { + public: + UserWidget() { + this->createConfig("User"); + this->addColor("Base", "Base"); + this->addValue("Size", "FontSize"); + this->addValue("Padding", "Padding"); + this->addColor("ColUser", "Front"); + this->addColor("Accent", "Accent"); + this->addValue("Rounding", "Rounding"); + } + + void proc(const Events& events, const RectF& areaParent, const RectF& aArea) override { + this->mArea = aArea; + this->mArea.w = 30; + this->mVisible = areaParent.isOverlap(aArea); + if (!this->mVisible) return; + mIsHover = aArea.isInside(events.getPos()); + } + + void draw(Canvas& canvas) const override { + if (!this->mVisible) return; + + if (mIsHover) canvas.rect(this->mArea, this->getColor("Accent"), this->getValue("Rounding")); + else canvas.rect(this->mArea, this->getColor("Base"), this->getValue("Rounding")); + + const auto padding = this->getValue("Padding"); + const auto size = this->getValue("Size"); + const auto colUser = this->getColor("ColUser"); + + canvas.text(mUser.read(), this->mArea, size, Canvas::CC, padding, colUser); + } + + public: + String mUser = "UserName"; + bool mIsHover = false; + }; + + template + class MessageWidget : public Widget { + public: + MessageWidget() { + this->createConfig("Message"); + this->addColor("Base", "Base"); + this->addValue("Size", "FontSize"); + this->addValue("SizeUser", "FontSizeDim"); + this->addValue("Padding", "Padding"); + this->addColor("ColUser", "Front"); + this->addColor("Col", "FrontDim"); + this->addValue("Rounding", "Rounding"); + } + + void proc(const Events& events, const RectF& areaParent, const RectF& aArea) override { + this->mArea = aArea; + this->mArea.w = 50; + this->mVisible = areaParent.isOverlap(aArea); + if (!this->mVisible) return; + mIsHover = aArea.isInside(events.getPos()); + } + + void draw(Canvas& canvas) const override { + if (!this->mVisible) return; + + if (mIsHover) canvas.rect(this->mArea, this->getColor("Base"), this->getValue("Rounding")); + + const auto padding = this->getValue("Padding"); + const auto size = this->getValue("Size"); + const auto sizeUser = this->getValue("SizeUser"); + const auto col = this->getColor("Col"); + const auto colUser = this->getColor("ColUser"); + + auto userName = this->mArea; + userName.w = 25; + + auto content = this->mArea; + content.y = userName.y + userName.w; + content.w = this->mArea.w - userName.w; + + canvas.text(mContent.read(), content, size, Canvas::LC, padding, col); + canvas.text(mUser.read(), userName, sizeUser, Canvas::LC, padding, colUser); + } + + public: + String mContent = "Message Content"; + String mUser = "UserName"; + bool mIsHover = false; + }; + + template + class LoginWidget : public Widget { + public: + explicit LoginWidget() { + this->createConfig("Login"); + this->addColor("Back", "Background"); + mPass.mId = "pass"; + mUser.mId = "user"; + mButton.mLabel.mLabel = "Login"; + } + + void proc(const Events& events, const RectF& areaParent, const RectF& aArea) override { + this->mArea = aArea; + mLogged = false; + + const auto xval = aArea.z / 2 - 100; + mUser.proc(events, aArea, { xval, 10, 200, 30 }); + mPass.proc(events, aArea, { xval, 50, 200, 30 }); + mButton.proc(events, aArea, { xval, 90, 200, 30 }); + + if (mButton.mIsReleased) { + mButton.mIsReleased = false; + mLogged = true; + } + } + + void draw(Canvas& canvas) const override { + canvas.rect(this->mArea, this->getColor("Back")); + mButton.draw(canvas); + mUser.draw(canvas); + mPass.draw(canvas); + } + + public: + TextInputWidget mUser; + TextInputWidget mPass; + ButtonWidget mButton; + bool mLogged = false; + }; + + template + class ActiveChatWidget : public Widget { + public: + ActiveChatWidget() { + this->createConfig("ActiveChat"); + this->addColor("Back", "Background"); + this->addValue("Padding", "Padding"); + mSend.mLabel.mLabel = "Send"; + mMessage.mId = "Message"; + } + + void proc(const Events& events, const RectF& areaParent, const RectF& aArea) override { + this->mArea = aArea; + + auto history = this->mArea; + history.w -= 50; + + auto input = this->mArea; + input.y = history.w + 10; + input.w = 40 - this->getValue("Padding"); + input.x += this->getValue("Padding"); + input.z -= this->getValue("Padding"); + + auto inputMessage = input; + inputMessage.z -= 100; + + auto inputSend = input; + inputSend.x = inputMessage.x + inputMessage.z + this->getValue("Padding"); + inputSend.z = 100 - this->getValue("Padding") * 2; + + mSend.proc(events, this->mArea, inputSend); + mMessage.proc(events, this->mArea, inputMessage); + + if (mSend.mIsReleased) { + mSend.mIsReleased = false; + } + + mHistoryView.proc(events, this->mArea, history); + } + + void draw(Canvas& canvas) const override { + canvas.rect(this->mArea, this->getColor("Back")); + mHistoryView.draw(canvas); + mMessage.draw(canvas); + mSend.draw(canvas); + } + + public: + Buffer> mMessages; + ScrollableWindow mHistoryView; + TextInputWidget mMessage; + ButtonWidget mSend; + }; + + template + class ChattingWidget : public Widget { + public: + explicit ChattingWidget() { + this->createConfig("Chatting"); + this->addColor("Back", "Background"); + this->addValue("Padding", "Padding"); + + // todo : fetch code + mUsers.append(UserWidget()); + mUsers.append(UserWidget()); + mUsers.append(UserWidget()); + + mUsers[0].mArea = { 0, 0, 100, 100 }; + mUsers[1].mArea = { 0, 0, 100, 100 }; + mUsers[2].mArea = { 0, 0, 100, 100 }; + + for (auto message : mUsers) { + mSideView.mContents.append(&message.data()); + } + + mActive.mMessages.append(MessageWidget()); + mActive.mMessages.append(MessageWidget()); + mActive.mMessages.append(MessageWidget()); + + mActive.mMessages[0].mArea = { 0, 0, 100, 100 }; + mActive.mMessages[1].mArea = { 0, 0, 100, 100 }; + mActive.mMessages[2].mArea = { 0, 0, 100, 100 }; + + for (auto message : mActive.mMessages) { + mActive.mHistoryView.mContents.append(&message.data()); + } + } + + void proc(const Events& events, const RectF& areaParent, const RectF& aArea) override { + this->mArea = aArea; + + mSplitView.proc(events, aArea, aArea); + + const auto padding = this->getValue("Padding"); + mSideView.proc(events, this->mArea, mSplitView.getSecond()); + + mActive.proc(events, aArea, mSplitView.getFirst()); + } + + void draw(Canvas& canvas) const override { + canvas.rect(this->mArea, this->getColor("Back")); + mSplitView.draw(canvas); + mSideView.draw(canvas); + mActive.draw(canvas); + } + + public: + Buffer> mUsers; + ScrollableWindow mSideView; + ActiveChatWidget mActive; + SplitView mSplitView; + }; + + template + class ComplexWidget : public Widget { + public: + ComplexWidget() { + this->createConfig("chat"); + this->addColor("Back", "Background"); + } + + void proc(const Events& events, const RectF& areaParent, const RectF& aArea) override { + this->mArea = aArea; + if (mLogged) { + mChatting.proc(events, aArea, aArea); + } else { + mLogin.proc(events, aArea, aArea); + mLogged = mLogin.mLogged; + if (mLogged) { + mChatting.proc(events, aArea, aArea); + } + } + } + + void draw(Canvas& canvas) const override { + canvas.rect(this->mArea, this->getColor("Back")); + if (mLogged) mChatting.draw(canvas); + else mLogin.draw(canvas); + } + + private: + bool mLogged = false; + LoginWidget mLogin; + ChattingWidget mChatting; + }; +} \ No newline at end of file diff --git a/Widgets/examples/Font.ttf b/Widgets/examples/Font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8a630544c99ec9956153ebd9509abeac421c6d75 GIT binary patch literal 42624 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 { public: - AnimRect() { + AnimRect() { setAnimTime(450); setNoTransition({ 0, 0, 0, 0 }); } diff --git a/Widgets/public/ButtonWidget.hpp b/Widgets/public/ButtonWidget.hpp new file mode 100644 index 0000000..cc5ac72 --- /dev/null +++ b/Widgets/public/ButtonWidget.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "LabelWidget.hpp" + +namespace tp { + + template + class ButtonWidget : public Widget { + public: + ButtonWidget() { + this->createConfig("Button"); + this->addColor("Pressed", "Action"); + this->addColor("Hovered", "Interaction"); + this->addColor("Default", "Accent"); + this->addValue("Rounding", "Rounding"); + } + + void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& aArea) override { + this->mArea = aArea; + mIsHover = false; + + if (!areaParent.isOverlap(aArea)) { + mIsReleased = false; + mIsPressed = false; + return; + } + + mIsHover = aArea.isInside(events.getPos()); + + if (events.isPressed() && mIsHover) { + mIsPressed = true; + } + + if (mIsPressed && mIsHover && events.isReleased()) { + mIsReleased = true; + mIsPressed = false; + } + + if (!mIsHover) mIsPressed = false; + + mLabel.proc(events, aArea, aArea); + } + + void draw(Canvas& canvas) const override { + if (mIsPressed) { + canvas.rect(this->mArea, this->getColor("Pressed"), this->getValue("Rounding")); + } else if (mIsHover) { + canvas.rect(this->mArea, this->getColor("Hovered"), this->getValue("Rounding")); + } else { + canvas.rect(this->mArea, this->getColor("Default"), this->getValue("Rounding")); + } + mLabel.draw(canvas); + } + + public: + LabelWidget mLabel; + bool mIsHover = false; + bool mIsPressed = false; + bool mIsReleased = false; + }; +} \ No newline at end of file diff --git a/Widgets/public/LabelWidget.hpp b/Widgets/public/LabelWidget.hpp new file mode 100644 index 0000000..ad946ee --- /dev/null +++ b/Widgets/public/LabelWidget.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "WidgetBase.hpp" + +namespace tp { + template + class LabelWidget : public Widget { + public: + LabelWidget() { + this->createConfig("Label"); + this->addValue("Size", "FontSize"); + this->addValue("Padding", "Padding"); + this->addColor("Default", "Front"); + } + + void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& aArea) override { + this->mArea = aArea; + } + + void draw(Canvas& canvas) const override { + canvas.text( + mLabel.read(), + this->mArea, + this->getValue("Size"), + Canvas::CC, + this->getValue("Padding"), + this->getColor("Default") + ); + } + + public: + String mLabel = "Label"; + }; +} \ No newline at end of file diff --git a/Widgets/public/ScrollableWidget.hpp b/Widgets/public/ScrollableWidget.hpp new file mode 100644 index 0000000..8fcd89b --- /dev/null +++ b/Widgets/public/ScrollableWidget.hpp @@ -0,0 +1,218 @@ +#pragma once + +#include "WidgetBase.hpp" + +namespace tp { + + template + class ScrollBarWidget : public Widget { + public: + ScrollBarWidget() { + this->createConfig("ScrollBar"); + this->addColor("Default", "Base"); + this->addColor("Handle", "Accent"); + this->addColor("Hovered", "Interaction"); + this->addColor("Scrolling", "Action"); + this->addValue("Padding", "Padding"); + this->addValue("HandleSize", 20.f); + this->addValue("MinSize", 20.f); + this->addValue("Rounding", "Rounding"); + } + + // takes whole area + void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& aArea) override { + this->mArea = aArea; + + auto area = getHandle(); + mHovered = getHandleHandle().isInside(events.getPos()); + + if (mSizeFraction > 1.f) { + mPositionFraction = 0; + return; + } + + if (!areaParent.isOverlap(area)) { + mIsScrolling = false; + return; + } + + if (events.getScrollY() != 0 && areaParent.isInside(events.getPos())) { + auto offset = events.getScrollY() < 0 ? 1.0f : -1.0f; + if (scrollInertia * offset > 0) { + scrollInertia += offset; + } else { + scrollInertia = -scrollInertia + offset; + } + } + + if (tp::abs(scrollInertia) > 0.1f) { + auto offset = scrollInertia * mScrollFactor; + mPositionFraction += offset; + mPositionFraction = tp::clamp(mPositionFraction, 0.f, 1.f - mSizeFraction); + scrollInertia *= 0.f; + return; + } + + if (events.isPressed() && area.isInside(events.getPos())) { + mIsScrolling = true; + } else if (!events.isDown()) { + mIsScrolling = false; + } + + if (mIsScrolling) { + tp::halnf pos = events.getPos().y; + pos = (pos - area.y - mSizeFraction * area.w / 2.f) / area.w; + mPositionFraction = tp::clamp(pos, 0.f, 1.f - mSizeFraction); + } + + mPositionFraction = tp::clamp(mPositionFraction, 0.f, 1.f - mSizeFraction); + } + + void draw(Canvas& canvas) const override { + auto area = getHandle(); + + if (mSizeFraction > 1.f) return; + // if (!areaParent.isOverlap(getHandle())) return; + + tp::RGBA col = this->getColor("Handle"); + + if (mIsScrolling) { + col = this->getColor("Scrolling"); + } else if (mHovered) { + col = this->getColor("Hovered"); + } + + canvas.rect(area, this->getColor("Default"), this->getValue("Rounding")); + + canvas.rect(getHandleHandle(), col, this->getValue("Rounding")); + } + + RectF getHandleHandle() const { + auto minSize = this->getValue("MinSize"); + if (mIsScrolling) { + minSize = this->getValue("MinSize") * 2; + } else if (mHovered) { + minSize = this->getValue("MinSize") * 2; + } + auto area = getHandle(); + auto sliderSize = tp::clamp(area.w * mSizeFraction, minSize, area.w); + auto diffSize = sliderSize - area.w * mSizeFraction; + return { area.x, area.y + (area.w - diffSize) * mPositionFraction, area.z, sliderSize }; + } + + RectF getViewport() const { + if (mSizeFraction > 1.f) { + return this->mArea; + } + return { this->mArea.x, this->mArea.y, this->mArea.z - this->getValue("HandleSize"), this->mArea.w }; + } + + RectF getHandle() const { + return { this->mArea.x + this->mArea.z - this->getValue("HandleSize") + this->getValue("Padding"), + this->mArea.y + this->getValue("Padding"), + this->getValue("HandleSize") - this->getValue("Padding") * 2, + this->mArea.w - this->getValue("Padding") * 2 }; + } + + public: + halnf mScrollFactor = 0.f; + halnf scrollInertia = 0.f; + bool mIsScrolling = false; + halnf mSizeFraction = 1.f; + halnf mPositionFraction = 0.f; + bool mHovered = false; + }; + + template + class ScrollableWindow : public Widget { + public: + ScrollableWindow() { + this->createConfig("ScrollableWindow"); + this->addColor("Default", "Base"); + this->addValue("Padding", "Padding"); + } + + // takes whole area + void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& aArea) override { + this->mArea = aArea; + this->mVisible = areaParent.isOverlap(aArea); + + if (!this->mVisible) return; + + updateContents(); + updateContentSize(); + + const auto padding = this->getValue("Padding"); + + mScroller.mSizeFraction = this->mArea.w / mContentSize; + mScroller.proc(events, this->mArea, this->mArea); + + if (mScroller.mSizeFraction > 1.f) { + setOffset(0); + } else { + setOffset((-mScroller.mPositionFraction) * mContentSize); + } + + for (auto widget : mContents) { + widget->proc( + events, + this->mArea, + { this->mArea.x + padding, widget->mArea.y, mScroller.getViewport().z - padding * 2, widget->mArea.w } + ); + } + } + + void draw(Canvas& canvas) const override { + if (!this->mVisible) return; + mScroller.draw(canvas); + + canvas.pushClamp(this->mArea); + for (auto widget : mContents) { + widget->draw(canvas); + } + canvas.popClamp(); + } + + private: + void updateContents() { + if (mContents.size()) { + const auto padding = this->getValue("Padding"); + const halnf offset = mContents.first()->mArea.y + padding; + + halnf start = 0; + for (auto widget : mContents) { + widget->mArea.y = start; + start += widget->mArea.w + padding; + } + + for (auto widget : mContents) { + widget->mArea.y += offset; + } + } + } + + void updateContentSize() { + mContentSize = 0; + if (mContents.size()) { + const auto padding = this->getValue("Padding"); + mContentSize = mContents.last()->mArea.y - mContents.first()->mArea.y; + mContentSize += mContents.last()->mArea.w; + mContentSize += (mContents.size()) * padding; + } + } + + void setOffset(const halnf offset) { + if (!mContents.size()) return; + const auto padding = this->getValue("Padding"); + auto newOffset = offset - mContents.first()->mArea.y + padding; + for (auto widget : mContents) { + widget->mArea.y += newOffset; + } + } + + public: + halnf mContentSize = 0; + Buffer*> mContents; + ScrollBarWidget mScroller; + }; +} \ No newline at end of file diff --git a/Widgets/public/SplitViewWidget.hpp b/Widgets/public/SplitViewWidget.hpp new file mode 100644 index 0000000..4c75dd1 --- /dev/null +++ b/Widgets/public/SplitViewWidget.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "WidgetBase.hpp" + +namespace tp { + template + class SplitView : public Widget { + public: + SplitView() { + this->createConfig("SplitView"); + this->addColor("Handle", "Accent"); + this->addColor("Hovered", "Interaction"); + this->addColor("Resizing", "Action"); + this->addValue("Min", 200.f); + this->addValue("HandleSize", 7.f); + } + + void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& aArea) override { + this->mArea = aArea; + + if (!areaParent.isOverlap(aArea)) { + mResizeInProcess = false; + return; + } + + mIsHover = getHandle().isInside(events.getPos()); + + if (events.isPressed() && mIsHover) { + mResizeInProcess = true; + } else if (!events.isDown()) { + mResizeInProcess = false; + } + + if (mResizeInProcess) { + halnf pos = events.getPos().x; + auto diff = pos - (this->mArea.x + mFactor * this->mArea.z); + mFactor += diff / this->mArea.z; + } + + mFactor = tp::clamp(mFactor, this->getValue("Min") / this->mArea.z, 1 - this->getValue("Min") / this->mArea.z); + + if (this->getValue("Min") * 2.f > this->mArea.z) { + mFactor = 0.5f; + } + } + + // takes whole area + void draw(Canvas& canvas) const override { + if (mResizeInProcess) canvas.rect(getHandle(), this->getColor("Resizing")); + else if (mIsHover) canvas.rect(getHandle(), this->getColor("Hovered")); + else canvas.rect(getHandle(), this->getColor("Handle")); + } + + RectF getFirst() const { + return { + this->mArea.x, this->mArea.y, mFactor * this->mArea.z - this->getValue("HandleSize") / 2.f, this->mArea.w + }; + } + + RectF getSecond() const { + return { this->mArea.x + mFactor * this->mArea.z + this->getValue("HandleSize") / 2.f, + this->mArea.y, + (1.f - mFactor) * this->mArea.z - this->getValue("HandleSize") / 2.f, + this->mArea.w }; + } + + RectF getHandle() const { + return { this->mArea.x + mFactor * this->mArea.z - this->getValue("HandleSize") / 2.f, + this->mArea.y, + this->getValue("HandleSize"), + this->mArea.w }; + } + + public: + halnf mFactor = 0.7f; + bool mResizeInProcess = false; + bool mIsHover = false; + }; +} \ No newline at end of file diff --git a/Widgets/public/TextInputWidget.hpp b/Widgets/public/TextInputWidget.hpp new file mode 100644 index 0000000..7739827 --- /dev/null +++ b/Widgets/public/TextInputWidget.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "WidgetBase.hpp" + +#include "imgui.h" +#include "imgui_internal.h" + +namespace tp { + template + class TextInputWidget : public Widget { + public: + TextInputWidget() { + this->createConfig("TextInput"); + this->addColor("Accent", "Accent"); + this->addColor("Base", "Base"); + this->addValue("Rounding", "Rounding"); + this->addColor("Hovered", "Interaction"); + this->addValue("Padding", "Padding"); + } + + void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& aArea) override { + this->mArea = aArea; + nChanged = false; + + const auto col = this->getColor("Accent"); + const auto colSel = this->getColor("Hovered"); + + ImGui::GetStyle().Colors[ImGuiCol_FrameBg] = { col.r, col.g, col.b, col.a }; + ImGui::GetStyle().Colors[ImGuiCol_TextSelectedBg] = { colSel.r, colSel.g, colSel.b, colSel.a }; + + ImGui::SetNextWindowPos({ this->mArea.x, this->mArea.y }); + ImGui::SetNextWindowSize({ this->mArea.z, this->mArea.w }); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 0, 0 }); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, this->getValue("Rounding") * 1.5f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { this->getValue("Padding"), this->getValue("Padding") }); + + // ImGui::PushID((int) alni(this)); + ImGui::Begin( + mId.read(), + 0, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoResize + ); + + if (mMultiline) { + if (ImGui::InputTextMultiline("input", mBuff, mMaxBufferSize, { this->mArea.z, this->mArea.w })) { + mValue = mBuff; + nChanged = true; + } + } else { + if (ImGui::InputTextEx("input", mId.read(), mBuff, mMaxBufferSize, { this->mArea.z, this->mArea.w }, 0)) { + mValue = mBuff; + nChanged = true; + } + } + + ImGui::End(); + ImGui::PopStyleVar(3); + } + + void draw(Canvas& canvas) const override { + // canvas.rect(this->mArea, this->getColor("Base")); + } + + enum { mMaxBufferSize = 512 }; + char mBuff[mMaxBufferSize] = ""; + bool nChanged = false; + String mValue; + String mId = "id"; + bool mMultiline = false; + }; +} \ No newline at end of file diff --git a/Widgets/public/WidgetBase.hpp b/Widgets/public/WidgetBase.hpp new file mode 100644 index 0000000..e47cb6e --- /dev/null +++ b/Widgets/public/WidgetBase.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "WidgetConfig.hpp" + +namespace tp { + + template + class Widget { + public: + Widget() = default; + + [[nodiscard]] const RGBA& getColor(const String& name) const { + const auto& node = cfg->values.get(name); + if (node.type == WidgetConfig::Node::REF) { + return globalCfg->configs.get(node.refGroup).values.get(node.refName).color; + } + return cfg->values.get(name).color; + } + + [[nodiscard]] halnf getValue(const String& name) const { + const auto& node = cfg->values.get(name); + if (node.type == WidgetConfig::Node::REF) { + return globalCfg->configs.get(node.refGroup).values.get(node.refName).value; + } + return cfg->values.get(name).value; + } + + void addColor(const String& name, const RGBA& val) { cfg->values.put(name, WidgetConfig::Node(val)); } + void addValue(const String& name, halnf val) { cfg->values.put(name, WidgetConfig::Node(val)); } + + void addColor(const String& name, const String& presetName) { + cfg->values.put(name, WidgetConfig::Node(presetName)); + } + void addValue(const String& name, const String& presetName) { + cfg->values.put(name, WidgetConfig::Node(presetName)); + } + + void createConfig(const String& name) { + globalCfg->configs.put(name, {}); + cfg = &globalCfg->configs.get(name); + } + + virtual void proc(const Events& events, const tp::RectF& areaParent, const tp::RectF& aArea) { + mVisible = areaParent.isOverlap(aArea); + if (!mVisible) { + return; + } + + this->mArea = aArea; + } + + virtual void draw(Canvas& canvas) const { + if (!mVisible) { + return; + } + } + + public: + GlobalGUIConfig* globalCfg = gGlobalGUIConfig; + WidgetConfig* cfg = nullptr; + RectF mArea; + bool mVisible = false; + }; +} \ No newline at end of file diff --git a/Widgets/public/WidgetConfig.hpp b/Widgets/public/WidgetConfig.hpp new file mode 100644 index 0000000..7e12f2e --- /dev/null +++ b/Widgets/public/WidgetConfig.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "Animations.hpp" +#include "Map.hpp" +#include "Rect.hpp" +#include "Strings.hpp" + +namespace tp { + + extern ModuleManifest gModuleWidgets; + + struct WidgetConfig { + struct Node { + enum Type { NONE, VAL, COL, REF }; + + halnf value = 0.f; + RGBA color = {}; + + Type type = NONE; + + String refGroup; + String refName; + + Node() = default; + + explicit Node(halnf val) { + type = VAL; + value = val; + } + + explicit Node(const RGBA& val) { + type = COL; + color = val; + } + + explicit Node(const String& val) { + refGroup = "Presets"; + refName = val; + type = REF; + } + }; + + Map values; + }; + + struct GlobalGUIConfig { + GlobalGUIConfig() { + configs.put("Presets", {}); + auto& presets = configs.get("Presets"); + + presets.values.put("FontSize", WidgetConfig::Node(15.f)); + presets.values.put("FontSizeDim", WidgetConfig::Node(12.f)); + presets.values.put("Rounding", WidgetConfig::Node(5.f)); + presets.values.put("Padding", WidgetConfig::Node(5.f)); + presets.values.put("HandleSize", WidgetConfig::Node(5.f)); + + presets.values.put("Background", WidgetConfig::Node({ 0.03f, 0.03f, 0.03f, 1.f })); + presets.values.put("Base", WidgetConfig::Node({ 0.07f, 0.07f, 0.07f, 1.f })); + presets.values.put("Accent", WidgetConfig::Node({ 0.13f, 0.13f, 0.13f, 1.f })); + presets.values.put("Interaction", WidgetConfig::Node({ 0.33f, 0.33f, 0.3f, 1.f })); + presets.values.put("Action", WidgetConfig::Node({ 0.44f, 0.44f, 0.4f, 1.f })); + presets.values.put("Front", WidgetConfig::Node({ 1.f, 1.f, 1.f, 1.f })); + presets.values.put("FrontDim", WidgetConfig::Node({ 0.7f, 0.7f, 0.7f, 1.f })); + } + + Map configs; + }; + + extern GlobalGUIConfig* gGlobalGUIConfig; +} \ No newline at end of file diff --git a/Widgets/public/Widgets.hpp b/Widgets/public/Widgets.hpp new file mode 100644 index 0000000..1661f73 --- /dev/null +++ b/Widgets/public/Widgets.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "ButtonWidget.hpp" +#include "LabelWidget.hpp" +#include "ScrollableWidget.hpp" +#include "SplitViewWidget.hpp" +#include "TextInputWidget.hpp" +#include "Animations.hpp" \ No newline at end of file diff --git a/Widgets/tests/Tests.cpp b/Widgets/tests/Tests.cpp new file mode 100644 index 0000000..c272dab --- /dev/null +++ b/Widgets/tests/Tests.cpp @@ -0,0 +1 @@ +int main() {} \ No newline at end of file