Step 342-343: PanelManager + Icons + KeybindingRegistry (24/24 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-16 08:24:20 -07:00
parent e26cbe8b32
commit e8c298ce9d
6 changed files with 923 additions and 0 deletions

92
editor/src/Icons.h Normal file
View File

@@ -0,0 +1,92 @@
#pragma once
// Step 343: Icon system — Unicode symbols for panels, diagnostics, workflow, AST nodes
// No external icon font dependency — pure Unicode + text fallbacks
#include <string>
#include <map>
namespace WhetstoneIcons {
// File types
constexpr const char* FileSource = "\xF0\x9F\x93\x84"; // document
constexpr const char* FileHeader = "\xF0\x9F\x93\x83"; // page with curl
constexpr const char* FileConfig = "\xE2\x9A\x99"; // gear
constexpr const char* Folder = "\xF0\x9F\x93\x81"; // folder
constexpr const char* FolderOpen = "\xF0\x9F\x93\x82"; // open folder
// Diagnostics
constexpr const char* DiagError = "\xE2\x9B\x94"; // no entry
constexpr const char* DiagWarning = "\xE2\x9A\xA0"; // warning
constexpr const char* DiagInfo = "\xE2\x84\xB9"; // info
constexpr const char* DiagHint = "\xF0\x9F\x92\xA1"; // light bulb
// Workflow status
constexpr const char* Pending = "\xE2\x8F\xB3"; // hourglass
constexpr const char* InProgress = "\xE2\x96\xB6"; // play
constexpr const char* Complete = "\xE2\x9C\x85"; // check mark
constexpr const char* Rejected = "\xE2\x9D\x8C"; // cross mark
constexpr const char* Review = "\xF0\x9F\x91\x81"; // eye
// AST nodes
constexpr const char* NodeFunction = "\xC6\x92"; // function ƒ
constexpr const char* NodeClass = "\xE2\x97\x86"; // diamond
constexpr const char* NodeVariable = "\xF0\x9D\x91\xA5"; // math x
constexpr const char* NodeModule = "\xE2\x96\xA3"; // square
constexpr const char* NodeAnnotation = "\xF0\x9F\x8F\xB7"; // label
// Navigation
constexpr const char* ArrowRight = "\xE2\x96\xB6";
constexpr const char* ArrowDown = "\xE2\x96\xBC";
constexpr const char* Search = "\xF0\x9F\x94\x8D";
constexpr const char* Settings = "\xE2\x9A\x99";
// Text fallbacks (ASCII-safe)
inline std::string fallback(const std::string& icon) {
static std::map<std::string, std::string> fallbacks = {
{DiagError, "[ERR]"}, {DiagWarning, "[WARN]"},
{DiagInfo, "[INFO]"}, {DiagHint, "[HINT]"},
{Pending, "[...]"}, {InProgress, "[>>]"},
{Complete, "[OK]"}, {Rejected, "[X]"}, {Review, "[?]"},
{NodeFunction, "fn"}, {NodeClass, "cls"},
{NodeVariable, "var"}, {NodeModule, "mod"},
{FileSource, "[]"}, {Folder, "/"}, {FolderOpen, "/"},
};
auto it = fallbacks.find(icon);
return it != fallbacks.end() ? it->second : icon;
}
// Get icon for AST node type
inline std::string iconForNodeType(const std::string& conceptType) {
if (conceptType == "Function" || conceptType == "AsyncFunction" ||
conceptType == "MethodDeclaration") return NodeFunction;
if (conceptType == "ClassDeclaration" || conceptType == "InterfaceDeclaration") return NodeClass;
if (conceptType == "Variable") return NodeVariable;
if (conceptType == "Module") return NodeModule;
if (conceptType.find("Annotation") != std::string::npos) return NodeAnnotation;
if (conceptType == "EnumDeclaration") return NodeClass;
if (conceptType == "NamespaceDeclaration") return Folder;
return NodeModule; // default
}
// Get icon for diagnostic severity
inline std::string iconForSeverity(int severity) {
switch (severity) {
case 1: return DiagError;
case 2: return DiagWarning;
case 3: return DiagInfo;
case 4: return DiagHint;
default: return DiagInfo;
}
}
// Get icon for workflow status
inline std::string iconForWorkflowStatus(const std::string& status) {
if (status == "pending" || status == "ready") return Pending;
if (status == "assigned" || status == "in-progress") return InProgress;
if (status == "complete") return Complete;
if (status == "rejected") return Rejected;
if (status == "review") return Review;
return Pending;
}
} // namespace WhetstoneIcons

View File

@@ -0,0 +1,222 @@
#pragma once
// Step 343-351: Keybinding registry — centralized action→key combo mapping
// Headless data model: no ImGui dependency, supports symbol rendering
#include <string>
#include <vector>
#include <map>
#include <optional>
#include <algorithm>
#include <nlohmann/json.hpp>
enum class KeyModifier : uint8_t {
None = 0,
Ctrl = 1 << 0,
Alt = 1 << 1,
Shift = 1 << 2,
Super = 1 << 3
};
inline uint8_t operator|(KeyModifier a, KeyModifier b) {
return static_cast<uint8_t>(a) | static_cast<uint8_t>(b);
}
inline bool hasModifier(uint8_t mods, KeyModifier m) {
return (mods & static_cast<uint8_t>(m)) != 0;
}
struct KeyCombo {
uint8_t modifiers = 0;
std::string key; // "S", "P", "Z", "F5", "F8", "Enter", "Escape", etc.
bool empty() const { return key.empty(); }
std::string toString() const {
std::string result;
if (hasModifier(modifiers, KeyModifier::Ctrl)) result += "Ctrl+";
if (hasModifier(modifiers, KeyModifier::Alt)) result += "Alt+";
if (hasModifier(modifiers, KeyModifier::Shift)) result += "Shift+";
if (hasModifier(modifiers, KeyModifier::Super)) result += "Super+";
result += key;
return result;
}
// Platform-aware symbols (macOS style vs Linux/Windows)
std::string toSymbols(bool macOS = false) const {
std::string result;
if (macOS) {
if (hasModifier(modifiers, KeyModifier::Ctrl)) result += "\xE2\x8C\x83"; // ⌃
if (hasModifier(modifiers, KeyModifier::Alt)) result += "\xE2\x8C\xA5"; // ⌥
if (hasModifier(modifiers, KeyModifier::Shift)) result += "\xE2\x87\xA7"; // ⇧
if (hasModifier(modifiers, KeyModifier::Super)) result += "\xE2\x8C\x98"; // ⌘
result += key;
} else {
result = toString(); // Linux/Windows uses text form
}
return result;
}
bool operator==(const KeyCombo& other) const {
return modifiers == other.modifiers && key == other.key;
}
nlohmann::json toJson() const {
return {{"modifiers", modifiers}, {"key", key}};
}
static KeyCombo fromJson(const nlohmann::json& j) {
KeyCombo c;
c.modifiers = j.value("modifiers", 0);
c.key = j.value("key", "");
return c;
}
// Helper constructors
static KeyCombo ctrl(const std::string& k) {
return {static_cast<uint8_t>(KeyModifier::Ctrl), k};
}
static KeyCombo ctrlShift(const std::string& k) {
return {KeyModifier::Ctrl | KeyModifier::Shift, k};
}
static KeyCombo alt(const std::string& k) {
return {static_cast<uint8_t>(KeyModifier::Alt), k};
}
static KeyCombo fkey(const std::string& k) {
return {0, k};
}
static KeyCombo shiftFkey(const std::string& k) {
return {static_cast<uint8_t>(KeyModifier::Shift), k};
}
};
struct KeyAction {
std::string id; // "save-buffer", "toggle-ast-panel", etc.
std::string label; // "Save Buffer", "Toggle AST Panel"
std::string category; // "File", "Edit", "View", "Tools", "Workflow"
std::string description; // optional longer description
};
class KeybindingRegistry {
public:
KeybindingRegistry() = default;
void registerAction(const std::string& id, const std::string& label,
const std::string& category, const std::string& description = "") {
actions_[id] = {id, label, category, description};
}
void bind(const std::string& actionId, const KeyCombo& combo) {
bindings_[actionId] = combo;
}
KeyCombo getBinding(const std::string& actionId) const {
auto it = bindings_.find(actionId);
return it != bindings_.end() ? it->second : KeyCombo{};
}
std::string getSymbols(const std::string& actionId, bool macOS = false) const {
auto combo = getBinding(actionId);
return combo.empty() ? "" : combo.toSymbols(macOS);
}
std::optional<std::string> findConflict(const std::string& actionId, const KeyCombo& combo) const {
for (const auto& [aid, c] : bindings_) {
if (aid != actionId && c == combo) return aid;
}
return std::nullopt;
}
std::vector<KeyAction> getActions() const {
std::vector<KeyAction> result;
for (const auto& [id, action] : actions_) result.push_back(action);
return result;
}
std::vector<KeyAction> getActionsByCategory(const std::string& category) const {
std::vector<KeyAction> result;
for (const auto& [id, action] : actions_) {
if (action.category == category) result.push_back(action);
}
return result;
}
std::map<std::string, KeyCombo> getAllBindings() const { return bindings_; }
int actionCount() const { return static_cast<int>(actions_.size()); }
int bindingCount() const { return static_cast<int>(bindings_.size()); }
void loadDefaults() {
// File actions
registerAction("save-buffer", "Save Buffer", "File");
registerAction("save-all", "Save All", "File");
registerAction("open-file", "Open File", "File");
registerAction("close-buffer", "Close Buffer", "File");
// Edit actions
registerAction("undo", "Undo", "Edit");
registerAction("redo", "Redo", "Edit");
registerAction("find-in-file", "Find in File", "Edit");
registerAction("find-in-project", "Find in Project", "Edit");
// View actions
registerAction("command-palette", "Command Palette", "View");
registerAction("toggle-file-tree", "Toggle File Tree", "View");
registerAction("toggle-ast-view", "Toggle AST View", "View");
registerAction("toggle-diagnostics", "Toggle Diagnostics", "View");
registerAction("toggle-annotations", "Toggle Annotations", "View");
// Tools actions
registerAction("run-pipeline", "Run Pipeline", "Tools");
registerAction("generate-code", "Generate Code", "Tools");
registerAction("infer-annotations", "Infer Annotations", "Tools");
registerAction("validate", "Validate", "Tools");
// Workflow actions
registerAction("create-workflow", "Create Workflow", "Workflow");
registerAction("route-all", "Route All Tasks", "Workflow");
// Navigation
registerAction("next-diagnostic", "Next Diagnostic", "Navigation");
registerAction("prev-diagnostic", "Previous Diagnostic", "Navigation");
registerAction("go-to-definition", "Go to Definition", "Navigation");
registerAction("go-to-symbol", "Go to Symbol", "Navigation");
// Default bindings
bind("save-buffer", KeyCombo::ctrl("S"));
bind("save-all", KeyCombo::ctrlShift("S"));
bind("undo", KeyCombo::ctrl("Z"));
bind("redo", KeyCombo::ctrlShift("Z"));
bind("find-in-file", KeyCombo::ctrl("F"));
bind("find-in-project", KeyCombo::ctrlShift("F"));
bind("command-palette", KeyCombo::ctrl("P"));
bind("toggle-file-tree", KeyCombo::ctrl("B"));
bind("toggle-diagnostics", KeyCombo::ctrlShift("D"));
bind("run-pipeline", KeyCombo::fkey("F5"));
bind("next-diagnostic", KeyCombo::fkey("F8"));
bind("prev-diagnostic", KeyCombo::shiftFkey("F8"));
bind("go-to-definition", KeyCombo::fkey("F12"));
bind("go-to-symbol", KeyCombo::ctrlShift("O"));
}
nlohmann::json toJson() const {
nlohmann::json j;
nlohmann::json bindingsJson;
for (const auto& [id, combo] : bindings_) {
bindingsJson[id] = combo.toJson();
}
j["bindings"] = bindingsJson;
return j;
}
void loadFromJson(const nlohmann::json& j) {
if (j.contains("bindings")) {
for (auto& [id, cj] : j["bindings"].items()) {
bindings_[id] = KeyCombo::fromJson(cj);
}
}
}
private:
std::map<std::string, KeyAction> actions_;
std::map<std::string, KeyCombo> bindings_;
};

230
editor/src/PanelManager.h Normal file
View File

@@ -0,0 +1,230 @@
#pragma once
// Step 342-343: Panel registration system and docking layout management
// Headless data model for panel state — no ImGui dependency
#include <string>
#include <vector>
#include <map>
#include <functional>
#include <algorithm>
#include <nlohmann/json.hpp>
enum class DockPosition {
Left,
CenterTop,
CenterBottom,
Right,
Bottom
};
inline std::string dockPositionToString(DockPosition p) {
switch (p) {
case DockPosition::Left: return "left";
case DockPosition::CenterTop: return "center-top";
case DockPosition::CenterBottom: return "center-bottom";
case DockPosition::Right: return "right";
case DockPosition::Bottom: return "bottom";
}
return "unknown";
}
inline DockPosition dockPositionFromString(const std::string& s) {
if (s == "left") return DockPosition::Left;
if (s == "center-top") return DockPosition::CenterTop;
if (s == "center-bottom") return DockPosition::CenterBottom;
if (s == "right") return DockPosition::Right;
if (s == "bottom") return DockPosition::Bottom;
return DockPosition::CenterTop;
}
struct PanelInfo {
std::string id;
std::string title;
DockPosition defaultDock;
DockPosition currentDock;
bool isVisible = true;
int order = 0;
std::string icon; // Unicode icon character
std::string category; // "navigation", "editor", "analysis", "output"
float widthPercent = 0; // 0 = auto
float heightPercent = 0; // 0 = auto
nlohmann::json toJson() const {
return {
{"id", id}, {"title", title},
{"defaultDock", dockPositionToString(defaultDock)},
{"currentDock", dockPositionToString(currentDock)},
{"isVisible", isVisible}, {"order", order},
{"icon", icon}, {"category", category},
{"widthPercent", widthPercent}, {"heightPercent", heightPercent}
};
}
static PanelInfo fromJson(const nlohmann::json& j) {
PanelInfo p;
p.id = j.value("id", "");
p.title = j.value("title", "");
p.defaultDock = dockPositionFromString(j.value("defaultDock", "center-top"));
p.currentDock = dockPositionFromString(j.value("currentDock", j.value("defaultDock", "center-top")));
p.isVisible = j.value("isVisible", true);
p.order = j.value("order", 0);
p.icon = j.value("icon", "");
p.category = j.value("category", "");
p.widthPercent = j.value("widthPercent", 0.0f);
p.heightPercent = j.value("heightPercent", 0.0f);
return p;
}
};
struct DockingLayout {
float leftWidth = 20.0f; // percent
float rightWidth = 20.0f; // percent
float bottomHeight = 25.0f; // percent
float centerWidth = 60.0f; // auto-computed
nlohmann::json toJson() const {
return {
{"leftWidth", leftWidth}, {"rightWidth", rightWidth},
{"bottomHeight", bottomHeight}
};
}
static DockingLayout fromJson(const nlohmann::json& j) {
DockingLayout l;
l.leftWidth = j.value("leftWidth", 20.0f);
l.rightWidth = j.value("rightWidth", 20.0f);
l.bottomHeight = j.value("bottomHeight", 25.0f);
l.centerWidth = 100.0f - l.leftWidth - l.rightWidth;
return l;
}
};
class PanelManager {
public:
PanelManager() = default;
void registerPanel(const std::string& id, const std::string& title,
DockPosition dock, const std::string& icon = "",
const std::string& category = "", int order = 0) {
PanelInfo info;
info.id = id;
info.title = title;
info.defaultDock = dock;
info.currentDock = dock;
info.icon = icon;
info.category = category;
info.order = order;
panels_[id] = info;
}
void setVisible(const std::string& id, bool visible) {
auto it = panels_.find(id);
if (it != panels_.end()) it->second.isVisible = visible;
}
bool isVisible(const std::string& id) const {
auto it = panels_.find(id);
return it != panels_.end() && it->second.isVisible;
}
void togglePanel(const std::string& id) {
auto it = panels_.find(id);
if (it != panels_.end()) it->second.isVisible = !it->second.isVisible;
}
void focusPanel(const std::string& id) {
focusedPanel_ = id;
}
std::string getFocusedPanel() const { return focusedPanel_; }
bool hasPanel(const std::string& id) const {
return panels_.count(id) > 0;
}
PanelInfo getPanel(const std::string& id) const {
auto it = panels_.find(id);
if (it != panels_.end()) return it->second;
return {};
}
std::vector<PanelInfo> getPanelList() const {
std::vector<PanelInfo> result;
for (const auto& [id, info] : panels_) result.push_back(info);
std::sort(result.begin(), result.end(),
[](const PanelInfo& a, const PanelInfo& b) { return a.order < b.order; });
return result;
}
std::vector<PanelInfo> getVisiblePanels() const {
std::vector<PanelInfo> result;
for (const auto& [id, info] : panels_) {
if (info.isVisible) result.push_back(info);
}
std::sort(result.begin(), result.end(),
[](const PanelInfo& a, const PanelInfo& b) { return a.order < b.order; });
return result;
}
std::vector<PanelInfo> getPanelsByDock(DockPosition dock) const {
std::vector<PanelInfo> result;
for (const auto& [id, info] : panels_) {
if (info.currentDock == dock && info.isVisible) result.push_back(info);
}
std::sort(result.begin(), result.end(),
[](const PanelInfo& a, const PanelInfo& b) { return a.order < b.order; });
return result;
}
void movePanelToDock(const std::string& id, DockPosition dock) {
auto it = panels_.find(id);
if (it != panels_.end()) it->second.currentDock = dock;
}
int panelCount() const { return static_cast<int>(panels_.size()); }
void registerDefaults() {
registerPanel("file-tree", "File Tree", DockPosition::Left, "\xF0\x9F\x93\x81", "navigation", 0);
registerPanel("code-editor", "Code Editor", DockPosition::CenterTop, "\xE2\x9C\x8F", "editor", 1);
registerPanel("ast-view", "AST View", DockPosition::Right, "\xF0\x9F\x8C\xB3", "analysis", 2);
registerPanel("annotations", "Annotations", DockPosition::Right, "\xF0\x9F\x8F\xB7", "analysis", 3);
registerPanel("properties", "Properties", DockPosition::Right, "\xE2\x9A\x99", "analysis", 4);
registerPanel("diagnostics", "Diagnostics", DockPosition::Bottom, "\xE2\x9A\xA0", "output", 5);
registerPanel("output", "Output", DockPosition::Bottom, "\xF0\x9F\x93\x84", "output", 6);
registerPanel("workflow", "Workflow", DockPosition::Bottom, "\xF0\x9F\x94\x84", "output", 7);
}
// Layout management
DockingLayout& layout() { return layout_; }
const DockingLayout& layout() const { return layout_; }
nlohmann::json toJson() const {
nlohmann::json j;
j["layout"] = layout_.toJson();
nlohmann::json panelsJson = nlohmann::json::array();
for (const auto& [id, info] : panels_) {
panelsJson.push_back(info.toJson());
}
j["panels"] = panelsJson;
j["focusedPanel"] = focusedPanel_;
return j;
}
static PanelManager fromJson(const nlohmann::json& j) {
PanelManager pm;
if (j.contains("layout")) pm.layout_ = DockingLayout::fromJson(j["layout"]);
if (j.contains("panels")) {
for (const auto& pj : j["panels"]) {
PanelInfo info = PanelInfo::fromJson(pj);
pm.panels_[info.id] = info;
}
}
pm.focusedPanel_ = j.value("focusedPanel", "");
return pm;
}
private:
std::map<std::string, PanelInfo> panels_;
DockingLayout layout_;
std::string focusedPanel_;
};