diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 38cd6e3..2fcd2b4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3487,4 +3487,13 @@ target_link_libraries(step511_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step512_test tests/step512_test.cpp) +target_include_directories(step512_test PRIVATE src) +target_link_libraries(step512_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/WindowHierarchyModel.h b/editor/src/WindowHierarchyModel.h new file mode 100644 index 0000000..737b8b6 --- /dev/null +++ b/editor/src/WindowHierarchyModel.h @@ -0,0 +1,142 @@ +#pragma once +// Step 512: Window Chrome + Hierarchy Pass +// Headless hierarchy model for surface classification and deterministic +// elevation/layering semantics between work surfaces, tool surfaces, and overlays. + +#include +#include +#include + +struct SurfaceStyle { + float borderAlpha = 0.0f; + float shadowAlpha = 0.0f; + float edgeHighlightAlpha = 0.0f; + int zIndex = 0; +}; + +enum class SurfaceRole { + Work, + Tool, + Overlay, + Modal, + Notification +}; + +struct SurfaceNode { + std::string id; + SurfaceRole role = SurfaceRole::Work; + bool modal = false; + bool interactive = true; + int depth = 0; + SurfaceStyle style; +}; + +struct HierarchyReport { + bool valid = true; + std::vector ordered; + std::vector violations; +}; + +class WindowHierarchyModel { +public: + static SurfaceStyle styleForRole(SurfaceRole role, int depth) { + SurfaceStyle s; + switch (role) { + case SurfaceRole::Work: + s.borderAlpha = 0.28f; + s.shadowAlpha = 0.05f; + s.edgeHighlightAlpha = 0.10f; + s.zIndex = 100 + depth; + break; + case SurfaceRole::Tool: + s.borderAlpha = 0.34f; + s.shadowAlpha = 0.10f; + s.edgeHighlightAlpha = 0.16f; + s.zIndex = 200 + depth; + break; + case SurfaceRole::Overlay: + s.borderAlpha = 0.40f; + s.shadowAlpha = 0.20f; + s.edgeHighlightAlpha = 0.24f; + s.zIndex = 300 + depth; + break; + case SurfaceRole::Modal: + s.borderAlpha = 0.46f; + s.shadowAlpha = 0.28f; + s.edgeHighlightAlpha = 0.30f; + s.zIndex = 400 + depth; + break; + case SurfaceRole::Notification: + s.borderAlpha = 0.42f; + s.shadowAlpha = 0.18f; + s.edgeHighlightAlpha = 0.26f; + s.zIndex = 350 + depth; + break; + } + return s; + } + + static HierarchyReport validateAndOrder(const std::vector& input) { + HierarchyReport out; + out.ordered = input; + + for (auto& n : out.ordered) { + n.style = styleForRole(n.role, n.depth); + } + + std::sort(out.ordered.begin(), out.ordered.end(), + [](const SurfaceNode& a, const SurfaceNode& b) { + if (a.style.zIndex != b.style.zIndex) return a.style.zIndex < b.style.zIndex; + return a.id < b.id; + }); + + bool seenModal = false; + for (size_t i = 0; i < out.ordered.size(); ++i) { + const auto& n = out.ordered[i]; + if (n.role == SurfaceRole::Modal) { + seenModal = true; + if (!n.modal) { + out.valid = false; + out.violations.push_back("modal-flag-missing:" + n.id); + } + } + if (seenModal && n.role == SurfaceRole::Work) { + out.valid = false; + out.violations.push_back("work-above-modal:" + n.id); + } + } + + for (const auto& n : out.ordered) { + if (n.role == SurfaceRole::Overlay && n.style.zIndex < 300) { + out.valid = false; + out.violations.push_back("overlay-z-too-low:" + n.id); + } + if (n.role == SurfaceRole::Tool && n.style.zIndex < 200) { + out.valid = false; + out.violations.push_back("tool-z-too-low:" + n.id); + } + } + + return out; + } + + static std::vector normalize(const std::vector& input) { + std::vector out = input; + for (auto& n : out) { + if (n.role == SurfaceRole::Modal) n.modal = true; + n.style = styleForRole(n.role, n.depth); + } + std::sort(out.begin(), out.end(), + [](const SurfaceNode& a, const SurfaceNode& b) { + return a.style.zIndex < b.style.zIndex; + }); + return out; + } + + static bool blocksInteraction(const SurfaceNode& top, const SurfaceNode& bottom) { + if (!top.interactive) return false; + if (top.role == SurfaceRole::Modal && top.style.zIndex > bottom.style.zIndex) return true; + if (top.role == SurfaceRole::Overlay && top.style.zIndex > bottom.style.zIndex) return true; + return false; + } +}; diff --git a/editor/tests/step512_test.cpp b/editor/tests/step512_test.cpp new file mode 100644 index 0000000..4f8d977 --- /dev/null +++ b/editor/tests/step512_test.cpp @@ -0,0 +1,153 @@ +// Step 512: Window Chrome + Hierarchy Pass (12 tests) + +#include "WindowHierarchyModel.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static std::vector baseHierarchy() { + return { + {"editor", SurfaceRole::Work, false, true, 0, {}}, + {"left-tool", SurfaceRole::Tool, false, true, 0, {}}, + {"completion", SurfaceRole::Overlay, false, true, 0, {}}, + {"dialog", SurfaceRole::Modal, true, true, 0, {}}, + {"toast", SurfaceRole::Notification, false, true, 0, {}} + }; +} + +void test_style_for_work_role_has_lowest_base_z() { + TEST(style_for_work_role_has_lowest_base_z); + auto s = WindowHierarchyModel::styleForRole(SurfaceRole::Work, 0); + CHECK(s.zIndex >= 100 && s.zIndex < 200, "work z-index range invalid"); + PASS(); +} + +void test_style_for_tool_role_above_work() { + TEST(style_for_tool_role_above_work); + auto work = WindowHierarchyModel::styleForRole(SurfaceRole::Work, 0); + auto tool = WindowHierarchyModel::styleForRole(SurfaceRole::Tool, 0); + CHECK(tool.zIndex > work.zIndex, "tool should be above work"); + PASS(); +} + +void test_overlay_role_above_tool() { + TEST(overlay_role_above_tool); + auto tool = WindowHierarchyModel::styleForRole(SurfaceRole::Tool, 0); + auto overlay = WindowHierarchyModel::styleForRole(SurfaceRole::Overlay, 0); + CHECK(overlay.zIndex > tool.zIndex, "overlay should be above tool"); + PASS(); +} + +void test_modal_role_above_overlay() { + TEST(modal_role_above_overlay); + auto overlay = WindowHierarchyModel::styleForRole(SurfaceRole::Overlay, 0); + auto modal = WindowHierarchyModel::styleForRole(SurfaceRole::Modal, 0); + CHECK(modal.zIndex > overlay.zIndex, "modal should be above overlay"); + PASS(); +} + +void test_validate_marks_modal_flag_missing() { + TEST(validate_marks_modal_flag_missing); + auto nodes = baseHierarchy(); + nodes[3].modal = false; + auto report = WindowHierarchyModel::validateAndOrder(nodes); + CHECK(!report.valid, "report should be invalid"); + CHECK(!report.violations.empty(), "expected violations"); + PASS(); +} + +void test_normalize_sets_modal_flag() { + TEST(normalize_sets_modal_flag); + auto nodes = baseHierarchy(); + nodes[3].modal = false; + auto normalized = WindowHierarchyModel::normalize(nodes); + bool fixed = false; + for (const auto& n : normalized) { + if (n.id == "dialog" && n.modal) fixed = true; + } + CHECK(fixed, "modal flag should be restored"); + PASS(); +} + +void test_order_is_sorted_by_zindex() { + TEST(order_is_sorted_by_zindex); + auto report = WindowHierarchyModel::validateAndOrder(baseHierarchy()); + bool sorted = true; + for (size_t i = 1; i < report.ordered.size(); ++i) { + if (report.ordered[i - 1].style.zIndex > report.ordered[i].style.zIndex) sorted = false; + } + CHECK(sorted, "order should be sorted by z-index"); + PASS(); +} + +void test_blocks_interaction_for_modal_over_work() { + TEST(blocks_interaction_for_modal_over_work); + auto modal = SurfaceNode{"dialog", SurfaceRole::Modal, true, true, 0, + WindowHierarchyModel::styleForRole(SurfaceRole::Modal, 0)}; + auto work = SurfaceNode{"editor", SurfaceRole::Work, false, true, 0, + WindowHierarchyModel::styleForRole(SurfaceRole::Work, 0)}; + CHECK(WindowHierarchyModel::blocksInteraction(modal, work), + "modal should block underlying work surface"); + PASS(); +} + +void test_non_interactive_overlay_does_not_block() { + TEST(non_interactive_overlay_does_not_block); + auto overlay = SurfaceNode{"overlay", SurfaceRole::Overlay, false, false, 0, + WindowHierarchyModel::styleForRole(SurfaceRole::Overlay, 0)}; + auto work = SurfaceNode{"editor", SurfaceRole::Work, false, true, 0, + WindowHierarchyModel::styleForRole(SurfaceRole::Work, 0)}; + CHECK(!WindowHierarchyModel::blocksInteraction(overlay, work), + "non-interactive overlay should not block"); + PASS(); +} + +void test_depth_increases_zindex_within_role() { + TEST(depth_increases_zindex_within_role); + auto a = WindowHierarchyModel::styleForRole(SurfaceRole::Tool, 0); + auto b = WindowHierarchyModel::styleForRole(SurfaceRole::Tool, 3); + CHECK(b.zIndex > a.zIndex, "deeper node should get higher z-index"); + PASS(); +} + +void test_notification_between_overlay_and_modal() { + TEST(notification_between_overlay_and_modal); + auto notif = WindowHierarchyModel::styleForRole(SurfaceRole::Notification, 0); + auto overlay = WindowHierarchyModel::styleForRole(SurfaceRole::Overlay, 0); + auto modal = WindowHierarchyModel::styleForRole(SurfaceRole::Modal, 0); + CHECK(notif.zIndex > overlay.zIndex, "notification should be above overlay"); + CHECK(notif.zIndex < modal.zIndex, "notification should be below modal"); + PASS(); +} + +void test_validate_passes_for_base_hierarchy() { + TEST(validate_passes_for_base_hierarchy); + auto report = WindowHierarchyModel::validateAndOrder(baseHierarchy()); + CHECK(report.valid, "base hierarchy should be valid"); + PASS(); +} + +int main() { + std::cout << "Step 512: Window Chrome + Hierarchy Pass\n"; + + test_style_for_work_role_has_lowest_base_z(); // 1 + test_style_for_tool_role_above_work(); // 2 + test_overlay_role_above_tool(); // 3 + test_modal_role_above_overlay(); // 4 + test_validate_marks_modal_flag_missing(); // 5 + test_normalize_sets_modal_flag(); // 6 + test_order_is_sorted_by_zindex(); // 7 + test_blocks_interaction_for_modal_over_work(); // 8 + test_non_interactive_overlay_does_not_block(); // 9 + test_depth_increases_zindex_within_role(); // 10 + test_notification_between_overlay_and_modal(); // 11 + test_validate_passes_for_base_hierarchy(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 8a2b329..c524882 100644 --- a/progress.md +++ b/progress.md @@ -8108,3 +8108,40 @@ checks for state transitions. - `editor/src/InteractionStateModel.h` within header-size limit (`166` <= `600`) - `editor/tests/step511_test.cpp` within test-file size guidance (`142` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 512: Window Chrome + Hierarchy Pass +**Status:** PASS (12/12 tests) + +Implements deterministic surface hierarchy and chrome/elevation semantics across +work surfaces, tool surfaces, overlays, modals, and notifications, including +validation of layering invariants and interaction blocking behavior. + +**Files added:** +- `editor/src/WindowHierarchyModel.h` - hierarchy/chrome model: + - role-based style + z-index synthesis + - deterministic ordering by elevation/layer + - hierarchy validation with explicit violation signals + - normalization pass for modal semantics + - modal/overlay interaction-blocking checks +- `editor/tests/step512_test.cpp` - 12 tests covering: + - role z-index ordering (`work < tool < overlay < modal`) + - modal-flag validation and normalization + - deterministic z-index sorting + - modal/overlay interaction blocking semantics + - depth-sensitive z-index behavior + - notification layering between overlay and modal + - baseline valid hierarchy behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step512_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step512_test` - PASS +- `./editor/build-native/step512_test` - PASS (12/12) +- `./editor/build-native/step511_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/WindowHierarchyModel.h` within header-size limit (`142` <= `600`) +- `editor/tests/step512_test.cpp` within test-file size guidance (`153` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`