Challenges: undo/redo, custom drawing, dialog control.
The task is to build a frame containing an undo and redo button as well as a canvas area underneath. Left-clicking inside an empty area inside the canvas will create an unfilled circle with a fixed diameter whose center is the left-clicked point. The circle nearest to the mouse pointer such that the distance from its center to the pointer is less than its radius, if it exists, is filled with the color gray. The gray circle is the selected circle C. Right-clicking C will make a popup menu appear with one entry “Adjust diameter..”. Clicking on this entry will open another frame with a slider inside that adjusts the diameter of C. Changes are applied immediately. Closing this frame will mark the last diameter as significant for the undo/redo history. Clicking undo will undo the last significant change (i.e. circle creation or diameter adjustment). Clicking redo will reapply the last undoed change unless new changes were made by the user in the meantime.
Circle Drawer’s goal is, among other things, to test how good the common challenge of implementing an undo/redo functionality for a GUI application can be solved. In an ideal solution the undo/redo functionality comes for free resp. just comes out as a natural consequence of the language / toolkit / paradigm. Moreover, Circle Drawer tests how dialog control, i.e. keeping the relevant context between several successive GUI interaction steps, is achieved in the source code. Last but not least, the ease of custom drawing is tested.
#include <AUI/Platform/Entry.h>
#include <AUI/Platform/AWindow.h>
#include <AUI/Util/UIBuildingHelpers.h>
#include "AUI/View/AProgressBar.h"
#include "AUI/View/ASlider.h"
#include "AUI/View/AButton.h"
using namespace declarative;
struct Circle {
glm::vec2 position;
float radius = 10_dp;
};
class IAction {
public:
virtual ~IAction() = default;
virtual void undo() = 0;
virtual void redo() = 0;
};
class UndoStack {
public:
using Container = std::list<_unique<IAction>>;
using Iterator = Container::const_iterator;
private:
Container mStack;
public:
void undo() {
if (nextAction == mStack.begin()) {
return;
}
nextAction = std::prev(*nextAction);
(**nextAction)->undo();
}
void redo() {
if (nextAction == mStack.end()) {
return;
}
(**nextAction)->redo();
nextAction = std::next(*nextAction);
}
void add(_unique<IAction> action) {
action->redo();
nextAction = std::next(mStack.insert(mStack.erase(*nextAction, mStack.end()), std::move(action)));
nextAction.notify();
}
Iterator begin() const {
return mStack.begin();
}
Iterator end() const {
return mStack.end();
}
AProperty<Iterator> nextAction = mStack.end();
};
struct State {
AProperty<std::list<Circle>> circles;
UndoStack history;
};
static constexpr auto MAX_RADIUS = 128.f;
class CircleDrawArea :
public AView {
public:
CircleDrawArea(_<State> state) : mState(std::move(state)) {
setCustomStyle({
Expanding(),
BackgroundSolid(AColor::WHITE),
Border(1_px, AColor::GRAY),
});
connect(mState->circles.changed, me::redraw);
connect(mHoveredCircle.changed, me::redraw);
}
void render(ARenderContext ctx)
override {
for (const auto& circle : *mState->circles) {
if (&circle == mHoveredCircle) {
ASolidBrush { AColor::GRAY }, circle.position - circle.radius, glm::vec2(circle.radius * 2.f),
circle.radius);
}
ASolidBrush { AColor::BLACK }, circle.position - circle.radius, glm::vec2(circle.radius * 2.f),
circle.radius, 1);
}
}
void onPointerMove(glm::vec2 pos,
const APointerMoveEvent& event)
override {
mHoveredCircle = [&] {
Circle* result = nullptr;
float nearestDistanceToCursor = std::numeric_limits<float>::max();
for (auto& circle : mState->circles.raw) {
float distanceToCursor = glm::distance2(circle.position, pos);
if (distanceToCursor > nearestDistanceToCursor) {
continue;
}
if (distanceToCursor > circle.radius * circle.radius) {
continue;
}
result = &circle;
nearestDistanceToCursor = distanceToCursor;
}
return result;
}();
}
protected:
auto circle = *mHoveredCircle;
if (circle == nullptr) {
return {};
}
return {
{},
{
.name = "Adjust radius...",
.onAction =
[this, circle] {
auto radiusPopup = _new<AWindow>(
"", 200_dp, 50_dp,
dynamic_cast<AWindow*
>(
AWindow::current()), WindowStyle::MODAL);
radiusPopup->setContents(Vertical {
Label { "Adjust diameter of circle at {}."_format(circle->position) },
it->setValue(circle->radius / MAX_RADIUS);
it->valueChanging, [this, circle](aui::float_within_0_1 s) {
circle->radius = s * MAX_RADIUS;
mState->circles.notify();
});
},
});
connect(radiusPopup->closed, [
this, circle, oldRadius = circle->radius] {
if (oldRadius == circle->radius) {
return;
}
class ActionChangeRadius : public IAction {
public:
ActionChangeRadius(Circle* circle, float prevRadius, float newRadius)
: mCircle(circle), mPrevRadius(prevRadius), mNewRadius(newRadius) {}
~ActionChangeRadius() override = default;
void undo() override {
mCircle->radius = mPrevRadius;
}
void redo() override {
mCircle->radius = mNewRadius;
}
private:
Circle* mCircle;
float mPrevRadius;
float mNewRadius;
};
mState->history.add(std::make_unique<ActionChangeRadius>(circle, oldRadius, circle->radius));
});
radiusPopup->show();
},
},
};
}
public:
if (event.
asButton != AInput::LBUTTON) {
return;
}
class ActionAddCircle : public IAction {
public:
ActionAddCircle(_<State> state, Circle circle) : mState(std::move(state)), mCircle(std::move(circle)) {}
~ActionAddCircle() override = default;
void undo() override {
mState->circles.writeScope()->pop_back();
}
void redo() override {
mState->circles.writeScope()->push_back(mCircle);
}
private:
_<State> mState;
Circle mCircle;
};
mState->history.add(std::make_unique<ActionAddCircle>(mState, Circle { .position = event.position }));
}
private:
};
class CircleDrawerWindow :
public AWindow {
public:
CircleDrawerWindow() : AWindow("AUI - 7GUIs - Circle Drawer", 300_dp, 250_dp) {
Centered {
Horizontal {
it & mState.history.nextAction.readProjected([&](UndoStack::Iterator i) { return i != mState.history.begin(); }) > &AView::setEnabled;
},
it & mState.history.nextAction.readProjected([&](UndoStack::Iterator i) { return i != mState.history.end(); }) > &AView::setEnabled;
},
},
},
});
}
private:
State mState;
void undo() {
mState.history.undo();
}
void redo() {
mState.history.redo();
}
};
_new<CircleDrawerWindow>()->show();
return 0;
}
void setContents(const _< AViewContainer > &container)
Moves (like via std::move) all children and layout of the specified container to this container.
Base class of all UI objects.
Definition AView.h:78
virtual void render(ARenderContext ctx)
Draws this AView. Noone should call this function except rendering routine.
virtual void onPointerMove(glm::vec2 pos, const APointerMoveEvent &event)
Handles pointer hover events.
virtual AMenuModel composeContextMenu()
Produce context (right click) menu.
virtual void onPointerReleased(const APointerReleasedEvent &event)
Called on pointer (mouse) released event.
Represents a window in the underlying windowing system.
Definition AWindow.h:45
static AWindowBase * current()
virtual void roundedRectangleBorder(const ABrush &brush, glm::vec2 position, glm::vec2 size, float radius, int borderWidth)=0
Draws rounded rectangle's border.
virtual void roundedRectangle(const ABrush &brush, glm::vec2 position, glm::vec2 size, float radius)=0
Draws rounded rect (with antialiasing, if msaa enabled).
An std::weak_ptr with AUI extensions.
Definition SharedPtrTypes.h:179
@ HIDDEN_FROM_THIS
Like HIDDEN, but view's ASS-styled background is also affected by mask.
Definition AOverflow.h:40
static decltype(auto) connect(const Signal &signal, Object *object, Function &&function)
Connects signal to the slot of the specified object.
Definition AObject.h:86
#define let
Performs multiple operations on a single object without repeating its name (in place) This function c...
Definition kAUI.h:262
#define AUI_ENTRY
Application entry point.
Definition Entry.h:90
Button
Specifies button(s) to be displayed.
Definition AMessageBox.h:79
Pointing method press event.
Definition APointerReleasedEvent.h:19
AInput::Key asButton
pointerIndex treated as mouse button.
Definition APointerReleasedEvent.h:41
Basic easy-to-use property implementation containing T.
Definition AProperty.h:30
static _< T > fake(T *raw)
Creates fake shared pointer to T* raw with empty destructor, which does nothing. It's useful when som...
Definition SharedPtrTypes.h:429