Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
command.hpp
Go to the documentation of this file.
1#pragma once
2
5#include "aria/detail/typed_signal.hpp"
6#include "aria/reactive/reactive.hpp" // Effect -- auto-tracks reads in predicate()
7
8#include <functional>
9#include <memory>
10#include <optional>
11#include <utility>
12
13namespace aria {
14
15namespace binding { class BindingEngine; }
16
57template<typename... Args>
58class Command {
59public:
60 using Action = std::function<void(Args...)>;
61 using Predicate = std::function<bool(const Args&...)>;
62 using CanExecuteSignal = detail::TypedSignal<bool>;
63
64 template<std::invocable<Args...> A>
65 explicit Command(A&& action)
66 : action_(std::forward<A>(action)),
67 predicate_([](const Args&...) noexcept { return true; }),
68 can_signal_(std::make_shared<CanExecuteSignal>()) {}
69
70 template<std::invocable<Args...> A, std::predicate<const Args&...> P>
71 Command(A&& action, P&& predicate)
72 : action_(std::forward<A>(action)),
73 predicate_(std::forward<P>(predicate)),
74 can_signal_(std::make_shared<CanExecuteSignal>()) {}
75
76 Command(const Command&) = delete;
77 Command& operator=(const Command&) = delete;
78 // Non-movable: a Command's identity is tied to its `can_signal_` and
79 // (for Command<>) the auto-tracking Effect, which captures a *copy* of
80 // the predicate at construction. Allowing moves would let the moved-to
81 // Command's `predicate_` diverge from the copy the Effect still calls,
82 // and silently re-home reactive plumbing. ViewModels hold Commands as
83 // direct members (never moved), exactly like Property / Computed, so
84 // deleting move costs nothing and removes a footgun.
85 Command(Command&&) = delete;
87
88 ~Command() { lifetime_.reset(); }
89
91 void execute(const Args&... args) {
92 if (predicate_(args...)) {
95 ::aria::trace::Command{"execute"});
96 }
97 action_(args...);
98 } else if (::aria::has_trace_sink()) {
100 ::aria::trace::Command{"rejected_can_execute"});
101 }
102 }
103
104 void operator()(const Args&... args) { execute(args...); }
105
106 [[nodiscard]] bool can_execute(const Args&... args) const {
107 return predicate_(args...);
108 }
109
118 void notify_can_execute_changed(const Args&... args) const {
121 ::aria::trace::Command{"can_execute_changed"});
122 }
123 can_signal_->emit(predicate_(args...));
124 }
125
126 [[nodiscard]] Subscription observe_can_execute(std::function<void(bool)> fn) {
127 return can_signal_->connect(std::move(fn));
128 }
129
130private:
132 std::weak_ptr<void> lifetime_token_() {
133 if (!lifetime_) lifetime_ = std::make_shared<char>();
134 return lifetime_;
135 }
136 Action action_;
137 Predicate predicate_;
138 std::shared_ptr<CanExecuteSignal> can_signal_;
139 std::shared_ptr<void> lifetime_;
140};
141
142// ----------------------------------------------------------------------------
143// Command<> specialisation: parameterless predicate can be auto-tracked.
144//
145// The predicate is wrapped in an `Effect`; every reactive read inside it
146// becomes an upstream edge, and the `Effect` re-fires whenever any of
147// those upstreams changes, emitting the fresh `can_execute()` on the
148// signal. As a result `BindingEngine::bind_command(cmd, button)` is
149// enough for the button's `enabled` to stay in sync with model state —
150// callers no longer need to sprinkle `notify_can_execute_changed()` in
151// their setters. This is the behaviour the docs describe.
152// ----------------------------------------------------------------------------
153template<>
155public:
156 using Action = std::function<void()>;
157 using Predicate = std::function<bool()>;
158 using CanExecuteSignal = detail::TypedSignal<bool>;
159
160 template<std::invocable<> A>
161 explicit Command(A&& action)
162 : action_(std::forward<A>(action)),
163 predicate_([]() noexcept { return true; }),
164 can_signal_(std::make_shared<CanExecuteSignal>()) {
165 // `can_execute` is a constant `true`; no Effect needed.
166 }
167
168 template<std::invocable<> A, std::predicate<> P>
169 Command(A&& action, P&& predicate)
170 : action_(std::forward<A>(action)),
171 predicate_(std::forward<P>(predicate)),
172 can_signal_(std::make_shared<CanExecuteSignal>()) {
173 // ── Eager auto-tracking contract ─────────────────────────────
174 // We install a reactive `Effect` right here in the constructor.
175 // The Effect runs the predicate exactly once synchronously, under
176 // a TrackingContext, so every reactive value it reads becomes an
177 // upstream edge. Subsequent changes to any of those upstreams
178 // re-fire the Effect, which then emits on `can_signal_` **only
179 // when the truth value actually flips** (equality gate).
180 //
181 // What this means for callers:
182 // * The predicate executes **once** during construction.
183 // It must therefore be safe to run at that moment — every
184 // Property / Computed it touches via `this->member` must
185 // already be fully constructed. Because C++ initialises
186 // non-static data members in declaration order, this is
187 // satisfied iff the Command<> is declared **after** every
188 // Property / Computed whose values its predicate reads.
189 // (Standard MVVM style already does this.)
190 // * Predicate should be side-effect free. Tracking is eager
191 // rather than lazy so that `can_execute()` is authoritative
192 // from the moment the Command exists and observers attached
193 // later do not need a priming emit.
194 // * The Effect stays alive for the lifetime of the Command,
195 // independent of any UI binding. This is intentional: a
196 // Command owns its own reactive plumbing so a ViewModel
197 // doesn't have to wire / unwire it in activate / deactivate.
198 // The cost is O(deps) shared_ptr / edge nodes; there is NO
199 // per-change work while upstreams stay stable.
200 auto signal = can_signal_;
201 auto last = std::make_shared<bool>();
202 effect_.emplace(
203 [pred = predicate_, signal, last,
204 primed = std::make_shared<bool>(false)]() mutable {
205 const bool now = pred();
206 if (!*primed) {
207 // First (eager) run: seed the cache and register
208 // every tracked source, but do NOT emit — no one
209 // can have observed us yet, and callers that
210 // connect later should read `can_execute()`
211 // synchronously to get the current state.
212 *primed = true;
213 *last = now;
214 return;
215 }
216 if (*last != now) {
217 *last = now;
218 signal->emit(now);
219 }
220 });
221 }
222
223 Command(const Command&) = delete;
224 Command& operator=(const Command&) = delete;
225 // Non-movable — see the primary template. The auto-tracking Effect
226 // captures a copy of the predicate, so a move would desync it.
227 Command(Command&&) = delete;
229
230 ~Command() { lifetime_.reset(); }
231
232 void execute() {
233 if (predicate_()) {
236 ::aria::trace::Command{"execute"});
237 }
238 action_();
239 } else if (::aria::has_trace_sink()) {
241 ::aria::trace::Command{"rejected_can_execute"});
242 }
243 }
244
245 void operator()() { execute(); }
246
247 [[nodiscard]] bool can_execute() const { return predicate_(); }
248
256 ::aria::trace::Command{"can_execute_changed"});
257 }
258 can_signal_->emit(predicate_());
259 }
260
261 [[nodiscard]] Subscription observe_can_execute(std::function<void(bool)> fn) {
262 return can_signal_->connect(std::move(fn));
263 }
264
265private:
267 std::weak_ptr<void> lifetime_token_() {
268 if (!lifetime_) lifetime_ = std::make_shared<char>();
269 return lifetime_;
270 }
271 Action action_;
272 Predicate predicate_;
273 std::shared_ptr<CanExecuteSignal> can_signal_;
274 // Effect owns the reactive node. `std::optional<Effect>` rather than a
275 // bare `Effect` so the default-true-predicate constructor can leave it
276 // empty (no auto-tracking node installed) without paying for a dummy
277 // Effect. The `std::nullopt` state represents "no auto-tracking effect
278 // was installed".
279 std::optional<reactive::Effect> effect_;
280 std::shared_ptr<void> lifetime_;
281};
282
283} // namespace aria
Definition command.hpp:154
Command & operator=(Command &&)=delete
Command(const Command &)=delete
Command(A &&action, P &&predicate)
Definition command.hpp:169
Command(A &&action)
Definition command.hpp:161
bool can_execute() const
Definition command.hpp:247
detail::TypedSignal< bool > CanExecuteSignal
Definition command.hpp:158
Subscription observe_can_execute(std::function< void(bool)> fn)
Definition command.hpp:261
void operator()()
Definition command.hpp:245
void notify_can_execute_changed() const
Force-emit a can_execute notification.
Definition command.hpp:253
std::function< void()> Action
Definition command.hpp:156
std::function< bool()> Predicate
Definition command.hpp:157
void execute()
Definition command.hpp:232
Command & operator=(const Command &)=delete
Command(Command &&)=delete
~Command()
Definition command.hpp:230
bool can_execute(const Args &... args) const
Definition command.hpp:106
Command(A &&action)
Definition command.hpp:65
void notify_can_execute_changed(const Args &... args) const
Manually notify observers that can_execute may have changed.
Definition command.hpp:118
Command(Command &&)=delete
Command(A &&action, P &&predicate)
Definition command.hpp:71
std::function< bool(const Args &...)> Predicate
Definition command.hpp:61
void operator()(const Args &... args)
Definition command.hpp:104
detail::TypedSignal< bool > CanExecuteSignal
Definition command.hpp:62
Subscription observe_can_execute(std::function< void(bool)> fn)
Definition command.hpp:126
std::function< void(Args...)> Action
Definition command.hpp:60
Command & operator=(const Command &)=delete
Command(const Command &)=delete
Command & operator=(Command &&)=delete
void execute(const Args &... args)
Invoke the action if can_execute(args...) is true.
Definition command.hpp:91
~Command()
Definition command.hpp:88
RAII handle to a single subscription.
Definition subscription.hpp:44
BindingEngine: connects ViewModel properties to platform views via an adapter.
Definition binding_engine.hpp:103
Definition binding_engine.hpp:25
Definition signal.hpp:12
void publish_trace_unchecked(const TraceEvent &event) noexcept
Publish an already-built event using one owning sink snapshot.
Definition diagnostics.hpp:293
bool has_trace_sink() noexcept
True iff a sink is currently installed.
Definition diagnostics.hpp:286
@ Command
Synchronous Command<Args...> execution.
Definition diagnostics.hpp:70
Definition validation_key.hpp:110
Synchronous Command<Args...> events.
Definition diagnostics.hpp:153