Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ add_executable(pyrite64 src/main.cpp
src/renderer/texture.cpp
src/renderer/texture.h
src/project/project.cpp
src/editor/recentProjects.h
src/editor/recentProjects.cpp
src/editor/actions.h
src/editor/actions.cpp
src/editor/undoRedo.h
Expand Down
3 changes: 3 additions & 0 deletions src/editor/globalActions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "../utils/json.h"
#include "../utils/proc.h"
#include "undoRedo.h"
#include "recentProjects.h"
#include "pages/editorScene.h"
//#include <stacktrace>

Expand All @@ -27,6 +28,7 @@ namespace Editor::Actions
UndoRedo::getHistory().clear();
try {
ctx.project = new Project::Project(path);
Editor::RecentProjects::setMostRecentPath(path);
if(ctx.project && !ctx.project->getScenes().getEntries().empty()) {
ctx.project->getScenes().loadScene(ctx.project->conf.sceneIdLastOpened);
}
Expand Down Expand Up @@ -96,6 +98,7 @@ namespace Editor::Actions
configJSON["name"] = args["name"];
configJSON["romName"] = args["rom"];
Utils::FS::saveTextFile(configPath, configJSON.dump(2));
Editor::RecentProjects::setMostRecentPath(configPath);

return true;
});
Expand Down
186 changes: 172 additions & 14 deletions src/editor/pages/launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,25 @@
#include <atomic>
#include <cstdio>
#include <mutex>
#include <string>
#include <vector>
#include <filesystem>
#include <format>

#include "imgui.h"
#include "../imgui/theme.h"
#include "../actions.h"
#include "../../utils/filePicker.h"
#include "../recentProjects.h"
#include "../../context.h"
#include "backends/imgui_impl_sdlgpu3.h"
#include "parts/createProjectOverlay.h"
#include "parts/toolchainOverlay.h"
#include "SDL3/SDL_dialog.h"
#include "../imgui/notification.h"

namespace fs = std::filesystem;

void ImDrawCallback_ImplSDLGPU3_SetSamplerRepeat(const ImDrawList* parent_list, const ImDrawCmd* cmd);

namespace
Expand Down Expand Up @@ -50,14 +57,33 @@ Editor::Launcher::Launcher(SDL_GPUDevice* device)
texBG{device, "data/img/splashBG.png"}
{
ctx.toolchain.scan();
updateProjectEntries();
}

Editor::Launcher::~Launcher() {
}

void Editor::Launcher::updateProjectEntries() {
Editor::RecentProjects::load();
projectEntries = {};
for(auto path : Editor::RecentProjects::recentPaths) {
auto json = Utils::JSON::loadFile(path);
if (json.empty()) continue;
Editor::ProjectEntry entry;
entry.name = json.value("name", "");
entry.path = path;
entry.editorVersion = json.value("editorVersion", PYRITE_VERSION);
fs::path projPath{path};
auto writeTime = fs::last_write_time(projPath);
entry.lastModified = std::format("{:%Y-%m-%d}", writeTime);
entry.expand = false;
projectEntries.push_back(entry);
}
}

