diff --git a/CMakeLists.txt b/CMakeLists.txt index a8086b5..c47acef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ add_subdirectory(Graphics) add_subdirectory(DataAnalysis) add_subdirectory(Objects) add_subdirectory(Widgets) +add_subdirectory(WidgetsNew) add_subdirectory(LibraryViewer) add_subdirectory(RasterRender) add_subdirectory(3DScene) diff --git a/WidgetsNew/CMakeLists.txt b/WidgetsNew/CMakeLists.txt new file mode 100644 index 0000000..43e03af --- /dev/null +++ b/WidgetsNew/CMakeLists.txt @@ -0,0 +1,17 @@ +project(WidgetsNew) + +### ---------------------- 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 Graphics Imgui) + +### -------------------------- Applications -------------------------- ### + +add_executable(Widgets2Example examples/Example.cpp) +target_link_libraries(Widgets2Example ${PROJECT_NAME} ${GLEW_LIB}) +target_include_directories(Widgets2Example 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/WidgetsNew/examples/Example.cpp b/WidgetsNew/examples/Example.cpp new file mode 100644 index 0000000..d07bb7b --- /dev/null +++ b/WidgetsNew/examples/Example.cpp @@ -0,0 +1,53 @@ + +#include "GraphicApplication.hpp" + +#include "RootWidget.hpp" +#include "AnimationTestWidget.hpp" +#include "FloatingWidget.hpp" +#include "SimpleWidgets.hpp" + +using namespace tp; + +class WidgetApplication : public Application { +public: + WidgetApplication() { + mRootWidget.setRootWidget(&mWidget); + + mWidget.addChild(&mWidgetFloating); + + RootWidget::setWidgetArea(mWidgetFloating, { 100, 100, 150, 200 }); + + mWidgetFloating.addChild(&mButton); + } + + void processFrame(EventHandler* eventHandler, halnf deltaTime) override { + const auto rec = RectF({ 0, 0 }, mWindow->getSize()); + mRootWidget.processFrame(eventHandler, rec); + } + + void drawFrame(Canvas* canvas) override { + mRootWidget.drawFrame(*canvas); + + drawDebug(); + } + + bool forceNewFrame() override { + return mRootWidget.needsUpdate(); + } + +private: + LabelWidget mLabel; + ButtonWidget mButton; + + FloatingWidget mWidgetFloating; + + AnimationTestWidget mWidget; + RootWidget mRootWidget; +}; + +int main() { + { + WidgetApplication gui; + gui.run(); + } +} diff --git a/WidgetsNew/examples/Font.ttf b/WidgetsNew/examples/Font.ttf new file mode 100644 index 0000000..8a63054 Binary files /dev/null and b/WidgetsNew/examples/Font.ttf differ diff --git a/WidgetsNew/private/AnimationTestWidget.cpp b/WidgetsNew/private/AnimationTestWidget.cpp new file mode 100644 index 0000000..7c13106 --- /dev/null +++ b/WidgetsNew/private/AnimationTestWidget.cpp @@ -0,0 +1,29 @@ +#include "AnimationTestWidget.hpp" + +using namespace tp; + +void AnimationTestWidget::process(const EventHandler& events) { + if (events.isDown(tp::InputID::MOUSE1)) { + mTestSpring.getStart().setTargetPosition(events.getPointer()); + } + + if (events.isDown(tp::InputID::MOUSE2)) { + mTestSpring.getEnd().setTargetPosition(events.getPointer()); + } + + mTestSpring.updateCurrentRect(); + + if (mTestSpring.checkAnimationShouldEnd()) { + mTestSpring.endAnimation(); + } +} + +void AnimationTestWidget::draw(Canvas& canvas) { + canvas.rect(getRelativeArea(), { 0, 0, 0, 1 }, 10); + canvas.rect(mTestSpring.getCurrentRect(), RGBA(1.f), 10); +} + +bool AnimationTestWidget::needUpdate() const { + if (Widget::needUpdate()) return true; + return !mTestSpring.checkAnimationShouldEnd(); +} diff --git a/WidgetsNew/private/FloatingWidget.cpp b/WidgetsNew/private/FloatingWidget.cpp new file mode 100644 index 0000000..697a915 --- /dev/null +++ b/WidgetsNew/private/FloatingWidget.cpp @@ -0,0 +1,32 @@ +#include "FloatingWidget.hpp" + +using namespace tp; + +void FloatingWidget::process(const EventHandler& events) { + const auto relativePointer = events.getPointer(); + + bool inside = getRelativeArea().isInside(relativePointer); + + if (inside && events.isPressed(InputID::MOUSE1)) { + mPointerStart = relativePointer; + mIsFloating = true; + } + + if (mIsFloating && events.isReleased(InputID::MOUSE1)) { + mIsFloating = false; + } + + mPointerCurrent = relativePointer; + + setActive(mIsFloating || needUpdate()); +} + +void FloatingWidget::updateArea(RectF& area) const { + if (mIsFloating) { + area.pos += mPointerCurrent - mPointerStart; + } +} + +void FloatingWidget::draw(Canvas& canvas) { + canvas.rect(getRelativeArea(), RGBA(0.5f), 10); +} \ No newline at end of file diff --git a/WidgetsNew/private/RootWidget.cpp b/WidgetsNew/private/RootWidget.cpp new file mode 100644 index 0000000..dc386e4 --- /dev/null +++ b/WidgetsNew/private/RootWidget.cpp @@ -0,0 +1,155 @@ +#include "RootWidget.hpp" + +using namespace tp; + +void RootWidget::setRootWidget(Widget* widget) { mRoot = widget; } + +void RootWidget::processFrame(EventHandler* events, const RectF& screenArea) { + mNeedUpdate = false; + + auto currentPaths = &mActivePaths[mFlipFlop]; + auto prevPaths = &mActivePaths[!mFlipFlop]; + + updateActivePaths(currentPaths, events->getPointer()); + + mRoot->setArea(screenArea); + + clearProcessedFlag(*currentPaths); + clearProcessedFlag(*prevPaths); + + processPaths(*currentPaths, *events); + processPaths(*prevPaths, *events); + + mActiveWidgets.erase_if([](auto widget) { return !widget->mIsActive; }); + + mFlipFlop = !mFlipFlop; +} + +bool RootWidget::needsUpdate() const { return mNeedUpdate; } + +void RootWidget::drawFrame(Canvas& canvas) { + // draw from top to bottom + drawRecursion(canvas, mRoot, { 0, 0 }); +} + +void RootWidget::drawRecursion(Canvas& canvas, Widget* active, const Vec2F& pos) { + canvas.setOrigin(pos); + canvas.pushClamp({ pos, active->getArea().size }); + + active->draw(canvas); + for (auto child : active->mChildWidgets) { + drawRecursion(canvas, child.data(), pos + child->getArea().pos); + } + + canvas.setOrigin(pos); + canvas.popClamp(); + + active->drawOverlay(canvas); +} + +void RootWidget::updateActivePaths(Buffer* activePaths, const Vec2F& pointer) { + activePaths->clear(); + + // based on pointer position + Widget* pointedWidget = nullptr; + + { + ActivePath& activePath = activePaths->append(ActivePath{}); + activePath.path.append({ mRoot, {} }); + + auto pointerIter = pointer; + + bool newActive = true; + while (newActive) { + newActive = false; + for (auto child : activePath.path.last().widget->mChildWidgets) { + const auto area = child->mArea.getCurrentRect(); + if (area.isInside(pointerIter)) { + activePath.path.append({ child.data(), {} }); + newActive = true; + pointerIter -= child->getArea().pos; + break; + } + } + } + + pointedWidget = activePath.path.last().widget; + } + + // based on individual widget requirements + { + for (auto widget : mActiveWidgets) { + if (widget.data() == pointedWidget) continue; + + ActivePath& activePath = activePaths->append(ActivePath{}); + + for (Widget* iter = widget.data(); iter; iter = iter->mParent) { + activePath.path.append({ iter, {} }); + } + + activePath.path.reverse(); + } + } + + // update paths global pos + for (auto activePath : *activePaths) { + Vec2F iterPos = { 0, 0 }; + for (auto activeWidget : activePath->path) { + iterPos += activeWidget->widget->getArea().pos; + activeWidget->globalPos = iterPos; + } + } +} + +void RootWidget::setWidgetArea(Widget& widget, const RectF& rect) { + widget.mArea.setTargetRect(rect); + widget.mArea.endAnimation(); +} + +void RootWidget::processPaths(const Buffer& paths, EventHandler& events) { + EventHandler dummyEvents; + bool eventsHandled = false; + + for (auto path : paths) { + + for (alni widgetIdx = (alni) path->path.size() - 1; widgetIdx >= 0; widgetIdx--) { + auto activeWidgetNode = path->path[widgetIdx]; + auto activeWidget = activeWidgetNode.widget; + + activeWidget->mArea.updateCurrentRect(); + + events.setCursorOrigin(activeWidgetNode.globalPos); + + bool useEvents = !eventsHandled || activeWidget->mIsActive; + + if (!activeWidget->mProcessedFlag) { + activeWidget->process(useEvents ? events : dummyEvents); + activeWidget->mProcessedFlag = true; + } + + eventsHandled = !activeWidget->isPassThroughEvents(); + + auto area = activeWidget->mArea.getCurrentRect(); + activeWidget->updateArea(area); + activeWidget->setArea(area); + + mNeedUpdate |= activeWidget->needUpdate(); + + if (!activeWidget->needUpdate()) activeWidget->finishAnimations(); + + if (activeWidget->mIsActive) { + if (mActiveWidgets.find(activeWidget) == -1) { + mActiveWidgets.append(activeWidget); + } + } + } + } +} + +void RootWidget::clearProcessedFlag(const Buffer& paths) { + for (auto path : paths) { + for (auto widget : path->path) { + widget->widget->mProcessedFlag = false; + } + } +} diff --git a/WidgetsNew/private/SimpleWidgets.cpp b/WidgetsNew/private/SimpleWidgets.cpp new file mode 100644 index 0000000..db9b936 --- /dev/null +++ b/WidgetsNew/private/SimpleWidgets.cpp @@ -0,0 +1,56 @@ + +#include "SimpleWidgets.hpp" + +using namespace tp; + +void LabelWidget::setText(const std::string& text) { + mText = text; +} + +const std::string& LabelWidget::getText() const { + return mText; +} + +void LabelWidget::draw(Canvas& canvas) { + canvas.text(mText.c_str(), getRelativeArea(), mSize, Canvas::LC, mPadding, mColor); +} + + +ButtonWidget::ButtonWidget() { + mAction = []() { + printf("Button Pressed!\n"); + }; +} + +void ButtonWidget::setAction(const std::function& action) { + mAction = action; +} + +void ButtonWidget::process(const EventHandler& eventHandler) { + if (getRelativeArea().isInside(eventHandler.getPointer())) { + if (eventHandler.isPressed(InputID::MOUSE1)) { + mAction(); + } + + mColorAnimated.setTargetColor(mColor); + } else { + mColorAnimated.setTargetColor(mColorHovered); + } + + mColorAnimated.updateCurrentRect(); + + setActive(needUpdate()); +} + +void ButtonWidget::draw(Canvas& canvas) { + canvas.rect(getRelativeArea(), mColorAnimated.getCurrentColor(), mRounding); + LabelWidget::draw(canvas); +} + +bool ButtonWidget::needUpdate() const { + return !mColorAnimated.checkAnimationShouldEnd(); +} + +void ButtonWidget::finishAnimations() { + mColorAnimated.endAnimation(); +} \ No newline at end of file diff --git a/WidgetsNew/private/Widget.cpp b/WidgetsNew/private/Widget.cpp new file mode 100644 index 0000000..9262000 --- /dev/null +++ b/WidgetsNew/private/Widget.cpp @@ -0,0 +1,50 @@ +#include "Widget.hpp" + +using namespace tp; + +Widget::Widget() { + mArea.setTargetRect({ 10, 10, 100, 40, }); + mArea.endAnimation(); +} + +void Widget::setArea(const RectF& area) { + mArea.setTargetRect(area); +} + +void Widget::draw(Canvas& canvas) { + canvas.rect(mArea.getCurrentRect(), RGBA(1.f), 10); +} + +RectF Widget::getArea() const { + return mArea.getCurrentRect(); +} + +RectF Widget::getRelativeArea() const { + return { {}, mArea.getCurrentRect().size }; +} + +void Widget::setActive(bool active) { + mIsActive = active; +} + +void Widget::addChild(Widget* widget) { + if (widget->mParent) { + widget->mParent->removeChild(widget); + } + mChildWidgets.pushBack(widget); + widget->mParent = this; +} + +void Widget::removeChild(Widget* widget) { + auto node = mChildWidgets.find(widget); + node->data->mParent = nullptr; + mChildWidgets.removeNode(node); +} + +bool Widget::needUpdate() const { + return !mArea.checkAnimationShouldEnd(); +} + +void Widget::finishAnimations() { + mArea.endAnimation(); +} \ No newline at end of file diff --git a/WidgetsNew/public/AnimationTestWidget.hpp b/WidgetsNew/public/AnimationTestWidget.hpp new file mode 100644 index 0000000..c4176d2 --- /dev/null +++ b/WidgetsNew/public/AnimationTestWidget.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "Widget.hpp" + +namespace tp { + class AnimationTestWidget : public Widget { + + public: + AnimationTestWidget() = default; + ~AnimationTestWidget() override = default; + + void draw(Canvas& canvas) override; + void process(const EventHandler& events) override; + + [[nodiscard]] bool needUpdate() const override; + + private: + SpringRect mTestSpring; + }; +} \ No newline at end of file diff --git a/WidgetsNew/public/FloatingWidget.hpp b/WidgetsNew/public/FloatingWidget.hpp new file mode 100644 index 0000000..47debad --- /dev/null +++ b/WidgetsNew/public/FloatingWidget.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Widget.hpp" + +namespace tp { + class FloatingWidget : public Widget { + public: + FloatingWidget() = default; + + void process(const EventHandler& events) override; + + void updateArea(RectF& area) const override; + + void draw(Canvas& canvas) override; + + private: + bool mIsFloating = false; + + Vec2F mPointerStart; + Vec2F mPointerCurrent; + }; +} \ No newline at end of file diff --git a/WidgetsNew/public/RootWidget.hpp b/WidgetsNew/public/RootWidget.hpp new file mode 100644 index 0000000..3d428fe --- /dev/null +++ b/WidgetsNew/public/RootWidget.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "Widget.hpp" + +namespace tp { + class RootWidget { + + // path from leaf to root of widgets that need to be updated + struct ActivePath { + struct ActiveNode { + Widget* widget = nullptr; + Vec2F globalPos; + }; + + Buffer path; + }; + + + public: + RootWidget() = default; + + void setRootWidget(Widget* widget); + + void processFrame(EventHandler* events, const RectF& screenArea); + void drawFrame(Canvas& canvas); + + [[nodiscard]] bool needsUpdate() const; + + static void setWidgetArea(Widget& widget, const RectF& rect); + + private: + void updateActivePaths(Buffer* activePaths, const Vec2F& pointer); + void drawRecursion(Canvas& canvas, Widget* active, const Vec2F& pos); + + void processPaths(const Buffer& path, EventHandler& events); + void clearProcessedFlag(const Buffer& paths); + + private: + Widget* mRoot = nullptr; + + Buffer mActiveWidgets; + Buffer mActivePaths[2]; // current and prev + bool mFlipFlop = false; + + bool mNeedUpdate = false; + }; +} \ No newline at end of file diff --git a/WidgetsNew/public/SimpleWidgets.hpp b/WidgetsNew/public/SimpleWidgets.hpp new file mode 100644 index 0000000..2886825 --- /dev/null +++ b/WidgetsNew/public/SimpleWidgets.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "Widget.hpp" + +#include + +namespace tp { + class LabelWidget : public Widget { + public: + LabelWidget() = default; + + void setText(const std::string& text); + [[nodiscard]] const std::string& getText() const; + + void draw(Canvas& canvas) override; + + [[nodiscard]] bool isPassThroughEvents() const override { return true; } + + private: + std::string mText = "Text"; + + halnf mPadding = 5; + RGBA mColor = 1.f; + halnf mSize = 17.f; + }; + + class ButtonWidget : public LabelWidget { + public: + ButtonWidget(); + + void setAction(const std::function& action); + + void process(const EventHandler& eventHandler) override; + void draw(Canvas& canvas) override; + + [[nodiscard]] bool isPassThroughEvents() const override { return false; } + + [[nodiscard]] bool needUpdate() const override; + + void finishAnimations() override; + + private: + std::function mAction; + + SpringRect mColorAnimated; + + halnf mRounding = 5; + RGBA mColor = { 1.0f, 0.0f, 0.0f, 1.f }; + RGBA mColorHovered = { 0.0f, 0.0f, 0.0f, 1.f }; + }; +} \ No newline at end of file diff --git a/WidgetsNew/public/SpringAnimations.hpp b/WidgetsNew/public/SpringAnimations.hpp new file mode 100644 index 0000000..ecab5bc --- /dev/null +++ b/WidgetsNew/public/SpringAnimations.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include "Color.hpp" +#include "Rect.hpp" +#include "Timing.hpp" + +namespace tp { + class SpringValue { + public: + SpringValue() = default; + + void updateCurrentPosition() { + const auto deltaTime = halnf(gCurrentTime - mPrevTime); + const auto deltaPos = mTargetPosition - mCurrentPosition; + + auto deltaVelocity = deltaPos * mSpringStiffness; + deltaVelocity -= mVelocity * (mVelocityDamping); + + mVelocity += deltaVelocity; + mCurrentPosition += mVelocity * deltaTime; + mPrevTime = gCurrentTime; + } + + void setTargetPosition(halnf pos) { + mTargetPosition = pos; + if (mVelocity == 0) mPrevTime = gCurrentTime; + } + + [[nodiscard]] halnf getCurrentPos() const { return mCurrentPosition; } + [[nodiscard]] halnf getTargetPos() const { return mTargetPosition; } + [[nodiscard]] halnf getVelocity() const { return mVelocity; } + [[nodiscard]] halnf getDeltaPos() const { return mTargetPosition - mCurrentPosition; } + + void endAnimation() { + mCurrentPosition = mTargetPosition; + mVelocity = 0; + } + + private: + time_ms mPrevTime = 0; + + halnf mTargetPosition = 0; + halnf mCurrentPosition = 0; + + halnf mVelocity = 0; + + halnf mSpringStiffness = 0.0041; + halnf mVelocityDamping = 0.25f; + }; + + class SpringVec { + public: + SpringVec() = default; + + void updateCurrentPosition() { + mPosX.updateCurrentPosition(); + mPosY.updateCurrentPosition(); + } + + void setTargetPosition(Vec2F pos) { + mPosX.setTargetPosition(pos.x); + mPosY.setTargetPosition(pos.y); + } + + [[nodiscard]] Vec2F getCurrentPos() const { + return { mPosX.getCurrentPos(), mPosY.getCurrentPos() }; + } + + [[nodiscard]] Vec2F getTargetPos() const { + return { mPosX.getTargetPos(), mPosY.getTargetPos() }; + } + + [[nodiscard]] Vec2F getVelocity() const { + return { mPosX.getVelocity(), mPosY.getVelocity() }; + } + + [[nodiscard]] bool checkAnimationShouldEnd() const { + halnf velocityEpsilon = 0.0001f; + halnf positionEpsilon = 1; + + const auto vel = abs(mPosY.getVelocity()) > velocityEpsilon || abs(mPosX.getVelocity()) > velocityEpsilon; + const auto pos = abs(mPosY.getDeltaPos()) > positionEpsilon || abs(mPosX.getDeltaPos()) > positionEpsilon; + + return !(pos || vel); + } + + void endAnimation() { + mPosX.endAnimation(); + mPosY.endAnimation(); + } + + private: + SpringValue mPosY; + SpringValue mPosX; + }; + + class SpringRect { + public: + SpringRect() = default; + + [[nodiscard]] RectF getTargetRect() const { + const auto start = mStartPos.getTargetPos(); + const auto end = mEndPos.getTargetPos(); + return { start, end - start }; + } + + [[nodiscard]] RectF getCurrentRect() const { + const auto start = mStartPos.getCurrentPos(); + const auto end = mEndPos.getCurrentPos(); + return { start, end - start }; + } + + [[nodiscard]] RGBA getCurrentColor() const { + const auto start = mStartPos.getCurrentPos(); + const auto end = mEndPos.getCurrentPos(); + return { start.x, start.y, end.x, end.y }; + } + + void setTargetRect(const RectF& rect) { + mStartPos.setTargetPosition(rect.p1()); + mEndPos.setTargetPosition(rect.p3()); + } + + void setTargetColor(const RGBA& color) { + mStartPos.setTargetPosition({ color.r, color.g }); + mEndPos.setTargetPosition({ color.b, color.a }); + } + + SpringVec& getStart() { + return mStartPos; + } + + SpringVec& getEnd() { + return mEndPos; + } + + void updateCurrentRect() { + mStartPos.updateCurrentPosition(); + mEndPos.updateCurrentPosition(); + } + + [[nodiscard]] bool checkAnimationShouldEnd() const { + return mStartPos.checkAnimationShouldEnd() && mEndPos.checkAnimationShouldEnd(); + } + + void endAnimation() { + mStartPos.endAnimation(); + mEndPos.endAnimation(); + } + + private: + SpringVec mStartPos; + SpringVec mEndPos; + }; +} \ No newline at end of file diff --git a/WidgetsNew/public/Widget.hpp b/WidgetsNew/public/Widget.hpp new file mode 100644 index 0000000..557beaa --- /dev/null +++ b/WidgetsNew/public/Widget.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "SpringAnimations.hpp" + +#include "EventHandler.hpp" +#include "Graphics.hpp" + +namespace tp { + class Widget { + friend class RootWidget; + + public: + Widget(); + virtual ~Widget() = default; + + virtual void process(const EventHandler& events) {} + virtual void updateArea(RectF& area) const {} + + virtual void draw(Canvas& canvas); + virtual void drawOverlay(Canvas& canvas) {} + + virtual bool isPassThroughEvents() const { return false; } + + [[nodiscard]] virtual bool needUpdate() const; + + virtual void finishAnimations(); + + public: + [[nodiscard]] RectF getRelativeArea() const; + void setActive(bool active); + + void addChild(Widget* widget); + + private: + [[nodiscard]] RectF getArea() const; + void setArea(const RectF& area); + void removeChild(Widget* widget); + + private: + // relative to the parent + SpringRect mArea; + + // tree structure of widgets + List mChildWidgets; + + // if needed updates even though pointer is not in area + bool mIsActive = false; + + Widget* mParent = nullptr; + + bool mProcessedFlag = false; + }; +} \ No newline at end of file