Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3148e16cf8 | |||
| 34eaa72033 | |||
| f49f1698f4 | |||
| f4b3188069 | |||
| 2571ab79c1 | |||
| d768e56727 |
@@ -4,7 +4,7 @@ project(kte)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(KTE_VERSION "1.7.1")
|
||||
set(KTE_VERSION "1.9.0")
|
||||
|
||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||
@@ -14,6 +14,7 @@ set(BUILD_TESTS ON CACHE BOOL "Enable building test programs.")
|
||||
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||
option(KTE_STATIC_LINK "Enable static linking on Linux" ON)
|
||||
|
||||
# Optionally enable AddressSanitizer (ASan)
|
||||
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
|
||||
@@ -285,7 +286,7 @@ endif ()
|
||||
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||
|
||||
# Static linking on Linux only (macOS does not support static linking of system libraries)
|
||||
if (NOT APPLE)
|
||||
if (NOT APPLE AND KTE_STATIC_LINK)
|
||||
target_link_options(kte PRIVATE -static)
|
||||
endif ()
|
||||
|
||||
@@ -326,6 +327,7 @@ if (BUILD_TESTS)
|
||||
tests/test_swap_edge_cases.cc
|
||||
tests/test_swap_recovery_prompt.cc
|
||||
tests/test_swap_cleanup.cc
|
||||
tests/test_swap_cleanup2.cc
|
||||
tests/test_swap_git_editor.cc
|
||||
tests/test_piece_table.cc
|
||||
tests/test_search.cc
|
||||
@@ -375,7 +377,7 @@ if (BUILD_TESTS)
|
||||
endif ()
|
||||
|
||||
# Static linking on Linux only (macOS does not support static linking of system libraries)
|
||||
if (NOT APPLE)
|
||||
if (NOT APPLE AND KTE_STATIC_LINK)
|
||||
target_link_options(kte_tests PRIVATE -static)
|
||||
endif ()
|
||||
endif ()
|
||||
@@ -418,7 +420,7 @@ if (BUILD_GUI)
|
||||
endif ()
|
||||
|
||||
# Static linking on Linux only (macOS does not support static linking of system libraries)
|
||||
if (NOT APPLE)
|
||||
if (NOT APPLE AND KTE_STATIC_LINK)
|
||||
target_link_options(kge PRIVATE -static)
|
||||
endif ()
|
||||
|
||||
|
||||
27
Command.cc
27
Command.cc
@@ -115,6 +115,14 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_new_window(CommandContext &ctx)
|
||||
{
|
||||
ctx.editor.SetNewWindowRequested(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_center_on_cursor(CommandContext &ctx)
|
||||
{
|
||||
@@ -744,6 +752,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
||||
if (buf->IsFileBacked()) {
|
||||
if (buf->Save(err)) {
|
||||
buf->SetDirty(false);
|
||||
if (auto *sm = ctx.editor.Swap())
|
||||
sm->ResetJournal(*buf);
|
||||
} else {
|
||||
ctx.editor.SetStatus(err);
|
||||
return false;
|
||||
@@ -751,6 +761,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
||||
} else if (!buf->Filename().empty()) {
|
||||
if (buf->SaveAs(buf->Filename(), err)) {
|
||||
buf->SetDirty(false);
|
||||
if (auto *sm = ctx.editor.Swap())
|
||||
sm->ResetJournal(*buf);
|
||||
} else {
|
||||
ctx.editor.SetStatus(err);
|
||||
return false;
|
||||
@@ -2254,10 +2266,8 @@ cmd_show_help(CommandContext &ctx)
|
||||
};
|
||||
|
||||
auto populate_from_text = [](Buffer &b, const std::string &text) {
|
||||
// Clear existing rows
|
||||
while (b.Nrows() > 0) {
|
||||
b.delete_row(0);
|
||||
}
|
||||
// Clear existing content
|
||||
b.replace_all_bytes("");
|
||||
// Parse text and insert rows
|
||||
std::string line;
|
||||
line.reserve(128);
|
||||
@@ -2562,6 +2572,10 @@ cmd_newline(CommandContext &ctx)
|
||||
ctx.editor.SetStatus(err);
|
||||
} else {
|
||||
buf->SetDirty(false);
|
||||
if (auto *sm = ctx.editor.Swap()) {
|
||||
sm->NotifyFilenameChanged(*buf);
|
||||
sm->ResetJournal(*buf);
|
||||
}
|
||||
ctx.editor.SetStatus("Saved as " + value);
|
||||
if (auto *u = buf->Undo())
|
||||
u->mark_saved();
|
||||
@@ -5002,6 +5016,11 @@ InstallDefaultCommands()
|
||||
CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor,
|
||||
false, false
|
||||
});
|
||||
// GUI: new window
|
||||
CommandRegistry::Register({
|
||||
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
|
||||
false, false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ enum class CommandId {
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
// Viewport control
|
||||
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
||||
// GUI: open a new editor window sharing the same buffer list
|
||||
NewWindow,
|
||||
};
|
||||
|
||||
|
||||
|
||||
75
Editor.cc
75
Editor.cc
@@ -69,20 +69,22 @@ Editor::SetStatus(const std::string &message)
|
||||
Buffer *
|
||||
Editor::CurrentBuffer()
|
||||
{
|
||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
||||
auto &bufs = Buffers();
|
||||
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &buffers_[curbuf_];
|
||||
return &bufs[curbuf_];
|
||||
}
|
||||
|
||||
|
||||
const Buffer *
|
||||
Editor::CurrentBuffer() const
|
||||
{
|
||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
||||
const auto &bufs = Buffers();
|
||||
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &buffers_[curbuf_];
|
||||
return &bufs[curbuf_];
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +119,9 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
||||
|
||||
// Prepare list of other buffer paths
|
||||
std::vector<std::vector<std::filesystem::path> > others;
|
||||
others.reserve(buffers_.size());
|
||||
for (const auto &b: buffers_) {
|
||||
const auto &bufs = Buffers();
|
||||
others.reserve(bufs.size());
|
||||
for (const auto &b: bufs) {
|
||||
if (&b == &buf)
|
||||
continue;
|
||||
if (b.Filename().empty())
|
||||
@@ -161,41 +164,44 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
||||
std::size_t
|
||||
Editor::AddBuffer(const Buffer &buf)
|
||||
{
|
||||
buffers_.push_back(buf);
|
||||
auto &bufs = Buffers();
|
||||
bufs.push_back(buf);
|
||||
// Attach swap recorder
|
||||
if (swap_) {
|
||||
swap_->Attach(&buffers_.back());
|
||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
||||
swap_->Attach(&bufs.back());
|
||||
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
if (bufs.size() == 1) {
|
||||
curbuf_ = 0;
|
||||
}
|
||||
return buffers_.size() - 1;
|
||||
return bufs.size() - 1;
|
||||
}
|
||||
|
||||
|
||||
std::size_t
|
||||
Editor::AddBuffer(Buffer &&buf)
|
||||
{
|
||||
buffers_.push_back(std::move(buf));
|
||||
auto &bufs = Buffers();
|
||||
bufs.push_back(std::move(buf));
|
||||
if (swap_) {
|
||||
swap_->Attach(&buffers_.back());
|
||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
||||
swap_->Attach(&bufs.back());
|
||||
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
if (bufs.size() == 1) {
|
||||
curbuf_ = 0;
|
||||
}
|
||||
return buffers_.size() - 1;
|
||||
return bufs.size() - 1;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Editor::OpenFile(const std::string &path, std::string &err)
|
||||
{
|
||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
||||
// of creating a new one.
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
// If the current buffer is an unnamed, empty, clean scratch buffer, reuse
|
||||
// it instead of creating a new one.
|
||||
auto &bufs_ref = Buffers();
|
||||
if (!bufs_ref.empty() && curbuf_ < bufs_ref.size()) {
|
||||
Buffer &cur = bufs_ref[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const std::size_t nrows = cur.Nrows();
|
||||
@@ -268,7 +274,7 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
if (swap_) {
|
||||
swap_->NotifyFilenameChanged(buffers_[idx]);
|
||||
swap_->NotifyFilenameChanged(Buffers()[idx]);
|
||||
}
|
||||
SwitchTo(idx);
|
||||
// Defensive: ensure any active prompt is closed after a successful open
|
||||
@@ -446,12 +452,13 @@ Editor::ProcessPendingOpens()
|
||||
bool
|
||||
Editor::SwitchTo(std::size_t index)
|
||||
{
|
||||
if (index >= buffers_.size()) {
|
||||
auto &bufs = Buffers();
|
||||
if (index >= bufs.size()) {
|
||||
return false;
|
||||
}
|
||||
curbuf_ = index;
|
||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||
Buffer &b = buffers_[curbuf_];
|
||||
Buffer &b = bufs[curbuf_];
|
||||
if (b.SyntaxEnabled()) {
|
||||
b.EnsureHighlighter();
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
@@ -478,21 +485,22 @@ Editor::SwitchTo(std::size_t index)
|
||||
bool
|
||||
Editor::CloseBuffer(std::size_t index)
|
||||
{
|
||||
if (index >= buffers_.size()) {
|
||||
auto &bufs = Buffers();
|
||||
if (index >= bufs.size()) {
|
||||
return false;
|
||||
}
|
||||
if (swap_) {
|
||||
// Always remove swap file when closing a buffer on normal exit.
|
||||
// Swap files are for crash recovery; on clean close, we don't need them.
|
||||
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
||||
swap_->Detach(&buffers_[index], true);
|
||||
buffers_[index].SetSwapRecorder(nullptr);
|
||||
swap_->Detach(&bufs[index], true);
|
||||
bufs[index].SetSwapRecorder(nullptr);
|
||||
}
|
||||
buffers_.erase(buffers_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (buffers_.empty()) {
|
||||
bufs.erase(bufs.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (bufs.empty()) {
|
||||
curbuf_ = 0;
|
||||
} else if (curbuf_ >= buffers_.size()) {
|
||||
curbuf_ = buffers_.size() - 1;
|
||||
} else if (curbuf_ >= bufs.size()) {
|
||||
curbuf_ = bufs.size() - 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -516,7 +524,12 @@ Editor::Reset()
|
||||
// Reset close-confirm/save state
|
||||
close_confirm_pending_ = false;
|
||||
close_after_save_ = false;
|
||||
buffers_.clear();
|
||||
auto &bufs = Buffers();
|
||||
if (swap_) {
|
||||
for (auto &buf : bufs)
|
||||
swap_->Detach(&buf, true);
|
||||
}
|
||||
bufs.clear();
|
||||
curbuf_ = 0;
|
||||
}
|
||||
|
||||
|
||||
44
Editor.h
44
Editor.h
@@ -246,6 +246,18 @@ public:
|
||||
}
|
||||
|
||||
|
||||
void SetNewWindowRequested(bool on)
|
||||
{
|
||||
new_window_requested_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool NewWindowRequested() const
|
||||
{
|
||||
return new_window_requested_;
|
||||
}
|
||||
|
||||
|
||||
void SetQuitConfirmPending(bool on)
|
||||
{
|
||||
quit_confirm_pending_ = on;
|
||||
@@ -509,7 +521,7 @@ public:
|
||||
// Buffers
|
||||
[[nodiscard]] std::size_t BufferCount() const
|
||||
{
|
||||
return buffers_.size();
|
||||
return Buffers().size();
|
||||
}
|
||||
|
||||
|
||||
@@ -519,6 +531,19 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// Clamp curbuf_ to valid range. Call when the shared buffer list may
|
||||
// have been modified by another editor (e.g., buffer closed in another window).
|
||||
void ValidateBufferIndex()
|
||||
{
|
||||
const auto &bufs = Buffers();
|
||||
if (bufs.empty()) {
|
||||
curbuf_ = 0;
|
||||
} else if (curbuf_ >= bufs.size()) {
|
||||
curbuf_ = bufs.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Buffer *CurrentBuffer();
|
||||
|
||||
const Buffer *CurrentBuffer() const;
|
||||
@@ -570,13 +595,22 @@ public:
|
||||
// Direct access when needed (try to prefer methods above)
|
||||
[[nodiscard]] const std::vector<Buffer> &Buffers() const
|
||||
{
|
||||
return buffers_;
|
||||
return shared_buffers_ ? *shared_buffers_ : buffers_;
|
||||
}
|
||||
|
||||
|
||||
std::vector<Buffer> &Buffers()
|
||||
{
|
||||
return buffers_;
|
||||
return shared_buffers_ ? *shared_buffers_ : buffers_;
|
||||
}
|
||||
|
||||
|
||||
// Share another editor's buffer list. When set, this editor operates on
|
||||
// the provided vector instead of its own. Pass nullptr to detach.
|
||||
void SetSharedBuffers(std::vector<Buffer> *shared)
|
||||
{
|
||||
shared_buffers_ = shared;
|
||||
curbuf_ = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -628,7 +662,8 @@ private:
|
||||
bool repeatable_ = false; // whether the next command is repeatable
|
||||
|
||||
std::vector<Buffer> buffers_;
|
||||
std::size_t curbuf_ = 0; // index into buffers_
|
||||
std::vector<Buffer> *shared_buffers_ = nullptr; // if set, use this instead of buffers_
|
||||
std::size_t curbuf_ = 0; // index into buffers_
|
||||
|
||||
// Swap journaling manager (lifetime = editor)
|
||||
std::unique_ptr<kte::SwapManager> swap_;
|
||||
@@ -639,6 +674,7 @@ private:
|
||||
|
||||
// Quit state
|
||||
bool quit_requested_ = false;
|
||||
bool new_window_requested_ = false;
|
||||
bool quit_confirm_pending_ = false;
|
||||
bool close_confirm_pending_ = false; // awaiting y/N to save-before-close
|
||||
bool close_after_save_ = false; // if true, close buffer after successful Save/SaveAs
|
||||
|
||||
37
GUITheme.h
37
GUITheme.h
@@ -330,6 +330,7 @@ enum class ThemeId {
|
||||
Amber = 10,
|
||||
WeylandYutani = 11,
|
||||
Orbital = 12,
|
||||
Tufte = 13,
|
||||
};
|
||||
|
||||
// Current theme tracking
|
||||
@@ -377,6 +378,7 @@ BackgroundModeName()
|
||||
#include "themes/WeylandYutani.h"
|
||||
#include "themes/Zenburn.h"
|
||||
#include "themes/Orbital.h"
|
||||
#include "themes/Tufte.h"
|
||||
|
||||
|
||||
// Theme abstraction and registry (generalized theme system)
|
||||
@@ -488,6 +490,28 @@ struct OrbitalTheme final : Theme {
|
||||
}
|
||||
};
|
||||
|
||||
struct TufteTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "tufte";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
if (gBackgroundMode == BackgroundMode::Dark)
|
||||
ApplyTufteDarkTheme();
|
||||
else
|
||||
ApplyTufteLightTheme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::Tufte;
|
||||
}
|
||||
};
|
||||
|
||||
struct ZenburnTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
@@ -657,7 +681,7 @@ ThemeRegistry()
|
||||
static std::vector<std::unique_ptr<Theme> > reg;
|
||||
if (reg.empty()) {
|
||||
// Alphabetical by canonical name:
|
||||
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, orbital, plan9, solarized, weyland-yutani, zenburn
|
||||
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn
|
||||
reg.emplace_back(std::make_unique<detail::AmberTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::EverforestTheme>());
|
||||
@@ -669,6 +693,7 @@ ThemeRegistry()
|
||||
reg.emplace_back(std::make_unique<detail::OrbitalTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
||||
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::TufteTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::WeylandYutaniTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::ZenburnTheme>());
|
||||
}
|
||||
@@ -855,10 +880,12 @@ ThemeIndexFromId(const ThemeId id)
|
||||
return 9;
|
||||
case ThemeId::Solarized:
|
||||
return 10;
|
||||
case ThemeId::WeylandYutani:
|
||||
case ThemeId::Tufte:
|
||||
return 11;
|
||||
case ThemeId::Zenburn:
|
||||
case ThemeId::WeylandYutani:
|
||||
return 12;
|
||||
case ThemeId::Zenburn:
|
||||
return 13;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -892,8 +919,10 @@ ThemeIdFromIndex(const size_t idx)
|
||||
case 10:
|
||||
return ThemeId::Solarized;
|
||||
case 11:
|
||||
return ThemeId::WeylandYutani;
|
||||
return ThemeId::Tufte;
|
||||
case 12:
|
||||
return ThemeId::WeylandYutani;
|
||||
case 13:
|
||||
return ThemeId::Zenburn;
|
||||
}
|
||||
}
|
||||
|
||||
15
HelpText.cc
15
HelpText.cc
@@ -27,6 +27,7 @@ HelpText::Text()
|
||||
" C-k SPACE Toggle mark\n"
|
||||
" C-k C-d Kill entire line\n"
|
||||
" C-k C-q Quit now (no confirm)\n"
|
||||
" C-k C-s Save\n"
|
||||
" C-k C-x Save and quit\n"
|
||||
" C-k a Mark start of file, jump to end\n"
|
||||
" C-k b Switch buffer\n"
|
||||
@@ -63,6 +64,10 @@ HelpText::Text()
|
||||
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
||||
" ESC q Reflow paragraph\n"
|
||||
"\n"
|
||||
"Universal argument:\n"
|
||||
" C-u Begin repeat count (then type digits); C-u alone multiplies by 4\n"
|
||||
" C-u N <cmd> Repeat <cmd> N times (e.g., C-u 8 C-f moves right 8 chars)\n"
|
||||
"\n"
|
||||
"Control keys:\n"
|
||||
" C-a C-e Line start / end\n"
|
||||
" C-b C-f Move left / right\n"
|
||||
@@ -74,12 +79,20 @@ HelpText::Text()
|
||||
" C-t Regex search & replace\n"
|
||||
" C-h Search & replace\n"
|
||||
" C-l / C-g Refresh / Cancel\n"
|
||||
" C-u [digits] Universal argument (repeat count)\n"
|
||||
"\n"
|
||||
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||
"\n"
|
||||
"GUI appearance (command prompt):\n"
|
||||
" : theme NAME Set GUI theme (amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, plan9, solarized, weyland-yutani, zenburn)\n"
|
||||
" : background MODE Set background: light | dark (affects eink, gruvbox, old-book, solarized)\n"
|
||||
"\n"
|
||||
"GUI config file options:\n"
|
||||
" font_size=NUM Set font size in pixels (default: 16; e.g., font_size=18)\n"
|
||||
"\n"
|
||||
"GUI window management:\n"
|
||||
" Cmd+N (macOS) Open a new editor window sharing the same buffers\n"
|
||||
" Ctrl+Shift+N (Linux) Open a new editor window sharing the same buffers\n"
|
||||
" Close window Secondary windows close independently; closing the\n"
|
||||
" primary window quits the editor\n"
|
||||
);
|
||||
}
|
||||
602
ImGuiFrontend.cc
602
ImGuiFrontend.cc
@@ -29,21 +29,143 @@
|
||||
|
||||
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void
|
||||
apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
|
||||
{
|
||||
if (!b)
|
||||
return;
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty())
|
||||
first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||
b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update editor logical rows/cols from current ImGui metrics for a given display size.
|
||||
static void
|
||||
update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
|
||||
{
|
||||
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
float ch_w = ImGui::CalcTextSize("M").x;
|
||||
if (row_h <= 0.0f)
|
||||
row_h = 16.0f;
|
||||
if (ch_w <= 0.0f)
|
||||
ch_w = 8.0f;
|
||||
|
||||
const float pad_x = 6.0f;
|
||||
const float pad_y = 6.0f;
|
||||
|
||||
float wanted_bar_h = ImGui::GetFrameHeight();
|
||||
float total_avail_h = std::max(0.0f, disp_h - 2.0f * pad_y);
|
||||
float actual_avail_h = std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h;
|
||||
|
||||
auto content_rows = static_cast<std::size_t>(std::max(0.0f, std::floor(actual_avail_h / row_h)));
|
||||
std::size_t rows = content_rows + 1;
|
||||
|
||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||
|
||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
||||
ed.SetDimensions(rows, cols);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SetupImGuiStyle_ — apply theme, fonts, and flags to the current ImGui context
|
||||
// ---------------------------------------------------------------------------
|
||||
void
|
||||
GUIFrontend::SetupImGuiStyle_()
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
// Disable imgui.ini for secondary windows (primary sets its own path in Init)
|
||||
io.IniFilename = nullptr;
|
||||
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
if (config_.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(config_.theme);
|
||||
|
||||
// Load fonts into this context's font atlas.
|
||||
// Font registry is global and already populated by Init; just load into this atlas.
|
||||
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
|
||||
LoadGuiFont_(nullptr, (float) config_.font_size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Destroy a single window's ImGui context + SDL/GL resources
|
||||
// ---------------------------------------------------------------------------
|
||||
void
|
||||
GUIFrontend::DestroyWindowResources_(WindowState &ws)
|
||||
{
|
||||
if (ws.imgui_ctx) {
|
||||
// Must activate this window's GL context before shutting down the
|
||||
// OpenGL3 backend, otherwise it deletes another context's resources.
|
||||
if (ws.window && ws.gl_ctx)
|
||||
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext(ws.imgui_ctx);
|
||||
ws.imgui_ctx = nullptr;
|
||||
}
|
||||
if (ws.gl_ctx) {
|
||||
SDL_GL_DeleteContext(ws.gl_ctx);
|
||||
ws.gl_ctx = nullptr;
|
||||
}
|
||||
if (ws.window) {
|
||||
SDL_DestroyWindow(ws.window);
|
||||
ws.window = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
||||
input_.Attach(&ed);
|
||||
// editor dimensions will be initialized during the first Step() frame
|
||||
|
||||
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
||||
config_ = GUIConfig::Load();
|
||||
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
||||
GUIConfig cfg = GUIConfig::Load();
|
||||
|
||||
// GL attributes for core profile
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
@@ -56,159 +178,114 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
// Compute desired window size from config
|
||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
|
||||
if (cfg.fullscreen) {
|
||||
// "Fullscreen": fill the usable bounds of the primary display.
|
||||
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
||||
int init_w = 1280, init_h = 800;
|
||||
if (config_.fullscreen) {
|
||||
SDL_Rect usable{};
|
||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||
width_ = usable.w;
|
||||
height_ = usable.h;
|
||||
init_w = usable.w;
|
||||
init_h = usable.h;
|
||||
}
|
||||
#if !defined(__APPLE__)
|
||||
// Non-macOS: desktop fullscreen uses the current display resolution.
|
||||
win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
#endif
|
||||
} else {
|
||||
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
||||
int w = cfg.columns * static_cast<int>(cfg.font_size);
|
||||
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
|
||||
|
||||
// As a safety, clamp to display usable bounds if retrievable
|
||||
int w = config_.columns * static_cast<int>(config_.font_size);
|
||||
int h = config_.rows * static_cast<int>(config_.font_size * 1.2);
|
||||
SDL_Rect usable{};
|
||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||
w = std::min(w, usable.w);
|
||||
h = std::min(h, usable.h);
|
||||
}
|
||||
width_ = std::max(320, w);
|
||||
height_ = std::max(200, h);
|
||||
init_w = std::max(320, w);
|
||||
init_h = std::max(200, h);
|
||||
}
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||
window_ = SDL_CreateWindow(
|
||||
SDL_Window *win = SDL_CreateWindow(
|
||||
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
width_, height_,
|
||||
init_w, init_h,
|
||||
win_flags);
|
||||
if (!window_) {
|
||||
if (!win) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_EnableScreenSaver();
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// macOS: when "fullscreen" is requested, position the window at the
|
||||
// top-left of the usable display area to mimic fullscreen while keeping
|
||||
// the system menu bar visible.
|
||||
if (cfg.fullscreen) {
|
||||
if (config_.fullscreen) {
|
||||
SDL_Rect usable{};
|
||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
||||
SDL_SetWindowPosition(win, usable.x, usable.y);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
gl_ctx_ = SDL_GL_CreateContext(window_);
|
||||
if (!gl_ctx_)
|
||||
SDL_GLContext gl_ctx = SDL_GL_CreateContext(win);
|
||||
if (!gl_ctx) {
|
||||
SDL_DestroyWindow(win);
|
||||
return false;
|
||||
SDL_GL_MakeCurrent(window_, gl_ctx_);
|
||||
}
|
||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||
SDL_GL_SetSwapInterval(1); // vsync
|
||||
|
||||
// Create primary ImGui context
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
||||
if (const char *home = std::getenv("HOME")) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path config_dir = fs::path(home) / ".config" / "kte";
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(config_dir)) {
|
||||
fs::create_directories(config_dir, ec);
|
||||
}
|
||||
|
||||
if (fs::exists(config_dir)) {
|
||||
static std::string ini_path = (config_dir / "imgui.ini").string();
|
||||
io.IniFilename = ini_path.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
if (config_.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
kte::ApplyThemeByName(config_.theme);
|
||||
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty())
|
||||
first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||
b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
apply_syntax_to_buffer(ed.CurrentBuffer(), config_);
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx))
|
||||
return false;
|
||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
||||
return false;
|
||||
|
||||
// Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists
|
||||
// Cache initial window size
|
||||
int w, h;
|
||||
SDL_GetWindowSize(window_, &w, &h);
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
init_w = w;
|
||||
init_h = h;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Workaround: On macOS Retina when starting maximized, we sometimes get a
|
||||
// subtle input vs draw alignment mismatch until the first manual resize.
|
||||
// Nudge the window size by 1px and back to trigger a proper internal
|
||||
// recomputation, without visible impact.
|
||||
if (w > 1 && h > 1) {
|
||||
SDL_SetWindowSize(window_, w - 1, h - 1);
|
||||
SDL_SetWindowSize(window_, w, h);
|
||||
// Update cached size in case backend reports immediately
|
||||
SDL_GetWindowSize(window_, &w, &h);
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
SDL_SetWindowSize(win, w - 1, h - 1);
|
||||
SDL_SetWindowSize(win, w, h);
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
init_w = w;
|
||||
init_h = h;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Install embedded fonts into registry and load configured font
|
||||
// Install embedded fonts
|
||||
kte::Fonts::InstallDefaultFonts();
|
||||
// Initialize font atlas using configured font name and size; fallback to embedded default helper
|
||||
if (!kte::Fonts::FontRegistry::Instance().LoadFont(cfg.font, (float) cfg.font_size)) {
|
||||
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
||||
// Record defaults in registry so subsequent size changes have a base
|
||||
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) cfg.font_size);
|
||||
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
|
||||
LoadGuiFont_(nullptr, (float) config_.font_size);
|
||||
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) config_.font_size);
|
||||
std::string n;
|
||||
float s = 0.0f;
|
||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(n, s)) {
|
||||
@@ -216,6 +293,90 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
}
|
||||
}
|
||||
|
||||
// Build primary WindowState
|
||||
auto ws = std::make_unique<WindowState>();
|
||||
ws->window = win;
|
||||
ws->gl_ctx = gl_ctx;
|
||||
ws->imgui_ctx = imgui_ctx;
|
||||
ws->width = init_w;
|
||||
ws->height = init_h;
|
||||
// The primary window's editor IS the editor passed in from main; we don't
|
||||
// use ws->editor for the primary — instead we keep a pointer to &ed.
|
||||
// We store a sentinel: window index 0 uses the external editor reference.
|
||||
// To keep things simple, attach input to the passed-in editor.
|
||||
ws->input.Attach(&ed);
|
||||
windows_.push_back(std::move(ws));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
GUIFrontend::OpenNewWindow_(Editor &primary)
|
||||
{
|
||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
int w = windows_[0]->width;
|
||||
int h = windows_[0]->height;
|
||||
|
||||
SDL_Window *win = SDL_CreateWindow(
|
||||
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
w, h,
|
||||
win_flags);
|
||||
if (!win)
|
||||
return false;
|
||||
|
||||
SDL_GLContext gl_ctx = SDL_GL_CreateContext(win);
|
||||
if (!gl_ctx) {
|
||||
SDL_DestroyWindow(win);
|
||||
return false;
|
||||
}
|
||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
|
||||
// Each window gets its own ImGui context — ImGui requires exactly one
|
||||
// NewFrame/Render cycle per context per frame.
|
||||
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||
ImGui::SetCurrentContext(imgui_ctx);
|
||||
|
||||
SetupImGuiStyle_();
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx)) {
|
||||
ImGui::DestroyContext(imgui_ctx);
|
||||
SDL_GL_DeleteContext(gl_ctx);
|
||||
SDL_DestroyWindow(win);
|
||||
return false;
|
||||
}
|
||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) {
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext(imgui_ctx);
|
||||
SDL_GL_DeleteContext(gl_ctx);
|
||||
SDL_DestroyWindow(win);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ws = std::make_unique<WindowState>();
|
||||
ws->window = win;
|
||||
ws->gl_ctx = gl_ctx;
|
||||
ws->imgui_ctx = imgui_ctx;
|
||||
ws->width = w;
|
||||
ws->height = h;
|
||||
|
||||
// Secondary editor shares the primary's buffer list
|
||||
ws->editor.SetSharedBuffers(&primary.Buffers());
|
||||
ws->editor.SetDimensions(primary.Rows(), primary.Cols());
|
||||
|
||||
// Open a new untitled buffer and switch to it in the new window.
|
||||
ws->editor.AddBuffer(Buffer());
|
||||
ws->editor.SwitchTo(ws->editor.BufferCount() - 1);
|
||||
|
||||
ws->input.Attach(&ws->editor);
|
||||
|
||||
windows_.push_back(std::move(ws));
|
||||
|
||||
// Restore primary context
|
||||
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -223,137 +384,214 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
void
|
||||
GUIFrontend::Step(Editor &ed, bool &running)
|
||||
{
|
||||
// --- Event processing ---
|
||||
// SDL events carry a window ID. Route each event to the correct window's
|
||||
// ImGui context (for ImGui_ImplSDL2_ProcessEvent) and input handler.
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||
// Determine which window this event belongs to
|
||||
Uint32 event_win_id = 0;
|
||||
switch (e.type) {
|
||||
case SDL_QUIT:
|
||||
running = false;
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
width_ = e.window.data1;
|
||||
height_ = e.window.data2;
|
||||
}
|
||||
event_win_id = e.window.windowID;
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP:
|
||||
event_win_id = e.key.windowID;
|
||||
break;
|
||||
case SDL_TEXTINPUT:
|
||||
event_win_id = e.text.windowID;
|
||||
break;
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
event_win_id = e.button.windowID;
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
event_win_id = e.wheel.windowID;
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
event_win_id = e.motion.windowID;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Map input to commands
|
||||
input_.ProcessSDLEvent(e);
|
||||
|
||||
if (e.type == SDL_QUIT) {
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Find the target window and route the event to its ImGui context
|
||||
WindowState *target = nullptr;
|
||||
std::size_t target_idx = 0;
|
||||
if (event_win_id != 0) {
|
||||
for (std::size_t i = 0; i < windows_.size(); ++i) {
|
||||
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
|
||||
target = windows_[i].get();
|
||||
target_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target && target->imgui_ctx) {
|
||||
// Set this window's ImGui context so ImGui_ImplSDL2_ProcessEvent
|
||||
// updates the correct IO state.
|
||||
ImGui::SetCurrentContext(target->imgui_ctx);
|
||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||
}
|
||||
|
||||
if (e.type == SDL_WINDOWEVENT) {
|
||||
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||
if (target) {
|
||||
if (target_idx == 0) {
|
||||
running = false;
|
||||
} else {
|
||||
target->alive = false;
|
||||
}
|
||||
}
|
||||
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
if (target) {
|
||||
target->width = e.window.data1;
|
||||
target->height = e.window.data2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Route input events to the correct window's input handler
|
||||
if (target) {
|
||||
target->input.ProcessSDLEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply pending font change before starting a new frame
|
||||
if (!running)
|
||||
return;
|
||||
|
||||
// --- Apply pending font change (to all contexts) ---
|
||||
{
|
||||
std::string fname;
|
||||
float fsize = 0.0f;
|
||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
||||
if (!fname.empty() && fsize > 0.0f) {
|
||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||
// Recreate backend font texture
|
||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
for (auto &ws : windows_) {
|
||||
if (!ws->alive || !ws->imgui_ctx)
|
||||
continue;
|
||||
ImGui::SetCurrentContext(ws->imgui_ctx);
|
||||
SDL_GL_MakeCurrent(ws->window, ws->gl_ctx);
|
||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start a new ImGui frame BEFORE processing commands so dimensions are correct
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window_);
|
||||
ImGui::NewFrame();
|
||||
// --- Step each window ---
|
||||
// We iterate by index because OpenNewWindow_ may append to windows_.
|
||||
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
|
||||
WindowState &ws = *windows_[wi];
|
||||
if (!ws.alive)
|
||||
continue;
|
||||
|
||||
// Update editor logical rows/cols using current ImGui metrics and display size
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
float ch_w = ImGui::CalcTextSize("M").x;
|
||||
if (row_h <= 0.0f)
|
||||
row_h = 16.0f;
|
||||
if (ch_w <= 0.0f)
|
||||
ch_w = 8.0f;
|
||||
// Prefer ImGui IO display size; fall back to cached SDL window size
|
||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
|
||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
|
||||
Editor &wed = (wi == 0) ? ed : ws.editor;
|
||||
|
||||
// Account for the GUI window padding and the status bar height used in ImGuiRenderer.
|
||||
const float pad_x = 6.0f;
|
||||
const float pad_y = 6.0f;
|
||||
// Shared buffer list may have been modified by another window.
|
||||
wed.ValidateBufferIndex();
|
||||
|
||||
// Use the same logic as ImGuiRenderer for available height and status bar reservation.
|
||||
float wanted_bar_h = ImGui::GetFrameHeight();
|
||||
float total_avail_h = std::max(0.0f, disp_h - 2.0f * pad_y);
|
||||
float actual_avail_h = std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h;
|
||||
// Activate this window's GL and ImGui contexts
|
||||
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||
|
||||
// Visible content rows inside the scroll child
|
||||
auto content_rows = static_cast<std::size_t>(std::max(0.0f, std::floor(actual_avail_h / row_h)));
|
||||
// Editor::Rows includes the status line; add 1 back for it.
|
||||
std::size_t rows = content_rows + 1;
|
||||
// Start a new ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(ws.window);
|
||||
ImGui::NewFrame();
|
||||
|
||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||
|
||||
// Only update if changed to avoid churn
|
||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
||||
ed.SetDimensions(rows, cols);
|
||||
// Update editor dimensions
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(ws.width);
|
||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(ws.height);
|
||||
update_editor_dimensions(wed, disp_w, disp_h);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow deferred opens (including swap recovery prompts) to run.
|
||||
ed.ProcessPendingOpens();
|
||||
// Allow deferred opens
|
||||
wed.ProcessPendingOpens();
|
||||
|
||||
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
if (!input_.Poll(mi))
|
||||
break;
|
||||
if (mi.hasCommand) {
|
||||
// Track kill ring before and after to sync GUI clipboard when it changes
|
||||
const std::string before = ed.KillRingHead();
|
||||
Execute(ed, mi.id, mi.arg, mi.count);
|
||||
const std::string after = ed.KillRingHead();
|
||||
if (after != before && !after.empty()) {
|
||||
// Update the system clipboard to mirror the kill ring head in GUI
|
||||
SDL_SetClipboardText(after.c_str());
|
||||
// Drain input queue
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
if (!ws.input.Poll(mi))
|
||||
break;
|
||||
if (mi.hasCommand) {
|
||||
if (mi.id == CommandId::NewWindow) {
|
||||
// Open a new window; handled after this loop
|
||||
wed.SetNewWindowRequested(true);
|
||||
} else {
|
||||
const std::string before = wed.KillRingHead();
|
||||
Execute(wed, mi.id, mi.arg, mi.count);
|
||||
const std::string after = wed.KillRingHead();
|
||||
if (after != before && !after.empty()) {
|
||||
SDL_SetClipboardText(after.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wi == 0 && wed.QuitRequested()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// Draw
|
||||
ws.renderer.Draw(wed);
|
||||
|
||||
// Render
|
||||
ImGui::Render();
|
||||
int display_w, display_h;
|
||||
SDL_GL_GetDrawableSize(ws.window, &display_w, &display_h);
|
||||
glViewport(0, 0, display_w, display_h);
|
||||
glClearColor(0.1f, 0.1f, 0.11f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
SDL_GL_SwapWindow(ws.window);
|
||||
}
|
||||
|
||||
if (ed.QuitRequested()) {
|
||||
running = false;
|
||||
// Handle deferred new-window requests (must happen outside the render loop
|
||||
// to avoid corrupting an in-progress ImGui frame).
|
||||
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
|
||||
Editor &wed = (wi == 0) ? ed : windows_[wi]->editor;
|
||||
if (wed.NewWindowRequested()) {
|
||||
wed.SetNewWindowRequested(false);
|
||||
OpenNewWindow_(ed);
|
||||
}
|
||||
}
|
||||
|
||||
// No runtime font UI; always use embedded font.
|
||||
// Remove dead secondary windows
|
||||
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
||||
if (!(*it)->alive) {
|
||||
DestroyWindowResources_(**it);
|
||||
it = windows_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw editor UI
|
||||
renderer_.Draw(ed);
|
||||
|
||||
// Render
|
||||
ImGui::Render();
|
||||
int display_w, display_h;
|
||||
SDL_GL_GetDrawableSize(window_, &display_w, &display_h);
|
||||
glViewport(0, 0, display_w, display_h);
|
||||
glClearColor(0.1f, 0.1f, 0.11f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
SDL_GL_SwapWindow(window_);
|
||||
// Restore primary context
|
||||
if (!windows_.empty()) {
|
||||
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GUIFrontend::Shutdown()
|
||||
{
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
if (gl_ctx_) {
|
||||
SDL_GL_DeleteContext(gl_ctx_);
|
||||
gl_ctx_ = nullptr;
|
||||
}
|
||||
if (window_) {
|
||||
SDL_DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
// Destroy all windows (secondary first, then primary)
|
||||
for (auto it = windows_.rbegin(); it != windows_.rend(); ++it) {
|
||||
DestroyWindowResources_(**it);
|
||||
}
|
||||
windows_.clear();
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
@@ -367,7 +605,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
||||
ImFontConfig config;
|
||||
config.MergeMode = false;
|
||||
|
||||
// Load Basic Latin + Latin Supplement
|
||||
io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||
kte::Fonts::DefaultFontData,
|
||||
kte::Fonts::DefaultFontSize,
|
||||
@@ -375,7 +612,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
||||
&config,
|
||||
io.Fonts->GetGlyphRangesDefault());
|
||||
|
||||
// Merge Greek and Mathematical symbols from IosevkaExtended
|
||||
config.MergeMode = true;
|
||||
static const ImWchar extended_ranges[] = {
|
||||
0x0370, 0x03FF, // Greek and Coptic
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
||||
*/
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Frontend.h"
|
||||
#include "GUIConfig.h"
|
||||
#include "ImGuiInputHandler.h"
|
||||
#include "ImGuiRenderer.h"
|
||||
#include "Editor.h"
|
||||
|
||||
|
||||
struct SDL_Window;
|
||||
struct ImGuiContext;
|
||||
typedef void *SDL_GLContext;
|
||||
|
||||
class GUIFrontend final : public Frontend {
|
||||
@@ -24,13 +29,31 @@ public:
|
||||
void Shutdown() override;
|
||||
|
||||
private:
|
||||
// Per-window state — each window owns its own ImGui context so that
|
||||
// NewFrame/Render cycles are fully independent (ImGui requires exactly
|
||||
// one NewFrame per Render per context).
|
||||
struct WindowState {
|
||||
SDL_Window *window = nullptr;
|
||||
SDL_GLContext gl_ctx = nullptr;
|
||||
ImGuiContext *imgui_ctx = nullptr;
|
||||
ImGuiInputHandler input{};
|
||||
ImGuiRenderer renderer{};
|
||||
Editor editor{};
|
||||
int width = 1280;
|
||||
int height = 800;
|
||||
bool alive = true;
|
||||
};
|
||||
|
||||
// Open a new secondary window sharing the primary editor's buffer list.
|
||||
// Returns false if window creation fails.
|
||||
bool OpenNewWindow_(Editor &primary);
|
||||
|
||||
// Initialize fonts and theme for a given ImGui context (must be current).
|
||||
void SetupImGuiStyle_();
|
||||
static void DestroyWindowResources_(WindowState &ws);
|
||||
static bool LoadGuiFont_(const char *path, float size_px);
|
||||
|
||||
GUIConfig config_{};
|
||||
ImGuiInputHandler input_{};
|
||||
ImGuiRenderer renderer_{};
|
||||
SDL_Window *window_ = nullptr;
|
||||
SDL_GLContext gl_ctx_ = nullptr;
|
||||
int width_ = 1280;
|
||||
int height_ = 800;
|
||||
};
|
||||
// Primary window (index 0 in windows_); created during Init.
|
||||
std::vector<std::unique_ptr<WindowState> > windows_;
|
||||
};
|
||||
@@ -337,6 +337,18 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
||||
const SDL_Keycode key = e.key.keysym.sym;
|
||||
|
||||
// New window: Cmd+N (macOS) or Ctrl+Shift+N (Linux/Windows)
|
||||
{
|
||||
const bool gui_n = (mods & KMOD_GUI) && !(mods & KMOD_CTRL) && (key == SDLK_n);
|
||||
const bool ctrl_sn = (mods & KMOD_CTRL) && (mods & KMOD_SHIFT) && (key == SDLK_n);
|
||||
if (gui_n || ctrl_sn) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, CommandId::NewWindow, std::string(), 0});
|
||||
suppress_text_input_once_ = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
|
||||
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
|
||||
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
|
||||
@@ -446,7 +458,7 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
if (ed_ &&ed_
|
||||
|
||||
|
||||
|
||||
|
||||
->
|
||||
UArg() != 0
|
||||
)
|
||||
|
||||
@@ -76,19 +76,16 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
||||
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
||||
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
||||
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
|
||||
|
||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||
|
||||
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
||||
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
||||
}
|
||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
||||
if (prev_buf_coloffs_ >= 0 && buf_coloffs != prev_buf_coloffs_) {
|
||||
float target_x = static_cast<float>(buf_coloffs) * space_w;
|
||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
||||
@@ -116,25 +113,22 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
||||
bool forced_scroll = false;
|
||||
{
|
||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
||||
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
|
||||
|
||||
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
||||
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
||||
|
||||
// Check if rowoffs was programmatically changed this frame
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
||||
forced_scroll = true;
|
||||
}
|
||||
|
||||
// If user scrolled (not programmatic), update buffer offsets accordingly
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) {
|
||||
if (prev_scroll_y_ >= 0.0f && scroll_y != prev_scroll_y_ && !forced_scroll) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||
mbuf->Coloffs());
|
||||
}
|
||||
}
|
||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
|
||||
if (prev_scroll_x_ >= 0.0f && scroll_x != prev_scroll_x_ && !forced_scroll) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||
@@ -142,11 +136,11 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
// Update trackers for next frame
|
||||
prev_scroll_y = scroll_y;
|
||||
prev_scroll_x = scroll_x;
|
||||
prev_scroll_y_ = scroll_y;
|
||||
prev_scroll_x_ = scroll_x;
|
||||
}
|
||||
prev_buf_rowoffs = buf_rowoffs;
|
||||
prev_buf_coloffs = buf_coloffs;
|
||||
prev_buf_rowoffs_ = buf_rowoffs;
|
||||
prev_buf_coloffs_ = buf_coloffs;
|
||||
// Cache current horizontal offset in rendered columns for click handling
|
||||
const std::size_t coloffs_now = buf->Coloffs();
|
||||
|
||||
@@ -169,7 +163,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
||||
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
||||
|
||||
static bool mouse_selecting = false;
|
||||
// (mouse_selecting__ is a member variable)
|
||||
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
// Convert mouse pos to buffer row
|
||||
@@ -211,7 +205,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
|
||||
// Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
mouse_selecting = true;
|
||||
mouse_selecting_ = true;
|
||||
auto [by, bx] = mouse_pos_to_buf();
|
||||
char tmp[64];
|
||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||
@@ -225,7 +219,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
if (mouse_selecting_ && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
auto [by, bx] = mouse_pos_to_buf();
|
||||
// If we are dragging (mouse moved while down), ensure mark is set to start selection
|
||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
|
||||
@@ -242,8 +236,8 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||
}
|
||||
if (mouse_selecting && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
mouse_selecting = false;
|
||||
if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
mouse_selecting_ = false;
|
||||
}
|
||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||
// Capture the screen position before drawing the line
|
||||
|
||||
@@ -11,4 +11,13 @@ public:
|
||||
~ImGuiRenderer() override = default;
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
|
||||
private:
|
||||
// Per-window scroll tracking for two-way sync between Buffer offsets and ImGui scroll.
|
||||
// These must be per-instance (not static) so each window maintains independent state.
|
||||
long prev_buf_rowoffs_ = -1;
|
||||
long prev_buf_coloffs_ = -1;
|
||||
float prev_scroll_y_ = -1.0f;
|
||||
float prev_scroll_x_ = -1.0f;
|
||||
bool mouse_selecting_ = false;
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ stdenv.mkDerivation {
|
||||
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
||||
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
|
||||
"-DCMAKE_BUILD_TYPE=Debug"
|
||||
"-DKTE_STATIC_LINK=OFF"
|
||||
];
|
||||
|
||||
installPhase = ''
|
||||
|
||||
125
tests/test_swap_cleanup2.cc
Normal file
125
tests/test_swap_cleanup2.cc
Normal file
@@ -0,0 +1,125 @@
|
||||
#include "Test.h"
|
||||
|
||||
#include "Command.h"
|
||||
#include "Editor.h"
|
||||
|
||||
#include "tests/TestHarness.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
static void
|
||||
write_file_bytes(const std::string &path, const std::string &bytes)
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out.write(bytes.data(), (std::streamsize) bytes.size());
|
||||
}
|
||||
|
||||
|
||||
// RAII helper to set XDG_STATE_HOME for the duration of a test and clean up.
|
||||
struct XdgStateGuard {
|
||||
fs::path root;
|
||||
std::string old_xdg;
|
||||
bool had_old;
|
||||
|
||||
explicit XdgStateGuard(const std::string &suffix)
|
||||
{
|
||||
root = fs::temp_directory_path() /
|
||||
(std::string("kte_ut_xdg_") + suffix + "_" + std::to_string((int) ::getpid()));
|
||||
fs::remove_all(root);
|
||||
fs::create_directories(root);
|
||||
|
||||
const char *p = std::getenv("XDG_STATE_HOME");
|
||||
had_old = (p != nullptr);
|
||||
if (p)
|
||||
old_xdg = p;
|
||||
setenv("XDG_STATE_HOME", root.string().c_str(), 1);
|
||||
}
|
||||
|
||||
~XdgStateGuard()
|
||||
{
|
||||
if (had_old)
|
||||
setenv("XDG_STATE_HOME", old_xdg.c_str(), 1);
|
||||
else
|
||||
unsetenv("XDG_STATE_HOME");
|
||||
fs::remove_all(root);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TEST(SwapCleanup_SaveAndQuit)
|
||||
{
|
||||
ktet::InstallDefaultCommandsOnce();
|
||||
XdgStateGuard xdg("save_quit");
|
||||
|
||||
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||
fs::create_directories(xdg.root / "work");
|
||||
write_file_bytes(path, "hello\n");
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
ed.AddBuffer(Buffer());
|
||||
std::string err;
|
||||
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||
Buffer *b = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(b != nullptr);
|
||||
|
||||
// Edit to create swap file
|
||||
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "Z"));
|
||||
ASSERT_TRUE(b->Dirty());
|
||||
|
||||
ed.Swap()->Flush(b);
|
||||
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||
ASSERT_TRUE(fs::exists(swp));
|
||||
|
||||
// Save-and-quit should clean up the swap file
|
||||
ASSERT_TRUE(Execute(ed, CommandId::SaveAndQuit));
|
||||
ed.Swap()->Flush(b);
|
||||
ASSERT_TRUE(!fs::exists(swp));
|
||||
|
||||
// Cleanup
|
||||
std::remove(path.c_str());
|
||||
}
|
||||
|
||||
|
||||
TEST(SwapCleanup_EditorReset)
|
||||
{
|
||||
ktet::InstallDefaultCommandsOnce();
|
||||
XdgStateGuard xdg("editor_reset");
|
||||
|
||||
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||
fs::create_directories(xdg.root / "work");
|
||||
write_file_bytes(path, "hello\n");
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
ed.AddBuffer(Buffer());
|
||||
std::string err;
|
||||
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||
Buffer *b = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(b != nullptr);
|
||||
|
||||
// Edit to create swap file
|
||||
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "W"));
|
||||
ASSERT_TRUE(b->Dirty());
|
||||
|
||||
ed.Swap()->Flush(b);
|
||||
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||
ASSERT_TRUE(fs::exists(swp));
|
||||
|
||||
// Reset (simulates clean editor exit) should remove swap files
|
||||
ed.Reset();
|
||||
ASSERT_TRUE(!fs::exists(swp));
|
||||
|
||||
// Cleanup
|
||||
std::remove(path.c_str());
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
203
themes/Tufte.h
Normal file
203
themes/Tufte.h
Normal file
@@ -0,0 +1,203 @@
|
||||
// themes/Tufte.h — Edward Tufte inspired ImGui theme (header-only)
|
||||
// Warm cream paper, dark ink, minimal chrome, restrained accent colors.
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Light variant (primary — Tufte's books are fundamentally light)
|
||||
static inline void
|
||||
ApplyTufteLightTheme()
|
||||
{
|
||||
// Tufte palette: warm cream paper with near-black ink
|
||||
const ImVec4 paper = RGBA(0xFFFFF8); // Tufte's signature warm white
|
||||
const ImVec4 bg1 = RGBA(0xF4F0E8); // slightly darker cream
|
||||
const ImVec4 bg2 = RGBA(0xEAE6DE); // UI elements
|
||||
const ImVec4 bg3 = RGBA(0xDDD9D1); // hover/active
|
||||
const ImVec4 ink = RGBA(0x111111); // near-black text
|
||||
const ImVec4 dim = RGBA(0x6B6B6B); // disabled/secondary text
|
||||
const ImVec4 border = RGBA(0xD0CCC4); // subtle borders
|
||||
|
||||
// Tufte uses color sparingly: muted red for emphasis, navy for links
|
||||
const ImVec4 red = RGBA(0xA00000); // restrained dark red
|
||||
const ImVec4 blue = RGBA(0x1F3F6F); // dark navy
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 12.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f; // sharp edges — typographic, not app-like
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 0.0f; // minimal frame borders
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = dim;
|
||||
colors[ImGuiCol_WindowBg] = paper;
|
||||
colors[ImGuiCol_ChildBg] = paper;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = border;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = ink;
|
||||
colors[ImGuiCol_SliderGrab] = ink;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg2;
|
||||
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg2;
|
||||
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||
colors[ImGuiCol_HeaderActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||
colors[ImGuiCol_SeparatorActive] = red;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = red;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = border;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.15f);
|
||||
colors[ImGuiCol_DragDropTarget] = red;
|
||||
colors[ImGuiCol_NavHighlight] = red;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||
colors[ImGuiCol_PlotLines] = blue;
|
||||
colors[ImGuiCol_PlotLinesHovered] = red;
|
||||
colors[ImGuiCol_PlotHistogram] = blue;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = red;
|
||||
}
|
||||
|
||||
|
||||
// Dark variant — warm charcoal with cream ink, same restrained accents
|
||||
static inline void
|
||||
ApplyTufteDarkTheme()
|
||||
{
|
||||
const ImVec4 bg0 = RGBA(0x1C1B19); // warm near-black
|
||||
const ImVec4 bg1 = RGBA(0x252420); // slightly lighter
|
||||
const ImVec4 bg2 = RGBA(0x302F2A); // UI elements
|
||||
const ImVec4 bg3 = RGBA(0x3D3C36); // hover/active
|
||||
const ImVec4 ink = RGBA(0xEAE6DE); // cream text (inverted paper)
|
||||
const ImVec4 dim = RGBA(0x9A9690); // disabled text
|
||||
const ImVec4 border = RGBA(0x4A4840); // subtle borders
|
||||
|
||||
const ImVec4 red = RGBA(0xD06060); // warmer red for dark bg
|
||||
const ImVec4 blue = RGBA(0x7098C0); // lighter navy for dark bg
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 12.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 0.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = dim;
|
||||
colors[ImGuiCol_WindowBg] = bg0;
|
||||
colors[ImGuiCol_ChildBg] = bg0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = border;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = dim;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = ink;
|
||||
colors[ImGuiCol_SliderGrab] = ink;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg2;
|
||||
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg2;
|
||||
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||
colors[ImGuiCol_HeaderActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||
colors[ImGuiCol_SeparatorActive] = red;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = red;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = border;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.20f);
|
||||
colors[ImGuiCol_DragDropTarget] = red;
|
||||
colors[ImGuiCol_NavHighlight] = red;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||
colors[ImGuiCol_PlotLines] = blue;
|
||||
colors[ImGuiCol_PlotLinesHovered] = red;
|
||||
colors[ImGuiCol_PlotHistogram] = blue;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = red;
|
||||
}
|
||||
Reference in New Issue
Block a user