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]
class LogObserver : public AObject {
public:
void log(const AString& msg) {
ALogger::info("LogObserver") << "Received value: " << msg;
}
};
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");
name
to ""
, we don't access surname
. If we try to trigger the fast path return:
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).
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#
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#
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.