Step 352: Key Symbol Rendering (12/12 tests)
This commit is contained in:
@@ -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)
|
||||
|
||||
123
editor/src/KeySymbolRenderer.h
Normal file
123
editor/src/KeySymbolRenderer.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#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<int>(label.size() + item.symbols.size());
|
||||
const int available = static_cast<int>(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<KeyCombo>& 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<KeyCombo>& 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();
|
||||
}
|
||||
};
|
||||
146
editor/tests/step352_test.cpp
Normal file
146
editor/tests/step352_test.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
// Step 352: Key Symbol Rendering (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#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<KeyCombo> 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<KeyCombo> 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;
|
||||
}
|
||||
36
progress.md
36
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)
|
||||
|
||||
Reference in New Issue
Block a user