diff --git a/CMakeLists.txt b/CMakeLists.txt index 69cb838..4d422bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -221,7 +221,8 @@ add_library(pp_ui_core STATIC src/ui_core/color.cpp src/ui_core/layout_value.cpp src/ui_core/layout_xml.cpp - src/ui_core/node_lifetime.cpp) + src/ui_core/node_lifetime.cpp + src/ui_core/overlay_lifetime.cpp) target_include_directories(pp_ui_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index dfe632e..a29133b 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -737,8 +737,11 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p node handles, hidden parent/child invariants, scoped callback connections, pointer/keyboard capture ownership, mutation-safe event dispatch, whole-tree reload invalidation, and focused destroy-during-callback/layout-reload - coverage in `pp_ui_core_node_lifetime_tests`. Retained `Node` still needs to - adopt those semantics under DEBT-0063. + coverage in `pp_ui_core_node_lifetime_tests`. It also owns a pure + `UiOverlayLifetime` coordinator with root/nested popup, modal/modeless + dialog, capture-restore, branch-close, and layout-reload coverage in + `pp_ui_core_overlay_lifetime_tests`. Retained `Node`/popup/dialog code still + needs to adopt those semantics under DEBT-0063. - `scripts/automation/analyze.*` runs shader validation plus a renderer-boundary guard that reports JSON and fails if active non-backend source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 3f72386..b81c051 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -58,7 +58,7 @@ and validation command. | XML layout parsing | `LayoutManager`, `Node` | `pp_ui_core` | Layout fixtures and malformed XML | | Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures | | Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch, layout, ownership-handle, callback-disconnect, and destroy-during-callback tests | -| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui` | UI automation scripts, command-dispatch view models, popup/dialog lifetime tests | +| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui`, `pp_ui_core` | UI automation scripts, command-dispatch view models, pure overlay lifetime tests, retained popup/dialog lifetime tests | | Canvas viewport UI | `NodeCanvas` | `pp_panopainter_ui`, `pp_paint_renderer` | Input-to-command automation | | Settings UI | `Settings`, `NodeSettings` | `pp_assets`, `pp_panopainter_ui` | Round-trip settings | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index ae1b4fc..ae420e1 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -452,6 +452,11 @@ agent or engineer to remove them without reconstructing context from chat. a captured node is destroyed, and whole-tree `clear()` invalidation for layout reloads. Retained `Node` adoption and app-specific popup/dialog lifetime tests remain open. +- 2026-06-06: DEBT-0063 was narrowed again. `pp_ui_core::UiOverlayLifetime` + now models root and nested popup ownership, modal/modeless dialog capture + policy, capture restoration after child/modal close, parent popup branch + teardown, untracked close rejection, and layout-reload invalidation. Retained + `NodePopupMenu`/`NodeDialog*` adoption remains open. - 2026-06-05: DEBT-0011 was narrowed. The Windows app package smoke target now passes the configure-time CMake executable into `package-smoke.ps1`, so VS 2026 generator validation does not depend on an older `cmake` on PATH, and @@ -645,7 +650,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0060 | Open | Modernization | Retained Android package CMake generates a patched `nanort.h` overlay in the build tree for `native-lib` instead of modifying the `libs/nanort` submodule | Current SDK Manager NDK/Clang rejects `TriangleSAHPred::operator=` assigning to a `const size_t` member, but the retained grid/lightmap path still includes `nanort` before that dependency is replaced or updated | `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; Quest/Focus retained package configure checks; Windows app build | Update/replace `nanort`, move grid/lightmap baking behind a component that owns its dependency, or retire the retained Android package CMake path so no generated vendor overlay is required | | DEBT-0061 | Open | Modernization | Desktop XR runtime selection now lives in tested `pp_platform_api` policy and prefers OpenXR, but `WindowsPlatformServices` still reports OpenXR unavailable and reaches the retained OpenVR SDK bridge as a legacy fallback; Windows runtime deployment copies `openvr_api.dll` beside `PanoPainter.exe` until that fallback is removed | Preserve current desktop VR behavior while replacing OpenVR with OpenXR behind the platform/renderer boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add an OpenXR SDK/package target, implement desktop OpenXR startup/shutdown/pose/controller submission behind `pp_platform_vr` or `PlatformServices`, validate parity with mocked/runtime smoke coverage, and remove `libs/openvr` plus the OpenVR link/include paths from root CMake | | DEBT-0062 | Open | Modernization | VS 2026 builds generate a patched fmt `format.h` overlay in the build tree for `pp_legacy_vendor`, disabling the old `_SECURE_SCL` checked-array iterator branch while leaving the fmt submodule clean | VS 2026's STL no longer exposes the legacy checked-array iterator used by this old fmt release, but replacing fmt is part of the dependency migration rather than this platform unblock | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `cmake --build --preset windows-msvc-default --config Debug --target pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | Move fmt to a supported vcpkg/package version or update the vendored fmt release, then remove the generated fmt overlay from `pp_legacy_vendor` | -| DEBT-0063 | Open | Modernization | The retained UI tree still exposes `Node* m_parent`, public `std::vector> m_children`, raw `find()` lookup results, `add_child()` allocation through `new`, callbacks/observers that take or capture raw `Node*`, and manual `destroy()`/`m_destroyed` semantics. `pp_ui_core` now owns a tested `NodeLifetimeTree` target model with checked node handles, scoped callback connections, subtree destruction, pointer/keyboard capture release, whole-tree clear for layout reload, and mutation-safe dispatch, but retained `Node` has not adopted it yet | Preserve current UI behavior while making panel/dialog extraction safe instead of spreading lifetime hazards into the new architecture | `pp_ui_core_layout_xml_tests`; `pp_ui_core_node_lifetime_tests`; future `pp_panopainter_ui_dialog_lifetime_tests`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Retained `Node` and `pp_panopainter_ui` adopt checked node handles or equivalent non-owning references, scoped callback connection/disconnect semantics, mutation-safe event dispatch, parent/child invariants hidden behind APIs, and destroy-during-callback/capture-release/popup-close/layout-reload tests; retained `Node*` APIs are removed or isolated behind compatibility adapters | +| DEBT-0063 | Open | Modernization | The retained UI tree still exposes `Node* m_parent`, public `std::vector> m_children`, raw `find()` lookup results, `add_child()` allocation through `new`, callbacks/observers that take or capture raw `Node*`, and manual `destroy()`/`m_destroyed` semantics. `pp_ui_core` now owns a tested `NodeLifetimeTree` target model with checked node handles, scoped callback connections, subtree destruction, pointer/keyboard capture release, whole-tree clear for layout reload, and mutation-safe dispatch, plus a tested `UiOverlayLifetime` popup/dialog stack model, but retained `Node`/`NodePopupMenu`/`NodeDialog*` have not adopted them yet | Preserve current UI behavior while making panel/dialog extraction safe instead of spreading lifetime hazards into the new architecture | `pp_ui_core_layout_xml_tests`; `pp_ui_core_node_lifetime_tests`; `pp_ui_core_overlay_lifetime_tests`; future `pp_panopainter_ui_dialog_lifetime_tests`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Retained `Node` and `pp_panopainter_ui` adopt checked node handles or equivalent non-owning references, scoped callback connection/disconnect semantics, mutation-safe event dispatch, parent/child invariants hidden behind APIs, and destroy-during-callback/capture-release/popup-close/layout-reload tests; retained `Node*` APIs are removed or isolated behind compatibility adapters | | DEBT-0057 | Open | Modernization | Default canvas allocation size now dispatches through `PlatformServices::default_canvas_resolution`, removing the `CANVAS_RES` platform macro from `src/canvas.h`; WebGL's retained 512 default now lives in tested `pp_platform_api::platform_policy`, but the Web shell still reaches it through the legacy platform fallback until injected Web services own the policy | Preserve WebGL memory behavior while moving canvas creation policy out of shared canvas headers and into the platform boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build; WebGL package smoke once root Web build exists | Default canvas resolution is owned by injected `pp_platform_*` services for every supported platform, with no WebGL branch in the legacy fallback | | DEBT-0058 | Open | Modernization | App-level progress/message/input dialog metadata, including message-dialog OK/cancel captions, now consumes pure `pp_app_core` through `App::show_progress`, `App::message_box`, `App::input_box`, `pano_cli plan-app-dialog`, and `pp_app_core_app_dialog_tests`; live execution is centralized in `src/legacy_app_dialog_services.*`, but the bridge still creates retained `NodeProgressBar`, `NodeMessageBox`, and `NodeInputBox` instances and inserts them into the legacy layout tree | Preserve current app-shell dialog behavior while moving shared dialog policy toward UI/app services | `pp_app_core_app_dialog_tests`; `pano_cli plan-app-dialog --kind progress --total -4`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-dialog --kind input --ok-caption Save`; `ctest --preset desktop-fast --build-config Debug`; Windows app build | Progress/message/input dialog creation, callback wiring, layout insertion, lifetime ownership, and headless automation are owned by injected app/UI services with `App` methods acting only as adapters | | DEBT-0059 | Open | Modernization | iOS root CMake headless builds assign generated bundle identifiers and disable code signing for executable test/tool targets | The current Apple gate is compile validation for shared component targets; signed iOS app/package validation is not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device`; `sh scripts/automation/platform-build.sh "ios-device"` on `panopainter-mac` | Root CMake owns the signed Apple app/package targets, package-smoke validates Apple bundles where signing material is available, and headless iOS test/tool targets are either excluded from signed package builds or use explicit test-runner signing policy | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 863053c..6e95fd0 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -471,8 +471,11 @@ migration accelerates. The first pure `pp_ui_core::NodeLifetimeTree` slice now models checked handles, parent/child invariants, scoped callback connections, and mutation-safe dispatch for destroy-during-callback and connection-addition cases. It now also models pointer/keyboard capture release and whole-tree -`clear()` invalidation for layout reload; wiring those semantics into retained -`Node` remains tracked by the same debt. +`clear()` invalidation for layout reload. `pp_ui_core::UiOverlayLifetime` now +layers popup/dialog stack ownership on top of that model, covering root and +nested popups, modal versus modeless dialogs, capture restoration, parent-popup +branch close, and layout-reload invalidation. Wiring those semantics into +retained `Node`/`NodePopupMenu`/`NodeDialog*` remains tracked by the same debt. `pano_cli inspect-image` exposes PNG IHDR metadata as JSON, `pano_cli import-image` accepts a PNG path and imports decoded RGBA8 pixels into a new pure `pp_document` face payload, diff --git a/src/ui_core/overlay_lifetime.cpp b/src/ui_core/overlay_lifetime.cpp new file mode 100644 index 0000000..e35998a --- /dev/null +++ b/src/ui_core/overlay_lifetime.cpp @@ -0,0 +1,173 @@ +#include "ui_core/overlay_lifetime.h" + +#include + +namespace pp::ui { + +UiOverlayLifetime::UiOverlayLifetime(NodeLifetimeTree& tree, NodeHandle root) noexcept + : tree_(tree) + , root_(root) +{ +} + +pp::foundation::Result UiOverlayLifetime::open_popup() +{ + return open_overlay(root_, UiOverlayKind::popup, true, false); +} + +pp::foundation::Result UiOverlayLifetime::open_child_popup(NodeHandle parent_popup) +{ + if (!tracked_alive(parent_popup)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("child popup requires a live tracked parent popup")); + } + + return open_overlay(parent_popup, UiOverlayKind::popup, true, false); +} + +pp::foundation::Result UiOverlayLifetime::open_dialog(bool modal) +{ + return open_overlay(root_, UiOverlayKind::dialog, modal, modal); +} + +pp::foundation::Status UiOverlayLifetime::close(NodeHandle overlay) noexcept +{ + if (!tracked_alive(overlay)) { + return pp::foundation::Status::invalid_argument("overlay close requires a live tracked overlay"); + } + + const auto status = tree_.destroy_subtree(overlay); + if (!status.ok()) { + return status; + } + + prune_dead_entries(); + restore_capture(UiCaptureKind::pointer); + restore_capture(UiCaptureKind::keyboard); + return pp::foundation::Status::success(); +} + +void UiOverlayLifetime::clear_for_layout_reload() noexcept +{ + tree_.clear(); + entries_.clear(); +} + +bool UiOverlayLifetime::tracks(NodeHandle overlay) const noexcept +{ + return tracked_alive(overlay); +} + +std::size_t UiOverlayLifetime::overlay_count() const noexcept +{ + std::size_t count = 0; + for (const auto& entry : entries_) { + if (tree_.contains(entry.node)) { + ++count; + } + } + + return count; +} + +pp::foundation::Result UiOverlayLifetime::top_overlay() const noexcept +{ + for (auto index = entries_.size(); index > 0U; --index) { + const auto& entry = entries_[index - 1U]; + if (tree_.contains(entry.node)) { + return pp::foundation::Result::success(entry.node); + } + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("overlay stack is empty")); +} + +pp::foundation::Result UiOverlayLifetime::open_overlay( + NodeHandle parent, + UiOverlayKind kind, + bool captures_pointer, + bool captures_keyboard) +{ + if (!tree_.contains(root_)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("overlay root is not live")); + } + + if (!tree_.contains(parent)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("overlay parent is not live")); + } + + auto node = tree_.create_child(parent); + if (!node) { + return node; + } + + if (captures_pointer) { + const auto status = tree_.capture(UiCaptureKind::pointer, node.value()); + if (!status.ok()) { + (void)tree_.destroy_subtree(node.value()); + return pp::foundation::Result::failure(status); + } + } + + if (captures_keyboard) { + const auto status = tree_.capture(UiCaptureKind::keyboard, node.value()); + if (!status.ok()) { + (void)tree_.destroy_subtree(node.value()); + return pp::foundation::Result::failure(status); + } + } + + entries_.push_back(UiOverlayEntry { + .node = node.value(), + .parent = parent, + .kind = kind, + .captures_pointer = captures_pointer, + .captures_keyboard = captures_keyboard, + }); + return node; +} + +void UiOverlayLifetime::prune_dead_entries() noexcept +{ + entries_.erase( + std::remove_if( + entries_.begin(), + entries_.end(), + [this](const UiOverlayEntry& entry) { + return !tree_.contains(entry.node); + }), + entries_.end()); +} + +void UiOverlayLifetime::restore_capture(UiCaptureKind kind) noexcept +{ + for (auto index = entries_.size(); index > 0U; --index) { + const auto& entry = entries_[index - 1U]; + const auto captures = kind == UiCaptureKind::pointer + ? entry.captures_pointer + : entry.captures_keyboard; + if (captures && tree_.contains(entry.node)) { + (void)tree_.capture(kind, entry.node); + return; + } + } +} + +bool UiOverlayLifetime::tracked_alive(NodeHandle overlay) const noexcept +{ + if (!tree_.contains(overlay)) { + return false; + } + + return std::any_of( + entries_.begin(), + entries_.end(), + [overlay](const UiOverlayEntry& entry) { + return entry.node == overlay; + }); +} + +} diff --git a/src/ui_core/overlay_lifetime.h b/src/ui_core/overlay_lifetime.h new file mode 100644 index 0000000..0ed0f3e --- /dev/null +++ b/src/ui_core/overlay_lifetime.h @@ -0,0 +1,54 @@ +#pragma once + +#include "foundation/result.h" +#include "ui_core/node_lifetime.h" + +#include +#include +#include + +namespace pp::ui { + +enum class UiOverlayKind : std::uint8_t { + popup, + dialog, +}; + +struct UiOverlayEntry { + NodeHandle node {}; + NodeHandle parent {}; + UiOverlayKind kind = UiOverlayKind::popup; + bool captures_pointer = false; + bool captures_keyboard = false; +}; + +class UiOverlayLifetime { +public: + UiOverlayLifetime(NodeLifetimeTree& tree, NodeHandle root) noexcept; + + [[nodiscard]] pp::foundation::Result open_popup(); + [[nodiscard]] pp::foundation::Result open_child_popup(NodeHandle parent_popup); + [[nodiscard]] pp::foundation::Result open_dialog(bool modal); + [[nodiscard]] pp::foundation::Status close(NodeHandle overlay) noexcept; + void clear_for_layout_reload() noexcept; + + [[nodiscard]] bool tracks(NodeHandle overlay) const noexcept; + [[nodiscard]] std::size_t overlay_count() const noexcept; + [[nodiscard]] pp::foundation::Result top_overlay() const noexcept; + +private: + [[nodiscard]] pp::foundation::Result open_overlay( + NodeHandle parent, + UiOverlayKind kind, + bool captures_pointer, + bool captures_keyboard); + void prune_dead_entries() noexcept; + void restore_capture(UiCaptureKind kind) noexcept; + [[nodiscard]] bool tracked_alive(NodeHandle overlay) const noexcept; + + NodeLifetimeTree& tree_; + NodeHandle root_ {}; + std::vector entries_; +}; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3fb85cd..eac9563 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -310,6 +310,16 @@ add_test(NAME pp_ui_core_node_lifetime_tests COMMAND pp_ui_core_node_lifetime_te set_tests_properties(pp_ui_core_node_lifetime_tests PROPERTIES LABELS "ui;desktop-fast") +add_executable(pp_ui_core_overlay_lifetime_tests + ui_core/overlay_lifetime_tests.cpp) +target_link_libraries(pp_ui_core_overlay_lifetime_tests PRIVATE + pp_ui_core + pp_test_harness) + +add_test(NAME pp_ui_core_overlay_lifetime_tests COMMAND pp_ui_core_overlay_lifetime_tests) +set_tests_properties(pp_ui_core_overlay_lifetime_tests PROPERTIES + LABELS "ui;desktop-fast") + add_executable(pp_app_core_about_menu_tests app_core/about_menu_tests.cpp) target_link_libraries(pp_app_core_about_menu_tests PRIVATE diff --git a/tests/ui_core/overlay_lifetime_tests.cpp b/tests/ui_core/overlay_lifetime_tests.cpp new file mode 100644 index 0000000..602ea77 --- /dev/null +++ b/tests/ui_core/overlay_lifetime_tests.cpp @@ -0,0 +1,227 @@ +#include "ui_core/overlay_lifetime.h" +#include "test_harness.h" + +using pp::foundation::StatusCode; +using pp::ui::NodeLifetimeTree; +using pp::ui::UiCaptureKind; +using pp::ui::UiOverlayLifetime; + +namespace { + +void opens_root_and_child_popups_with_capture_restore(pp::tests::Harness& h) +{ + NodeLifetimeTree tree; + const auto root = tree.create_root(); + PP_EXPECT(h, root); + if (!root) { + return; + } + + UiOverlayLifetime overlays(tree, root.value()); + const auto popup = overlays.open_popup(); + PP_EXPECT(h, popup); + if (!popup) { + return; + } + + const auto child = overlays.open_child_popup(popup.value()); + PP_EXPECT(h, child); + if (!child) { + return; + } + + PP_EXPECT(h, overlays.overlay_count() == 2U); + const auto captured_child = tree.captured_node(UiCaptureKind::pointer); + PP_EXPECT(h, captured_child); + if (captured_child) { + PP_EXPECT(h, captured_child.value() == child.value()); + } + + PP_EXPECT(h, overlays.close(child.value()).ok()); + PP_EXPECT(h, overlays.overlay_count() == 1U); + PP_EXPECT(h, !tree.contains(child.value())); + PP_EXPECT(h, tree.contains(popup.value())); + + const auto captured_parent = tree.captured_node(UiCaptureKind::pointer); + PP_EXPECT(h, captured_parent); + if (captured_parent) { + PP_EXPECT(h, captured_parent.value() == popup.value()); + } +} + +void closing_parent_popup_closes_child_branch(pp::tests::Harness& h) +{ + NodeLifetimeTree tree; + const auto root = tree.create_root(); + PP_EXPECT(h, root); + if (!root) { + return; + } + + UiOverlayLifetime overlays(tree, root.value()); + const auto popup = overlays.open_popup(); + PP_EXPECT(h, popup); + if (!popup) { + return; + } + + const auto child = overlays.open_child_popup(popup.value()); + PP_EXPECT(h, child); + if (!child) { + return; + } + + PP_EXPECT(h, overlays.close(popup.value()).ok()); + PP_EXPECT(h, overlays.overlay_count() == 0U); + PP_EXPECT(h, !tree.contains(popup.value())); + PP_EXPECT(h, !tree.contains(child.value())); + PP_EXPECT(h, !tree.captured_node(UiCaptureKind::pointer).ok()); +} + +void modal_dialog_captures_pointer_and_keyboard(pp::tests::Harness& h) +{ + NodeLifetimeTree tree; + const auto root = tree.create_root(); + PP_EXPECT(h, root); + if (!root) { + return; + } + + UiOverlayLifetime overlays(tree, root.value()); + const auto popup = overlays.open_popup(); + PP_EXPECT(h, popup); + if (!popup) { + return; + } + + const auto dialog = overlays.open_dialog(true); + PP_EXPECT(h, dialog); + if (!dialog) { + return; + } + + const auto pointer = tree.captured_node(UiCaptureKind::pointer); + const auto keyboard = tree.captured_node(UiCaptureKind::keyboard); + PP_EXPECT(h, pointer); + PP_EXPECT(h, keyboard); + if (pointer) { + PP_EXPECT(h, pointer.value() == dialog.value()); + } + if (keyboard) { + PP_EXPECT(h, keyboard.value() == dialog.value()); + } + + PP_EXPECT(h, overlays.close(dialog.value()).ok()); + const auto restored_pointer = tree.captured_node(UiCaptureKind::pointer); + PP_EXPECT(h, restored_pointer); + if (restored_pointer) { + PP_EXPECT(h, restored_pointer.value() == popup.value()); + } + PP_EXPECT(h, !tree.captured_node(UiCaptureKind::keyboard).ok()); +} + +void modeless_dialog_does_not_steal_capture(pp::tests::Harness& h) +{ + NodeLifetimeTree tree; + const auto root = tree.create_root(); + PP_EXPECT(h, root); + if (!root) { + return; + } + + UiOverlayLifetime overlays(tree, root.value()); + const auto popup = overlays.open_popup(); + PP_EXPECT(h, popup); + if (!popup) { + return; + } + + const auto dialog = overlays.open_dialog(false); + PP_EXPECT(h, dialog); + if (!dialog) { + return; + } + + const auto pointer = tree.captured_node(UiCaptureKind::pointer); + PP_EXPECT(h, pointer); + if (pointer) { + PP_EXPECT(h, pointer.value() == popup.value()); + } + PP_EXPECT(h, !tree.captured_node(UiCaptureKind::keyboard).ok()); + PP_EXPECT(h, overlays.top_overlay()); + if (const auto top = overlays.top_overlay()) { + PP_EXPECT(h, top.value() == dialog.value()); + } +} + +void rejects_untracked_or_dead_overlay_closes(pp::tests::Harness& h) +{ + NodeLifetimeTree tree; + const auto root = tree.create_root(); + PP_EXPECT(h, root); + if (!root) { + return; + } + + const auto raw_child = tree.create_child(root.value()); + PP_EXPECT(h, raw_child); + if (!raw_child) { + return; + } + + UiOverlayLifetime overlays(tree, root.value()); + const auto close_raw = overlays.close(raw_child.value()); + const auto child_popup = overlays.open_child_popup(raw_child.value()); + PP_EXPECT(h, !close_raw.ok()); + PP_EXPECT(h, close_raw.code == StatusCode::invalid_argument); + PP_EXPECT(h, !child_popup.ok()); + PP_EXPECT(h, child_popup.status().code == StatusCode::invalid_argument); +} + +void clear_for_layout_reload_invalidates_overlays(pp::tests::Harness& h) +{ + NodeLifetimeTree tree; + const auto root = tree.create_root(); + PP_EXPECT(h, root); + if (!root) { + return; + } + + UiOverlayLifetime overlays(tree, root.value()); + const auto popup = overlays.open_popup(); + PP_EXPECT(h, popup); + if (!popup) { + return; + } + + const auto dialog = overlays.open_dialog(true); + PP_EXPECT(h, dialog); + if (!dialog) { + return; + } + + overlays.clear_for_layout_reload(); + PP_EXPECT(h, overlays.overlay_count() == 0U); + PP_EXPECT(h, !overlays.top_overlay().ok()); + PP_EXPECT(h, !tree.contains(root.value())); + PP_EXPECT(h, !tree.contains(popup.value())); + PP_EXPECT(h, !tree.contains(dialog.value())); + PP_EXPECT(h, !tree.captured_node(UiCaptureKind::pointer).ok()); + PP_EXPECT(h, !tree.captured_node(UiCaptureKind::keyboard).ok()); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run( + "opens_root_and_child_popups_with_capture_restore", + opens_root_and_child_popups_with_capture_restore); + harness.run("closing_parent_popup_closes_child_branch", closing_parent_popup_closes_child_branch); + harness.run("modal_dialog_captures_pointer_and_keyboard", modal_dialog_captures_pointer_and_keyboard); + harness.run("modeless_dialog_does_not_steal_capture", modeless_dialog_does_not_steal_capture); + harness.run("rejects_untracked_or_dead_overlay_closes", rejects_untracked_or_dead_overlay_closes); + harness.run("clear_for_layout_reload_invalidates_overlays", clear_for_layout_reload_invalidates_overlays); + return harness.finish(); +}