Compare commits

...

10 Commits

Author SHA1 Message Date
1ed9949bf3 save work
All checks were successful
Build / build (push) Successful in 1m17s
2025-06-20 00:03:33 -05:00
9d5915d54a [studio] Add Back/Forward navigation 2025-06-20 00:03:31 -05:00
78e9f70db6 [nostalgia] Update release notes
All checks were successful
Build / build (push) Successful in 1m20s
2025-06-19 23:58:42 -05:00
12e5623fe6 [ox/logconn] Add exception handling for logger thread
All checks were successful
Build / build (push) Successful in 1m18s
2025-06-19 23:56:26 -05:00
cfdfb0a8c9 [studio] Fix file deletion to close file even if not active 2025-06-19 23:55:47 -05:00
56e665301f [studio] Cleanup
All checks were successful
Build / build (push) Successful in 1m29s
2025-06-19 21:37:57 -05:00
7415ce4bd9 [nostalgia/gfx/studio] Cleanup
All checks were successful
Build / build (push) Successful in 2m53s
2025-06-17 20:45:24 -05:00
05f42150a1 [olympic] Add new loc command to Makefile
All checks were successful
Build / build (push) Successful in 1m19s
2025-06-09 21:29:27 -05:00
8ea2bc6934 [nostalgia] Update release notes
All checks were successful
Build / build (push) Successful in 1m16s
2025-06-09 00:22:17 -05:00
c780924122 [studio] Add [DEBUG] tag to About in debug builds
All checks were successful
Build / build (push) Successful in 1m18s
2025-06-08 21:33:17 -05:00
22 changed files with 360 additions and 145 deletions

View File

@@ -52,3 +52,25 @@ configure-gba:
.PHONY: configure-gba-debug
configure-gba-debug:
${BC_CMD_SETUP_BUILD} --toolchain=deps/gbabuildcore/cmake/modules/GBA.cmake --target=gba --current_build=0 --build_type=debug --build_root=${BC_VAR_BUILD_PATH}
.PHONY: loc
loc:
${BC_PY3} util/scripts/loc.py \
--search-dirs \
src \
deps/ox/src \
deps/buildcore \
deps/gbabuildcore \
deps/glutils \
deps/teagba \
--include-exts \
.cpp \
.hpp \
.py \
.s \
.cmake \
--exclude-paths \
deps/teagba/src/gba_crt0.s \
src/olympic/studio/applib/src/font.cpp \
src/olympic/studio/applib/src/font.hpp \
src/nostalgia/studio/icondata.cpp

View File

