Skip to content

AProperty#

Observable container of T.

Header:#include <AUI/Common/AProperty.h>
CMake:aui_link(my_target PUBLIC aui::core)

Detailed Description#

Experimental Feature

This API is experimental. Experimental APIs are likely to contain bugs, might be changed or removed in the future.

AProperty<T> is a container holding an instance of T. You can assign a value to it with operator= and read value with value() method or implicit conversion operator T().

See property system for usage examples.

Declaration#

To declare a property inside your data model, use AProperty template:

struct User {
    AProperty<AString> name;
    AProperty<AString> surname;
};
LogObserver() { ON_CALL(*this, log(testing::_)).WillByDefault( { }); } AProperty<T> is a container holding an instance of T. You can assign a value to it with operator= and read value with value() method or implicit conversion operator T().

AProperty behaves like a class/struct data member:

User u;
u.name = "Hello";
EXPECT_EQ(u.name, "Hello");
Non-const operators have side effects; const operators don't, so you can perform seamlessly:
User u;
u.name = "Hello";
AString helloWorld = u.name + " world";
EXPECT_EQ(helloWorld, "Hello world");
In most cases, property is implicitly convertible to its underlying type (const only):
auto doSomethingWithName = [](const AString& name) { EXPECT_EQ(name, "Hello"); };
User u;
u.name = "Hello";
doSomethingWithName(u.name);
If it doesn't, simply put an asterisk:
doSomethingWithName(*u.name);
//                 ^^^ HERE

Observing changes#

All property types offer .changed field which is a signal reporting value changes. Let's make little observer object for demonstration:

class LogObserver : public AObject {
public:
    void log(const AString& msg) {
        ALogger::info("LogObserver") << "Received value: " << msg;
    }
};

Example usage:

auto observer = _new<LogObserver>();
auto u = aui::ptr::manage_shared(new User { .name = "Chloe" });
AObject::connect(u->name.changed, AUI_SLOT(observer)::log);

At the moment, the program prints nothing. When we change the property:

u->name = "Marinette";
Code produces the following output:
[07:58:59][][LogObserver][INFO]: Received value: Marinette

As you can see, observer received the update. But, for example, if we would like to display the value via label, the label wouldn't display the current value until the next update. We want the label to display current value without requiring an update. To do this, connect to the property directly, without explicitly asking for changed:

auto observer = _new<LogObserver>();
auto u = aui::ptr::manage_shared(new User { .name = "Chloe" });

EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(u->name, AUI_SLOT(observer)::log);
Code above produces the following output:
[07:58:59][][LogObserver][INFO]: Received value: Chloe
As you can see, observer receives the value without making updates to the value. The call of LogObserver::log is made by AObject::connect itself. In this document, we will call this behaviour as "pre-fire".

Subsequent changes to field would send updates as well:

EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
u->name = "Marinette";
Assignment operation above makes an additional line to output:
[07:58:59][][LogObserver][INFO]: Received value: Marinette

Whole program output when connecting to property directly:

[07:58:59][][LogObserver][INFO]: Received value: Chloe
[07:58:59][][LogObserver][INFO]: Received value: Marinette

Copy constructing AProperty#

Copying AProperty is considered as a valid operation as it's a data holder. However, it's worth to note that AProperty copies it's underlying data field only, the signal-slot relations are not borrowed.

auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });

EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
This part is similar to previous examples, nothing new. Let's introduce a copy:
auto copy = _new<User>(*original);
EXPECT_EQ(copy->name, "Chloe"); // copied name
Now, let's change origin->name and check that observer received an update, but value in the copy remains:
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
original->name = "Marinette";
EXPECT_EQ(copy->name, "Chloe"); // still
In this example, observer is aware of changes "Chloe" -> "Marinette". The copy is not aware because it is a copy. If we try to change the copy's name:
copy->name = "Adrien";
The observer is not aware about changes in copy. In fact. copy->name has zero connections.

Copy assigning AProperty#

The situation with copy assigning auto copy = _new<User>(); *copy = *original; is similar to copy construction auto copy = _new<User>(*original);, except that we are copying to some pre-existing data structure that potentially have signal-skit relations already. So, not only connections should be kept as is but a notification for copy destination's observers is needed.

As with copy construction, copy operation of AProperty does not affect signal-slot relations. Moreover, it notifies the observers.

auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });

EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
This part is similar to previous examples, nothing new. Let's perform copy-assignment:
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
User copy { .name = "Marinette" };
*original = copy;
See, not only the connection remains, but it also receives notification about the change.

Moving AProperty#

