Step 512: add window hierarchy and chrome layering model
This commit is contained in:
@@ -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)
|
||||
|
||||
142
editor/src/WindowHierarchyModel.h
Normal file
142
editor/src/WindowHierarchyModel.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
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<SurfaceNode> ordered;
|
||||
std::vector<std::string> 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<SurfaceNode>& 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<SurfaceNode> normalize(const std::vector<SurfaceNode>& input) {
|
||||
std::vector<SurfaceNode> 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;
|
||||
}
|
||||
};
|
||||
153
editor/tests/step512_test.cpp
Normal file
153
editor/tests/step512_test.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
// Step 512: Window Chrome + Hierarchy Pass (12 tests)
|
||||
|
||||
#include "WindowHierarchyModel.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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<SurfaceNode> 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;
|
||||
}
|
||||
37
progress.md
37
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`
|
||||
|
||||
Reference in New Issue
Block a user