Step 87: welcome screen
This commit is contained in:
@@ -184,6 +184,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
|
||||
- [x] Step 84: **IMPLEMENTED** — Native file dialogs via tinyfiledialogs, FileDialog wrapper with test provider injection (2/2 tests pass)
|
||||
- [x] Step 85: **IMPLEMENTED** — Filesystem tree with .gitignore filtering, recursive Explorer rendering (1/1 tests pass)
|
||||
- [x] Step 86: **IMPLEMENTED** — Multi-tab editing with BufferManager and per-buffer editor state (3/3 tests pass)
|
||||
- [x] Step 87: **IMPLEMENTED** — Welcome screen wired with recent files persistence and quick actions (2/2 tests pass)
|
||||
|
||||
---
|
||||
|
||||
@@ -249,6 +250,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
**Step 84:** Compile and pass (2/2)
|
||||
**Step 85:** Compile and pass (1/1)
|
||||
**Step 86:** Compile and pass (3/3)
|
||||
**Step 87:** Compile and pass (2/2)
|
||||
|
||||
---
|
||||
|
||||
@@ -343,3 +345,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e
|
||||
| 2026-02-09 | Codex | Step 84: Native file dialogs via tinyfiledialogs; FileDialog wrapper with injectable provider. 2/2 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 85: Filesystem tree with .gitignore filtering and Explorer rendering. 1/1 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 86: Multi-tab editing wired via BufferManager with per-buffer state. 3/3 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 87: Welcome screen wired with recent files persistence and quick actions. 2/2 tests pass. |
|
||||
|
||||
@@ -493,6 +493,9 @@ target_include_directories(step85_test PRIVATE src)
|
||||
add_executable(step86_test tests/step86_test.cpp)
|
||||
target_include_directories(step86_test PRIVATE src)
|
||||
|
||||
add_executable(step87_test tests/step87_test.cpp)
|
||||
target_include_directories(step87_test PRIVATE src)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "EditorMode.h"
|
||||
#include "FileDialog.h"
|
||||
#include "FileTree.h"
|
||||
#include "WelcomeScreen.h"
|
||||
#include "ast/Generator.h"
|
||||
|
||||
#include <cstdio>
|
||||
@@ -33,6 +34,8 @@
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <cstdlib>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor application state
|
||||
@@ -71,6 +74,7 @@ struct EditorState {
|
||||
// Custom editor widget state
|
||||
bool showWhitespace = false;
|
||||
bool showMinimap = false;
|
||||
WelcomeScreen welcome;
|
||||
std::string workspaceRoot;
|
||||
FileTree fileTree;
|
||||
FileNode fileTreeRoot;
|
||||
@@ -115,27 +119,49 @@ struct EditorState {
|
||||
activeBuffer = bufferStates[path].get();
|
||||
}
|
||||
|
||||
void init() {
|
||||
std::string defaultContent =
|
||||
"def add(x, y):\n"
|
||||
" \"\"\"Add two numbers.\"\"\"\n"
|
||||
" result = x + y\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def multiply(a, b):\n"
|
||||
" return a * b\n"
|
||||
"\n"
|
||||
"# Calculate the sum of a list\n"
|
||||
"def calculate_sum(items):\n"
|
||||
" total = 0\n"
|
||||
" for item in items:\n"
|
||||
" total = total + item\n"
|
||||
" return total\n";
|
||||
std::filesystem::path configDir() const {
|
||||
const char* home = std::getenv("USERPROFILE");
|
||||
if (!home) home = std::getenv("HOME");
|
||||
std::filesystem::path base = home ? std::filesystem::path(home) : std::filesystem::current_path();
|
||||
return base / ".whetstone";
|
||||
}
|
||||
|
||||
createBuffer(makeUntitledName(), defaultContent, "python");
|
||||
std::filesystem::path recentFilePath() const {
|
||||
return configDir() / "recent.json";
|
||||
}
|
||||
|
||||
void loadRecentFiles() {
|
||||
std::filesystem::path p = recentFilePath();
|
||||
if (!std::filesystem::exists(p)) return;
|
||||
std::ifstream in(p.string());
|
||||
if (!in.is_open()) return;
|
||||
nlohmann::json j;
|
||||
in >> j;
|
||||
if (!j.is_array()) return;
|
||||
for (const auto& item : j) {
|
||||
if (!item.contains("path")) continue;
|
||||
std::string path = item.value("path", "");
|
||||
std::string lang = item.value("language", "");
|
||||
if (!path.empty()) welcome.addRecentFile(path, lang);
|
||||
}
|
||||
}
|
||||
|
||||
void saveRecentFiles() {
|
||||
std::filesystem::path p = recentFilePath();
|
||||
std::filesystem::create_directories(p.parent_path());
|
||||
nlohmann::json j = nlohmann::json::array();
|
||||
for (const auto& rf : welcome.getRecentFiles()) {
|
||||
j.push_back({{"path", rf.path}, {"language", rf.language}});
|
||||
}
|
||||
std::ofstream out(p.string(), std::ios::binary);
|
||||
out << j.dump(2);
|
||||
}
|
||||
|
||||
void init() {
|
||||
outputLog = "Whetstone Editor ready.\n";
|
||||
workspaceRoot = std::filesystem::current_path().string();
|
||||
fileTreeDirty = true;
|
||||
loadRecentFiles();
|
||||
}
|
||||
|
||||
void setLanguage(const std::string& lang) {
|
||||
@@ -250,6 +276,8 @@ struct EditorState {
|
||||
else if (path.size() > 3 && path.substr(path.size() - 3) == ".go")
|
||||
language = "go";
|
||||
createBuffer(path, content, language);
|
||||
welcome.addRecentFile(path, language);
|
||||
saveRecentFiles();
|
||||
outputLog += "Opened: " + path + "\n";
|
||||
} else {
|
||||
outputLog += "Error opening: " + path + "\n";
|
||||
@@ -482,6 +510,50 @@ static void RenderFileTree(const FileNode& node, EditorState& state) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Welcome screen rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
static void RenderWelcome(WelcomeScreen& welcome, EditorState& state, std::string& lastDialogPath) {
|
||||
ImGui::Text("%s", welcome.getTitle().c_str());
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", welcome.getSubtitle().c_str());
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Quick Actions");
|
||||
if (ImGui::Button("New File")) {
|
||||
std::string lang = state.active() ? state.active()->language : "python";
|
||||
state.createBuffer(state.makeUntitledName(), "", lang);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Open File")) {
|
||||
auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go"}, lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
lastDialogPath = path;
|
||||
state.doOpen(path);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Open Folder")) {
|
||||
auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot});
|
||||
if (!path.empty()) {
|
||||
state.workspaceRoot = path;
|
||||
state.fileTreeDirty = true;
|
||||
lastDialogPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Recent Files");
|
||||
for (const auto& rf : welcome.getRecentFiles()) {
|
||||
if (ImGui::Selectable(rf.displayName.c_str())) {
|
||||
state.doOpen(rf.path);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Tip");
|
||||
ImGui::TextWrapped("%s", welcome.getTip(0).c_str());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -763,44 +835,51 @@ int main(int, char**) {
|
||||
|
||||
// Tab bar for the file
|
||||
if (ImGui::BeginTabBar("EditorTabs")) {
|
||||
auto openBuffers = state.buffers.getOpenBuffers();
|
||||
for (const auto& path : openBuffers) {
|
||||
auto* buf = state.bufferStates[path].get();
|
||||
if (!buf) continue;
|
||||
std::string tabLabel = path;
|
||||
if (buf->modified) tabLabel += " *";
|
||||
bool open = true;
|
||||
if (ImGui::BeginTabItem(tabLabel.c_str(), &open)) {
|
||||
if (!state.active() || state.active()->path != path) {
|
||||
state.switchToBuffer(path);
|
||||
}
|
||||
// Editable text area fills available space
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
avail.y -= 4; // small margin
|
||||
|
||||
state.updateHighlights();
|
||||
CodeEditorOptions opts;
|
||||
opts.showWhitespace = state.showWhitespace;
|
||||
opts.mode = &buf->mode;
|
||||
opts.enableFolding = true;
|
||||
opts.showMinimap = state.showMinimap;
|
||||
|
||||
CodeEditorResult res = buf->widget.render("##editor",
|
||||
buf->editBuf, buf->highlights, opts, avail, monoFont);
|
||||
state.updateCursorPos(res.cursorByte);
|
||||
if (res.changed) {
|
||||
state.onTextChanged();
|
||||
}
|
||||
|
||||
if (state.buffers.bufferCount() == 0) {
|
||||
if (ImGui::BeginTabItem("Welcome")) {
|
||||
RenderWelcome(state.welcome, state, lastDialogPath);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (!open) {
|
||||
state.buffers.closeBuffer(path);
|
||||
state.bufferStates.erase(path);
|
||||
if (state.buffers.bufferCount() > 0) {
|
||||
state.switchToBuffer(state.buffers.getOpenBuffers().front());
|
||||
} else {
|
||||
state.activeBuffer = nullptr;
|
||||
} else {
|
||||
auto openBuffers = state.buffers.getOpenBuffers();
|
||||
for (const auto& path : openBuffers) {
|
||||
auto* buf = state.bufferStates[path].get();
|
||||
if (!buf) continue;
|
||||
std::string tabLabel = path;
|
||||
if (buf->modified) tabLabel += " *";
|
||||
bool open = true;
|
||||
if (ImGui::BeginTabItem(tabLabel.c_str(), &open)) {
|
||||
if (!state.active() || state.active()->path != path) {
|
||||
state.switchToBuffer(path);
|
||||
}
|
||||
// Editable text area fills available space
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
avail.y -= 4; // small margin
|
||||
|
||||
state.updateHighlights();
|
||||
CodeEditorOptions opts;
|
||||
opts.showWhitespace = state.showWhitespace;
|
||||
opts.mode = &buf->mode;
|
||||
opts.enableFolding = true;
|
||||
opts.showMinimap = state.showMinimap;
|
||||
|
||||
CodeEditorResult res = buf->widget.render("##editor",
|
||||
buf->editBuf, buf->highlights, opts, avail, monoFont);
|
||||
state.updateCursorPos(res.cursorByte);
|
||||
if (res.changed) {
|
||||
state.onTextChanged();
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (!open) {
|
||||
state.buffers.closeBuffer(path);
|
||||
state.bufferStates.erase(path);
|
||||
if (state.buffers.bufferCount() > 0) {
|
||||
state.switchToBuffer(state.buffers.getOpenBuffers().front());
|
||||
} else {
|
||||
state.activeBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
editor/tests/step87_test.cpp
Normal file
29
editor/tests/step87_test.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
// Step 87 TDD Test: Welcome screen
|
||||
//
|
||||
// Tests:
|
||||
// 1. Default actions and tips are present
|
||||
// 2. Recent files added and de-duplicated
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include "WelcomeScreen.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
WelcomeScreen w;
|
||||
assert(!w.getActions().empty());
|
||||
assert(!w.getTips().empty());
|
||||
std::cout << "Test 1 PASS: Actions and tips" << std::endl;
|
||||
++passed;
|
||||
|
||||
w.addRecentFile("C:/tmp/a.py", "python");
|
||||
w.addRecentFile("C:/tmp/a.py", "python");
|
||||
assert(w.getRecentFiles().size() == 1);
|
||||
std::cout << "Test 2 PASS: Recent files de-duplicate" << std::endl;
|
||||
++passed;
|
||||
|
||||
std::cout << "\n=== Step 87 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user