Similary to copy, AProperty is both move assignable and constructible except that underlying value is moved instead of copying. Also, the observers of the source object receive notification that the value was emptied. The signal-slot relations are left unchanged.

auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });

EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
This part is similar to previous examples, nothing new. Let's introduce a move:
// by move operation, we've affected the source, hence the
// empty string notification
EXPECT_CALL(*observer, log(AString(""))).Times(1);
auto moved = _new<User>(std::move(*original));

EXPECT_EQ(original->name, ""); // empty
EXPECT_EQ(moved->name, "Chloe"); // moved name
Now, let's change origin->name and check that observer received an update, but value in the moved remains:
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
original->name = "Marinette";
EXPECT_EQ(moved->name, "Chloe"); // still
In this example, observer is aware of changes "Chloe" -> "" -> "Marinette". The moved is not aware. If we try to change the moved's name:
moved->name = "Adrien";
The observer is not aware about changes in moved. In fact. moved->name has zero connections. Move assignment work in a similar way to copy assignment:
auto observer = _new<LogObserver>();
auto original = aui::ptr::manage_shared(new User { .name = "Chloe" });

EXPECT_CALL(*observer, log(AString("Chloe"))).Times(1);
AObject::connect(original->name, AUI_SLOT(observer)::log);
This part is similar to previous examples, nothing new. Let's perform move-assignment:
EXPECT_CALL(*observer, log(AString("Marinette"))).Times(1);
User copy { .name = "Marinette" };
*original = std::move(copy);
See, not only the connection remains, but it also receives notification about the change.

Non-const operators#

Refer to aui::PropertyModifier.

AProperty and AVector#

Assuming you have an AVector wrapped with AProperty:

AProperty<AVector<int>> ints = AVector<int>{1, 2, 3};
You can use square brackets to access items transparently:
EXPECT_EQ(ints[0], 1);
EXPECT_EQ(ints[1], 2);
EXPECT_EQ(ints[2], 3);
For ranged for loop, you need to dereference ints:
for (int i : *ints) {
    // i = 1, 2, 3...
}
To modify the vector, you need to use writeScope():
ints.writeScope()->push_back(2);
// or
ints.writeScope() << 2;
// or
ints << 2; // implies writeScope()

Examples#

examples/app/game_of_life/src/main.cpp

Game of Life - Game of Life implementation that uses advanced large dynamic data rendering techniques such as ITexture, AImage to be GPU friendly. The computation is performed in AThreadPool.

        });
        emit frameComplete;
    }

    AProperty<bool> isRunning = false;
    emits<> frameComplete;

private:
    _<ATimer> mTimer = _new<ATimer>(100ms);
    AFuture<> mFrame;

examples/ui/contacts/src/view/ContactDetailsView.cpp

AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.

    };
}

template <typename T>
_<AView> viewer(AProperty<T>& property) {
    return Label {} & property.readProjected([](const T& v) { return "{}"_format(v); });
}

template <typename T>
_<AView> editor(AProperty<T>& property);

examples/7guis/flight_booker/src/main.cpp

7GUIs Flight Booker - Flight Booker.

constexpr auto REGEX_DATE = ctre::match<"([0-9]+)\\.([0-9]+)\\.([0-9]{4})">;

struct DateTextFieldState {
    AProperty<AOptional<system_clock::time_point>> parsed;
};

auto formatDate(system_clock::time_point date) { return "{0:%d}.{0:%m}.{0:%G}"_format(date); }

auto dateTextField(DateTextFieldState& state) {

examples/7guis/cells/src/Cell.h

7GUIs Cells - Spreadsheet processor (Excel).

    formula::Value evaluate();

public:
    Spreadsheet* spreadsheet = nullptr;
    AProperty<AString> expression;
    APropertyPrecomputed<formula::Value> value = [&] { return (*expressionPrecompiled)(*spreadsheet); };

    static AString columnName(unsigned index);
    static AString rowName(unsigned index);

examples/7guis/circle_drawer/src/main.cpp

7GUIs Circle Drawer - Undo, redo, dialog control.

    Iterator end() const {
        return mStack.end();
    }

    AProperty<Iterator> nextAction = mStack.end();
};

struct State {
    AProperty<std::list<Circle>> circles;
    UndoStack history;

examples/7guis/timer/src/main.cpp

7GUIs Timer - Timer example.

private:
    _<ATimer> mTimer = _new<ATimer>(100ms);
    high_resolution_clock::time_point mStartTime = high_resolution_clock::now();
    AProperty<high_resolution_clock::time_point> mCurrentTime;
    AProperty<high_resolution_clock::duration> mDuration = 30s;

