Step 76: LayoutManager with docking layout presets (Sprint 4 start)

Three preset layouts (VSCode/Emacs/JetBrains) with panel visibility,
size ratios, dirty flag for rebuild, and file-based persistence.
Also includes revised sprint4_plan.md and new sprint5_plan.md.
10/10 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-09 08:22:02 -07:00
parent dc94ad7274
commit a65e0b13ee
6 changed files with 1342 additions and 4 deletions

View File

@@ -320,6 +320,9 @@ target_link_libraries(step74_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step75_test tests/step75_test.cpp)
target_include_directories(step75_test PRIVATE src)
add_executable(step76_test tests/step76_test.cpp)
target_include_directories(step76_test PRIVATE src)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

190
editor/src/LayoutManager.h Normal file
View File

@@ -0,0 +1,190 @@
// LayoutManager.h — Docking layout presets (Step 76)
//
// Defines preset layouts (VSCode, Emacs, JetBrains) with panel sizes
// and dock configuration. The actual ImGui DockBuilder calls happen in
// main.cpp; LayoutManager provides the declarative data.
//
// Presets:
// VSCode — Explorer 20% left, Editor 60% center, Panel 20% bottom
// Emacs — Full-frame editor, optional side panels, minibuffer bar
// JetBrains — Project tree 20% left, Editor center, tool windows right+bottom
#pragma once
#include <string>
#include <vector>
#include <map>
#include <fstream>
#include <algorithm>
// ---- Enums ----------------------------------------------------------------
enum class LayoutPreset { VSCode, Emacs, JetBrains };
enum class DockDirection { Left, Right, Bottom, Top, Center };
// ---- Data types -----------------------------------------------------------
struct DockPanel {
std::string id; // ImGui window name ("Explorer", "Editor", "Panel", etc.)
DockDirection direction; // Where this panel docks relative to the main area
float sizeRatio; // Fraction of the split axis (0.0 1.0)
bool visible; // Starts visible?
};
struct LayoutConfig {
LayoutPreset preset;
std::string name;
std::string description;
std::vector<DockPanel> panels;
bool showStatusBar = true;
};
// ---- LayoutManager --------------------------------------------------------
class LayoutManager {
LayoutPreset currentPreset_ = LayoutPreset::VSCode;
std::map<LayoutPreset, LayoutConfig> presets_;
bool layoutDirty_ = true;
public:
LayoutManager() { initPresets(); }
// --- Preset access ------------------------------------------------------
static std::vector<LayoutPreset> availablePresets() {
return { LayoutPreset::VSCode, LayoutPreset::Emacs, LayoutPreset::JetBrains };
}
static const char* presetName(LayoutPreset p) {
switch (p) {
case LayoutPreset::VSCode: return "VSCode";
case LayoutPreset::Emacs: return "Emacs";
case LayoutPreset::JetBrains: return "JetBrains";
}
return "Unknown";
}
static LayoutPreset presetFromName(const std::string& name) {
if (name == "Emacs") return LayoutPreset::Emacs;
if (name == "JetBrains") return LayoutPreset::JetBrains;
return LayoutPreset::VSCode; // default
}
// --- Current preset -----------------------------------------------------
LayoutPreset getPreset() const { return currentPreset_; }
void setPreset(LayoutPreset p) {
if (currentPreset_ != p) {
currentPreset_ = p;
layoutDirty_ = true;
}
}
bool isLayoutDirty() const { return layoutDirty_; }
void clearDirty() { layoutDirty_ = false; }
void markDirty() { layoutDirty_ = true; }
const LayoutConfig& getConfig() const {
return presets_.at(currentPreset_);
}
const LayoutConfig& getConfig(LayoutPreset p) const {
return presets_.at(p);
}
// --- Panel queries ------------------------------------------------------
const DockPanel* findPanel(const std::string& panelId) const {
const auto& cfg = getConfig();
for (const auto& p : cfg.panels) {
if (p.id == panelId) return &p;
}
return nullptr;
}
bool isPanelVisible(const std::string& panelId) const {
const auto* p = findPanel(panelId);
return p ? p->visible : false;
}
std::vector<std::string> getVisiblePanelIds() const {
std::vector<std::string> ids;
const auto& cfg = getConfig();
for (const auto& p : cfg.panels) {
if (p.visible) ids.push_back(p.id);
}
return ids;
}
float getPanelRatio(const std::string& panelId) const {
const auto* p = findPanel(panelId);
return p ? p->sizeRatio : 0.0f;
}
// --- Persistence (simple key=value) -------------------------------------
bool saveToFile(const std::string& path) const {
std::ofstream f(path);
if (!f.is_open()) return false;
f << "layout=" << presetName(currentPreset_) << "\n";
return true;
}
bool loadFromFile(const std::string& path) {
std::ifstream f(path);
if (!f.is_open()) return false;
std::string line;
while (std::getline(f, line)) {
if (line.substr(0, 7) == "layout=") {
setPreset(presetFromName(line.substr(7)));
return true;
}
}
return false;
}
private:
void initPresets() {
// ---- VSCode --------------------------------------------------------
LayoutConfig vscode;
vscode.preset = LayoutPreset::VSCode;
vscode.name = "VSCode";
vscode.description = "Explorer 20% left, Editor center, Panel 20% bottom, status bar";
vscode.showStatusBar = true;
vscode.panels = {
{ "Explorer", DockDirection::Left, 0.20f, true },
{ "Editor", DockDirection::Center, 0.60f, true },
{ "Panel", DockDirection::Bottom, 0.20f, true },
};
presets_[LayoutPreset::VSCode] = std::move(vscode);
// ---- Emacs ---------------------------------------------------------
LayoutConfig emacs;
emacs.preset = LayoutPreset::Emacs;
emacs.name = "Emacs";
emacs.description = "Full-frame editor, optional side panels, minibuffer bottom bar";
emacs.showStatusBar = true;
emacs.panels = {
{ "Explorer", DockDirection::Left, 0.15f, false },
{ "Editor", DockDirection::Center, 0.85f, true },
{ "Minibuffer", DockDirection::Bottom, 0.04f, true },
{ "Panel", DockDirection::Bottom, 0.15f, false },
};
presets_[LayoutPreset::Emacs] = std::move(emacs);
// ---- JetBrains -----------------------------------------------------
LayoutConfig jb;
jb.preset = LayoutPreset::JetBrains;
jb.name = "JetBrains";
jb.description = "Project tree left, Editor center, tool windows right and bottom";
jb.showStatusBar = true;
jb.panels = {
{ "Explorer", DockDirection::Left, 0.20f, true },
{ "Editor", DockDirection::Center, 0.50f, true },
{ "ToolWindow", DockDirection::Right, 0.15f, true },
{ "Panel", DockDirection::Bottom, 0.25f, true },
};
presets_[LayoutPreset::JetBrains] = std::move(jb);
}
};

