lxgui
Loading...
Searching...
No Matches
utils_signal.hpp
1#ifndef LXGUI_UTILS_SIGNAL_HPP
2#define LXGUI_UTILS_SIGNAL_HPP
3
4#include "lxgui/utils.hpp"
5#include "lxgui/utils_observer.hpp"
6#include "lxgui/utils_view.hpp"
7
8#include <algorithm>
9#include <functional>
10#include <memory>
11#include <vector>
12
13namespace lxgui::utils {
14
17namespace signal_impl {
18
19struct slot_base {
20 bool disconnected = false;
21};
22
23} // namespace signal_impl
27
29 template<typename T>
30 friend class signal;
31
32 explicit connection(utils::observer_ptr<signal_impl::slot_base> slot) noexcept :
33 slot_(std::move(slot)) {}
34
35public:
37 connection() = default;
38
39 // Copiable, movable.
40 connection(const connection&) = default;
41 connection(connection&&) = default;
42 connection& operator=(const connection&) = default;
44
46 void disconnect() noexcept {
47 if (auto* raw = slot_.get()) {
48 raw->disconnected = true;
49 slot_ = nullptr;
50 }
51 }
52
54 [[nodiscard]] bool connected() const noexcept {
55 if (auto* raw = slot_.get())
56 return !raw->disconnected;
57 else
58 return false;
59 }
60
61private:
62 utils::observer_ptr<signal_impl::slot_base> slot_;
63};
64
67public:
69 scoped_connection() = default;
70
72 scoped_connection(connection c) noexcept : connection(std::move(c)) {}
73
75 ~scoped_connection() noexcept {
76 disconnect();
77 }
78
79 // Non-copiable, movable.
84
87
90};
91
141template<typename T>
142class signal {
143public:
151 using function_type = std::function<T>;
152
153private:
159 struct slot : signal_impl::slot_base {
160 explicit slot(function_type func) noexcept : callback(std::move(func)) {}
161
162 function_type callback;
163 };
164
187 template<typename U>
188 using container_type = std::vector<U>;
189
197 using slot_list = container_type<utils::owner_ptr<slot>>;
198
200 template<typename BaseIterator>
201 struct slot_dereferencer {
202 using data_type = const function_type&;
203 static data_type dereference(const BaseIterator& iter) noexcept {
204 return (*iter)->callback;
205 }
206 };
207
209 template<typename BaseIterator>
210 struct non_disconnected_filter {
211 static bool is_included(const BaseIterator& iter) noexcept {
212 return !(*iter)->disconnected;
213 }
214 };
215
216public:
220
222 signal() : impl_(std::make_shared<implementation>()) {}
223
226 // Mark all slots as disconnected, in case the destructor
227 // is called midway through the signal being emitted.
229 }
230
231 // Non-copiable, movable.
232 signal(const signal&) = delete;
233 signal(signal&&) = default;
234 signal& operator=(const signal&) = delete;
235 signal& operator=(signal&&) = default;
236
238 void disconnect_all() noexcept {
239 if (impl_->recursion == 0u) {
240 impl_->slots.clear();
241 return;
242 }
243
244 // We are in the middle of an iteration; just mark slot as
245 // disconnected. The slots will not be called, however they still
246 // occupy a spot in the slot list. This is done to avoid invalidating
247 // iterators while iterating over the slot list, if one of the
248 // slot happens to disconnect itself. The memory will be cleared
249 // up when the signal is no longer being iterated on.
250 for (auto& slt : impl_->slots)
251 slt->disconnected = true;
252 }
253
258 [[nodiscard]] bool empty() const noexcept {
259 const auto& slots = impl_->slots;
260 return std::none_of(
261 slots.begin(), slots.end(), [](const auto& slt) { return !slt->disconnected; });
262 }
263
269 [[nodiscard]] slot_list_view slots() const noexcept {
270 return slot_list_view(impl_->slots);
271 }
272
284 impl_->slots.push_back(utils::make_owned<slot>(std::move(function)));
285 return connection(impl_->slots.back());
286 }
287
292 template<typename... Args>
293 void operator()(Args&&... args) {
294 // Make a shared-ownership copy of the slot list, so that the list
295 // survives even if this signal is destroyed midway during a slot.
296 // To support this, we must not use any member variable or member
297 // function of the signal object, which may be destroyed at any time.
298 [[maybe_unused]] const auto impl_copy_ptr = impl_;
299 auto& impl = *impl_;
300 const auto& slots = impl.slots;
301 auto& recursion = impl.recursion;
302
303 ++recursion;
304
305 // Call the slots
306 // NB: Cache the size here, so new slots connected will not trigger
307 // NB: Use integer-based iteration since iterators may be invalidated on insertion
308 const std::size_t num_slots = slots.size();
309 for (std::size_t i = 0; i < num_slots; ++i) {
310 auto& slt = *slots[i];
311 if (!slt.disconnected)
312 slt.callback(args...);
313 }
314
315 --recursion;
316
317 if (recursion == 0u)
318 impl.garbage_collect();
319 }
320
321private:
322 struct implementation {
323 slot_list slots;
324 std::size_t recursion = 0u;
325
326 void garbage_collect() noexcept {
327 auto iter = std::remove_if(
328 slots.begin(), slots.end(), [](const auto& slt) { return slt->disconnected; });
329
330 slots.erase(iter, slots.end());
331 }
332 };
333
334 std::shared_ptr<implementation> impl_;
335};
336
337} // namespace lxgui::utils
338
339#endif
Object representing the connection between a slot and a signal.
connection()=default
Default constructor, no connection.
bool connected() const noexcept
Check if this slot is still connected.
connection(connection &&)=default
connection(const connection &)=default
connection & operator=(const connection &)=default
connection & operator=(connection &&)=default
void disconnect() noexcept
Disconnect the slot.
A connection that automatically disconnects when going out of scope.
scoped_connection & operator=(scoped_connection &&)=default
scoped_connection & operator=(const scoped_connection &)=delete
scoped_connection(scoped_connection &&)=default
~scoped_connection() noexcept
Destructor, disconnects.
scoped_connection(connection c) noexcept
Conversion constructor from a raw connection.
scoped_connection(const scoped_connection &)=delete
void disconnect() noexcept
Disconnect the slot.
scoped_connection()=default
Default constructor, no connection.
Generic class for observing and triggering events.
slot_list_view slots() const noexcept
Return a constant view onto the connected slots.
signal & operator=(const signal &)=delete
utils::view::adaptor< slot_list, slot_dereferencer, non_disconnected_filter > slot_list_view
Type of the view returned by slots().
void disconnect_all() noexcept
Disconnects all slots.
bool empty() const noexcept
Check if this signal contains any slot.
connection connect(function_type function)
Connect a new slot to this signal.
signal(const signal &)=delete
signal()
Default constructor (no slot).
signal(signal &&)=default
signal & operator=(signal &&)=default
void operator()(Args &&... args)
Trigger the signal.
std::function< T > function_type
Type of the callable function stored in a slot. Can use any function/delegate type here,...
Allow iterating over a container without access to the container itself.
STL namespace.