diff --git a/src/kdbindings/signal.h b/src/kdbindings/signal.h index 9cfc66e..cbd5605 100644 --- a/src/kdbindings/signal.h +++ b/src/kdbindings/signal.h @@ -242,6 +242,46 @@ class Signal return m_connections.insert({ slot }); } + // Connect a callback function with Throttling + Private::GenerationalIndex connectWithThrottling(std::function const &slot, int interval) + { + std::chrono::milliseconds throttleDelay(interval); + auto lastCallTime = std::chrono::high_resolution_clock::now() - throttleDelay; // Initialize so it can be triggered immediately the first time. + + auto throttleCallBack = [slot = std::move(slot), throttleDelay, lastCallTime](Args... args) mutable { + auto now = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastCallTime); + + if (elapsed.count() >= throttleDelay.count()) { + slot(args...); + lastCallTime = now; + } + }; + + return m_connections.insert({ throttleCallBack }); + } + + // Connect a callback function with Debouncing + Private::GenerationalIndex connectWithDebouncing(std::function const &slot, int interval) + { + std::chrono::milliseconds debounceDelay(interval); + auto lastEventTime = std::chrono::high_resolution_clock::now(); + auto lastCallTime = lastEventTime - debounceDelay; // Initialize so it can be triggered immediately the first time. + + auto debounceCallBack = [slot = std::move(slot), debounceDelay, lastEventTime, lastCallTime](Args... args) mutable { + auto now = std::chrono::high_resolution_clock::now(); + lastEventTime = now; + + auto timeSinceLastCall = std::chrono::duration_cast(now - lastCallTime); + if (timeSinceLastCall.count() >= debounceDelay.count()) { + slot(args...); + lastCallTime = now; + } + }; + + return m_connections.insert({ debounceCallBack }); + } + // Disconnects a previously connected function void disconnect(const Private::GenerationalIndex &id) override { diff --git a/tests/signal/tst_signal.cpp b/tests/signal/tst_signal.cpp index 6636368..bd5f1f5 100644 --- a/tests/signal/tst_signal.cpp +++ b/tests/signal/tst_signal.cpp @@ -93,6 +93,48 @@ TEST_CASE("Signal connections") REQUIRE(lambdaCalled == true); } + SUBCASE("Test connectWithThrottling") + { + Signal signal; + + int count = 0; + auto handle = signal.connectWithThrottling([&count](int value) { + count++; + }, + 100); + + signal.emit(2); + REQUIRE(count == 1); // First emission should trigger the slot immediately + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Within the throttling interval + signal.emit(2); + REQUIRE(count == 1); // Second emission shouldn't trigger the slot due to throttling + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); // After the throttling interval + signal.emit(2); + REQUIRE(count == 2); // Third emission should trigger the slot + } + SUBCASE("Test connectWithDebouncing") + { + Signal signal; + int count = 0; + auto handle = signal.connectWithDebouncing([&count](int value) { + count++; + }, + 100); + + signal.emit(1); + REQUIRE(count == 1); // Debouncing interval hasn't passed + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Still within the debouncing interval + signal.emit(2); + REQUIRE(count == 1); // Debouncing interval still hasn't passed + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); // After the debouncing interval + signal.emit(2); + REQUIRE(count == 2); + } + SUBCASE("A signal with arguments can be connected to a lambda and invoked with const l-value args") { Signal signal;