Model UI overlay lifetime in ui core

This commit is contained in:
2026-06-06 09:48:00 +02:00
parent 4071919124
commit 3101e65dd3
9 changed files with 483 additions and 7 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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 |

View File

@@ -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<std::shared_ptr<Node>> m_children`, raw `find<T>()` lookup results, `add_child<T>()` 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<std::shared_ptr<Node>> m_children`, raw `find<T>()` lookup results, `add_child<T>()` 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 |

View File

@@ -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,

View File

@@ -0,0 +1,173 @@
#include "ui_core/overlay_lifetime.h"
#include <algorithm>
namespace pp::ui {
UiOverlayLifetime::UiOverlayLifetime(NodeLifetimeTree& tree, NodeHandle root) noexcept
: tree_(tree)
, root_(root)
{
}
pp::foundation::Result<NodeHandle> UiOverlayLifetime::open_popup()
{
return open_overlay(root_, UiOverlayKind::popup, true, false);
}
pp::foundation::Result<NodeHandle> UiOverlayLifetime::open_child_popup(NodeHandle parent_popup)
{
if (!tracked_alive(parent_popup)) {
return pp::foundation::Result<NodeHandle>::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<NodeHandle> 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<NodeHandle> 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<NodeHandle>::success(entry.node);
}
}
return pp::foundation::Result<NodeHandle>::failure(
pp::foundation::Status::invalid_argument("overlay stack is empty"));
}
pp::foundation::Result<NodeHandle> UiOverlayLifetime::open_overlay(
NodeHandle parent,
UiOverlayKind kind,
bool captures_pointer,
bool captures_keyboard)
{
if (!tree_.contains(root_)) {
return pp::foundation::Result<NodeHandle>::failure(
pp::foundation::Status::invalid_argument("overlay root is not live"));
}
if (!tree_.contains(parent)) {
return pp::foundation::Result<NodeHandle>::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<NodeHandle>::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<NodeHandle>::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;
});
}
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include "foundation/result.h"
#include "ui_core/node_lifetime.h"
#include <cstdint>
#include <cstddef>
#include <vector>
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<NodeHandle> open_popup();
[[nodiscard]] pp::foundation::Result<NodeHandle> open_child_popup(NodeHandle parent_popup);
[[nodiscard]] pp::foundation::Result<NodeHandle> 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<NodeHandle> top_overlay() const noexcept;
private:
[[nodiscard]] pp::foundation::Result<NodeHandle> 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<UiOverlayEntry> entries_;
};
}

View File

@@ -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

View File

@@ -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();
}