Add widget state support.

This commit is contained in:
jmsgrogan 2023-01-18 13:29:31 +00:00
parent 19091a0e80
commit 8536908eab
19 changed files with 385 additions and 46 deletions

View file

@ -25,6 +25,11 @@ public:
std::string toString() const; std::string toString() const;
void setAlpha(float alpha)
{
mAlpha = static_cast<double>(alpha);
}
bool operator==(const Color& rhs) const bool operator==(const Color& rhs) const
{ {
return (mR == rhs.mR) return (mR == rhs.mR)

View file

@ -50,4 +50,14 @@ namespace ntk {
{ {
return mBottomLeft; return mBottomLeft;
} }
double Rectangle::getRadius() const
{
return mRadius;
}
void Rectangle::setRadius(double radius)
{
mRadius = radius;
}
} }

View file

@ -19,12 +19,17 @@ public:
Type getType() const override; Type getType() const override;
double getRadius() const;
void setRadius(double radius);
void sample(SparseGrid<bool>* grid) const override; void sample(SparseGrid<bool>* grid) const override;
private: private:
Point mBottomLeft; Point mBottomLeft;
double mWidth{0}; double mWidth{0};
double mHeight{0}; double mHeight{0};
double mRadius{ 0.0 };
}; };
} }

View file

@ -33,6 +33,11 @@ double RectangleNode::getHeight() const
return mHeight; return mHeight;
} }
double RectangleNode::getRadius() const
{
return mRadius;
}
void RectangleNode::setWidth(double width) void RectangleNode::setWidth(double width)
{ {
if (mWidth != width) if (mWidth != width)
@ -51,20 +56,42 @@ void RectangleNode::setHeight(double height)
} }
} }
void RectangleNode::setRadius(double radius)
{
if (mRadius != radius)
{
mGeometryIsDirty = true;
mRadius = radius;
}
}
void RectangleNode::createOrUpdateGeometry(SceneInfo* sceneInfo) void RectangleNode::createOrUpdateGeometry(SceneInfo* sceneInfo)
{ {
if (sceneInfo->mSupportsGeometryPrimitives) if (!mBackgroundItem)
{ {
auto rect = std::make_unique<ntk::Rectangle>(Point{ 0, 0 }, 1, 1); if (sceneInfo->mSupportsGeometryPrimitives)
mBackgroundItem = std::make_unique<SceneModel>(std::move(rect)); {
auto rect = std::make_unique<ntk::Rectangle>(Point{ 0, 0 }, 1, 1);
rect->setRadius(mRadius);
mBackgroundItem = std::make_unique<SceneModel>(std::move(rect));
}
else
{
auto mesh = MeshPrimitives::buildRectangleAsTriMesh();
mBackgroundItem = std::make_unique<SceneModel>(std::move(mesh));
}
mBackgroundItem->setName(mName + "_Model");
} }
else else
{ {
auto mesh = MeshPrimitives::buildRectangleAsTriMesh(); if (sceneInfo->mSupportsGeometryPrimitives)
mBackgroundItem = std::make_unique<SceneModel>(std::move(mesh)); {
auto rect = std::make_unique<ntk::Rectangle>(Point{ 0, 0 }, 1, 1);
rect->setRadius(mRadius);
mBackgroundItem->updateGeometry(std::move(rect));
}
} }
mBackgroundItem->setName(mName + "_Model");
} }
void RectangleNode::updateTransform() void RectangleNode::updateTransform()

View file

@ -14,15 +14,19 @@ public:
double getWidth() const; double getWidth() const;
double getHeight() const; double getHeight() const;
double getRadius() const;
void setWidth(double width); void setWidth(double width);
void setHeight(double height); void setHeight(double height);
void setRadius(double radius);
private: private:
void createOrUpdateGeometry(SceneInfo* sceneInfo) override; void createOrUpdateGeometry(SceneInfo* sceneInfo) override;
void updateTransform() override; void updateTransform() override;
double mWidth{1}; double mWidth{1};
double mHeight{1}; double mHeight{1};
double mRadius{ 0.0 };
}; };
using RectangleNodePtr = std::unique_ptr<RectangleNode>; using RectangleNodePtr = std::unique_ptr<RectangleNode>;

View file

