Skip to content

APropertyPrecomputed#

Readonly property that holds a value computed by an expression.

Header:#include <AUI/Common/APropertyPrecomputed.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.

APropertyPrecomputed<T> is a readonly property similar to AProperty<T>. It holds an instance of T as well. Its value is determined by the C++ function specified in its constructor, typically a C++ lambda expression.

See property system for usage info.

Despite properties offer projection methods, you might want to track and process values of several properties.

APropertyPrecomputed<T> is a readonly property similar to AProperty<T>. It holds an instance of T as well. Its value is determined by a reactive expression specified in APropertyPrecomputed<T>'s constructor, typically a C++ lambda.

It's convenient to access values from another properties inside the expression. The properties accessed during invocation of the expression are tracked behind the scenes so they become dependencies of APropertyPrecomputed automatically. If one of the tracked properties fires changed signal, APropertyPrecomputed invalidates its T. APropertyPrecomputed follows lazy semantics so the expression is re-evaluated and the new result is applied to APropertyPrecomputed as soon as the latter is accessed for the next time.

In other words, it allows to specify relationships between different object properties and reactively update APropertyPrecomputed value whenever its dependencies change. APropertyPrecomputed<T> is somewhat similar to Qt Bindable Properties.

APropertyPrecomputed is a readonly property, hence you can't update its value with assignment. You can get its value with value() method or implicit conversion operator T() as with other properties.

Declaration#

Declare a property with custom expression determining it's value as follows:

/// [APropertyPrecomputed User]
struct User {
    AProperty<AString> name;
    AProperty<AString> surname;
    APropertyPrecomputed<AString> fullName = [&] { return "{} {}"_format(name, surname); };
};
/// [APropertyPrecomputed User]
Let's make little observer object for demonstration:
class LogObserver : public AObject {
public:
    void log(const AString& msg) {
        ALogger::info("LogObserver") << "Received value: " << msg;
    }
};
Usage:
auto u = aui::ptr::manage_shared(new User {
    .name = "Emma",
    .surname = "Watson",
});

auto observer = _new<LogObserver>();
EXPECT_CALL(*observer, log(AString("Emma Watson"))).Times(1);
AObject::connect(u->fullName, AUI_SLOT(observer)::log);

The example above prints "Emma Watson". If we try to update one of dependencies of APropertyPrecomputed (i.e., name or surname), APropertyPrecomputed responds immediately:

EXPECT_CALL(*observer, log(AString("Emma Stone"))).Times(1);
u->surname = "Stone";

The example above prints "Emma Stone".

Valid Expressions#

Any C++ callable evaluating to T can be used as an expression for APropertyPrecomputed<T>. However, to formulate correct expression, some rules must be satisfied.

Dependency tracking only works on other properties. It is the developer's responsibility to ensure all values referenced in the expression are properties, or, at least, non-property values that wouldn't change or whose changes are not interesting. You definitely can use branching inside the expression, but you must be confident about what are you doing. Generally speaking, use as trivial expressions as possible.

struct User {
    AProperty<AString> name;
    AProperty<AString> surname;
    APropertyPrecomputed<AString> fullName = [&]() -> AString {
        if (name->empty()) {
            return "-";
        }
        if (surname->empty()) {
            return "-";
        }
        return "{} {}"_format(name, surname);
    };
};

In this expression, we have a fast path return if name is empty.

User u = {
    .name = "Emma",
    .surname = "Watson",
};
// trivial: we've accessed all referenced properties
EXPECT_EQ(u.fullName, "Emma Watson");
As soon as we set name to "", we don't access surname. If we try to trigger the fast path return:
u.name = "";
surname can't trigger re-evaluation anyhow. Re-evaluation can be triggered by name only. So, at the moment, we are interested in name changes only.

APropertyPrecomputed might evaluate its expression several times during its lifetime. The developer must make sure that all objects referenced in the expression live longer than APropertyPrecomputed.

The expression should not read from the property it's a binding for, including other referenced APropertyPrecomputes. Otherwise, there's an infinite evaluation loop, and AEvaluationLoopException is thrown.

Copying and moving APropertyPrecomputed#

Warning

Despite the underlying value and factory callback are both copy constructible and movable, the copy and move constructor are explicitly deleted to avoid potential object lifetime errors created by the lambda capture and prevent non-intuitive behavior.

User

If copy construction of APropertyPrecomputed were possible, consider the following code:

User user { .name = "Hello" };
auto copy = user;             // WON'T COMPILE
auto moved = std::move(user); // WON'T COMPILE
copy has copied factory function of user, which refers to fields of user, not to copy's fields. Copy construction of a class or struct discards default values of all fields - this is the way APropertyPrecomputed's factory function is set to APropertyPrecomputed.

Examples#

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

7GUIs Flight Booker - Flight Booker.

private:
    DateTextFieldState mDepartureDate { system_clock::now() }, mReturnDate { system_clock::now() };
    AProperty<bool> mIsReturnFlight;
    APropertyPrecomputed<bool> mIsValid = [&] {
        if (!mDepartureDate.parsed->hasValue()) {
            return false;
        }
        if (!mIsReturnFlight) {
            return true;

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

7GUIs Cells - Spreadsheet processor (Excel).

struct Cell {
private:
    formula::Precompiled precompile();
    APropertyPrecomputed<formula::Precompiled> expressionPrecompiled = [&] { return precompile(); };
    formula::Value evaluate();

public:
    Spreadsheet* spreadsheet = nullptr;
    AProperty<AString> expression;

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

7GUIs Timer - Timer example.

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

    APropertyPrecomputed<aui::float_within_0_1> mElapsedTimeRatio = [&] {
        return float(mElapsedTime->count()) / float(mDuration->count());

Public Methods#

invalidate#


void APropertyPrecomputed::invalidate()

Marks this precomputed property to be reevaluated.

In common, you won't need to use this function. APropertyPrecomputed is reevaluated automatically as soon as one updates a property expression depends on.

readProjected#


auto APropertyPrecomputed::readProjected(Projection&& projection)

Makes a readonly projection of this property.

Examples#

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/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/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 {