void Editor::Launcher::draw()
{
float BTN_SPACING = 300_px;
float BTN_SPACING = 160_px;
const auto &toolState = ctx.toolchain.getState();
auto &io = ImGui::GetIO();

Expand All @@ -74,8 +100,8 @@ void Editor::Launcher::draw()
// BG
ImGui::GetWindowDrawList()->AddCallback(ImDrawCallback_ImplSDLGPU3_SetSamplerRepeat, nullptr);

float topBgHeight = 7_px;
float bottomBgHeight = 3_px;
float topBgHeight = 4.5_px;
float bottomBgHeight = 2.5_px;
float bgRepeatsX = io.DisplaySize.x / texBG.getWidth();
ImGui::SetCursorPos({0,0});
ImGui::Image(ImTextureID(texBG.getGPUTex()),
Expand Down Expand Up @@ -103,21 +129,18 @@ void Editor::Launcher::draw()
ImGui::SetMouseCursor(ImGuiMouseCursor_Arrow);
}

auto logoSize = texTitle.getSize(0.65 * ImGui::Theme::zoomFactor);
ImGui::SetCursorPos({
centerPos.x - (logoSize.x/2) + 16_px,
28_px
});
auto logoSize = texTitle.getSize(0.4 * ImGui::Theme::zoomFactor);
ImGui::SetCursorPos({32_px, 24_px});
ImGui::Image(ImTextureID(texTitle.getGPUTex()),logoSize);

auto renderButton = [&](Renderer::Texture &img, const char* text, bool& hover, int &posX) -> bool
{
auto btnSizeAdd = img.getSize(hover ? 0.85f : 0.8f);
auto btnSizeAdd = img.getSize(hover ? 0.45f : 0.4f);
btnSizeAdd *= ImGui::Theme::zoomFactor;

ImVec2 btnPos{
posX - (btnSizeAdd.x/2),
midBgPointY - (btnSizeAdd.y/2),
72_px - (btnSizeAdd.y/2),
};

ImGui::SetCursorPos(btnPos);
Expand All @@ -130,7 +153,7 @@ void Editor::Launcher::draw()

renderSubText(
btnPos.x + (btnSizeAdd.x / 2),
btnSizeAdd, midBgPointY, text
btnSizeAdd, 72_px, text
);

posX += BTN_SPACING;
Expand All @@ -145,10 +168,9 @@ void Editor::Launcher::draw()
bool validToolchain = toolState.hasToolchain && toolState.hasLibdragon && toolState.hasTiny3d;
int buttonCount = (validToolchain && toolState.upToDateLibs) ? 3 : 1;

// screen center
int posX = (int)centerPos.x - 6_px;
int posX = (int)io.DisplaySize.x - BTN_SPACING + 48_px;
if(buttonCount == 3) {
posX -= (BTN_SPACING);
posX -= (BTN_SPACING * 2);
}

if(buttonCount == 3)
Expand Down Expand Up @@ -206,6 +228,112 @@ void Editor::Launcher::draw()

ImGui::PopStyleColor(3);

// recent files
ImGui::SetCursorPos({8_px, (float)texBG.getHeight() * topBgHeight + 8_px});
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
if (ImGui::BeginTable("RecentProjects", 5, ImGuiTableFlags_NoBordersInBody)) {

//header
ImGui::PushStyleColor(ImGuiCol_TableHeaderBg, ImVec4(0,0,0,0));
const char* expandLabel = expandAll ? ICON_MDI_CHEVRON_DOWN : ICON_MDI_CHEVRON_RIGHT;
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 32_px);
ImGui::TableSetupColumn("Project\nName");
ImGui::TableSetupColumn("Last\nModified", ImGuiTableColumnFlags_WidthFixed, 120_px);
ImGui::TableSetupColumn("Editor\nVersion", ImGuiTableColumnFlags_WidthFixed, 80_px);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 32_px);
ImGui::PushFont(nullptr, 16_px);
ImGui::TableNextRow(ImGuiTableRowFlags_Headers, 50_px);
for (int column = 0; column < 5; column++) {
ImGui::TableSetColumnIndex(column);
//ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 8_px);
const char* columnName = ImGui::TableGetColumnName(column);
if (column == 0 && ImGui::Button(expandLabel, ImVec2(32_px, 32_px))) {
expandAll = !expandAll;
for(auto &entry : projectEntries) entry.expand = expandAll;
} else if (column == 4 && ImGui::Button(ICON_MDI_COG, ImVec2(32_px, 32_px))) {
ImGui::OpenPopup("HeaderContextMenu");
} else ImGui::TextUnformatted(columnName);
}
ImGui::PopFont();
ImGui::PopStyleColor();
if (ImGui::BeginPopup("HeaderContextMenu")) {
Editor::Launcher::showHeaderContextMenu();
ImGui::EndPopup();
}

//separator
float y = ImGui::GetCursorScreenPos().y;
y -= 16_px;
ImGui::SetCursorPosY(y);
ImGui::GetWindowDrawList()->AddLine(
ImVec2(8_px, y),
ImVec2(io.DisplaySize.x - 8_px, y),
ImGui::GetColorU32(ImGuiCol_Separator),
1_px
);

auto paths = Editor::RecentProjects::recentPaths;
int index = 0;
for (auto& entry : projectEntries) {
//expand arrow
ImGui::PushID(index);
float rowHeight = entry.expand ? 48_px : 32_px;
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowHeight);
ImGui::TableSetColumnIndex(0);
float y = ImGui::GetCursorPosY();
ImGuiSelectableFlags selectableFlags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap;
if (ImGui::Selectable("##SelectableRow", false, selectableFlags, ImVec2(0, rowHeight))) {
Editor::Actions::call(Editor::Actions::Type::PROJECT_OPEN, entry.path);
}
ImGui::SetCursorPosY(y);
if (ImGui::Button(entry.expand ? ICON_MDI_CHEVRON_DOWN : ICON_MDI_CHEVRON_RIGHT)) {
entry.expand = !entry.expand;
}

//project name
ImGui::TableSetColumnIndex(1);
ImGui::AlignTextToFramePadding();
ImGui::BeginGroup();
ImGui::PushFont(nullptr, 16_px);
ImGui::TextUnformatted(entry.name.c_str());
ImGui::PopFont();
if (entry.expand) {
ImGui::PushFont(ImGui::Theme::getFontMono(), 16_px);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]);
ImGui::TextUnformatted(entry.path.c_str());
ImGui::PopStyleColor();
ImGui::PopFont();
}
ImGui::EndGroup();