@ -29,6 +29,15 @@ void MaterialNode::setHasStrokeColor(bool hasStroke)
} }
} }
void MaterialNode::setHasFillColor(bool hasFill)
{
if (mHasFillColor != hasFill)
{
mHasFillColor = hasFill;
mMaterialIsDirty = true;
}
}
void MaterialNode::setFillColor(const Color& color) void MaterialNode::setFillColor(const Color& color)
{ {
mHasFillColor = true; mHasFillColor = true;

View file

@ -13,6 +13,7 @@ public:
const Color& getStrokeColor() const; const Color& getStrokeColor() const;
void setHasStrokeColor(bool hasStroke); void setHasStrokeColor(bool hasStroke);
void setHasFillColor(bool hasFill);
void setFillColor(const Color& color); void setFillColor(const Color& color);
void setStrokeColor(const Color& color); void setStrokeColor(const Color& color);

View file

@ -42,22 +42,79 @@ void Button::setLabel(const std::string& text)
} }
} }
void Button::setState(ButtonData::State state)
{
if (mStyle.mState != state)
{
mStyle.mState = state;
mStateDirty = true;
}
}
void Button::setEnabled(bool isEnabled)
{
if (mEnabled != isEnabled)
{
mEnabled = isEnabled;
if (mStyle.mState == ButtonData::State::Disabled)
{
setState(ButtonData::State::Enabled);
}
}
}
void Button::updateState()
{
setBackground(mStyle.getContainerColor());
}
void Button::onMyMouseEvent(const MouseEvent* event) void Button::onMyMouseEvent(const MouseEvent* event)
{ {
MLOG_INFO("Widget mouse event"); if (!mEnabled)
if(event->getAction() == MouseEvent::Action::Pressed)
{ {
mStyle.mState = ButtonData::State::Pressed; return;
setBackground(mStyle.getContainerColor()); }
if(mClickFunc)
if (event->getAction() == MouseEvent::Action::Pressed)
{
setState(ButtonData::State::Pressed);
}
else if (event->getAction() == MouseEvent::Action::Released)
{
resetState();
if (mClickFunc)
{ {
mClickFunc(this); mClickFunc(this);
} }
} }
else if(event->getAction() == MouseEvent::Action::Released) else if (event->getAction() == MouseEvent::Action::Enter)
{ {
mStyle.mState = ButtonData::State::Enabled; resetState();
setBackground(mStyle.getContainerColor()); }
else if (event->getAction() == MouseEvent::Action::Leave)
{
resetState();
}
}
void Button::resetState()
{
if (mHasFocus)
{
setState(ButtonData::State::Focused);
}
else if (mContainsMouse)
{
setState(ButtonData::State::Hovered);
}
else if (mEnabled)
{
setState(ButtonData::State::Enabled);
}
else
{
setState(ButtonData::State::Disabled);
} }
} }

View file

@ -24,6 +24,8 @@ public:
void setLabel(const std::string& text); void setLabel(const std::string& text);
void setEnabled(bool isEnabled);
void setOnClickFunction(clickFunc func); void setOnClickFunction(clickFunc func);
protected: protected:
@ -34,6 +36,10 @@ protected:
void updateLabel(const PaintEvent* event); void updateLabel(const PaintEvent* event);
void setState(ButtonData::State state);
void resetState();
void updateState() override;
private: private:
ButtonData mStyle; ButtonData mStyle;
@ -42,6 +48,8 @@ private:
std::unique_ptr<TextNode> mTextNode; std::unique_ptr<TextNode> mTextNode;
bool mContentDirty{true}; bool mContentDirty{true};
bool mEnabled{ true };
}; };
using ButtonUPtr = std::unique_ptr<Button>; using ButtonUPtr = std::unique_ptr<Button>;

View file

@ -11,7 +11,10 @@ public:
enum class Action enum class Action
{ {
Pressed, Pressed,
Released Released,
Move,
Enter,
Leave
}; };
public: public:

View file

@ -16,7 +16,7 @@ std::unique_ptr<UiEvent> UiEvent::Create()
return std::make_unique<UiEvent>(); return std::make_unique<UiEvent>();
} }
UiEvent::Type UiEvent::GetType() const UiEvent::Type UiEvent::getType() const
{ {
return mType; return mType;
} }

View file

