WidgetsNew Initial

This commit is contained in:
IlyaShurupov 2024-07-17 09:44:18 +03:00 committed by Ilya Shurupov
parent 3f49ee11ae
commit 279cf58dff
15 changed files with 741 additions and 0 deletions

17
WidgetsNew/CMakeLists.txt Normal file
View file

@ -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}/")

View file

@ -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();
}
}

Binary file not shown.

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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<ActivePath>* 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<ActivePath>& 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<ActivePath>& paths) {
for (auto path : paths) {
for (auto widget : path->path) {
widget->widget->mProcessedFlag = false;
}
}
}

View file

@ -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<void()>& 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();
}

View file

@ -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();
}

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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<ActiveNode> 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<ActivePath>* activePaths, const Vec2F& pointer);
void drawRecursion(Canvas& canvas, Widget* active, const Vec2F& pos);
void processPaths(const Buffer<ActivePath>& path, EventHandler& events);
void clearProcessedFlag(const Buffer<ActivePath>& paths);
private:
Widget* mRoot = nullptr;
Buffer<Widget*> mActiveWidgets;
Buffer<ActivePath> mActivePaths[2]; // current and prev
bool mFlipFlop = false;
bool mNeedUpdate = false;
};
}

View file

@ -0,0 +1,51 @@
#pragma once
#include "Widget.hpp"
#include <functional>
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<void()>& 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<void()> 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 };
};
}

View file

@ -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;
};
}

View file

@ -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<Widget*> mChildWidgets;
// if needed updates even though pointer is not in area
bool mIsActive = false;
Widget* mParent = nullptr;
bool mProcessedFlag = false;
};
}