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:
@@ -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
190
editor/src/LayoutManager.h
Normal 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);
|
||||
}
|
||||
};
|
||||
228
editor/tests/step76_test.cpp
Normal file
228
editor/tests/step76_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user