@ -5,26 +5,38 @@
class UiEvent class UiEvent
{ {
public: public:
enum class Type {
enum class Type{
Unknown, Unknown,
Paint, Paint,
Mouse, Mouse,
Keyboard, Keyboard,
Resize Resize,
Focus
}; };
protected:
UiEvent::Type mType;
public:
UiEvent(); UiEvent();
virtual ~UiEvent(); virtual ~UiEvent();
static std::unique_ptr<UiEvent> Create(); static std::unique_ptr<UiEvent> Create();
Type GetType() const; Type getType() const;
protected:
UiEvent::Type mType;
}; };
using UiEventUPtr = std::unique_ptr<UiEvent>; using UiEventUPtr = std::unique_ptr<UiEvent>;
class FocusEvent : public UiEvent
{
public:
FocusEvent()
: UiEvent()
{
};
virtual ~FocusEvent() = default;
};

View file

@ -64,8 +64,6 @@ TransformNode* Widget::getRootNode() const
return mRootNode.get(); return mRootNode.get();
} }
void Widget::setBackground(Theme::Sys::Color token) void Widget::setBackground(Theme::Sys::Color token)
{ {
if (mBackground != token) if (mBackground != token)
@ -75,6 +73,42 @@ void Widget::setBackground(Theme::Sys::Color token)
} }
} }
void Widget::setBackgroundOpacity(float opacity)
{
if (mBackgroundOpacity != opacity)
{
mBackgroundOpacity = opacity;
mMaterialDirty = true;
}
}
void Widget::setOutlineThickness(double thickness)
{
if (mBorderThickness != thickness)
{
mBorderThickness = thickness;
mMaterialDirty = true;
}
}
void Widget::setOutline(Theme::Sys::Color token)
{
if (mBorder != token)
{
mBorder = token;
mMaterialDirty = true;
}
}
void Widget::setRadius(double radius)
{
if (mRadius != radius)
{
mRadius = radius;
mGeometryDirty = true;
}
}
void Widget::setVisible(bool visible) void Widget::setVisible(bool visible)
{ {
if (mVisible != visible) if (mVisible != visible)
@ -92,7 +126,7 @@ void Widget::setVisible(bool visible)
bool Widget::isDirty() const bool Widget::isDirty() const
{ {
return mTransformDirty || mMaterialDirty || mVisibilityDirty || mPendingChildNodes.size() > 0; return mStateDirty || mGeometryDirty || mTransformDirty || mMaterialDirty || mVisibilityDirty || mPendingChildNodes.size() > 0;
} }
bool Widget::needsUpdate() const bool Widget::needsUpdate() const
@ -111,6 +145,11 @@ bool Widget::needsUpdate() const
return false; return false;
} }
void Widget::updateState()
{
}
void Widget::doPaint(const PaintEvent* event) void Widget::doPaint(const PaintEvent* event)
{ {
updateBackground(event); updateBackground(event);
@ -127,16 +166,19 @@ void Widget::onPaintEvent(const PaintEvent* event)
{ {
mRootNode->setName(mName + "_RootNode"); mRootNode->setName(mName + "_RootNode");
if (mStateDirty)
{
updateState();
mStateDirty = false;
}
doPaint(event); doPaint(event);
if (mVisibilityDirty) if (mVisibilityDirty)
{ {
mRootNode->setIsVisible(mVisible); mRootNode->setIsVisible(mVisible);
} mVisibilityDirty = false;
}
mTransformDirty = false;
mMaterialDirty = false;
mVisibilityDirty = false;
} }
for (auto node : mPendingChildNodes) for (auto node : mPendingChildNodes)
@ -212,9 +254,96 @@ bool Widget::onMouseEvent(const MouseEvent* event)
return true; return true;
} }
bool Widget::onFocusInEvent(const FocusEvent* event)
{
bool inChild = false;
for (const auto& child : mChildren)
{
if (child->onFocusInEvent(event))
{
inChild = true;
break;
}
}
if (inChild)
{
return true;
}
else if (mAcceptsFocus)
{
setFocus(true);
return true;
}
return false;
}
void Widget::setFocus(bool hasFocus)
{
if (mHasFocus != hasFocus)
{
mHasFocus = hasFocus;
mStateDirty = true;
}
}
void Widget::onMyMouseEvent(const MouseEvent* event) void Widget::onMyMouseEvent(const MouseEvent* event)
{ {
MLOG_INFO("Widget mouse event");
}
void Widget::createOrUpdateGeometry()
{
if (!mBackgroundNode)
{
unsigned locX = mLocation.getX() + mMargin.mLeft;
unsigned locY = mLocation.getY() + mMargin.mTop;
unsigned deltaX = mSize.mWidth - mMargin.mLeft - mMargin.mRight;
unsigned deltaY = mSize.mHeight - mMargin.mTop - mMargin.mBottom;
mBackgroundNode = std::make_unique<RectangleNode>(DiscretePoint(locX, locY), deltaX, deltaY);
mBackgroundNode->setName(mName + "_BackgroundNode");
mRootNode->addChild(mBackgroundNode.get());
}
else
{
mBackgroundNode->setRadius(mRadius);
}
}
void Widget::updateTransform()
{
unsigned locX = mLocation.getX() + mMargin.mLeft;
unsigned locY = mLocation.getY() + mMargin.mTop;
unsigned deltaX = mSize.mWidth - mMargin.mLeft - mMargin.mRight;
unsigned deltaY = mSize.mHeight - mMargin.mTop - mMargin.mBottom;
mBackgroundNode->setWidth(deltaX);
mBackgroundNode->setHeight(deltaY);
mBackgroundNode->setLocation(DiscretePoint(locX, locY));
}
void Widget::updateMaterial(const PaintEvent* event)
{
if (mBackground != Theme::Sys::Color::None)
{
auto background_color = event->getThemesManager()->getColor(mBackground);
background_color.setAlpha(mBackgroundOpacity);
mBackgroundNode->setFillColor(background_color);
}
else
{
mBackgroundNode->setHasFillColor(false);
}
if (mBorder != Theme::Sys::Color::None)
{
mBackgroundNode->setStrokeColor(event->getThemesManager()->getColor(mBorder));
mBackgroundNode->setStrokeThickness(mBorderThickness);
}
else
{
mBackgroundNode->setHasStrokeColor(false);
}
} }
void Widget::updateBackground(const PaintEvent* event) void Widget::updateBackground(const PaintEvent* event)
@ -233,16 +362,13 @@ void Widget::updateBackground(const PaintEvent* event)
if (mTransformDirty) if (mTransformDirty)
{ {
mBackgroundNode->setWidth(deltaX); updateTransform();
mBackgroundNode->setHeight(deltaY); mTransformDirty = false;
mBackgroundNode->setLocation(DiscretePoint(locX, locY));
} }
if (mMaterialDirty) if (mMaterialDirty)
{ {
mBackgroundNode->setFillColor(event->getThemesManager()->getColor(mBackground)); updateMaterial(event);
mBackgroundNode->setStrokeColor(event->getThemesManager()->getColor(mBorder));
mBackgroundNode->setStrokeThickness(mBorderThickness);
} }
if (mVisibilityDirty) if (mVisibilityDirty)

