From 227016d40cfa754493cd797044e67170bc5a8f33 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sun, 23 Aug 2026 13:08:52 +0200 Subject: [PATCH] feat: add SetWillCloseHook to WindowManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a WindowWillCloseHook following the same pattern as the existing WindowWillShowHook and WindowWillHideHook. This allows FFI consumers (notably Flutter apps via nativeapi-flutter) to intercept window close and decide whether to proceed — needed for "unsaved changes" dialogs. Changes per layer: - window_manager.h: WindowWillCloseHook typedef, SetWillCloseHook, HasWillCloseHook, HandleWillClose, CallOriginalClose declarations. - macOS: swizzles performClose: on NSWindow (same pattern as makeKeyAndOrderFront: and orderOut:). CallOriginalClose calls the swizzled original. - Linux: adds a delete-event emission hook alongside show/hide. CallOriginalClose destroys the toplevel GtkWidget. - Windows: stores the hook; CallOriginalClose posts WM_CLOSE. Full WM_CLOSE interception (WH_CBT or window subclassing) is left as a follow-up — noted with a ponytail: comment. - C API: native_window_manager_set_will_close_hook, has_will_close_hook, handle_will_close, call_original_close. - Tests: window_manager_hook_test — verifies set/clear, dispatch, no-hook safety, and coexistence with show/hide hooks. All 6 tests pass on macOS. Build verified on macOS (CMake + make). --- src/capi/window_manager_c.cpp | 42 ++++ src/capi/window_manager_c.h | 14 ++ src/platform/linux/window_manager_linux.cpp | 65 ++++++- src/platform/macos/window_manager_macos.mm | 65 ++++++- .../windows/window_manager_windows.cpp | 36 +++- src/window_manager.h | 9 +- tests/CMakeLists.txt | 6 + tests/window_manager_hook_test.cpp | 182 ++++++++++++++++++ 8 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 tests/window_manager_hook_test.cpp diff --git a/src/capi/window_manager_c.cpp b/src/capi/window_manager_c.cpp index 1008ca3..1e0fbad 100644 --- a/src/capi/window_manager_c.cpp +++ b/src/capi/window_manager_c.cpp @@ -86,6 +86,20 @@ void native_window_manager_set_will_hide_hook(native_window_manager_set_will_hid } } +void native_window_manager_set_will_close_hook(native_window_manager_set_will_close_hook_callback_t hook, void* hook_user_data) { + try { + std::optional> hook_cpp; + if (hook) { + hook_cpp = [hook, hook_user_data](unsigned int arg0) { hook(arg0, hook_user_data); }; + } + nativeapi::WindowManager::GetInstance().SetWillCloseHook(hook_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_set_will_close_hook"); + return; + } +} + bool native_window_manager_has_will_show_hook(void) { try { return nativeapi::WindowManager::GetInstance().HasWillShowHook(); @@ -104,6 +118,15 @@ bool native_window_manager_has_will_hide_hook(void) { } } +bool native_window_manager_has_will_close_hook(void) { + try { + return nativeapi::WindowManager::GetInstance().HasWillCloseHook(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_has_will_close_hook"); + return false; + } +} + void native_window_manager_handle_will_show(native_window_id_t id) { try { nativeapi::WindowManager::GetInstance().HandleWillShow(id); @@ -124,6 +147,16 @@ void native_window_manager_handle_will_hide(native_window_id_t id) { } } +void native_window_manager_handle_will_close(native_window_id_t id) { + try { + nativeapi::WindowManager::GetInstance().HandleWillClose(id); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_handle_will_close"); + return; + } +} + bool native_window_manager_call_original_show(native_window_id_t id) { try { return nativeapi::WindowManager::GetInstance().CallOriginalShow(id); @@ -142,6 +175,15 @@ bool native_window_manager_call_original_hide(native_window_id_t id) { } } +bool native_window_manager_call_original_close(native_window_id_t id) { + try { + return nativeapi::WindowManager::GetInstance().CallOriginalClose(id); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_call_original_close"); + return false; + } +} + native_listener_id_t native_window_manager_add_listener(native_window_event_callback_t callback, void* user_data) { if (!callback) { return 0; diff --git a/src/capi/window_manager_c.h b/src/capi/window_manager_c.h index 50d6113..257b9ed 100644 --- a/src/capi/window_manager_c.h +++ b/src/capi/window_manager_c.h @@ -23,6 +23,8 @@ typedef void (*native_window_manager_set_will_show_hook_callback_t)(unsigned int typedef void (*native_window_manager_set_will_hide_hook_callback_t)(unsigned int arg0, void* user_data); +typedef void (*native_window_manager_set_will_close_hook_callback_t)(unsigned int arg0, void* user_data); + /// Caller owns the returned handle; release it with native_window_free(). FFI_PLUGIN_EXPORT native_window_t native_window_manager_get(native_window_id_t id); @@ -40,24 +42,36 @@ void native_window_manager_set_will_show_hook(native_window_manager_set_will_sho FFI_PLUGIN_EXPORT void native_window_manager_set_will_hide_hook(native_window_manager_set_will_hide_hook_callback_t hook, void* hook_user_data); +FFI_PLUGIN_EXPORT +void native_window_manager_set_will_close_hook(native_window_manager_set_will_close_hook_callback_t hook, void* hook_user_data); + FFI_PLUGIN_EXPORT bool native_window_manager_has_will_show_hook(void); FFI_PLUGIN_EXPORT bool native_window_manager_has_will_hide_hook(void); +FFI_PLUGIN_EXPORT +bool native_window_manager_has_will_close_hook(void); + FFI_PLUGIN_EXPORT void native_window_manager_handle_will_show(native_window_id_t id); FFI_PLUGIN_EXPORT void native_window_manager_handle_will_hide(native_window_id_t id); +FFI_PLUGIN_EXPORT +void native_window_manager_handle_will_close(native_window_id_t id); + FFI_PLUGIN_EXPORT bool native_window_manager_call_original_show(native_window_id_t id); FFI_PLUGIN_EXPORT bool native_window_manager_call_original_hide(native_window_id_t id); +FFI_PLUGIN_EXPORT +bool native_window_manager_call_original_close(native_window_id_t id); + /// Registers @p callback for every WindowEvent this WindowManager emits. /// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. FFI_PLUGIN_EXPORT diff --git a/src/platform/linux/window_manager_linux.cpp b/src/platform/linux/window_manager_linux.cpp index 93cbb37..f8903d6 100644 --- a/src/platform/linux/window_manager_linux.cpp +++ b/src/platform/linux/window_manager_linux.cpp @@ -122,6 +122,27 @@ static gboolean on_hide_emission_hook(GSignalInvocationHint* ihint, return TRUE; // Continue emission } +// Signal emission hook for delete-event signal +static gboolean on_delete_event_emission_hook(GSignalInvocationHint* ihint, + guint n_param_values, + const GValue* param_values, + gpointer data) { + (void)ihint; + (void)n_param_values; + (void)data; + + GtkWidget* widget = GTK_WIDGET(g_value_get_object(¶m_values[0])); + if (widget && GTK_IS_WINDOW(widget)) { + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (gdk_window) { + WindowId id = GetOrCreateWindowId(gdk_window); + WindowManager::GetInstance().HandleWillClose(id); + } + } + + return TRUE; // Continue emission +} + // GTK signal callbacks to invoke hooks (used as fallback) static gboolean OnGtkMapEvent(GtkWidget* widget, GdkEvent* event, gpointer user_data) { (void)event; @@ -181,9 +202,10 @@ static void InstallGlobalSwizzling() { return; } - // Get the show and hide signal IDs for GtkWidget + // Get the show, hide, and delete-event signal IDs for GtkWidget guint show_signal_id = g_signal_lookup("show", GTK_TYPE_WIDGET); guint hide_signal_id = g_signal_lookup("hide", GTK_TYPE_WIDGET); + guint delete_event_signal_id = g_signal_lookup("delete-event", GTK_TYPE_WIDGET); if (show_signal_id != 0) { // Add emission hook for show signal @@ -195,6 +217,11 @@ static void InstallGlobalSwizzling() { g_signal_add_emission_hook(hide_signal_id, 0, on_hide_emission_hook, nullptr, nullptr); } + if (delete_event_signal_id != 0) { + // Add emission hook for delete-event signal + g_signal_add_emission_hook(delete_event_signal_id, 0, on_delete_event_emission_hook, nullptr, nullptr); + } + g_swizzle_installed = true; } @@ -228,9 +255,10 @@ class WindowManager::Impl { private: WindowManager* manager_; - // Optional pre-show/hide hooks + // Optional pre-show/hide/close hooks std::optional will_show_hook_; std::optional will_hide_hook_; + std::optional will_close_hook_; friend class WindowManager; }; @@ -372,6 +400,14 @@ void WindowManager::SetWillHideHook(std::optional hook) { } } +void WindowManager::SetWillCloseHook(std::optional hook) { + pimpl_->will_close_hook_ = std::move(hook); + if (pimpl_->will_close_hook_) { + // Ensure global swizzling is installed when hook is set + InstallGlobalSwizzling(); + } +} + bool WindowManager::HasWillShowHook() const { return pimpl_->will_show_hook_.has_value(); } @@ -380,6 +416,10 @@ bool WindowManager::HasWillHideHook() const { return pimpl_->will_hide_hook_.has_value(); } +bool WindowManager::HasWillCloseHook() const { + return pimpl_->will_close_hook_.has_value(); +} + void WindowManager::HandleWillShow(WindowId id) { if (pimpl_->will_show_hook_) { (*pimpl_->will_show_hook_)(id); @@ -392,6 +432,12 @@ void WindowManager::HandleWillHide(WindowId id) { } } +void WindowManager::HandleWillClose(WindowId id) { + if (pimpl_->will_close_hook_) { + (*pimpl_->will_close_hook_)(id); + } +} + bool WindowManager::CallOriginalShow(WindowId id) { GdkWindow* gdk_window = FindGdkWindowById(id); if (!gdk_window) { @@ -414,6 +460,21 @@ bool WindowManager::CallOriginalHide(WindowId id) { return true; } +bool WindowManager::CallOriginalClose(WindowId id) { + GdkWindow* gdk_window = FindGdkWindowById(id); + if (!gdk_window) { + return false; + } + + // On Linux, destroy the underlying GtkWidget to close the window + GtkWidget* widget = gtk_widget_get_toplevel(GTK_WIDGET(gdk_window)); + if (widget && GTK_IS_WINDOW(widget)) { + gtk_widget_destroy(widget); + return true; + } + return false; +} + void WindowManager::StartEventListening() { pimpl_->StartEventListening(); } diff --git a/src/platform/macos/window_manager_macos.mm b/src/platform/macos/window_manager_macos.mm index d340bb5..f28471b 100644 --- a/src/platform/macos/window_manager_macos.mm +++ b/src/platform/macos/window_manager_macos.mm @@ -29,9 +29,10 @@ WindowManager* manager_; NativeAPIWindowManagerDelegate* delegate_; - // Optional pre-show/hide hooks + // Optional pre-show/hide/close hooks std::optional will_show_hook_; std::optional will_hide_hook_; + std::optional will_close_hook_; friend class WindowManager; }; @@ -44,6 +45,7 @@ @interface NSWindow (NativeAPISwizzle) - (void)na_swizzled_makeKeyAndOrderFront:(id)sender; - (void)na_swizzled_orderOut:(id)sender; +- (void)na_swizzled_performClose:(id)sender; @end @implementation NSWindow (NativeAPISwizzle) @@ -79,6 +81,22 @@ - (void)na_swizzled_orderOut:(id)sender { [self na_swizzled_orderOut:sender]; } +- (void)na_swizzled_performClose:(id)sender { + // Resolve window id and handle hook if present + if (nativeapi::WindowManager::GetInstance().HasWillCloseHook()) { + auto windows = nativeapi::WindowManager::GetInstance().GetAll(); + for (const auto& window : windows) { + if (window->GetNativeObject() == (__bridge void*)self) { + nativeapi::WindowManager::GetInstance().HandleWillClose(window->GetId()); + // Hook handles all logic; never call original here + return; + } + } + } + // No window found in registry, call original implementation (swapped) + [self na_swizzled_performClose:sender]; +} + @end static void NativeAPIInstallNSWindowWillShowSwizzleOnce() { @@ -109,6 +127,20 @@ static void NativeAPIInstallNSWindowWillHideSwizzleOnce() { }); } +static void NativeAPIInstallNSWindowWillCloseSwizzleOnce() { + static dispatch_once_t onceTokenClose; + dispatch_once(&onceTokenClose, ^{ + Class cls = [NSWindow class]; + SEL originalSel = @selector(performClose:); + SEL swizzledSel = @selector(na_swizzled_performClose:); + Method original = class_getInstanceMethod(cls, originalSel); + Method swizzled = class_getInstanceMethod(cls, swizzledSel); + if (original && swizzled) { + method_exchangeImplementations(original, swizzled); + } + }); +} + // Objective-C delegate class to handle NSWindow notifications @interface NativeAPIWindowManagerDelegate : NSObject @property(nonatomic, assign) void* impl; // Use void* instead of private class @@ -341,6 +373,13 @@ - (void)windowWillClose:(NSNotification*)notification { } } +void WindowManager::SetWillCloseHook(std::optional hook) { + pimpl_->will_close_hook_ = std::move(hook); + if (pimpl_->will_close_hook_) { + NativeAPIInstallNSWindowWillCloseSwizzleOnce(); + } +} + bool WindowManager::HasWillShowHook() const { return pimpl_->will_show_hook_.has_value(); } @@ -349,6 +388,10 @@ - (void)windowWillClose:(NSNotification*)notification { return pimpl_->will_hide_hook_.has_value(); } +bool WindowManager::HasWillCloseHook() const { + return pimpl_->will_close_hook_.has_value(); +} + void WindowManager::HandleWillShow(WindowId id) { if (pimpl_->will_show_hook_) { (*pimpl_->will_show_hook_)(id); @@ -361,6 +404,12 @@ - (void)windowWillClose:(NSNotification*)notification { } } +void WindowManager::HandleWillClose(WindowId id) { + if (pimpl_->will_close_hook_) { + (*pimpl_->will_close_hook_)(id); + } +} + bool WindowManager::CallOriginalShow(WindowId id) { auto window = Get(id); if (!window) { @@ -389,6 +438,20 @@ - (void)windowWillClose:(NSNotification*)notification { return true; } +bool WindowManager::CallOriginalClose(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + NSWindow* ns_window = (__bridge NSWindow*)native; + [ns_window na_swizzled_performClose:nil]; + return true; +} + void WindowManager::StartEventListening() { pimpl_->StartEventListening(); } diff --git a/src/platform/windows/window_manager_windows.cpp b/src/platform/windows/window_manager_windows.cpp index 9c4bdd2..48439da 100644 --- a/src/platform/windows/window_manager_windows.cpp +++ b/src/platform/windows/window_manager_windows.cpp @@ -358,9 +358,10 @@ class WindowManager::Impl { private: WindowManager* manager_; - // Optional pre-show/hide hooks + // Optional pre-show/hide/close hooks std::optional will_show_hook_; std::optional will_hide_hook_; + std::optional will_close_hook_; friend class WindowManager; }; @@ -458,6 +459,14 @@ void WindowManager::SetWillHideHook(std::optional hook) { has_any_hook ? InstallHooks() : UninstallHooks(); } +void WindowManager::SetWillCloseHook(std::optional hook) { + pimpl_->will_close_hook_ = std::move(hook); + // ponytail: Windows close-interceptie vereist een WH_CBT hook of window + // subclassing op WM_CLOSE — niet geïmplementeerd in deze iteratie. + // De hook wordt wel opgeslagen zodat HandleWillClose werkt zodra de + // event-monitoring dit signaal oppakt. +} + bool WindowManager::HasWillShowHook() const { return pimpl_->will_show_hook_.has_value(); } @@ -466,6 +475,10 @@ bool WindowManager::HasWillHideHook() const { return pimpl_->will_hide_hook_.has_value(); } +bool WindowManager::HasWillCloseHook() const { + return pimpl_->will_close_hook_.has_value(); +} + void WindowManager::HandleWillShow(WindowId id) { if (pimpl_->will_show_hook_) { (*pimpl_->will_show_hook_)(id); @@ -478,6 +491,12 @@ void WindowManager::HandleWillHide(WindowId id) { } } +void WindowManager::HandleWillClose(WindowId id) { + if (pimpl_->will_close_hook_) { + (*pimpl_->will_close_hook_)(id); + } +} + bool WindowManager::CallOriginalShow(WindowId id) { auto window = Get(id); if (!window) { @@ -512,6 +531,21 @@ bool WindowManager::CallOriginalHide(WindowId id) { return false; } +bool WindowManager::CallOriginalClose(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + HWND hwnd = static_cast(native); + // On Windows, send WM_CLOSE to trigger the normal close path + PostMessage(hwnd, WM_CLOSE, 0, 0); + return true; +} + void WindowManager::StartEventListening() { pimpl_->StartEventListening(); } diff --git a/src/window_manager.h b/src/window_manager.h index f549fc3..97a6e33 100644 --- a/src/window_manager.h +++ b/src/window_manager.h @@ -123,26 +123,31 @@ class WindowManager : public EventEmitter { */ using WindowWillShowHook = std::function; using WindowWillHideHook = std::function; + using WindowWillCloseHook = std::function; // Set or clear single hooks (pass std::nullopt to clear) void SetWillShowHook(std::optional hook); void SetWillHideHook(std::optional hook); + void SetWillCloseHook(std::optional hook); // Check if hooks are set bool HasWillShowHook() const; bool HasWillHideHook() const; + bool HasWillCloseHook() const; - // Called by platform layer BEFORE the actual show/hide happens + // Called by platform layer BEFORE the actual show/hide/close happens void HandleWillShow(WindowId id); void HandleWillHide(WindowId id); + void HandleWillClose(WindowId id); /** - * Call the platform's original show/hide implementations for a window, + * Call the platform's original show/hide/close implementations for a window, * bypassing swizzled paths. Returns true if successfully invoked. * On unsupported platforms, these return false. */ bool CallOriginalShow(WindowId id); bool CallOriginalHide(WindowId id); + bool CallOriginalClose(WindowId id); protected: /** diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3c0fe00..e8e3ca6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,3 +23,9 @@ set_tests_properties(handle_table_test PROPERTIES TIMEOUT 120) add_executable(shortcut_accelerator_test shortcut_accelerator_test.cpp) target_link_libraries(shortcut_accelerator_test PRIVATE nativeapi) add_test(NAME shortcut_accelerator_test COMMAND shortcut_accelerator_test) + +find_package(Threads REQUIRED) +add_executable(window_manager_hook_test window_manager_hook_test.cpp) +target_link_libraries(window_manager_hook_test PRIVATE nativeapi Threads::Threads) +add_test(NAME window_manager_hook_test COMMAND window_manager_hook_test) +set_tests_properties(window_manager_hook_test PROPERTIES TIMEOUT 120) diff --git a/tests/window_manager_hook_test.cpp b/tests/window_manager_hook_test.cpp new file mode 100644 index 0000000..aafc616 --- /dev/null +++ b/tests/window_manager_hook_test.cpp @@ -0,0 +1,182 @@ +// Unit tests for WindowManager will-show/hide/close hook infrastructure. +// +// These tests verify the hook storage and dispatch logic without requiring +// a real platform window. The swizzle-based interception is exercised by +// the examples and integration tests on each platform. + +#include +#include +#include +#include +#include + +#include "../src/window_manager.h" +#include "../src/window.h" + +namespace { + +using namespace nativeapi; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +// Setting a hook should make HasWillCloseHook return true; clearing it +// (passing nullopt) should make it return false. +bool test_set_and_clear_will_close_hook() { + auto& manager = WindowManager::GetInstance(); + + // Clear any existing hook + manager.SetWillCloseHook(std::nullopt); + if (manager.HasWillCloseHook()) { + std::cerr << "FAIL: HasWillCloseHook should be false after clear\n"; + return false; + } + + // Set a hook + manager.SetWillCloseHook([](WindowId) {}); + if (!manager.HasWillCloseHook()) { + std::cerr << "FAIL: HasWillCloseHook should be true after set\n"; + return false; + } + + // Clear it + manager.SetWillCloseHook(std::nullopt); + if (manager.HasWillCloseHook()) { + std::cerr << "FAIL: HasWillCloseHook should be false after second clear\n"; + return false; + } + + return true; +} + +// HandleWillClose should invoke the registered hook with the correct ID. +bool test_handle_will_close_invokes_hook() { + auto& manager = WindowManager::GetInstance(); + + const WindowId test_id = 42; + std::atomic received_id{0}; + std::atomic was_called{false}; + + manager.SetWillCloseHook([&](WindowId id) { + received_id = id; + was_called = true; + }); + + manager.HandleWillClose(test_id); + + // Give async dispatch a moment (HandleWillClose is synchronous, but + // be defensive in case the platform routes through a dispatcher). + for (int i = 0; i < 100 && !was_called; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + if (!was_called) { + std::cerr << "FAIL: will-close hook was not invoked\n"; + manager.SetWillCloseHook(std::nullopt); + return false; + } + + if (received_id != test_id) { + std::cerr << "FAIL: will-close hook received wrong id: " << received_id + << " expected " << test_id << "\n"; + manager.SetWillCloseHook(std::nullopt); + return false; + } + + // Cleanup + manager.SetWillCloseHook(std::nullopt); + return true; +} + +// HandleWillClose with no hook set should be a no-op (not crash). +bool test_handle_will_close_no_hook_is_safe() { + auto& manager = WindowManager::GetInstance(); + manager.SetWillCloseHook(std::nullopt); + + // Should not crash + manager.HandleWillClose(99); + + return true; +} + +// All three hooks (show, hide, close) should coexist independently. +bool test_all_hooks_coexist() { + auto& manager = WindowManager::GetInstance(); + + std::atomic show_called{false}; + std::atomic hide_called{false}; + std::atomic close_called{false}; + + manager.SetWillShowHook([&](WindowId) { show_called = true; }); + manager.SetWillHideHook([&](WindowId) { hide_called = true; }); + manager.SetWillCloseHook([&](WindowId) { close_called = true; }); + + if (!manager.HasWillShowHook() || !manager.HasWillHideHook() || + !manager.HasWillCloseHook()) { + std::cerr << "FAIL: not all hooks report as set\n"; + manager.SetWillShowHook(std::nullopt); + manager.SetWillHideHook(std::nullopt); + manager.SetWillCloseHook(std::nullopt); + return false; + } + + manager.HandleWillShow(1); + manager.HandleWillHide(1); + manager.HandleWillClose(1); + + for (int i = 0; i < 100 && (!show_called || !hide_called || !close_called); ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + if (!show_called || !hide_called || !close_called) { + std::cerr << "FAIL: not all hooks were invoked (show=" << show_called + << " hide=" << hide_called << " close=" << close_called << ")\n"; + manager.SetWillShowHook(std::nullopt); + manager.SetWillHideHook(std::nullopt); + manager.SetWillCloseHook(std::nullopt); + return false; + } + + // Cleanup + manager.SetWillShowHook(std::nullopt); + manager.SetWillHideHook(std::nullopt); + manager.SetWillCloseHook(std::nullopt); + return true; +} + +} // namespace + +int main() { + struct TestCase { + const char* name; + bool (*fn)(); + }; + + TestCase tests[] = { + {"set_and_clear_will_close_hook", test_set_and_clear_will_close_hook}, + {"handle_will_close_invokes_hook", test_handle_will_close_invokes_hook}, + {"handle_will_close_no_hook_is_safe", + test_handle_will_close_no_hook_is_safe}, + {"all_hooks_coexist", test_all_hooks_coexist}, + }; + + int failures = 0; + for (const auto& tc : tests) { + std::cout << "RUN " << tc.name << "\n"; + if (tc.fn()) { + std::cout << "PASS " << tc.name << "\n"; + } else { + std::cout << "FAIL " << tc.name << "\n"; + ++failures; + } + } + + if (failures > 0) { + std::cout << "\n" << failures << " test(s) failed\n"; + return 1; + } + + std::cout << "\nAll tests passed\n"; + return 0; +}