@@ -91,23 +91,28 @@ ox::Error LoggerConn::sendInit(const InitTraceMsg &msg) noexcept {
}
void LoggerConn::msgSend() noexcept {
while (true) {
std::unique_lock lk(m_waitMut);
m_waitCond.wait(lk);
if (!m_running) {
break;
}
std::lock_guard const buffLk(m_buffMut);
try {
while (true) {
Array<char, units::KB> tmp;
const auto read = m_buff.read(tmp.data(), tmp.size());
if (!read) {
std::unique_lock lk(m_waitMut);
m_waitCond.wait(lk);
if (!m_running) {
break;
}
oxAssert(read <= tmp.size(), "logger trying to read too much data");
//std::printf("LoggerConn: sending %lu bytes\n", read);
std::ignore = send(tmp.data(), read);
std::lock_guard const buffLk(m_buffMut);
while (true) {
Array<char, units::KB> tmp;
const auto read = m_buff.read(tmp.data(), tmp.size());
if (!read) {
break;
}
oxAssert(read <= tmp.size(), "logger trying to read too much data");
//std::printf("LoggerConn: sending %lu bytes\n", read);
std::ignore = send(tmp.data(), read);
}
}
} catch (std::exception const &e) {
oxErrf("Exception in logger thread: {}\n", e.what());
oxAssert(false, "logger thread exception");
}
}

View File

@@ -1,8 +1,11 @@
# d2025.06.0
* Add ability to remember recent projects in config
* Fix file deletion to close file even if not active
* PaletteEditor: Add RGB key shortcuts for focusing color channels
* PaletteEditor: Add color preview to color editor
* PaletteEditor: Make RGB key shortcuts work when color channel inputs are
focused
# d2025.05.2

View File

@@ -8,7 +8,7 @@
namespace nostalgia::gfx {
RemovePageCommand::RemovePageCommand(Palette &pal, size_t idx) noexcept:
RemovePageCommand::RemovePageCommand(Palette &pal, size_t const idx) noexcept:
m_pal(pal),
m_idx(idx) {}

View File

@@ -9,13 +9,13 @@ namespace nostalgia::gfx {
UpdateColorCommand::UpdateColorCommand(
Palette &pal,
size_t page,
size_t idx,
Color16 newColor):
m_pal(pal),
m_page(page),
m_idx(idx),
m_altColor(newColor) {
size_t const page,
size_t const idx,
Color16 const newColor):
m_pal{pal},
m_page{page},
m_idx{idx},
m_altColor{newColor} {
if (color(m_pal, m_page, m_idx) == newColor) {
throw studio::NoChangesException();
}

View File

@@ -13,20 +13,19 @@ DeleteTilesCommand::DeleteTilesCommand(
TileSheet::SubSheetIdx idx,
std::size_t const tileIdx,
std::size_t const tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
constexpr unsigned bytesPerTile = PixelsPerTile;
m_deletePos = tileIdx * bytesPerTile;
m_deleteSz = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_deleteSz);
// copy pixels to be erased
{
auto &s = getSubSheet(m_img, m_idx);
auto dst = m_deletedPixels.begin();
auto src = s.pixels.begin() + m_deletePos;
ox::copy_n(src, m_deleteSz, dst);
}
}
m_img{img},
m_idx{std::move(idx)},
m_deletePos{tileIdx * PixelsPerTile},
m_deleteSz{tileCnt * PixelsPerTile},
m_deletedPixels{[this] {
ox::Vector<uint8_t> deletedPixels(m_deleteSz);
// copy pixels to be erased
auto const &s = getSubSheet(m_img, m_idx);
auto const dst = deletedPixels.begin();
auto const src = s.pixels.begin() + m_deletePos;
ox::copy_n(src, m_deleteSz, dst);
return deletedPixels;
}()} {}
ox::Error DeleteTilesCommand::redo() noexcept {
auto &s = getSubSheet(m_img, m_idx);

View File

@@ -11,10 +11,10 @@ namespace nostalgia::gfx {
class DeleteTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_deletePos = 0;
std::size_t m_deleteSz = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
TileSheet::SubSheetIdx const m_idx;
std::size_t const m_deletePos = 0;
std::size_t const m_deleteSz = 0;
ox::Vector<uint8_t> const m_deletedPixels;
public:
DeleteTilesCommand(

View File

@@ -62,11 +62,11 @@ constexpr void iterateLine(ox::Point const &a, ox::Point const &b, auto const &f
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
std::size_t const idx,
int const palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)),
m_palIdx(palIdx) {
m_img{img},
m_subSheetIdx{std::move(subSheetIdx)},
m_palIdx{palIdx} {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(subsheet, idx));
}

View File

@@ -12,19 +12,19 @@ InsertTilesCommand::InsertTilesCommand(
std::size_t const tileIdx,
std::size_t const tileCnt) noexcept:
m_img{img},
m_idx{std::move(idx)} {
m_insertPos = tileIdx * PixelsPerTile;
m_insertCnt = tileCnt * PixelsPerTile;
m_deletedPixels.resize(m_insertCnt);
// copy pixels to be erased
{
auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels;
auto const dst = m_deletedPixels.begin();
auto const src = p.begin() + p.size() - m_insertCnt;
ox::copy_n(src, m_insertCnt, dst);
}
}
m_idx{std::move(idx)},
m_insertPos{tileIdx * PixelsPerTile},
m_insertCnt{tileCnt * PixelsPerTile},
m_insertedPixels{[this] {
ox::Vector<uint8_t> insertedPixels(m_insertCnt);
// copy pixels to be erased
auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels;
auto const dst = insertedPixels.begin();
auto const src = p.begin() + p.size() - m_insertCnt;
ox::copy_n(src, m_insertCnt, dst);
return insertedPixels;
}()} {}
OX_ALLOW_UNSAFE_BUFFERS_BEGIN
@@ -52,7 +52,7 @@ ox::Error InsertTilesCommand::undo() noexcept {
auto const src = &p[srcIdx];
ox::memmove(dst1, src, sz);
}
ox::memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
ox::memcpy(dst2, m_insertedPixels.data(), m_insertedPixels.size());
return {};
}

View File

@@ -11,10 +11,10 @@ namespace nostalgia::gfx {
class InsertTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_insertPos = 0;
std::size_t m_insertCnt = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
TileSheet::SubSheetIdx const m_idx;
std::size_t const m_insertPos{};
std::size_t const m_insertCnt{};
ox::Vector<uint8_t> const m_insertedPixels;
public:
InsertTilesCommand(

View File

@@ -15,7 +15,6 @@ UpdateSubSheetCommand::UpdateSubSheetCommand(
m_img{img},
m_idx{std::move(idx)},
m_sheet{getSubSheet(m_img, m_idx)} {
m_sheet = getSubSheet(m_img, m_idx);
m_sheet.name = std::move(name);
OX_THROW_ERROR(resizeSubsheet(m_sheet, {cols, rows}));
}

View File

@@ -11,7 +11,7 @@ namespace nostalgia::gfx {
class UpdateSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheetIdx const m_idx;
TileSheet::SubSheet m_sheet;
public:

View File

@@ -15,7 +15,11 @@ namespace studio {
AboutPopup::AboutPopup(turbine::Context &ctx) noexcept:
Popup("About"),
#ifdef DEBUG
m_text{sfmt("{} [DEBUG] - {}", keelCtx(ctx).appName, olympic::appVersion)} {
#else
m_text{sfmt("{} - {}", keelCtx(ctx).appName, olympic::appVersion)} {
#endif
}
void AboutPopup::draw(Context &sctx) noexcept {

View File

@@ -34,18 +34,6 @@ static bool shutdownHandler(turbine::Context &ctx) {
return sctx->ui.handleShutdown();
}
void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept {
ox::String path = std::move(filePath);
if (beginsWith(path, "uuid://")) {
auto [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path);
if (err) {
return;
}
path = p;
}
ctx.ui.navigateTo(std::move(path), std::move(navArgs));
}
namespace ig {
extern bool s_mainWinHasFocus;
}
@@ -126,9 +114,8 @@ static ox::Error convertStudioConfigV1ToStudioConfigV2(
using StudioConfig = StudioConfigV2;
StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept:
m_sctx{*this, ctx},
m_tctx{ctx},
StudioUI::StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexcept:
m_sctx{*this, tctx},
m_projectDataDir{std::move(projectDataDir)} {
{
ImFontConfig fontCfg;
@@ -139,7 +126,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
// but AddFontFromMemoryTTF requires a mutable buffer.
// However, setting fontCfg.FontDataOwnedByAtlas ensures
// that it will still be treated as const.
// ImGui documentation recognize that this is a bad design,
// ImGui documentation recognizes that this is a bad design,
// and hopefully it will change at some point.
io.Fonts->AddFontFromMemoryTTF(const_cast<uint8_t*>(font.data()), static_cast<int>(font.size()), 13, &fontCfg);
}
@@ -148,6 +135,9 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
oxLogError(headerizeConfigFile<StudioConfigV1>(kctx));
turbine::setApplicationData(m_tctx, &m_sctx);
turbine::setShutdownHandler(m_tctx, shutdownHandler);
m_sctx.navCallback = [this](ox::StringParam filePath, ox::StringParam navArgs) {
handleNavigationChange(std::move(filePath), std::move(navArgs));
};
m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile);
m_projectExplorer.addDir.connect(this, &StudioUI::addDir);
m_projectExplorer.addItem.connect(this, &StudioUI::addFile);
@@ -187,7 +177,7 @@ void StudioUI::handleKeyEvent(turbine::Key const key, bool const down) noexcept
}
}
void StudioUI::navigateTo(ox::StringParam path, ox::StringParam navArgs) noexcept {
void StudioUI::handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept {
m_navAction.emplace(std::move(path), std::move(navArgs));
}
@@ -271,7 +261,7 @@ void StudioUI::drawMenu() noexcept {
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (ImGui::MenuItem(
"Undo", STUDIO_CTRL "+Z", false, undoStack && undoStack->canUndo())) {
oxLogError(undoStack->undo());
@@ -311,6 +301,17 @@ void StudioUI::drawMenu() noexcept {
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Navigate")) {
if (ImGui::MenuItem("Back", STUDIO_CTRL "+{", false, m_sctx.navIdx > 1)) {
navigateBack(m_sctx);
}
if (ImGui::MenuItem(
"Forward", STUDIO_CTRL "+}", false,
m_sctx.navIdx < m_sctx.navStack.size())) {
navigateForward(m_sctx);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About")) {
m_aboutPopup.open();
@@ -354,10 +355,16 @@ void StudioUI::drawTabs() noexcept {
}
if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] {
m_activeEditor->onActivated();
if (!m_sctx.navIdx ||
(m_sctx.navIdx <= m_sctx.navStack.size() &&
m_sctx.navStack[m_sctx.navIdx - 1].filePath != m_activeEditor->itemPath())) {
m_sctx.navStack.resize(m_sctx.navIdx);
++m_sctx.navIdx;
m_sctx.navStack.emplace_back(ox::String{m_activeEditor->itemPath()}, ox::String{""});
}
}
if (m_closeActiveTab) [[unlikely]] {
ImGui::SetTabItemClosed(e->itemDisplayName().c_str());
} else if (open) [[likely]] {
e->draw(m_sctx);
}
@@ -397,9 +404,14 @@ void StudioUI::drawTabs() noexcept {
m_closeActiveTab = false;
}
if (m_navAction) {
oxLogError(openFile(m_navAction->path));
m_activeEditor->navigateTo(m_navAction->args);
m_navAction.reset();
if (!openFile(m_navAction->path)) {
m_activeEditor->navigateTo(m_navAction->args);
m_navAction.reset();
} else {
//auto const i = m_sctx.navIdx - 1;
//oxDebugf("deleting {}", m_sctx.navStack[i].filePath);
//std::ignore = m_sctx.navStack.erase(i);
}
}
}
@@ -508,6 +520,22 @@ void StudioUI::handleKeyInput() noexcept {
}
}
}
if constexpr (ox::defines::OS == ox::OS::Darwin) {
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
navigateBack(m_sctx);
} else if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
navigateForward(m_sctx);
}
}
}
if constexpr (ox::defines::OS != ox::OS::Darwin) {
if (ImGui::IsKeyDown(ImGuiKey_ModAlt)) {
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
navigateBack(m_sctx);
} else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
navigateForward(m_sctx);
}
}
}
}
@@ -553,24 +581,36 @@ ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR ne
ox::Error StudioUI::handleDeleteDir(ox::StringViewCR path) noexcept {
auto const p = sfmt("{}/", path);
for (auto &e : m_editors) {
if (beginsWith(e->itemPath(), p)) {
oxLogError(closeFile(path));
m_closeActiveTab = true;
break;
}
}
std::ignore = m_editors.erase(
std::remove_if(
m_editors.begin(), m_editors.end(),
[&](ox::UPtr<BaseEditor> const &e) {
if (beginsWith(e->itemPath(), p)) {
oxLogError(closeFile(path));
if (e.get() != m_activeEditor) {
return true;
}
m_closeActiveTab = true;
}
return false;
}));
return m_projectExplorer.refreshProjectTreeModel();
}
ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept {
for (auto &e : m_editors) {
if (path == e->itemPath()) {
oxLogError(closeFile(path));
m_closeActiveTab = true;
break;
}
}
std::ignore = m_editors.erase(
std::remove_if(
m_editors.begin(), m_editors.end(),
[&](ox::UPtr<BaseEditor> const &e) {
if (path == e->itemPath()) {
oxLogError(closeFile(path));
if (e.get() != m_activeEditor) {
return true;
}
m_closeActiveTab = true;
}
return false;
}));
return m_projectExplorer.refreshProjectTreeModel();
}
@@ -654,7 +694,10 @@ ox::Error StudioUI::openFile(ox::StringViewCR path) noexcept {
ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActiveTab) noexcept {
if (!m_project) {
return ox::Error(1, "No project open to open a file from");
return ox::Error(1, "no project open to open a file from");
}
if (!m_project->romFs().exists(path)) {
return ox::Error{1, "file does note exist"};
}
if (m_openFiles.contains(path)) {
for (auto &e : m_editors) {
@@ -668,10 +711,10 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi
}
OX_REQUIRE(ext, fileExt(path));
// create Editor
BaseEditor *editor = nullptr;
ox::UPtr<BaseEditor> editor;
auto const err = m_editorMakers.contains(ext) ?
m_editorMakers[ext](path).moveTo(editor) :
ox::makeCatch<ClawEditor>(m_sctx, path).moveTo(editor);
m_editorMakers[ext](path).to<ox::UPtr<BaseEditor>>().moveTo(editor) :
ox::make_unique_catch<ClawEditor>(m_sctx, path).moveTo(editor);
if (err) {
if constexpr(!ox::defines::Debug) {
oxErrf("Could not open Editor: {}\n", toStr(err));
@@ -681,11 +724,11 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi
return err;
}
editor->closed.connect(this, &StudioUI::closeFile);
m_editors.emplace_back(editor);
auto const &e = m_editors.emplace_back(std::move(editor));
m_openFiles.emplace_back(path);
if (makeActiveTab) {
m_activeEditor = m_editors.back().value->get();
m_activeEditorUpdatePending = editor;
m_activeEditorUpdatePending = e.get();
}
// save to config
studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&path](StudioConfig &config) {

View File

@@ -25,12 +25,12 @@
namespace studio {
class StudioUI: public ox::SignalHandler {
class StudioUI final: public ox::SignalHandler {
friend class StudioUIDrawer;
private:
Context m_sctx;
turbine::Context &m_tctx;
turbine::Context &m_tctx{m_sctx.tctx};
ox::String m_projectDataDir;
ox::UPtr<Project> m_project;
TaskRunner m_taskRunner;
@@ -83,11 +83,11 @@ class StudioUI: public ox::SignalHandler {
ox::Optional<NavAction> m_navAction;
public:
explicit StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept;
explicit StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexcept;
void handleKeyEvent(turbine::Key, bool down) noexcept;
void navigateTo(ox::StringParam path, ox::StringParam navArgs) noexcept;
void handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept;
[[nodiscard]]
constexpr Project *project() noexcept {

View File

@@ -15,11 +15,24 @@ namespace studio {
class StudioUI;
struct Context {
StudioUI &ui;
Project *project = nullptr;
turbine::Context &tctx;
Context(StudioUI &pUi, turbine::Context &pTctx) noexcept:
ui(pUi), tctx(pTctx) {}
public:
friend void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept;
friend void navigateBack(Context &ctx) noexcept;
friend void navigateForward(Context &ctx) noexcept;
friend StudioUI;
StudioUI &ui;
Project *project = nullptr;
turbine::Context &tctx;
protected:
struct NavPath {
ox::String filePath;
ox::String navArgs;
};
size_t navIdx{};
ox::Vector<NavPath> navStack;
std::function<void(ox::StringParam filePath, ox::StringParam navArgs)> navCallback;
Context(StudioUI &pUi, turbine::Context &pTctx) noexcept:
ui{pUi}, tctx{pTctx} {}
};
[[nodiscard]]
@@ -27,6 +40,15 @@ inline keel::Context &keelCtx(Context &ctx) noexcept {
return keelCtx(ctx.tctx);
}
[[nodiscard]]
inline keel::Context const &keelCtx(Context const &ctx) noexcept {
return keelCtx(ctx.tctx);
}
void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs = "") noexcept;
void navigateBack(Context &ctx) noexcept;
void navigateForward(Context &ctx) noexcept;
}

View File

@@ -46,14 +46,14 @@ ox::Result<T> getDragDropPayload() noexcept {
static_cast<size_t>(payload->DataSize)});
}
ox::Error setDragDropPayload(ox::CStringViewCR name, auto const&obj) noexcept {
ox::Error setDragDropPayload(ox::CStringViewCR name, auto const &obj) noexcept {
OX_REQUIRE(buff, ox::writeClaw(obj, ox::ClawFormat::Metal));
ImGui::SetDragDropPayload(name.c_str(), buff.data(), buff.size());
return {};
}
template<typename T>
ox::Error setDragDropPayload(T const&obj) noexcept {
ox::Error setDragDropPayload(T const &obj) noexcept {
OX_REQUIRE(buff, ox::writeClaw(obj, ox::ClawFormat::Metal));
ImGui::SetDragDropPayload(ox::ModelTypeName_v<T>, buff.data(), buff.size());
return {};
@@ -77,7 +77,7 @@ class DragDropSource {
}
};
auto dragDropSource(auto const&cb, ImGuiDragDropFlags const flags = 0) noexcept {
auto dragDropSource(auto const &cb, ImGuiDragDropFlags const flags = 0) noexcept {
if constexpr(ox::is_same_v<decltype(cb()), ox::Error>) {
if (ig::DragDropSource const tgt{flags}; tgt) [[unlikely]] {
return cb();
@@ -108,7 +108,7 @@ class DragDropTarget {
}
};
auto dragDropTarget(auto const&cb) noexcept {
auto dragDropTarget(auto const &cb) noexcept {
if constexpr(ox::is_same_v<decltype(cb()), ox::Error>) {
if (ig::DragDropTarget const tgt; tgt) [[unlikely]] {
return cb();
@@ -124,7 +124,7 @@ auto dragDropTarget(auto const&cb) noexcept {
class ChildStackItem {
public:
explicit ChildStackItem(ox::CStringViewCR id, ImVec2 const&sz = {}) noexcept;
explicit ChildStackItem(ox::CStringViewCR id, ImVec2 const &sz = {}) noexcept;
~ChildStackItem() noexcept;
};
@@ -146,7 +146,7 @@ class IndentStackItem {
void centerNextWindow(turbine::Context &ctx) noexcept;
bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz = BtnSz) noexcept;
bool PushButton(ox::CStringViewCR lbl, ImVec2 const &btnSz = BtnSz) noexcept;
template<typename Str>
struct TextInput {
@@ -234,7 +234,7 @@ PopupResponse PopupControlsOk(
ox::CStringViewCR ok);
[[nodiscard]]
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0});
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const &sz = {285, 0});
/**
*
@@ -266,7 +266,7 @@ bool ComboBox(ox::CStringView lbl, ox::Span<const ox::String> list, size_t &sele
*/
bool ComboBox(
ox::CStringViewCR lbl,
std::function<ox::CStringView(size_t)> const&f,
std::function<ox::CStringView(size_t)> const &f,
size_t strCnt,
size_t &selectedIdx) noexcept;
@@ -278,10 +278,10 @@ bool FileComboBox(
bool ListBox(
ox::CStringViewCR name,
std::function<ox::CStringView(size_t)> const&f,
std::function<ox::CStringView(size_t)> const &f,
size_t strCnt,
size_t &selIdx,
ImVec2 const&sz = {0, 0}) noexcept;
ImVec2 const &sz = {0, 0}) noexcept;
/**
*
@@ -290,7 +290,7 @@ bool ListBox(
* @param selIdx
* @return true if new value selected, false otherwise
*/
bool ListBox(ox::CStringViewCR name, ox::SpanView<ox::String> const&list, size_t &selIdx) noexcept;
bool ListBox(ox::CStringViewCR name, ox::SpanView<ox::String> const &list, size_t &selIdx) noexcept;
class FilePicker {
private:
@@ -306,7 +306,7 @@ class FilePicker {
studio::Context &sctx,
ox::StringParam title,
ox::StringParam fileExt,
ImVec2 const&size = {}) noexcept;
ImVec2 const &size = {}) noexcept;
void draw() noexcept;
@@ -348,6 +348,8 @@ class QuestionPopup: public Popup {
QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept;
void setQuestion(ox::StringParam msg) noexcept;
void draw(Context &ctx) noexcept override;
};

View File

@@ -1,6 +1,7 @@
add_library(
Studio
configio.cpp
context.cpp
editor.cpp
filepickerpopup.cpp
filetreemodel.cpp

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/context.hpp>
namespace studio {
void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept {
ox::String path = std::move(filePath);
if (beginsWith(path, "uuid://")) {
auto [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path);
if (err) {
return;
}
path = p;
}
ctx.navStack.resize(ctx.navIdx + 1);
ctx.navStack.emplace_back(ox::String{path}, ox::String{navArgs.view()});
ctx.navCallback(std::move(path), std::move(navArgs));
}
void navigateBack(Context &ctx) noexcept {
if (!ctx.navIdx) {
return;
}
--ctx.navIdx;
if (ctx.navIdx < ctx.navStack.size() && ctx.navIdx) {
auto const &n = ctx.navStack[ctx.navIdx - 1];
try {
ctx.navCallback(n.filePath, n.navArgs);
} catch (std::exception const &e) {
oxAssert(ctx.navCallback != nullptr, "navCallback is null");
oxErrf("navigateForward failed: {}", e.what());
}
}
}
void navigateForward(Context &ctx) noexcept {
if (ctx.navIdx < ctx.navStack.size()) {
auto const &n = ctx.navStack[ctx.navIdx];
try {
ctx.navCallback(n.filePath, n.navArgs);
} catch (std::exception const &e) {
oxAssert(ctx.navCallback != nullptr, "navCallback is null");
oxErrf("navigateForward failed: {}", e.what());
}
++ctx.navIdx;
}
}
}

View File

@@ -10,7 +10,7 @@
namespace studio::ig {
ChildStackItem::ChildStackItem(ox::CStringViewCR id, ImVec2 const&sz) noexcept {
ChildStackItem::ChildStackItem(ox::CStringViewCR id, ImVec2 const &sz) noexcept {
ImGui::BeginChild(id.c_str(), sz);
}
@@ -50,7 +50,7 @@ void centerNextWindow(turbine::Context &ctx) noexcept {
ImGui::SetNextWindowPos(ImVec2(screenW / mod, screenH / mod), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
}
bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz) noexcept {
bool PushButton(ox::CStringViewCR lbl, ImVec2 const &btnSz) noexcept {
return ImGui::Button(lbl.c_str(), btnSz);
}
@@ -101,7 +101,7 @@ PopupResponse PopupControlsOk(
return out;
}
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz) {
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const &sz) {
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
centerNextWindow(ctx);
ImGui::OpenPopup(popupName.c_str());
@@ -149,7 +149,7 @@ bool ComboBox(
bool ComboBox(
ox::CStringViewCR lbl,
std::function<ox::CStringView(size_t)> const&f,
std::function<ox::CStringView(size_t)> const &f,
size_t strCnt,
size_t &selectedIdx) noexcept {
bool out{};
@@ -172,16 +172,16 @@ bool FileComboBox(
Context &sctx,
ox::StringViewCR fileExt,
size_t &selectedIdx) noexcept {
auto const&list = sctx.project->fileList(fileExt);
auto const &list = sctx.project->fileList(fileExt);
return ComboBox(lbl, list, selectedIdx);
}
bool ListBox(
ox::CStringViewCR name,
std::function<ox::CStringView(size_t)> const&f,
std::function<ox::CStringView(size_t)> const &f,
size_t const strCnt,
size_t &selIdx,
ImVec2 const&sz) noexcept {
ImVec2 const &sz) noexcept {
auto out = false;
if (ImGui::BeginListBox(name.c_str(), sz)) {
for (size_t i = 0; i < strCnt; ++i) {
@@ -199,13 +199,13 @@ bool ListBox(
return out;
}
bool ListBox(ox::CStringViewCR name, ox::SpanView<ox::String> const&list, size_t &selIdx) noexcept {
bool ListBox(ox::CStringViewCR name, ox::SpanView<ox::String> const &list, size_t &selIdx) noexcept {
return ListBox(name, [list](size_t i) -> ox::CStringView {
return list[i];
}, list.size(), selIdx);
}
bool ListBox(ox::CStringViewCR name, ox::SpanView<ox::CStringView> const&list, size_t &selIdx) noexcept {
bool ListBox(ox::CStringViewCR name, ox::SpanView<ox::CStringView> const &list, size_t &selIdx) noexcept {
return ListBox(name, [list](size_t i) -> ox::CStringView {
return list[i];
}, list.size(), selIdx);
@@ -216,7 +216,7 @@ FilePicker::FilePicker(
Context &sctx,
ox::StringParam title,
ox::StringParam fileExt,
ImVec2 const&size) noexcept:
ImVec2 const &size) noexcept:
m_sctx(sctx),
m_title(std::move(title)),
m_fileExt(std::move(fileExt)),
@@ -230,7 +230,7 @@ void FilePicker::draw() noexcept {
auto constexpr popupSz = ImVec2{450.f, 0};
IDStackItem const idStackItem(m_title);
if (BeginPopup(m_sctx.tctx, m_title, m_show, popupSz)) {
auto const&list = m_sctx.project->fileList(m_fileExt);
auto const &list = m_sctx.project->fileList(m_fileExt);
size_t selIdx{};
ComboBox(m_title, list, selIdx);
if (PopupControlsOkCancel(popupSz.x, m_show) == ig::PopupResponse::OK) {
@@ -267,6 +267,10 @@ QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) no
m_question{std::move(question)} {
}
void QuestionPopup::setQuestion(ox::StringParam msg) noexcept {
m_question = std::move(msg);
}
void QuestionPopup::draw(Context &ctx) noexcept {
switch (m_stage) {
case Stage::Closed:

View File

@@ -402,14 +402,14 @@ ox::Error run(Context &ctx) noexcept {
glfwPollEvents();
auto const ticks = ticksMs(ctx);
if (ctx.wakeupTime <= ticks) {
auto const st = ctx.updateHandler(ctx);
if (st >= 0) {
ctx.wakeupTime = ticks + static_cast<unsigned>(st);
sleepTime = static_cast<uint64_t>(st);
} else {
ctx.wakeupTime = ~uint64_t{0};
sleepTime = ctx.wakeupTime - ticks;
}
auto const st = ctx.updateHandler(ctx);
if (st >= 0) {
ctx.wakeupTime = ticks + static_cast<unsigned>(st);
sleepTime = static_cast<uint64_t>(st);
} else {
ctx.wakeupTime = ~uint64_t{0};
sleepTime = ctx.wakeupTime - ticks;
}
} else {
sleepTime = ctx.wakeupTime - ticks;
}

59
util/scripts/loc.py Executable file
View File

@@ -0,0 +1,59 @@
#! /usr/bin/env python3
from pathlib import Path
import argparse
def parse_args():
parser = argparse.ArgumentParser(description="Count and sort lines of code in selected files.")
parser.add_argument(
"--search-dirs", nargs="+", required=True,
help="List of directories to search (recursively)."
)
parser.add_argument(
"--include-exts", nargs="+", required=True,
help="List of file extensions to include (e.g. .cpp .hpp .py)"
)
parser.add_argument(
"--exclude-paths", nargs="*", default=[],
help="Full file paths to exclude from counting."
)
return parser.parse_args()
def main():
args = parse_args()
include_exts = tuple(args.include_exts)
exclude_paths = {Path(p).resolve() for p in args.exclude_paths}
files = []
for base in args.search_dirs:
path = Path(base)
if path.is_dir():
for file in path.rglob("*"):
try:
resolved_file = file.resolve()
if (
file.is_file()
and file.name.endswith(include_exts)
and resolved_file not in exclude_paths
):
files.append(str(resolved_file))
except FileNotFoundError:
continue
line_counts = []
total_lines = 0
for f in files:
try:
with open(f, "r", encoding="utf-8", errors="ignore") as file:
lines = sum(1 for _ in file)
line_counts.append((lines, f))
total_lines += lines
except Exception as e:
print(f"Failed to read {f}: {e}")
line_counts.sort()
for count, filename in line_counts:
print(f"{count:>7} {filename}")
print(f"{total_lines:>7} total")
if __name__ == "__main__":
main()