QmlMaterial 0.1.0
Loading...
Searching...
No Matches
state_bindings.hpp
1#pragma once
2
3#include <QDebug>
4#include <QPointer>
5#include <QProperty>
6#include <algorithm>
7#include <functional>
8#include <map>
9#include <memory>
10#include <optional>
11#include <typeindex>
12#include <utility>
13#include <vector>
14
15namespace qml_material
16{
17namespace detail
18{
19struct StateBindingOwner {
20 std::function<bool()> alive;
21 bool stopped = false;
22
23 bool isActive() {
24 if (stopped) return false;
25 if (alive && ! alive()) stopped = true;
26 return ! stopped;
27 }
28};
29
30struct StateProperty {
31 QPointer<QObject> owner;
32 std::type_index accessor;
33 std::shared_ptr<StateBindingOwner> lifetime;
34 StateProperty(QObject* object, std::type_index type, std::shared_ptr<StateBindingOwner> guard)
35 : owner(object), accessor(type), lifetime(std::move(guard)) {}
36 virtual ~StateProperty() = default;
37 virtual void apply(std::optional<qint64>) = 0;
38 virtual void abandon() = 0;
39};
40
41template<typename T>
42struct StatePropertyOf : StateProperty {
43 std::function<QBindable<T>()> access;
44 std::map<qint64, QPropertyBinding<T>> overrides;
45 // Reconstruct handles; Qt 6.11's copy assignment can free the replacement binding.
46 std::optional<QPropertyBinding<T>> base, saved;
47 T initial {}, savedValue {};
48 bool covered = false, stopped = false, baseDeclared = false;
49
50 StatePropertyOf(QObject* object, std::type_index type, std::shared_ptr<StateBindingOwner> guard,
51 std::function<QBindable<T>()> getter)
52 : StateProperty(object, type, std::move(guard)), access(std::move(getter)) {
53 if (! isActive()) return;
54 initial = access().value();
55 if (! isActive()) return;
56 const auto binding = access().binding();
57 if (! binding.isNull())
58 base.emplace(binding);
59 else
60 base.emplace(Qt::makePropertyBinding([value = initial] {
61 return value;
62 }));
63 }
64
65 bool isActive() {
66 if (! stopped && owner && lifetime->isActive() && owner && ! stopped) return true;
67 abandon();
68 return false;
69 }
70
71 void install(QPropertyBinding<T> binding) {
72 if (! isActive()) return;
73 access().setBinding(binding);
74 }
75
76 void reset() {
77 if (! isActive()) return;
78 if (! base)
79 access().setValue(initial);
80 else
81 install(*base);
82 }
83
84 void apply(std::optional<qint64> state) override {
85 if (! isActive()) return;
86 const auto next = state ? overrides.find(*state) : overrides.end();
87 if (next != overrides.end()) {
88 const auto binding = next->second;
89 if (! covered) {
90 const auto original = access().binding();
91 if (! isActive()) return;
92 const auto value = original.isNull() ? access().value() : T {};
93 if (! isActive()) return;
94 saved.emplace(original);
95 savedValue = value;
96 covered = true;
97 }
98 install(binding);
99 } else if (covered) {
100 covered = false;
101 const auto original = std::move(*saved);
102 saved.reset();
103 const auto value = savedValue;
104 if (! isActive()) return;
105 if (original.isNull())
106 access().setValue(value);
107 else
108 access().setBinding(original);
109 }
110 }
111
112 void abandon() override {
113 stopped = true;
114 saved.reset();
115 base.reset();
116 overrides.clear();
117 }
118};
119
120struct StateBindingData {
121 std::shared_ptr<StateBindingOwner> lifetime = std::make_shared<StateBindingOwner>();
122 std::vector<std::shared_ptr<StateProperty>> properties;
123 std::map<qint64, std::vector<std::shared_ptr<StateProperty>>> states;
124 std::optional<qint64> current;
125 bool frozen = false, switching = false, stopped = false;
126
127 void abandon() {
128 if (std::exchange(stopped, true)) return;
129 lifetime->stopped = true;
130 for (const auto& property : properties) property->abandon();
131 }
132
133 bool isActive() {
134 if (! stopped && lifetime->isActive() && ! stopped) return true;
135 abandon();
136 return false;
137 }
138
139 bool select(std::optional<qint64> next) {
140 if (! isActive()) return false;
141 if (switching) {
142 qWarning("StateBindings: recursive state change");
143 return false;
144 }
145 frozen = true;
146 if (current == next) return true;
147 auto affected = current ? states[*current] : std::vector<std::shared_ptr<StateProperty>> {};
148 if (next) {
149 for (const auto& property : states[*next]) {
150 if (std::find(affected.begin(), affected.end(), property) == affected.end())
151 affected.push_back(property);
152 }
153 }
154 switching = true;
155 current = next;
156 {
157 const QScopedPropertyUpdateGroup group;
158 for (const auto& property : affected) {
159 if (! isActive()) break;
160 property->apply(next);
161 }
162 }
163 switching = false;
164 return isActive();
165 }
166};
167
168template<auto Accessor>
169struct AccessorTag {};
170template<typename>
171struct BindableValue;
172template<typename T>
173struct BindableValue<QBindable<T>> {
174 using Type = T;
175};
176} // namespace detail
177
178template<typename T>
179class PropertyKey {
180 template<typename>
181 friend class StateBindingSet;
182 friend class StateBindings;
183 std::weak_ptr<detail::StateBindingData> m_owner;
184 std::shared_ptr<detail::StatePropertyOf<T>> m_property;
185
186public:
187 bool isValid() const {
188 const auto property = m_property;
189 return property && property->isActive();
190 }
191 void reset() const {
192 const auto property = m_property;
193 if (property) property->reset();
194 }
195};
196
197class StateBindings {
198 template<typename>
199 friend class StateBindingSet;
200 std::shared_ptr<detail::StateBindingData> m_data;
201 std::optional<qint64> m_state;
202 StateBindings(std::shared_ptr<detail::StateBindingData> data, std::optional<qint64> state)
203 : m_data(std::move(data)), m_state(state) {}
204
205public:
206 template<typename T>
207 bool bind(const PropertyKey<T>& key, const QPropertyBinding<T>& binding) const {
208 const auto data = m_data;
209 if (! data->isActive()) return false;
210 if (data->frozen || data->stopped || key.m_owner.lock() != data || ! key.isValid() ||
211 binding.isNull()) {
212 qWarning("StateBindings: invalid or frozen declaration");
213 return false;
214 }
215 const auto property = key.m_property;
216 if (m_state) {
217 if (! property->overrides.emplace(*m_state, binding).second) {
218 qWarning("StateBindings: duplicate property declaration");
219 return false;
220 }
221 data->states[*m_state].push_back(property);
222 } else {
223 if (property->baseDeclared) {
224 qWarning("StateBindings: duplicate base declaration");
225 return false;
226 }
227 property->base.emplace(binding);
228 property->baseDeclared = true;
229 property->install(binding);
230 }
231 return data->isActive();
232 }
233
234 template<typename T, typename F>
235 requires std::is_invocable_r_v<T, F> &&
236 std::is_same_v<std::remove_cvref_t<std::invoke_result_t<F>>, T>
237 bool bind(const PropertyKey<T>& key, F&& function) const {
238 return bind(key, Qt::makePropertyBinding(std::forward<F>(function)));
239 }
240};
241
242template<typename StateId>
243class StateBindingSet {
244 static_assert(std::is_enum_v<StateId>);
245 std::shared_ptr<detail::StateBindingData> m_data = std::make_shared<detail::StateBindingData>();
246 StateId m_base;
247
248public:
249 explicit StateBindingSet(StateId base): m_base(base) {}
250 StateBindingSet(const StateBindingSet&) = delete;
251 StateBindingSet& operator=(const StateBindingSet&) = delete;
252 ~StateBindingSet() { abandon(); }
253
254 // A false result is terminal, even while the property objects still exist.
255 bool setOwnerAliveCheck(std::function<bool()> check) {
256 const auto data = m_data;
257 if (data->frozen || data->stopped || ! data->properties.empty()) {
258 qWarning("StateBindings: owner check must precede property registration");
259 return false;
260 }
261 data->lifetime->alive = std::move(check);
262 return true;
263 }
264
265 template<auto Accessor, typename Object>
266 auto property(Object* object) {
267 using T =
268 typename detail::BindableValue<std::invoke_result_t<decltype(Accessor), Object*>>::Type;
269 PropertyKey<T> key;
270 const auto data = m_data;
271 if (! data->isActive()) return key;
272 if (! object || data->frozen || data->stopped) {
273 qWarning("StateBindings: invalid or frozen property registration");
274 return key;
275 }
276 const auto bindable = std::invoke(Accessor, object);
277 if (! bindable.isValid() || ! bindable.isBindable() || bindable.isReadOnly()) {
278 qWarning("StateBindings: property must be writable and bindable");
279 return key;
280 }
281 const std::type_index type(typeid(detail::AccessorTag<Accessor>));
282 key.m_owner = data;
283 for (const auto& existing : data->properties) {
284 if (existing->owner == object && existing->accessor == type) {
285 key.m_property = std::static_pointer_cast<detail::StatePropertyOf<T>>(existing);
286 return key;
287 }
288 }
289 const auto property =
290 std::make_shared<detail::StatePropertyOf<T>>(object, type, data->lifetime, [object] {
291 return std::invoke(Accessor, object);
292 });
293 if (! data->isActive() || ! property->isActive()) return key;
294 key.m_property = property;
295 data->properties.push_back(property);
296 return key;
297 }
298
299 StateBindings base() const { return { m_data, {} }; }
300 StateBindings state(StateId id) const {
301 return { m_data, id == m_base ? std::nullopt : std::optional<qint64>(qint64(id)) };
302 }
303 bool select(StateId id) {
304 const auto data = m_data;
305 return data->select(id == m_base ? std::nullopt : std::optional<qint64>(qint64(id)));
306 }
307 bool detach() {
308 const auto data = m_data;
309 const bool restored = data->select({});
310 data->abandon();
311 return restored;
312 }
313 void abandon() {
314 const auto data = m_data;
315 data->abandon();
316 }
317 class Lifetime {
318 std::shared_ptr<detail::StateBindingData> m_data;
319 friend class StateBindingSet;
320 explicit Lifetime(std::shared_ptr<detail::StateBindingData> data)
321 : m_data(std::move(data)) {}
322
323 public:
324 Lifetime(const Lifetime&) = delete;
325 ~Lifetime() { m_data->abandon(); }
326 };
327 Lifetime lifetime() const { return Lifetime(m_data); }
328};
329} // namespace qml_material