View File

@@ -0,0 +1,228 @@
// Step 76 TDD Test: Layout presets and docking configuration
//
// Tests:
// 1. Three presets available (VSCode, Emacs, JetBrains)
// 2. Default preset is VSCode
// 3. VSCode layout: Explorer left 20%, Editor center, Panel bottom 20%
// 4. Emacs layout: full-frame editor, Explorer hidden, Minibuffer visible
// 5. JetBrains layout: Explorer left, Editor center, ToolWindow right, Panel bottom
// 6. Switching preset marks layout dirty
// 7. Preset names round-trip (string ↔ enum)
// 8. Save/load persistence round-trip
// 9. Panel visibility queries work correctly
// 10. getVisiblePanelIds returns only visible panels
#include <iostream>
#include <string>
#include <cassert>
#include <cstdio>
#include <algorithm>
#include "LayoutManager.h"
int main() {
int passed = 0;
int failed = 0;
// --- Test 1: Three presets available ---
{
auto presets = LayoutManager::availablePresets();
assert(presets.size() == 3 && "Should have 3 presets");
assert(presets[0] == LayoutPreset::VSCode);
assert(presets[1] == LayoutPreset::Emacs);
assert(presets[2] == LayoutPreset::JetBrains);
std::cout << "Test 1 PASS: Three presets available" << std::endl;
++passed;
}
// --- Test 2: Default preset is VSCode ---
{
LayoutManager mgr;
assert(mgr.getPreset() == LayoutPreset::VSCode && "Default should be VSCode");
assert(std::string(LayoutManager::presetName(mgr.getPreset())) == "VSCode");
std::cout << "Test 2 PASS: Default preset is VSCode" << std::endl;
++passed;
}
// --- Test 3: VSCode layout ---
{
LayoutManager mgr;
const auto& cfg = mgr.getConfig();
assert(cfg.name == "VSCode");
assert(cfg.panels.size() == 3);
// Explorer: left, 20%
assert(cfg.panels[0].id == "Explorer");
assert(cfg.panels[0].direction == DockDirection::Left);
assert(cfg.panels[0].sizeRatio >= 0.19f && cfg.panels[0].sizeRatio <= 0.21f);
assert(cfg.panels[0].visible == true);
// Editor: center
assert(cfg.panels[1].id == "Editor");
assert(cfg.panels[1].direction == DockDirection::Center);
assert(cfg.panels[1].visible == true);
// Panel: bottom, 20%
assert(cfg.panels[2].id == "Panel");
assert(cfg.panels[2].direction == DockDirection::Bottom);
assert(cfg.panels[2].sizeRatio >= 0.19f && cfg.panels[2].sizeRatio <= 0.21f);
assert(cfg.panels[2].visible == true);
assert(cfg.showStatusBar == true);
std::cout << "Test 3 PASS: VSCode layout correct" << std::endl;
++passed;
}
// --- Test 4: Emacs layout ---
{
LayoutManager mgr;
mgr.setPreset(LayoutPreset::Emacs);
const auto& cfg = mgr.getConfig();
assert(cfg.name == "Emacs");
// Explorer should be hidden
const auto* explorer = mgr.findPanel("Explorer");
assert(explorer != nullptr);
assert(explorer->visible == false && "Emacs: Explorer should be hidden");
// Editor should be visible and large
const auto* editor = mgr.findPanel("Editor");
assert(editor != nullptr);
assert(editor->visible == true);
assert(editor->sizeRatio >= 0.80f);
// Minibuffer should be visible (Emacs-specific)
const auto* minibuf = mgr.findPanel("Minibuffer");
assert(minibuf != nullptr);
assert(minibuf->visible == true);
assert(minibuf->direction == DockDirection::Bottom);
// Panel should be hidden by default
assert(mgr.isPanelVisible("Panel") == false);
std::cout << "Test 4 PASS: Emacs layout correct" << std::endl;
++passed;
}
// --- Test 5: JetBrains layout ---
{
LayoutManager mgr;
mgr.setPreset(LayoutPreset::JetBrains);
const auto& cfg = mgr.getConfig();
assert(cfg.name == "JetBrains");
assert(cfg.panels.size() == 4);
// Has ToolWindow on the right
const auto* tw = mgr.findPanel("ToolWindow");
assert(tw != nullptr);
assert(tw->direction == DockDirection::Right);
assert(tw->visible == true);
// Explorer left, Editor center, Panel bottom
assert(mgr.isPanelVisible("Explorer") == true);
assert(mgr.isPanelVisible("Editor") == true);
assert(mgr.isPanelVisible("Panel") == true);
std::cout << "Test 5 PASS: JetBrains layout correct" << std::endl;
++passed;
}
// --- Test 6: Switching preset marks layout dirty ---
{
LayoutManager mgr;
mgr.clearDirty();
assert(mgr.isLayoutDirty() == false);
mgr.setPreset(LayoutPreset::Emacs);
assert(mgr.isLayoutDirty() == true && "Should be dirty after switch");
mgr.clearDirty();
assert(mgr.isLayoutDirty() == false);
// Switching to same preset should NOT mark dirty
mgr.setPreset(LayoutPreset::Emacs);
assert(mgr.isLayoutDirty() == false && "Same preset should not mark dirty");
std::cout << "Test 6 PASS: Dirty flag works correctly" << std::endl;
++passed;
}
// --- Test 7: Preset names round-trip ---
{
for (auto p : LayoutManager::availablePresets()) {
std::string name = LayoutManager::presetName(p);
LayoutPreset roundTripped = LayoutManager::presetFromName(name);
assert(roundTripped == p && "Name should round-trip");
}
// Unknown name defaults to VSCode
assert(LayoutManager::presetFromName("Unknown") == LayoutPreset::VSCode);
assert(LayoutManager::presetFromName("") == LayoutPreset::VSCode);
std::cout << "Test 7 PASS: Preset name round-trip" << std::endl;
++passed;
}
// --- Test 8: Save/load persistence ---
{
const std::string tmpFile = "test_layout_76.tmp";
LayoutManager mgr1;
mgr1.setPreset(LayoutPreset::JetBrains);
assert(mgr1.saveToFile(tmpFile) == true);
LayoutManager mgr2;
assert(mgr2.getPreset() == LayoutPreset::VSCode && "Should start as VSCode");
assert(mgr2.loadFromFile(tmpFile) == true);
assert(mgr2.getPreset() == LayoutPreset::JetBrains && "Should load JetBrains");
// Cleanup
std::remove(tmpFile.c_str());
// Load from nonexistent file should fail gracefully
assert(mgr2.loadFromFile("nonexistent_file.tmp") == false);
std::cout << "Test 8 PASS: Save/load persistence" << std::endl;
++passed;
}
// --- Test 9: Panel ratio queries ---
{
LayoutManager mgr;
assert(mgr.getPanelRatio("Explorer") >= 0.19f);
assert(mgr.getPanelRatio("NonExistent") == 0.0f);
std::cout << "Test 9 PASS: Panel ratio queries" << std::endl;
++passed;
}
// --- Test 10: getVisiblePanelIds ---
{
LayoutManager mgr;
// VSCode: all 3 visible
auto vscodeVisible = mgr.getVisiblePanelIds();
assert(vscodeVisible.size() == 3);
// Emacs: Editor + Minibuffer visible (Explorer and Panel hidden)
mgr.setPreset(LayoutPreset::Emacs);
auto emacsVisible = mgr.getVisiblePanelIds();
assert(emacsVisible.size() == 2);
assert(std::find(emacsVisible.begin(), emacsVisible.end(), "Editor") != emacsVisible.end());
assert(std::find(emacsVisible.begin(), emacsVisible.end(), "Minibuffer") != emacsVisible.end());
// JetBrains: all 4 visible
mgr.setPreset(LayoutPreset::JetBrains);
auto jbVisible = mgr.getVisiblePanelIds();
assert(jbVisible.size() == 4);
std::cout << "Test 10 PASS: getVisiblePanelIds" << std::endl;
++passed;
}
// --- Summary ---
std::cout << "\n=== Step 76 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
return failed > 0 ? 1 : 0;
}