    APropertyPrecomputed<high_resolution_clock::duration> mElapsedTime = [&] {
        return std::min(mCurrentTime - mStartTime, *mDuration);
    };

examples/ui/contacts/src/view/ContactDetailsView.h

AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.

private:
    _<Contact> mContact;
    _<Contact> mOriginalContact;
    AProperty<bool> mEditorMode = false;

    template<typename T>
    _<AView> presentation(AProperty<T>& property);

    template<typename T>

examples/ui/contacts/src/model/Contact.h

AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.

#include <AUI/Common/AObject.h>
#include "AUI/Common/AProperty.h"

struct Contact {
    AProperty<AString> displayName;
    AProperty<AString> phone;
    AProperty<AString> address;
    AProperty<AString> email;
    AProperty<AString> homepage;
    AProperty<AString> note;

examples/7guis/cells/src/main.cpp

7GUIs Cells - Spreadsheet processor (Excel).

using namespace ass;

struct State {
    Spreadsheet spreadsheet{glm::uvec2 { 'Z' - 'A' + 1, 100 }};
    AProperty<AString> currentExpression;
};

static _<AView> labelTitle(AString s) {
    return _new<ALabel>(std::move(s)) AUI_WITH_STYLE {
        Opacity { 0.5f },

Public fields and Signals#


changed#

emits<T> changed

Signal that notifies data changes.


raw#

T raw

Stored value.

This field stores AProperty's wrapped value. It is not recommended to use this in casual use; although there are might be an use case to modify the value without notifying. You can use notify() to send a change notification. Use at your own risk.

Examples#

examples/app/notes/CMakeLists.txt

Notes App - Note taking app that demonstrates usage of AListModel, AProperty, user data saving and loading.

# Uncomment this code to pull AUI:
#
# file(
#         DOWNLOAD
#         https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake
#         ${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
# include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
#
# auib_import(aui https://github.com/aui-framework/aui
#             COMPONENTS core views json)
examples/app/game_of_life/CMakeLists.txt

Game of Life - Game of Life implementation that uses advanced large dynamic data rendering techniques such as ITexture, AImage to be GPU friendly. The computation is performed in AThreadPool.

# Uncomment this code to pull AUI:
#
# file(
#         DOWNLOAD
#         https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake
#         ${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
# include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
#
# auib_import(aui https://github.com/aui-framework/aui
#             COMPONENTS core views json)
examples/ui/views/CMakeLists.txt

Views Example - All-in-one views building example.

# Uncomment this code to pull AUI:
#
# file(
#         DOWNLOAD
#         https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake
#         ${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
# include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
#
# auib_import(aui https://github.com/aui-framework/aui
#             COMPONENTS core views audio curl)
examples/basic/hello_world/CMakeLists.txt

Console Hello World Example - Basic CLI Hello World application.

# Use AUI.Boot
file(
    DOWNLOAD 
    https://raw.githubusercontent.com/aui-framework/aui/master/aui.boot.cmake 
    ${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)
include(${CMAKE_CURRENT_BINARY_DIR}/aui.boot.cmake)

# link AUI
auib_import(

Public Methods#

biProjected#


auto AProperty::biProjected(ProjectionRead&& projectionRead, ProjectionWrite&& projectionWrite)

Makes a bidirectional projection of this property.

Examples#

examples/7guis/flight_booker/src/main.cpp

7GUIs Flight Booker - Flight Booker.

auto dateTextField(DateTextFieldState& state) {
    return _new<ATextField>() AUI_LET {
        AObject::biConnect(
            state.parsed.biProjected(aui::lambda_overloaded {
              [](const AOptional<system_clock::time_point>& v) -> AString {
                  if (!v) {
                      return "";
                  }
                  return formatDate(*v);
examples/7guis/timer/src/main.cpp

7GUIs Timer - Timer example.

            }),
            Horizontal {
              Label { "Duration:" },
              _new<ASlider>() AUI_LET {
                      it&& mDuration.biProjected(aui::lambda_overloaded {
                        [](high_resolution_clock::duration d) -> aui::float_within_0_1 {
                            return float(d.count()) / float(MAX_DURATION.count());
                        },
                        [](aui::float_within_0_1 d) -> high_resolution_clock::duration {
                            return high_resolution_clock::duration(long(float(d) * float(MAX_DURATION.count())));

auto AProperty::biProjected(Projection&& projectionBidirectional)

Makes a bidirectional projection of this property (by a single aui::lambda_overloaded).

Examples#

examples/7guis/flight_booker/src/main.cpp

7GUIs Flight Booker - Flight Booker.

auto dateTextField(DateTextFieldState& state) {
    return _new<ATextField>() AUI_LET {
        AObject::biConnect(
            state.parsed.biProjected(aui::lambda_overloaded {
              [](const AOptional<system_clock::time_point>& v) -> AString {
                  if (!v) {
                      return "";
                  }
                  return formatDate(*v);
examples/7guis/timer/src/main.cpp

7GUIs Timer - Timer example.

            }),
            Horizontal {
              Label { "Duration:" },
              _new<ASlider>() AUI_LET {
                      it&& mDuration.biProjected(aui::lambda_overloaded {
                        [](high_resolution_clock::duration d) -> aui::float_within_0_1 {
                            return float(d.count()) / float(MAX_DURATION.count());
                        },
                        [](aui::float_within_0_1 d) -> high_resolution_clock::duration {
                            return high_resolution_clock::duration(long(float(d) * float(MAX_DURATION.count())));

notify#


void AProperty::notify()

Notify observers that a change was occurred (no preconditions).

In common, you won't need to use this function. AProperty is reevaluated automatically as soon as one updates the value within property.

If your scenario goes beyond AProperty::writeScope() that explicitly defines modification scope within RAII scope, you can modify the underlying value by accessing AProperty::raw and then call AProperty::notify() to notify the observers value is changed.

Examples#

examples/7guis/circle_drawer/src/main.cpp

7GUIs Circle Drawer - Undo, redo, dialog control.

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

readProjected#


auto AProperty::readProjected(Projection&& projection)

Makes a readonly projection of this property.

Examples#

examples/7guis/counter/src/main.cpp

7GUIs Counter - Simple counter.

    CounterWindow(): AWindow("AUI - 7GUIs - Counter", 200_dp, 100_dp) {
        setContents(Centered {
          Horizontal {
            _new<ATextField>() AUI_LET {
                AObject::connect(mCounter.readProjected(AString::number<int>), it->text());
                it->setEditable(false);
            },
            Button { "Count" }.connect(&AView::clicked, [&] { mCounter += 1; }),
          },
        });
examples/ui/contacts/src/view/ContactDetailsView.cpp

AUI Contacts - Usage of AUI_DECLARATIVE_FOR to make a contacts-like application.

namespace {
_<AView> profilePhoto(const _<Contact>& contact) {
    return Centered {
        Label {} & contact->displayName.readProjected([](const AString& s) {
            return s.empty() ? "?" : AString(1, s.first()).uppercase();
        }) AUI_WITH_STYLE { Opacity(0.5f), FontSize { 32_dp } },
    } AUI_WITH_STYLE {
        FixedSize { 64_dp },
        BorderRadius { 32_dp },
examples/7guis/flight_booker/src/main.cpp

7GUIs Flight Booker - Flight Booker.

        } });
        setContents(Centered {
          Vertical {
            _new<ADropdownList>(AListModel<AString>::make({ "one-way flight", "return flight" })) AUI_LET {
                    connect(it->selectionId().readProjected([](int selectionId) { return selectionId == 1; }),
                            mIsReturnFlight);
                },
            dateTextField(mDepartureDate),
            dateTextField(mReturnDate) AUI_LET { connect(mIsReturnFlight, AUI_SLOT(it)::setEnabled); },
            _new<AButton>("Book") AUI_LET {
examples/7guis/circle_drawer/src/main.cpp

7GUIs Circle Drawer - Undo, redo, dialog control.

          Centered {
            Horizontal {
              Button { "Undo" } AUI_LET {
                  connect(it->clicked, me::undo);
                  it & mState.history.nextAction.readProjected([&](UndoStack::Iterator i) { return i != mState.history.begin(); }) > &AView::setEnabled;
              },
              Button { "Redo" } AUI_LET {
                connect(it->clicked, me::redo);
                it & mState.history.nextAction.readProjected([&](UndoStack::Iterator i) { return i != mState.history.end(); }) > &AView::setEnabled;
              },
examples/7guis/timer/src/main.cpp

7GUIs Timer - Timer example.

                        it->setCustomStyle({ Expanding { 1, 0 } });
                    },
              },
            },
            Label {} & mElapsedTime.readProjected([](high_resolution_clock::duration d) {
                return "{:.1f}s"_format(duration_cast<milliseconds>(d).count() / 1000.f);
            }),
            Horizontal {
              Label { "Duration:" },
              _new<ASlider>() AUI_LET {

writeScope#


aui::PropertyModifier<AProperty> AProperty::writeScope()
Returns
aui::PropertyModifier of this property.

Examples#

examples/7guis/circle_drawer/src/main.cpp

7GUIs Circle Drawer - Undo, redo, dialog control.

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