Aurore
A Switch-like interface for your app, on every screen
Loading...
Searching...
No Matches
observable.hpp
Go to the documentation of this file.
1/*
2 Copyright 2026 JigSawFr
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17#pragma once
18
19#include <cstddef>
20#include <functional>
21#include <memory>
22#include <string>
23#include <type_traits>
24#include <typeindex>
25#include <unordered_map>
26#include <utility>
27#include <vector>
28
29namespace aurore
30{
31
32// What subscribe() returns: the subscriber stays subscribed as long as the
33// Subscription lives. Destroying it (or calling cancel()) unsubscribes; it
34// can outlive the Observable (it then does nothing). A view keeps the
35// subscriptions of its bindings (View::keepSubscription), so they end with it.
37{
38 public:
39 Subscription() = default;
40 explicit Subscription(std::function<void()> cancel)
41 : cancelFunction(std::move(cancel))
42 {
43 }
44
45 Subscription(Subscription&& other) noexcept
46 : cancelFunction(std::move(other.cancelFunction))
47 {
48 other.cancelFunction = nullptr;
49 }
50
52 {
53 if (this != &other)
54 {
55 this->cancel();
56 this->cancelFunction = std::move(other.cancelFunction);
57 other.cancelFunction = nullptr;
58 }
59 return *this;
60 }
61
62 Subscription(const Subscription&) = delete;
64
66 {
67 this->cancel();
68 }
69
70 void cancel()
71 {
72 if (this->cancelFunction)
73 {
74 auto function = std::move(this->cancelFunction);
75 this->cancelFunction = nullptr;
76 function();
77 }
78 }
79
80 bool active() const
81 {
82 return this->cancelFunction != nullptr;
83 }
84
85 private:
86 std::function<void()> cancelFunction;
87};
88
89// A value that tells its subscribers when it changes:
90//
91// aurore::Observable<std::string> name { "Ada" };
92// label->bindText(name); // the label shows "Ada"
93// name = "Grace"; // ... then "Grace"
94//
95// set() notifies only when the value changes (operator== when T has one).
96// Subscribers are called on the thread that calls set(): set it from the UI
97// thread (aurore::sync from a worker).
98template <typename T>
100{
101 public:
102 using Subscriber = std::function<void(const T&)>;
103
105 : state(std::make_shared<State>())
106 {
107 }
108
109 Observable(T value)
110 : state(std::make_shared<State>())
111 {
112 this->state->value = std::move(value);
113 }
114
115 // An Observable is a place: copying it would leave the subscribers on
116 // the original.
117 Observable(const Observable&) = delete;
118 Observable& operator=(const Observable&) = delete;
119
120 const T& get() const
121 {
122 return this->state->value;
123 }
124
125 operator const T&() const
126 {
127 return this->state->value;
128 }
129
130 void set(T value)
131 {
132 if constexpr (std::is_invocable_r_v<bool, std::equal_to<>, const T&, const T&>)
133 {
134 if (this->state->value == value)
135 return;
136 }
137 this->state->value = std::move(value);
138 this->notify();
139 }
140
142 {
143 this->set(std::move(value));
144 return *this;
145 }
146
147 // Calls every subscriber with the value, changed or not (after an
148 // in-place change of a value set() cannot compare).
149 void notify()
150 {
151 // A copy: a subscriber may subscribe or unsubscribe while called.
152 std::shared_ptr<State> keep = this->state;
153 std::vector<std::pair<size_t, Subscriber>> subscribers = keep->subscribers;
154 for (auto& [id, subscriber] : subscribers)
155 {
156 if (keep->contains(id))
157 subscriber(keep->value);
158 }
159 }
160
161 // `subscriber` called with the new value at each change, until the
162 // returned Subscription is destroyed.
163 [[nodiscard]] Subscription subscribe(Subscriber subscriber)
164 {
165 size_t id = ++this->state->lastId;
166 this->state->subscribers.emplace_back(id, std::move(subscriber));
167 std::weak_ptr<State> weak = this->state;
168 return Subscription([weak, id]
169 {
170 if (auto state = weak.lock())
171 state->remove(id); });
172 }
173
174 size_t getSubscriberCount() const
175 {
176 return this->state->subscribers.size();
177 }
178
179 private:
180 struct State
181 {
182 T value {};
183 size_t lastId = 0;
184 std::vector<std::pair<size_t, Subscriber>> subscribers;
185
186 bool contains(size_t id) const
187 {
188 for (auto& entry : this->subscribers)
189 if (entry.first == id)
190 return true;
191 return false;
192 }
193
194 void remove(size_t id)
195 {
196 for (auto it = this->subscribers.begin(); it != this->subscribers.end(); ++it)
197 {
198 if (it->first == id)
199 {
200 this->subscribers.erase(it);
201 return;
202 }
203 }
204 }
205 };
206
207 std::shared_ptr<State> state;
208};
209
210// Observables named for XML bindings (bind="text: profile.name"). A model is
211// registered under a name on the Application (every screen) or on an
212// Activity (its own screen, looked up first); its observables must outlive
213// it, or be removed first.
214//
215// aurore::Observable<std::string> name { "Ada" };
216// aurore::Observable<bool> online { true };
217// aurore::DataModel profile;
218// profile.add("name", name);
219// profile.add("online", online);
220// aurore::Application::registerModel("profile", &profile);
222{
223 public:
224 template <typename T>
225 void add(const std::string& key, Observable<T>& observable)
226 {
227 this->entries.insert_or_assign(key, Entry { std::type_index(typeid(T)), &observable });
228 }
229
230 void remove(const std::string& key)
231 {
232 this->entries.erase(key);
233 }
234
235 // The observable under `key` when it holds a T, else nullptr.
236 template <typename T>
237 Observable<T>* get(const std::string& key) const
238 {
239 auto it = this->entries.find(key);
240 if (it == this->entries.end() || it->second.type != std::type_index(typeid(T)))
241 return nullptr;
242 return static_cast<Observable<T>*>(it->second.observable);
243 }
244
245 bool has(const std::string& key) const
246 {
247 return this->entries.count(key) > 0;
248 }
249
250 private:
251 struct Entry
252 {
253 std::type_index type;
254 void* observable;
255 };
256
257 std::unordered_map<std::string, Entry> entries;
258};
259
260} // namespace aurore
Definition observable.hpp:222
void add(const std::string &key, Observable< T > &observable)
Definition observable.hpp:225
Observable< T > * get(const std::string &key) const
Definition observable.hpp:237
void remove(const std::string &key)
Definition observable.hpp:230
bool has(const std::string &key) const
Definition observable.hpp:245
Definition observable.hpp:100
Observable(const Observable &)=delete
Observable & operator=(const Observable &)=delete
Observable(T value)
Definition observable.hpp:109
void notify()
Definition observable.hpp:149
Observable()
Definition observable.hpp:104
Subscription subscribe(Subscriber subscriber)
Definition observable.hpp:163
std::function< void(const T &)> Subscriber
Definition observable.hpp:102
size_t getSubscriberCount() const
Definition observable.hpp:174
const T & get() const
Definition observable.hpp:120
void set(T value)
Definition observable.hpp:130
Observable & operator=(T value)
Definition observable.hpp:141
Definition observable.hpp:37
Subscription(const Subscription &)=delete
void cancel()
Definition observable.hpp:70
Subscription(std::function< void()> cancel)
Definition observable.hpp:40
Subscription & operator=(Subscription &&other) noexcept
Definition observable.hpp:51
~Subscription()
Definition observable.hpp:65
bool active() const
Definition observable.hpp:80
Subscription & operator=(const Subscription &)=delete
Subscription(Subscription &&other) noexcept
Definition observable.hpp:45
Definition actions.hpp:25