View file

@ -4,6 +4,7 @@
#include "ITheme.h" #include "ITheme.h"
#include "WidgetState.h" #include "WidgetState.h"
#include "BoxGeometry.h" #include "BoxGeometry.h"
#include "TransformNode.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -12,6 +13,7 @@
class MouseEvent; class MouseEvent;
class KeyboardEvent; class KeyboardEvent;
class PaintEvent; class PaintEvent;
class FocusEvent;
class AbstractVisualNode; class AbstractVisualNode;
class TransformNode; class TransformNode;
@ -47,8 +49,18 @@ public:
virtual bool onKeyboardEvent(const KeyboardEvent* event); virtual bool onKeyboardEvent(const KeyboardEvent* event);
virtual bool onFocusInEvent(const FocusEvent* event);
void setBackgroundOpacity(float opacity);
void setBackground(Theme::Sys::Color token); void setBackground(Theme::Sys::Color token);
void setOutlineThickness(double thickness);
void setOutline(Theme::Sys::Color token);
void setRadius(double radius);
void setVisible(bool visible); void setVisible(bool visible);
void setName(const std::string& name); void setName(const std::string& name);
@ -60,6 +72,14 @@ protected:
virtual void onMyMouseEvent(const MouseEvent* event); virtual void onMyMouseEvent(const MouseEvent* event);
virtual void createOrUpdateGeometry();
virtual void updateMaterial(const PaintEvent* event);
virtual void updateTransform();
virtual void updateState();
virtual void updateBackground(const PaintEvent* event); virtual void updateBackground(const PaintEvent* event);
virtual void doPaint(const PaintEvent* event); virtual void doPaint(const PaintEvent* event);
@ -68,6 +88,8 @@ protected:
virtual bool isDirty() const; virtual bool isDirty() const;
void setFocus(bool hasFocus);
void setParent(Widget* parent); void setParent(Widget* parent);
Widget* getParent() const; Widget* getParent() const;
@ -77,16 +99,26 @@ protected:
std::unique_ptr<TransformNode> mRootNode; std::unique_ptr<TransformNode> mRootNode;
std::vector<std::unique_ptr<Widget> > mChildren; std::vector<std::unique_ptr<Widget> > mChildren;
unsigned mBorderThickness{0};
Theme::Sys::Color mBackground;
Theme::Sys::Color mBorder; Theme::Sys::Color mBorder;
double mBorderThickness{0};
Theme::Sys::Color mBackground;
float mBackgroundOpacity{ 1.0 };
bool mVisible{true}; bool mVisible{true};
double mRadius{ 0.0 };
std::unique_ptr<RectangleNode> mBackgroundNode; std::unique_ptr<RectangleNode> mBackgroundNode;
bool mStateDirty{ true };
bool mMaterialDirty{true}; bool mMaterialDirty{true};
bool mVisibilityDirty{true}; bool mVisibilityDirty{true};
bool mGeometryDirty{ true };
bool mAcceptsFocus{ false };
bool mHasFocus{ false };
bool mContainsMouse{ false };
std::string mName; std::string mName;
std::vector<TransformNode*> mPendingChildNodes; std::vector<TransformNode*> mPendingChildNodes;

