From 5ff2992c0ec8f8a59c18543a242c6890053590f5 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sat, 6 Jun 2026 10:37:14 +0200 Subject: [PATCH] Centralize retained menu popup attachment --- docs/modernization/build-inventory.md | 6 +- docs/modernization/debt.md | 8 ++- docs/modernization/roadmap.md | 5 +- scripts/automation/quiet-validate.ps1 | 22 +++++- src/app_layout.cpp | 100 ++++++++++++-------------- src/legacy_ui_overlay_services.cpp | 42 +++++++++++ src/legacy_ui_overlay_services.h | 8 +++ 7 files changed, 128 insertions(+), 63 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 040ba93..2ca6ebb 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -756,8 +756,10 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p app-dialog overlay adapter. App-level progress/message/input dialogs and the retained about/manual/changelog, document open/save/new/browse/resize/ layer-rename, PPBR export, shortcuts, and what's-new overlays route their root - attachment through that helper while raw `Node` ownership and callbacks remain - legacy debt. + attachment through that helper. Retained app-menu popup template cloning and + root attachment for File/Export/Edit/Tools/Panels/Options/About/Layers menus + also route through the same helper while raw `Node` ownership and callbacks + remain legacy debt. - `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/debt.md b/docs/modernization/debt.md index 2564d88..ecdd9b3 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -463,6 +463,12 @@ agent or engineer to remove them without reconstructing context from chat. document open/save/new/browse/resize/layer-rename, PPBR export, shortcuts, and what's-new overlays route through it. Raw `Node` ownership, callback captures, and retained popup/menu lifetime semantics remain open. +- 2026-06-06: DEBT-0063 was narrowed again. Retained File, Export, Edit, Tools, + Panels, Options, About, and Layers app-menu popups now clone and attach through + `src/legacy_ui_overlay_services.*`, so missing popup templates return explicit + status/logging instead of direct `m_children[0]` dereferences in + `App::init_menu_*`. Raw popup callback captures and retained close semantics + remain 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 @@ -656,7 +662,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, plus a tested `UiOverlayLifetime` popup/dialog stack model. Retained app-dialog root insertion is now centralized in `src/legacy_ui_overlay_services.*`, but retained `Node`/`NodePopupMenu`/`NodeDialog*` still have not adopted checked handles or scoped callback ownership | 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-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. Retained app-dialog root insertion and app-menu popup template cloning/root attachment are now centralized in `src/legacy_ui_overlay_services.*`, but retained `Node`/`NodePopupMenu`/`NodeDialog*` still have not adopted checked handles or scoped callback ownership | 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.*`, and retained root insertion now routes through `src/legacy_ui_overlay_services.*`, but the bridge still creates retained `NodeProgressBar`, `NodeMessageBox`, and `NodeInputBox` instances with raw callback/lifetime ownership | 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 130cd5c..1efa20a 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -480,7 +480,10 @@ The first retained adoption point is `src/legacy_ui_overlay_services.*`, which now centralizes app-dialog overlay initialization and root attachment for progress/message/input dialogs, about/manual/changelog, document open/save/new/ browse/resize/layer-rename dialogs, PPBR export, shortcuts, and the what's-new -remote page before those paths reach raw `Node` insertion. +remote page before those paths reach raw `Node` insertion. It also now owns +retained app-menu popup template cloning and root attachment for File, Export, +Edit, Tools, Panels, Options, About, and Layers menu popups, replacing the +previous direct template-child indexing in `App::init_menu_*`. `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/scripts/automation/quiet-validate.ps1 b/scripts/automation/quiet-validate.ps1 index 08a100f..e413c08 100644 --- a/scripts/automation/quiet-validate.ps1 +++ b/scripts/automation/quiet-validate.ps1 @@ -89,6 +89,18 @@ function Expand-ArgumentList { return @($expanded) } +function Limit-LogSlug { + param( + [string]$Value, + [int]$MaxLength = 96 + ) + + if ($Value.Length -le $MaxLength) { + return $Value + } + return $Value.Substring(0, $MaxLength) +} + function Test-IgnoredLine { param( [string]$Line, @@ -190,7 +202,7 @@ function Invoke-QuietStep { } if ($exitCode -ne 0 -and $FailureTailLines -gt 0 -and (Test-Path -LiteralPath $LogPath)) { - $result.failureTail = @(Get-Content -LiteralPath $LogPath -Tail $FailureTailLines) + $result.failureTail = @(Get-Content -LiteralPath $LogPath -Tail $FailureTailLines | ForEach-Object { [string]$_ }) } return $result @@ -226,7 +238,7 @@ if ($Configure) { if (-not $SkipBuild) { $targets = @($BuildTargets | Where-Object { $_ -and $_.Length -gt 0 }) if ($targets.Count -gt 0) { - $safeTargets = ($targets -join "_") -replace "[^A-Za-z0-9_.-]", "_" + $safeTargets = Limit-LogSlug -Value (($targets -join "_") -replace "[^A-Za-z0-9_.-]", "_") $log = Join-Path -Path $LogDir -ChildPath "$runId-build-$BuildPreset-$Configuration-$safeTargets.log" $buildArgs = @("--build", "--preset", $BuildPreset, "--config", $Configuration, "--target") + $targets $result = Invoke-QuietStep ` @@ -245,7 +257,11 @@ if (-not $SkipBuild) { } if (-not $SkipTests) { - $safeRegex = if ($TestRegex.Length -gt 0) { ($TestRegex -replace "[^A-Za-z0-9_.-]", "_") } else { "all" } + $safeRegex = if ($TestRegex.Length -gt 0) { + Limit-LogSlug -Value ($TestRegex -replace "[^A-Za-z0-9_.-]", "_") + } else { + "all" + } $log = Join-Path -Path $LogDir -ChildPath "$runId-test-$TestPreset-$Configuration-$safeRegex.log" $testArgs = @("--preset", $TestPreset, "--build-config", $Configuration, "--output-on-failure") if ($TestRegex.Length -gt 0) { diff --git a/src/app_layout.cpp b/src/app_layout.cpp index b78d6e9..1f7d82e 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -24,6 +24,7 @@ #include "legacy_canvas_tool_services.h" #include "legacy_document_layer_services.h" #include "legacy_history_services.h" +#include "legacy_ui_overlay_services.h" #include "settings.h" #include "serializer.h" #include "font.h" @@ -59,6 +60,25 @@ void apply_file_menu_plan(App& app, pp::app::FileMenuCommand command) pp::panopainter::apply_legacy_file_menu_command(app, command); } +std::shared_ptr add_menu_popup( + App& app, + const char* template_id, + glm::vec2 position, + float rtl_anchor_width) +{ + const auto popup = pp::panopainter::add_legacy_popup_menu( + app, + template_id, + position.x, + position.y, + rtl_anchor_width); + if (!popup) { + LOG("Popup menu '%s' failed: %s", template_id ? template_id : "", popup.status().message); + return nullptr; + } + return popup.value(); +} + pp::app::DocumentLayerMenuPlan make_layer_menu_plan( pp::app::DocumentLayerMenuCommand command, App& app) @@ -655,13 +675,9 @@ void App::init_menu_file() { menu_file->on_click = [=](Node*) { glm::vec2 pos = menu_file->m_pos + glm::vec2(0, menu_file->m_size.y); - auto popup = layout[const_hash("file-menu")]->m_children[0]->clone(); - popup->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - popup->m_size.x + menu_file->m_size.x; - popup->SetPositioning(YGPositionTypeAbsolute); - popup->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(popup); + auto popup = add_menu_popup(*this, "file-menu", pos, menu_file->m_size.x); + if (!popup) + return; if (auto b = popup->find("file-newdoc")) b->on_click = [this, popup](Node*) { @@ -714,13 +730,9 @@ void App::init_menu_file() if (auto b = popup->find("file-export-tick")) b->on_click = [this, b, popup](Node*) { glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0); - auto subpopup = layout[const_hash("file-submenu-export")]->m_children[0]->clone(); - subpopup->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - subpopup->m_size.x + b->m_size.x; - subpopup->SetPositioning(YGPositionTypeAbsolute); - subpopup->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(subpopup); + auto subpopup = add_menu_popup(*this, "file-submenu-export", pos, b->m_size.x); + if (!subpopup) + return; subpopup->find("file-submenu-export-png")->on_click = [this, subpopup, popup](Node*) { apply_document_export_menu_plan(*this, pp::app::DocumentExportMenuKind::png); popup->mouse_release(); @@ -805,13 +817,9 @@ void App::init_menu_edit() { menu_file->on_click = [=](Node*) { glm::vec2 pos = menu_file->m_pos + glm::vec2(0, menu_file->m_size.y); - auto popup = layout[const_hash("edit-menu")]->m_children[0]->clone(); - popup->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - popup->m_size.x + menu_file->m_size.x; - popup->SetPositioning(YGPositionTypeAbsolute); - popup->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(popup); + auto popup = add_menu_popup(*this, "edit-menu", pos, menu_file->m_size.x); + if (!popup) + return; }; } } @@ -824,13 +832,9 @@ void App::init_menu_tools() { menu_exp->on_click = [this, menu_exp, main](Node*) { glm::vec2 pos = menu_exp->m_pos + glm::vec2(0, menu_exp->m_size.y); - auto popup_exp = layout[const_hash("tools-menu")]->m_children[0]->clone(); - popup_exp->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - popup_exp->m_size.x + menu_exp->m_size.x; - popup_exp->SetPositioning(YGPositionTypeAbsolute); - popup_exp->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(popup_exp); + auto popup_exp = add_menu_popup(*this, "tools-menu", pos, menu_exp->m_size.x); + if (!popup_exp) + return; if (auto tick = popup_exp->find("tools-panels")) tick->on_click = [this, popup_exp](Node* b) { @@ -839,13 +843,9 @@ void App::init_menu_tools() return; glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0); - auto popup_time = layout[const_hash("panels-menu")]->m_children[0]->clone(); - popup_time->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - popup_time->m_size.x + b->m_size.x; - popup_time->SetPositioning(YGPositionTypeAbsolute); - popup_time->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(popup_time); + auto popup_time = add_menu_popup(*this, "panels-menu", pos, b->m_size.x); + if (!popup_time) + return; auto visible = [this](Node* panel) { if (!panel) @@ -1030,13 +1030,9 @@ void App::init_menu_tools() return; glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0); - auto popup_time = layout[const_hash("options-menu")]->m_children[0]->clone(); - popup_time->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - popup_time->m_size.x + b->m_size.x; - popup_time->SetPositioning(YGPositionTypeAbsolute); - popup_time->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(popup_time); + auto popup_time = add_menu_popup(*this, "options-menu", pos, b->m_size.x); + if (!popup_time) + return; if (auto ui_scale = popup_time->find("tools-ui-scale")) { @@ -1244,13 +1240,9 @@ void App::init_menu_about() { menu_file->on_click = [=](Node*) { glm::vec2 pos = menu_file->m_pos + glm::vec2(0, menu_file->m_size.y); - auto popup = layout[const_hash("about-menu")]->m_children[0]->clone(); - popup->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - popup->m_size.x + menu_file->m_size.x; - popup->SetPositioning(YGPositionTypeAbsolute); - popup->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(popup); + auto popup = add_menu_popup(*this, "about-menu", pos, menu_file->m_size.x); + if (!popup) + return; popup->find("about-app")->on_click = [this, popup](Node*) { const auto plan = pp::app::plan_about_menu_command( @@ -1411,13 +1403,9 @@ void App::init_menu_layer() { menu_file->on_click = [=](Node*) { glm::vec2 pos = menu_file->m_pos + glm::vec2(0, menu_file->m_size.y); - auto popup = layout[const_hash("layers-menu")]->m_children[0]->clone(); - popup->update(); - if (YGNodeStyleGetDirection(layout[main_id]->y_node) == YGDirectionRTL) - pos.x = pos.x - popup->m_size.x + menu_file->m_size.x; - popup->SetPositioning(YGPositionTypeAbsolute); - popup->SetPosition(pos.x, pos.y); - layout[main_id]->add_child(popup); + auto popup = add_menu_popup(*this, "layers-menu", pos, menu_file->m_size.x); + if (!popup) + return; popup->find("layer-clear")->on_click = [this, popup](Node*) { const auto plan = make_layer_menu_plan(pp::app::DocumentLayerMenuCommand::clear, *this); diff --git a/src/legacy_ui_overlay_services.cpp b/src/legacy_ui_overlay_services.cpp index e35083d..cd8c4aa 100644 --- a/src/legacy_ui_overlay_services.cpp +++ b/src/legacy_ui_overlay_services.cpp @@ -3,6 +3,7 @@ #include "app.h" #include "node.h" +#include "node_popup_menu.h" namespace pp::panopainter { @@ -31,4 +32,45 @@ pp::foundation::Status attach_legacy_overlay_node( return pp::foundation::Status::success(); } +pp::foundation::Result> add_legacy_popup_menu( + App& app, + const char* template_id, + float x, + float y, + float rtl_anchor_width) noexcept +{ + if (!template_id) { + return pp::foundation::Result>::failure( + pp::foundation::Status::invalid_argument("legacy popup template id is null")); + } + + auto* template_root = app.layout[const_hash(template_id)]; + if (!template_root || template_root->m_children.empty()) { + return pp::foundation::Result>::failure( + pp::foundation::Status::invalid_argument("legacy popup template is missing")); + } + + auto popup = template_root->m_children[0]->clone(); + if (!popup) { + return pp::foundation::Result>::failure( + pp::foundation::Status::invalid_argument("legacy popup clone failed")); + } + + popup->update(); + if (auto* root = app.layout[app.main_id]) { + if (YGNodeStyleGetDirection(root->y_node) == YGDirectionRTL) { + x = x - popup->m_size.x + rtl_anchor_width; + } + } + popup->SetPositioning(YGPositionTypeAbsolute); + popup->SetPosition(x, y); + + const auto status = attach_legacy_overlay_node(app, popup); + if (!status.ok()) { + return pp::foundation::Result>::failure(status); + } + + return pp::foundation::Result>::success(popup); +} + } // namespace pp::panopainter diff --git a/src/legacy_ui_overlay_services.h b/src/legacy_ui_overlay_services.h index 0d33fb8..549c3a3 100644 --- a/src/legacy_ui_overlay_services.h +++ b/src/legacy_ui_overlay_services.h @@ -6,6 +6,7 @@ class App; class Node; +class NodePopupMenu; namespace pp::panopainter { @@ -15,6 +16,13 @@ void initialize_legacy_overlay_node(App& app, Node& node); App& app, const std::shared_ptr& node) noexcept; +[[nodiscard]] pp::foundation::Result> add_legacy_popup_menu( + App& app, + const char* template_id, + float x, + float y, + float rtl_anchor_width) noexcept; + template std::shared_ptr make_legacy_overlay_node(App& app) {