Step 352: Key Symbol Rendering (12/12 tests)

This commit is contained in:
Bill
2026-02-16 08:59:56 -07:00
parent fc5483e0b6
commit 0ba936da2b
4 changed files with 309 additions and 0 deletions

View File

@@ -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)

View 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();
}
};

View 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;
}

View File

@@ -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)