VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
eventHandler.h
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis Lektauers on 12.10.2025.
5//
6#pragma once
7#include <algorithm>
8#include <any>
9#include <functional>
10#include <memory>
11#include <string>
12#include <tuple>
13#include <type_traits>
14#include <unordered_map>
15#include <utility>
16#include <vector>
17
18namespace visutwin::canvas
19{
20 using EventArgs = std::vector<std::any>;
21 using HandleEventCallback = std::function<void(const EventArgs&)>;
22
23 class EventHandler;
24
26 {
27 public:
28 EventHandle(EventHandler* handler, const std::string& name, HandleEventCallback callback, void* scope = nullptr,
29 bool once = false);
30
31 void callback(const EventArgs& args) const;
32
33 // Remove this event from its handler.
34 void off();
35
36 // Mark if the event has been removed
37 void setRemoved(bool value);
38
39 [[nodiscard]] bool removed() const { return _removed; }
40
41 private:
42 friend class EventHandler;
43
44 EventHandler* _handler;
45
46 std::string _name;
47
48 HandleEventCallback _callback;
49
50 void* _scope;
51
52 bool _once;
53
54 bool _removed;
55 };
56
61 {
62 public:
63 virtual ~EventHandler() = default;
64
65 EventHandle* on(const std::string& name, HandleEventCallback callback, void* scope = nullptr);
66 EventHandle* once(const std::string& name, HandleEventCallback callback, void* scope = nullptr);
67
68 template<typename Callback>
69 EventHandle* on(const std::string& name, Callback&& callback, void* scope = nullptr)
70 requires(!std::is_same_v<std::decay_t<Callback>, HandleEventCallback>)
71 {
72 return on(name, adaptCallback(std::forward<Callback>(callback)), scope);
73 }
74
75 template<typename Callback>
76 EventHandle* once(const std::string& name, Callback&& callback, void* scope = nullptr)
77 requires(!std::is_same_v<std::decay_t<Callback>, HandleEventCallback>)
78 {
79 return once(name, adaptCallback(std::forward<Callback>(callback)), scope);
80 }
81
82 // Removes callbacks. If name is empty, all events are removed.
83 EventHandler* off(const std::string& name = "", const HandleEventCallback& callback = HandleEventCallback(),
84 void* scope = nullptr);
86
87 template<typename Callback>
88 EventHandler* off(const std::string& name, Callback&& callback, void* scope = nullptr)
89 requires(!std::is_same_v<std::decay_t<Callback>, HandleEventCallback>)
90 {
91 return off(name, adaptCallback(std::forward<Callback>(callback)), scope);
92 }
93
94 void initEventHandler();
95
96 template<typename... Args>
97 EventHandler* fire(const std::string& name, Args&&... args);
98
99 [[nodiscard]] bool hasEvent(const std::string& name) const;
100
101 protected:
102 EventHandle* addCallback(const std::string& name, HandleEventCallback callback, void* scope = nullptr, bool once = false);
103
104 private:
105 void compactRemovedHandles();
106
107 template<typename T>
108 struct function_traits;
109
110 template<typename R, typename... FnArgs>
111 struct function_traits<R(*)(FnArgs...)>
112 {
113 using args_tuple = std::tuple<FnArgs...>;
114 static constexpr size_t arity = sizeof...(FnArgs);
115 };
116
117 template<typename R, typename... FnArgs>
118 struct function_traits<std::function<R(FnArgs...)>>
119 {
120 using args_tuple = std::tuple<FnArgs...>;
121 static constexpr size_t arity = sizeof...(FnArgs);
122 };
123
124 template<typename C, typename R, typename... FnArgs>
125 struct function_traits<R(C::*)(FnArgs...) const>
126 {
127 using args_tuple = std::tuple<FnArgs...>;
128 static constexpr size_t arity = sizeof...(FnArgs);
129 };
130
131 template<typename C, typename R, typename... FnArgs>
132 struct function_traits<R(C::*)(FnArgs...)>
133 {
134 using args_tuple = std::tuple<FnArgs...>;
135 static constexpr size_t arity = sizeof...(FnArgs);
136 };
137
138 template<typename F>
139 struct function_traits : function_traits<decltype(&F::operator())> {};
140
141 template<typename Arg>
142 static bool canReadArg(const std::any& value)
143 {
144 using Value = std::remove_cvref_t<Arg>;
145 return std::any_cast<Value>(&value) != nullptr;
146 }
147
148 template<typename Arg>
149 static decltype(auto) readArg(const std::any& value)
150 {
151 using Value = std::remove_cvref_t<Arg>;
152 static_assert(!std::is_lvalue_reference_v<Arg> || std::is_const_v<std::remove_reference_t<Arg>>,
153 "Event callback args cannot be non-const lvalue references.");
154
155 const auto* ptr = std::any_cast<Value>(&value);
156 if constexpr (std::is_reference_v<Arg>) {
157 return static_cast<const Value&>(*ptr);
158 } else {
159 return static_cast<Value>(*ptr);
160 }
161 }
162
163 template<typename Tuple, typename Callback, size_t... I>
164 static bool invokeTuple(Callback& callback, const EventArgs& args, std::index_sequence<I...>)
165 {
166 if (args.size() < sizeof...(I)) {
167 return false;
168 }
169
170 if (!(canReadArg<std::tuple_element_t<I, Tuple>>(args[I]) && ...)) {
171 return false;
172 }
173
174 callback(readArg<std::tuple_element_t<I, Tuple>>(args[I])...);
175 return true;
176 }
177
178 template<typename Callback>
179 static HandleEventCallback adaptCallback(Callback&& callback)
180 {
181 using Fn = std::decay_t<Callback>;
182 static_assert(std::is_invocable_v<Fn, const EventArgs&> || std::is_invocable_v<Fn> ||
183 (function_traits<Fn>::arity > 0),
184 "Event callback must be invocable as fn(), fn(const EventArgs&) or fn(arg0, ...).");
185
186 if constexpr (std::is_invocable_v<Callback, const EventArgs&>) {
187 return [fn = std::forward<Callback>(callback)](const EventArgs& args) mutable {
188 fn(args);
189 };
190 } else if constexpr (std::is_invocable_v<Callback>) {
191 return [fn = std::forward<Callback>(callback)](const EventArgs&) mutable {
192 fn();
193 };
194 } else {
195 using Traits = function_traits<Fn>;
196 using Tuple = typename Traits::args_tuple;
197 return [fn = std::forward<Callback>(callback)](const EventArgs& args) mutable {
198 (void)invokeTuple<Tuple>(fn, args, std::make_index_sequence<Traits::arity>{});
199 };
200 }
201 }
202
203 // Map of event names to their callback lists
204 std::unordered_map<std::string, std::vector<EventHandle*>> _callbacks;
205
206 // Map of currently active (executing) callback lists
207 std::unordered_map<std::string, std::vector<EventHandle*>> _callbackActive;
208
209 // Owner storage to avoid leaks while preserving stable handles.
210 std::vector<std::unique_ptr<EventHandle>> _eventHandles;
211 };
212
213 template<typename... Args>
214 EventHandler* EventHandler::fire(const std::string& name, Args&&... args)
215 {
216 if (name.empty()) {
217 return this;
218 }
219
220 auto callbacksIt = _callbacks.find(name);
221 if (callbacksIt == _callbacks.end()) {
222 return this;
223 }
224
225 std::vector<EventHandle*>* callbacks = nullptr;
226 std::vector<EventHandle*>& callbacksInitial = callbacksIt->second;
227
228 auto activeIt = _callbackActive.find(name);
229 if (activeIt == _callbackActive.end()) {
230 // when starting callback execution ensure we store a list of initial callbacks
231 _callbackActive[name] = callbacksInitial;
232 } else if (activeIt->second != callbacksInitial) {
233 // if we are trying to execute a callback while there is an active execution right now
234 // and the active list has been already modified,
235 // then we go to an unoptimized path and clone the callbacks list to ensure execution consistency
236 static thread_local std::vector<EventHandle*> tempCallbacks;
237 tempCallbacks = callbacksInitial;
238 callbacks = &tempCallbacks;
239 }
240
241 std::vector<EventHandle*>& activeCallbacks = callbacks ? *callbacks : _callbackActive[name];
242 EventArgs eventArgs;
243 eventArgs.reserve(sizeof...(Args));
244 (eventArgs.emplace_back(std::forward<Args>(args)), ...);
245
246 for (size_t i = 0; i < activeCallbacks.size(); i++) {
247 EventHandle* evt = activeCallbacks[i];
248 if (!evt || !evt->_callback || evt->_removed)
249 {
250 continue;
251 }
252
253 evt->callback(eventArgs);
254
255 if (evt->_once) {
256 // check that callback still exists because user may have unsubscribed in the event handler
257 auto existingCallbackIt = _callbacks.find(name);
258 if (existingCallbackIt == _callbacks.end())
259 {
260 continue;
261 }
262
263 std::vector<EventHandle*>& existingCallback = existingCallbackIt->second;
264 auto it = std::find(existingCallback.begin(), existingCallback.end(), evt);
265 int ind = (it != existingCallback.end()) ? std::distance(existingCallback.begin(), it) : -1;
266
267 if (ind != -1) {
268 if (_callbackActive[name] == existingCallback) {
269 _callbackActive[name] = std::vector<EventHandle*>(existingCallback);
270 }
271
272 auto callbacksIt2 = _callbacks.find(name);
273 if (callbacksIt2 == _callbacks.end()) continue;
274
275 std::vector<EventHandle*>& callbacks2 = callbacksIt2->second;
276 callbacks2[ind]->setRemoved(true);
277 callbacks2[ind]->_handler = nullptr;
278 callbacks2[ind]->_callback = HandleEventCallback();
279 callbacks2.erase(callbacks2.begin() + ind);
280
281 if (callbacks2.empty()) {
282 _callbacks.erase(name);
283 }
284 }
285 }
286 }
287
288 if (!callbacks) {
289 _callbackActive.erase(name);
290 compactRemovedHandles();
291 }
292
293 return this;
294 }
295}
void callback(const EventArgs &args) const
EventHandle(EventHandler *handler, const std::string &name, HandleEventCallback callback, void *scope=nullptr, bool once=false)
EventHandler * fire(const std::string &name, Args &&... args)
EventHandle * once(const std::string &name, Callback &&callback, void *scope=nullptr)
EventHandle * once(const std::string &name, HandleEventCallback callback, void *scope=nullptr)
EventHandle * addCallback(const std::string &name, HandleEventCallback callback, void *scope=nullptr, bool once=false)
bool hasEvent(const std::string &name) const
EventHandle * on(const std::string &name, Callback &&callback, void *scope=nullptr)
EventHandler * offByHandle(EventHandle *handle)
EventHandler * off(const std::string &name="", const HandleEventCallback &callback=HandleEventCallback(), void *scope=nullptr)
EventHandler * off(const std::string &name, Callback &&callback, void *scope=nullptr)
virtual ~EventHandler()=default
EventHandle * on(const std::string &name, HandleEventCallback callback, void *scope=nullptr)
std::function< void(const EventArgs &)> HandleEventCallback
std::vector< std::any > EventArgs