From 0ba936da2bde91739df252111a690ffe30646756 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 08:59:56 -0700 Subject: [PATCH] Step 352: Key Symbol Rendering (12/12 tests) --- editor/CMakeLists.txt | 4 + editor/src/KeySymbolRenderer.h | 123 +++++++++++++++++++++++++++ editor/tests/step352_test.cpp | 146 +++++++++++++++++++++++++++++++++ progress.md | 36 ++++++++ 4 files changed, 309 insertions(+) create mode 100644 editor/src/KeySymbolRenderer.h create mode 100644 editor/tests/step352_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 2d7f2e4..4451822 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2102,4 +2102,8 @@ add_executable(step351_test tests/step351_test.cpp) target_include_directories(step351_test PRIVATE src) target_link_libraries(step351_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step352_test tests/step352_test.cpp) +target_include_directories(step352_test PRIVATE src) +target_link_libraries(step352_test PRIVATE nlohmann_json::nlohmann_json) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/KeySymbolRenderer.h b/editor/src/KeySymbolRenderer.h new file mode 100644 index 0000000..abed1b8 --- /dev/null +++ b/editor/src/KeySymbolRenderer.h @@ -0,0 +1,123 @@ +#pragma once +// Step 352: Key Symbol Renderer — headless key badge data model +// Produces styled key symbol text for menus, tooltips, and command palette +// No ImGui dependency — outputs text + style info that renderers can use + +#include +#include +#include "KeybindingRegistry.h" +#include "ThemeData.h" + +struct KeyBadge { + std::string text; // e.g. "Ctrl+S" or "⌃S" + Color4 textColor; + Color4 bgColor; + Color4 borderColor; + float fontSize; + float paddingX = 6.0f; + float paddingY = 2.0f; + float cornerRadius = 4.0f; +}; + +struct MenuItemLayout { + std::string label; // e.g. "Save Buffer" + std::string shortcut; // e.g. "Ctrl+S" + std::string symbols; // e.g. "⌃S" + std::string category; + bool hasBadge; + bool overlapRisk = false; + int minGapSpaces = 4; +}; + +class KeySymbolRenderer { +public: + // Render a key badge for a key combo (text form). + static KeyBadge renderKeySymbol(const KeyCombo& combo, const WhetstoneTheme& theme) { + KeyBadge badge; + badge.text = combo.toString(); + badge.textColor = theme.text; + badge.bgColor = theme.bgAlt; + badge.borderColor = theme.border; + badge.fontSize = theme.fontSizeSmall; + return badge; + } + + // Backward-compatible alias. + static KeyBadge renderKeyBadge(const KeyCombo& combo, const WhetstoneTheme& theme) { + return renderKeySymbol(combo, theme); + } + + // Render a key badge with platform-aware symbols. + static KeyBadge renderKeyBadgeSymbols(const KeyCombo& combo, const WhetstoneTheme& theme, + bool macOS = false) { + KeyBadge badge; + badge.text = combo.toSymbols(macOS); + badge.textColor = theme.text; + badge.bgColor = theme.bgAlt; + badge.borderColor = theme.border; + badge.fontSize = theme.fontSizeSmall; + return badge; + } + + // Create menu item layout with label left, key right + static MenuItemLayout renderActionWithKey( + const std::string& label, + const std::string& actionId, + const KeybindingRegistry& registry, + const WhetstoneTheme& theme, + const std::string& category = "") + { + MenuItemLayout item; + item.label = label; + item.category = category; + auto binding = registry.getBinding(actionId); + if (shouldRender(binding)) { + item.shortcut = binding.toString(); + item.symbols = binding.toSymbolsAuto(); + item.hasBadge = true; + } else { + item.hasBadge = false; + } + // Rough overlap heuristic for headless layout validation. + const int roughTextWidth = static_cast(label.size() + item.symbols.size()); + const int available = static_cast(theme.fontSizeUI * 8.0f); + item.overlapRisk = roughTextWidth > available; + return item; + } + + // Backward-compatible overload without theme. + static MenuItemLayout renderActionWithKey( + const std::string& label, + const std::string& actionId, + const KeybindingRegistry& registry, + const std::string& category = "") + { + return renderActionWithKey(label, actionId, registry, getDefaultDarkTheme(), category); + } + + // Render key chord (multi-key sequence like Ctrl+K Ctrl+S). + static std::string renderKeyChord(const std::vector& combos) { + std::string result; + for (size_t i = 0; i < combos.size(); ++i) { + if (i > 0) result += " "; + result += combos[i].toString(); + } + return result; + } + + // Render key chord with symbols + static std::string renderKeyChordSymbols(const std::vector& combos, + bool macOS = false) { + std::string result; + for (size_t i = 0; i < combos.size(); ++i) { + if (i > 0) result += " "; + result += combos[i].toSymbols(macOS); + } + return result; + } + + // Check if a binding should render (non-empty key) + static bool shouldRender(const KeyCombo& combo) { + return !combo.key.empty(); + } +}; diff --git a/editor/tests/step352_test.cpp b/editor/tests/step352_test.cpp new file mode 100644 index 0000000..df73b10 --- /dev/null +++ b/editor/tests/step352_test.cpp @@ -0,0 +1,146 @@ +// Step 352: Key Symbol Rendering (12 tests) + +#include +#include +#include +#include +#include "KeySymbolRenderer.h" + +int main() { + int passed = 0; + WhetstoneTheme theme = getDefaultDarkTheme(); + + // Test 1: single key renders + { + KeyCombo combo = KeyCombo::fkey("F5"); + KeyBadge badge = KeySymbolRenderer::renderKeySymbol(combo, theme); + assert(badge.text == "F5"); + std::cout << "Test 1 PASSED: single key renders\n"; + passed++; + } + + // Test 2: modifier+key renders + { + KeyCombo combo = KeyCombo::ctrl("S"); + KeyBadge badge = KeySymbolRenderer::renderKeySymbol(combo, theme); + assert(badge.text == "Ctrl+S"); + std::cout << "Test 2 PASSED: modifier+key renders\n"; + passed++; + } + + // Test 3: platform symbols correct + { + KeyCombo combo = KeyCombo::ctrlShift("P"); + KeyBadge mac = KeySymbolRenderer::renderKeyBadgeSymbols(combo, theme, true); + assert(mac.text.find("\xE2\x8C\x83") != std::string::npos); // ⌃ + assert(mac.text.find("\xE2\x87\xA7") != std::string::npos); // ⇧ + std::cout << "Test 3 PASSED: platform symbols\n"; + passed++; + } + + // Test 4: menu item layout label left + key right data present + { + KeybindingRegistry reg; + reg.loadDefaults(); + auto item = KeySymbolRenderer::renderActionWithKey("Save Buffer", "save-buffer", reg, theme, "File"); + assert(item.label == "Save Buffer"); + assert(item.shortcut == "Ctrl+S"); + assert(item.hasBadge); + std::cout << "Test 4 PASSED: menu item layout data\n"; + passed++; + } + + // Test 5: chord rendering + { + std::vector chord = {KeyCombo::ctrl("K"), KeyCombo::ctrl("S")}; + std::string text = KeySymbolRenderer::renderKeyChord(chord); + assert(text == "Ctrl+K Ctrl+S"); + std::cout << "Test 5 PASSED: chord rendering\n"; + passed++; + } + + // Test 6: key badge has background + { + KeyCombo combo = KeyCombo::ctrl("F"); + KeyBadge badge = KeySymbolRenderer::renderKeySymbol(combo, theme); + assert(badge.bgColor.toHex() == theme.bgAlt.toHex()); + assert(badge.borderColor.toHex() == theme.border.toHex()); + std::cout << "Test 6 PASSED: key badge background/border\n"; + passed++; + } + + // Test 7: theme colors used + { + KeyCombo combo = KeyCombo::ctrl("P"); + KeyBadge badge = KeySymbolRenderer::renderKeySymbol(combo, theme); + assert(badge.textColor.toHex() == theme.text.toHex()); + assert(badge.fontSize == theme.fontSizeSmall); + std::cout << "Test 7 PASSED: theme colors used\n"; + passed++; + } + + // Test 8: empty binding shows nothing + { + KeybindingRegistry reg; + reg.loadDefaults(); + auto item = KeySymbolRenderer::renderActionWithKey("Unknown", "no-such-action", reg, theme); + assert(!item.hasBadge); + assert(item.shortcut.empty()); + assert(item.symbols.empty()); + std::cout << "Test 8 PASSED: empty binding hidden\n"; + passed++; + } + + // Test 9: special keys render symbols on mac + { + KeyCombo enter = KeyCombo::fkey("Enter"); + KeyCombo esc = KeyCombo::fkey("Escape"); + KeyCombo tab = KeyCombo::fkey("Tab"); + auto b1 = KeySymbolRenderer::renderKeyBadgeSymbols(enter, theme, true); + auto b2 = KeySymbolRenderer::renderKeyBadgeSymbols(esc, theme, true); + auto b3 = KeySymbolRenderer::renderKeyBadgeSymbols(tab, theme, true); + assert(b1.text.find("\xE2\x86\xB5") != std::string::npos); // ↵ + assert(b2.text.find("\xE2\x8E\x8B") != std::string::npos); // ⎋ + assert(b3.text.find("\xE2\x87\xA5") != std::string::npos); // ⇥ + std::cout << "Test 9 PASSED: special keys render symbols\n"; + passed++; + } + + // Test 10: long label does not trigger overlap risk with normal widths + { + KeybindingRegistry reg; + reg.loadDefaults(); + WhetstoneTheme roomy = theme; + roomy.fontSizeUI = 18.0f; + auto item = KeySymbolRenderer::renderActionWithKey( + "Very Long Menu Action Label For Save Buffer", "save-buffer", reg, roomy, "File"); + assert(!item.overlapRisk); + std::cout << "Test 10 PASSED: long label no overlap risk\n"; + passed++; + } + + // Test 11: chord symbols render + { + std::vector chord = {KeyCombo::ctrl("K"), KeyCombo::ctrlShift("S")}; + std::string syms = KeySymbolRenderer::renderKeyChordSymbols(chord, true); + assert(syms.find("\xE2\x8C\x83") != std::string::npos); // ⌃ + assert(syms.find("K") != std::string::npos); + assert(syms.find("S") != std::string::npos); + std::cout << "Test 11 PASSED: chord symbols render\n"; + passed++; + } + + // Test 12: shouldRender logic + { + KeyCombo none; + KeyCombo set = KeyCombo::ctrl("B"); + assert(!KeySymbolRenderer::shouldRender(none)); + assert(KeySymbolRenderer::shouldRender(set)); + std::cout << "Test 12 PASSED: shouldRender\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index 55f0c51..9afc787 100644 --- a/progress.md +++ b/progress.md @@ -1716,6 +1716,42 @@ runtime and persistence primitives for the upcoming command palette/menu work. - `step350_test` — PASS (8/8) latest completed sprint checkpoint - `step351_test` — PASS (12/12) new step coverage +### Step 352: Key Symbol Rendering +**Status:** PASS (12/12 tests) + +Implemented a headless key symbol rendering layer for menus/tooltips/command +palette UI data. The renderer now builds styled key badge data, action rows +with shortcut columns, and multi-key chord strings in both text and symbol forms. + +**Files modified:** +- `editor/src/KeySymbolRenderer.h` — added/expanded: + - `renderKeySymbol(...)` (text badge) + backward-compatible `renderKeyBadge(...)` + - `renderKeyBadgeSymbols(..., macOS)` for symbol mode + - `renderActionWithKey(..., theme, category)` with layout metadata + - overload preserving previous call shape + - `renderKeyChord(...)` + `renderKeyChordSymbols(..., macOS)` + - badge styling fields (padding/radius) and menu overlap heuristic +- `editor/CMakeLists.txt` — `step352_test` target + +**Files created:** +- `editor/tests/step352_test.cpp` — 12 tests covering: + 1. single key badge rendering + 2. modifier+key rendering + 3. platform symbol output + 4. menu item label/shortcut layout data + 5. key chord text rendering + 6. badge background/border assignment + 7. theme color/font propagation + 8. empty binding behavior + 9. special key symbol mapping (Enter/Escape/Tab) + 10. long-label overlap check + 11. chord symbols rendering + 12. `shouldRender` behavior + +**Verification run:** +- `step351_test` — PASS (12/12) regression coverage +- `step352_test` — PASS (12/12) new step coverage + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)