View file

@ -62,7 +62,7 @@ void DesktopManager::onMouseEvent(const MouseEvent* event)
void DesktopManager::onUiEvent(UiEventUPtr eventUPtr) void DesktopManager::onUiEvent(UiEventUPtr eventUPtr)
{ {
const auto event = mEventManager->AddEvent(std::move(eventUPtr)); const auto event = mEventManager->AddEvent(std::move(eventUPtr));
switch (event->GetType()) switch (event->getType())
{ {
case (UiEvent::Type::Paint): case (UiEvent::Type::Paint):
{ {

View file

@ -1,5 +1,7 @@
add_subdirectory(test_utils) add_subdirectory(test_utils)
add_subdirectory(ui_controls)
file(COPY data/ DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test_data) file(COPY data/ DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test_data)
set(TEST_MODULES set(TEST_MODULES

View file

@ -25,8 +25,6 @@ public:
mDrawingContext = std::make_unique<DrawingContext>(mSurface.get()); mDrawingContext = std::make_unique<DrawingContext>(mSurface.get());
} }
Scene* getScene() const Scene* getScene() const
{ {
return mSurface->getScene(); return mSurface->getScene();

View file

@ -0,0 +1,11 @@
set(MODULE_NAME ui_controls)
list(APPEND UNIT_TEST_FILES
TestButton.cpp
)
set(UNIT_TEST_TARGET_NAME ${MODULE_NAME}_unit_tests)
add_executable(${UNIT_TEST_TARGET_NAME} ${CMAKE_SOURCE_DIR}/test/test_runner.cpp ${UNIT_TEST_FILES})
target_link_libraries(${UNIT_TEST_TARGET_NAME} PUBLIC test_utils ui_controls)
set_property(TARGET ${UNIT_TEST_TARGET_NAME} PROPERTY FOLDER test/${MODULE_NAME})

View file

@ -0,0 +1,29 @@
#include "TestFramework.h"
#include "TestUtils.h"
#include "TestRenderUtils.h"
#include "ThemeManager.h"
#include "PaintEvent.h"
#include "Button.h"
TEST_CASE(TestButton_Elevated, "ui_controls")
{
auto theme_manager = std::make_unique<ThemeManager>();
auto paint_event = PaintEvent::Create(theme_manager.get(), nullptr);
Button button(ButtonData::Component::Elevated);
button.setLabel("Enabled");
button.onPaintEvent(paint_event.get());
auto node = button.getRootNode();
TestRenderer renderer;
renderer.getScene()->addNode(node);
renderer.writeSvg(TestUtils::getTestOutputDir(__FILE__) / "Elevated_Enabled.svg");
renderer.write(TestUtils::getTestOutputDir(__FILE__) / "Elevated_Enabled.png");
};