//last modified
ImGui::TableSetColumnIndex(2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(entry.lastModified.c_str());

//editor ersion
ImGui::TableSetColumnIndex(3);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(entry.editorVersion.c_str());

//open context menu
ImGui::TableSetColumnIndex(4);
ImGui::AlignTextToFramePadding();
if (ImGui::Button(ICON_MDI_DOTS_HORIZONTAL)) {
ImGui::OpenPopup("ProjectContextMenu");
}

if (ImGui::BeginPopup("ProjectContextMenu")) {
Editor::Launcher::showProjectContextMenu(entry.path);
ImGui::EndPopup();
}
ImGui::PopID();
index++;
}
ImGui::EndTable();
}
ImGui::PopStyleColor();

// version + credits
{
float PADDING = 24_px;
Expand All @@ -229,3 +357,33 @@ void Editor::Launcher::draw()

ImGui::End();
}

void Editor::Launcher::showHeaderContextMenu() {
if(ImGui::MenuItem(ICON_MDI_RELOAD " Reload List")) {
updateProjectEntries();
}
}

void Editor::Launcher::showProjectContextMenu(const std::string& path) {
#if defined(_WIN32)
std::string showPrompt = ICON_MDI_FOLDER_OPEN " Show in Explorer";
#elif defined(__APPLE__)
std::string showPrompt = ICON_MDI_FOLDER_OPEN " Show in Finder";
#else
std::string showPrompt = ICON_MDI_FOLDER_OPEN " Show in File Manager";
#endif
if(ImGui::MenuItem(showPrompt.c_str())) {
if (!Utils::Proc::openInFileBrowser(path)) {
Editor::Noti::add(Editor::Noti::Type::ERROR, "Failed to open File Explorer. This may be due to WSL path conversion failure.");
}
}

if(ImGui::MenuItem(ICON_MDI_CONTENT_COPY " Copy Path")) {
SDL_SetClipboardText(path.c_str());
}

if(ImGui::MenuItem(ICON_MDI_CLOSE_CIRCLE " Remove from List")) {
Editor::RecentProjects::removePath(path);
updateProjectEntries();
}
}
15 changes: 15 additions & 0 deletions src/editor/pages/launcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
* @license MIT
*/
#pragma once
#include <vector>
#include "SDL3/SDL_gpu.h"
#include "../../renderer/texture.h"

namespace Editor
{
struct ProjectEntry {
std::string name;
std::string path;
std::string editorVersion;
std::string lastModified;
bool expand;
};

class Launcher
{
private:
Expand All @@ -16,11 +25,17 @@ namespace Editor
Renderer::Texture texBtnOpen;
Renderer::Texture texBtnTool;
Renderer::Texture texBG;
std::vector<ProjectEntry> projectEntries;
bool expandAll;

public:
Launcher(SDL_GPUDevice* device);
~Launcher();

void draw();
void updateProjectEntries();
void showHeaderContextMenu();
void showProjectContextMenu(const std::string& path);

};
}
56 changes: 56 additions & 0 deletions src/editor/recentProjects.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @copyright 2026 - Nolan Baker
* @license MIT
*/

#include "recentProjects.h"
#include <filesystem>
#include "json.hpp"
#include "../utils/fs.h"
#include "../utils/proc.h"
#include "../utils/json.h"

namespace Editor::RecentProjects {
std::vector<std::string> recentPaths = {};

std::string getJsonPath() {
auto path = Utils::Proc::getAppDataPath() / "recent.json";
return path.string();
}

std::string getMostRecentPath() {
if (recentPaths.empty()) load();
if (recentPaths.empty()) return "";
return recentPaths.front();
}

void setMostRecentPath(const std::string &path) {
recentPaths.erase(std::remove(recentPaths.begin(), recentPaths.end(), path), recentPaths.end());
recentPaths.insert(recentPaths.begin(), path);
save();
}

void removePath(const std::string &path) {
recentPaths.erase(std::remove(recentPaths.begin(), recentPaths.end(), path), recentPaths.end());
save();
}

void save() {
try {
nlohmann::json json = recentPaths;
Utils::FS::saveTextFile(getJsonPath(), json.dump(2));
} catch (const std::exception& e) {
fprintf(stderr, "Error saving recent.json: %s\n", e.what());
}
}

void load() {
nlohmann::json json;
try {
json = Utils::JSON::loadFile(getJsonPath());
if (json.is_array()) recentPaths = json.get<std::vector<std::string>>();
} catch (const std::exception& e) {
fprintf(stderr, "Error loading recent.json: %s\n", e.what());
}
}
}
Loading
Loading