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
17 changed files with 205 additions and 99 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

@@ -114,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;
@@ -127,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);
}
@@ -366,7 +365,6 @@ void StudioUI::drawTabs() noexcept {
}
if (m_closeActiveTab) [[unlikely]] {
ImGui::SetTabItemClosed(e->itemDisplayName().c_str());
} else if (open) [[likely]] {
e->draw(m_sctx);
}
@@ -583,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();
}
@@ -701,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));
@@ -714,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,7 +83,7 @@ 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;

View File

@@ -15,10 +15,14 @@ namespace studio {
class StudioUI;
struct Context {
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;
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;
@@ -27,12 +31,8 @@ struct Context {
size_t navIdx{};
ox::Vector<NavPath> navStack;
std::function<void(ox::StringParam filePath, ox::StringParam navArgs)> navCallback;
public:
StudioUI &ui;
Project *project = nullptr;
turbine::Context &tctx;
Context(StudioUI &pUi, turbine::Context &pTctx) noexcept:
ui(pUi), tctx(pTctx) {}
ui{pUi}, tctx{pTctx} {}
};
[[nodiscard]]
@@ -40,6 +40,11 @@ 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;

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()