Fix multi-window architecture and swap file cleanup
Multi-window: - Per-window ImGui contexts (fixes input, scroll, and rendering isolation) - Per-instance scroll and mouse state in ImGuiRenderer (no more statics) - Proper GL context activation during window destruction - ValidateBufferIndex guards against stale curbuf_ across shared buffers - Editor methods (CurrentBuffer, SwitchTo, CloseBuffer, etc.) use Buffers() accessor to respect shared buffer lists - New windows open with an untitled buffer - Scratch buffer reuse works in secondary windows - CMD-w on macOS closes only the focused window - Deferred new-window creation to avoid mid-frame ImGui context corruption Swap file cleanup: - SaveAs prompt handler now calls ResetJournal - cmd_save_and_quit now calls ResetJournal - Editor::Reset detaches all buffers before clearing - Tests for save-and-quit and editor-reset swap cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ project(kte)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(KTE_VERSION "1.8.3")
|
set(KTE_VERSION "1.9.0")
|
||||||
|
|
||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
@@ -327,6 +327,7 @@ if (BUILD_TESTS)
|
|||||||
tests/test_swap_edge_cases.cc
|
tests/test_swap_edge_cases.cc
|
||||||
tests/test_swap_recovery_prompt.cc
|
tests/test_swap_recovery_prompt.cc
|
||||||
tests/test_swap_cleanup.cc
|
tests/test_swap_cleanup.cc
|
||||||
|
tests/test_swap_cleanup2.cc
|
||||||
tests/test_swap_git_editor.cc
|
tests/test_swap_git_editor.cc
|
||||||
tests/test_piece_table.cc
|
tests/test_piece_table.cc
|
||||||
tests/test_search.cc
|
tests/test_search.cc
|
||||||
|
|||||||
@@ -752,6 +752,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
|||||||
if (buf->IsFileBacked()) {
|
if (buf->IsFileBacked()) {
|
||||||
if (buf->Save(err)) {
|
if (buf->Save(err)) {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap())
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -759,6 +761,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
|||||||
} else if (!buf->Filename().empty()) {
|
} else if (!buf->Filename().empty()) {
|
||||||
if (buf->SaveAs(buf->Filename(), err)) {
|
if (buf->SaveAs(buf->Filename(), err)) {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap())
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -2568,6 +2572,10 @@ cmd_newline(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
} else {
|
} else {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap()) {
|
||||||
|
sm->NotifyFilenameChanged(*buf);
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
|
}
|
||||||
ctx.editor.SetStatus("Saved as " + value);
|
ctx.editor.SetStatus("Saved as " + value);
|
||||||
if (auto *u = buf->Undo())
|
if (auto *u = buf->Undo())
|
||||||
u->mark_saved();
|
u->mark_saved();
|
||||||
|
|||||||
75
Editor.cc
75
Editor.cc
@@ -69,20 +69,22 @@ Editor::SetStatus(const std::string &message)
|
|||||||
Buffer *
|
Buffer *
|
||||||
Editor::CurrentBuffer()
|
Editor::CurrentBuffer()
|
||||||
{
|
{
|
||||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &buffers_[curbuf_];
|
return &bufs[curbuf_];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const Buffer *
|
const Buffer *
|
||||||
Editor::CurrentBuffer() const
|
Editor::CurrentBuffer() const
|
||||||
{
|
{
|
||||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
const auto &bufs = Buffers();
|
||||||
|
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &buffers_[curbuf_];
|
return &bufs[curbuf_];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -117,8 +119,9 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
|||||||
|
|
||||||
// Prepare list of other buffer paths
|
// Prepare list of other buffer paths
|
||||||
std::vector<std::vector<std::filesystem::path> > others;
|
std::vector<std::vector<std::filesystem::path> > others;
|
||||||
others.reserve(buffers_.size());
|
const auto &bufs = Buffers();
|
||||||
for (const auto &b: buffers_) {
|
others.reserve(bufs.size());
|
||||||
|
for (const auto &b: bufs) {
|
||||||
if (&b == &buf)
|
if (&b == &buf)
|
||||||
continue;
|
continue;
|
||||||
if (b.Filename().empty())
|
if (b.Filename().empty())
|
||||||
@@ -161,41 +164,44 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
|||||||
std::size_t
|
std::size_t
|
||||||
Editor::AddBuffer(const Buffer &buf)
|
Editor::AddBuffer(const Buffer &buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(buf);
|
auto &bufs = Buffers();
|
||||||
|
bufs.push_back(buf);
|
||||||
// Attach swap recorder
|
// Attach swap recorder
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->Attach(&buffers_.back());
|
swap_->Attach(&bufs.back());
|
||||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
}
|
}
|
||||||
if (buffers_.size() == 1) {
|
if (bufs.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
return buffers_.size() - 1;
|
return bufs.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t
|
||||||
Editor::AddBuffer(Buffer &&buf)
|
Editor::AddBuffer(Buffer &&buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(std::move(buf));
|
auto &bufs = Buffers();
|
||||||
|
bufs.push_back(std::move(buf));
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->Attach(&buffers_.back());
|
swap_->Attach(&bufs.back());
|
||||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
}
|
}
|
||||||
if (buffers_.size() == 1) {
|
if (bufs.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
return buffers_.size() - 1;
|
return bufs.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
Editor::OpenFile(const std::string &path, std::string &err)
|
Editor::OpenFile(const std::string &path, std::string &err)
|
||||||
{
|
{
|
||||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
// If the current buffer is an unnamed, empty, clean scratch buffer, reuse
|
||||||
// of creating a new one.
|
// it instead of creating a new one.
|
||||||
if (buffers_.size() == 1) {
|
auto &bufs_ref = Buffers();
|
||||||
Buffer &cur = buffers_[curbuf_];
|
if (!bufs_ref.empty() && curbuf_ < bufs_ref.size()) {
|
||||||
|
Buffer &cur = bufs_ref[curbuf_];
|
||||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||||
const bool clean = !cur.Dirty();
|
const bool clean = !cur.Dirty();
|
||||||
const std::size_t nrows = cur.Nrows();
|
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
|
// Add as a new buffer and switch to it
|
||||||
std::size_t idx = AddBuffer(std::move(b));
|
std::size_t idx = AddBuffer(std::move(b));
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->NotifyFilenameChanged(buffers_[idx]);
|
swap_->NotifyFilenameChanged(Buffers()[idx]);
|
||||||
}
|
}
|
||||||
SwitchTo(idx);
|
SwitchTo(idx);
|
||||||
// Defensive: ensure any active prompt is closed after a successful open
|
// Defensive: ensure any active prompt is closed after a successful open
|
||||||
@@ -446,12 +452,13 @@ Editor::ProcessPendingOpens()
|
|||||||
bool
|
bool
|
||||||
Editor::SwitchTo(std::size_t index)
|
Editor::SwitchTo(std::size_t index)
|
||||||
{
|
{
|
||||||
if (index >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (index >= bufs.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
curbuf_ = index;
|
curbuf_ = index;
|
||||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||||
Buffer &b = buffers_[curbuf_];
|
Buffer &b = bufs[curbuf_];
|
||||||
if (b.SyntaxEnabled()) {
|
if (b.SyntaxEnabled()) {
|
||||||
b.EnsureHighlighter();
|
b.EnsureHighlighter();
|
||||||
if (auto *eng = b.Highlighter()) {
|
if (auto *eng = b.Highlighter()) {
|
||||||
@@ -478,21 +485,22 @@ Editor::SwitchTo(std::size_t index)
|
|||||||
bool
|
bool
|
||||||
Editor::CloseBuffer(std::size_t index)
|
Editor::CloseBuffer(std::size_t index)
|
||||||
{
|
{
|
||||||
if (index >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (index >= bufs.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
// Always remove swap file when closing a buffer on normal exit.
|
// 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.
|
// 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).
|
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
||||||
swap_->Detach(&buffers_[index], true);
|
swap_->Detach(&bufs[index], true);
|
||||||
buffers_[index].SetSwapRecorder(nullptr);
|
bufs[index].SetSwapRecorder(nullptr);
|
||||||
}
|
}
|
||||||
buffers_.erase(buffers_.begin() + static_cast<std::ptrdiff_t>(index));
|
bufs.erase(bufs.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
if (buffers_.empty()) {
|
if (bufs.empty()) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
} else if (curbuf_ >= buffers_.size()) {
|
} else if (curbuf_ >= bufs.size()) {
|
||||||
curbuf_ = buffers_.size() - 1;
|
curbuf_ = bufs.size() - 1;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -516,7 +524,12 @@ Editor::Reset()
|
|||||||
// Reset close-confirm/save state
|
// Reset close-confirm/save state
|
||||||
close_confirm_pending_ = false;
|
close_confirm_pending_ = false;
|
||||||
close_after_save_ = false;
|
close_after_save_ = false;
|
||||||
buffers_.clear();
|
auto &bufs = Buffers();
|
||||||
|
if (swap_) {
|
||||||
|
for (auto &buf : bufs)
|
||||||
|
swap_->Detach(&buf, true);
|
||||||
|
}
|
||||||
|
bufs.clear();
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
Editor.h
15
Editor.h
@@ -521,7 +521,7 @@ public:
|
|||||||
// Buffers
|
// Buffers
|
||||||
[[nodiscard]] std::size_t BufferCount() const
|
[[nodiscard]] std::size_t BufferCount() const
|
||||||
{
|
{
|
||||||
return buffers_.size();
|
return Buffers().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -531,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();
|
Buffer *CurrentBuffer();
|
||||||
|
|
||||||
const Buffer *CurrentBuffer() const;
|
const Buffer *CurrentBuffer() const;
|
||||||
|
|||||||
230
ImGuiFrontend.cc
230
ImGuiFrontend.cc
@@ -30,7 +30,7 @@
|
|||||||
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers shared between Init and OpenNewWindow_
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -96,6 +96,63 @@ update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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
|
bool
|
||||||
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||||
{
|
{
|
||||||
@@ -172,8 +229,9 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1); // vsync
|
SDL_GL_SetSwapInterval(1); // vsync
|
||||||
|
|
||||||
|
// Create primary ImGui context
|
||||||
IMGUI_CHECKVERSION();
|
IMGUI_CHECKVERSION();
|
||||||
ImGui::CreateContext();
|
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
|
|
||||||
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
||||||
@@ -239,6 +297,7 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
auto ws = std::make_unique<WindowState>();
|
auto ws = std::make_unique<WindowState>();
|
||||||
ws->window = win;
|
ws->window = win;
|
||||||
ws->gl_ctx = gl_ctx;
|
ws->gl_ctx = gl_ctx;
|
||||||
|
ws->imgui_ctx = imgui_ctx;
|
||||||
ws->width = init_w;
|
ws->width = init_w;
|
||||||
ws->height = init_h;
|
ws->height = init_h;
|
||||||
// The primary window's editor IS the editor passed in from main; we don't
|
// The primary window's editor IS the editor passed in from main; we don't
|
||||||
@@ -255,8 +314,6 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
bool
|
bool
|
||||||
GUIFrontend::OpenNewWindow_(Editor &primary)
|
GUIFrontend::OpenNewWindow_(Editor &primary)
|
||||||
{
|
{
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
|
||||||
|
|
||||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
int w = windows_[0]->width;
|
int w = windows_[0]->width;
|
||||||
int h = windows_[0]->height;
|
int h = windows_[0]->height;
|
||||||
@@ -277,25 +334,48 @@ GUIFrontend::OpenNewWindow_(Editor &primary)
|
|||||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1);
|
SDL_GL_SetSwapInterval(1);
|
||||||
|
|
||||||
// Secondary windows share the ImGui context already created in Init.
|
// Each window gets its own ImGui context — ImGui requires exactly one
|
||||||
// We need to init the SDL2/OpenGL backends for this new window.
|
// NewFrame/Render cycle per context per frame.
|
||||||
// ImGui_ImplSDL2 supports multiple windows via SDL_GetWindowID checks.
|
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||||
ImGui_ImplOpenGL3_Init(kGlslVersion);
|
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>();
|
auto ws = std::make_unique<WindowState>();
|
||||||
ws->window = win;
|
ws->window = win;
|
||||||
ws->gl_ctx = gl_ctx;
|
ws->gl_ctx = gl_ctx;
|
||||||
|
ws->imgui_ctx = imgui_ctx;
|
||||||
ws->width = w;
|
ws->width = w;
|
||||||
ws->height = h;
|
ws->height = h;
|
||||||
|
|
||||||
// Secondary editor shares the primary's buffer list
|
// Secondary editor shares the primary's buffer list
|
||||||
ws->editor.SetSharedBuffers(&primary.Buffers());
|
ws->editor.SetSharedBuffers(&primary.Buffers());
|
||||||
ws->editor.SetDimensions(primary.Rows(), primary.Cols());
|
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);
|
ws->input.Attach(&ws->editor);
|
||||||
|
|
||||||
windows_.push_back(std::move(ws));
|
windows_.push_back(std::move(ws));
|
||||||
|
|
||||||
// Restore primary GL context as current
|
// Restore primary context
|
||||||
|
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -305,10 +385,10 @@ void
|
|||||||
GUIFrontend::Step(Editor &ed, bool &running)
|
GUIFrontend::Step(Editor &ed, bool &running)
|
||||||
{
|
{
|
||||||
// --- Event processing ---
|
// --- 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;
|
SDL_Event e;
|
||||||
while (SDL_PollEvent(&e)) {
|
while (SDL_PollEvent(&e)) {
|
||||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
|
||||||
|
|
||||||
// Determine which window this event belongs to
|
// Determine which window this event belongs to
|
||||||
Uint32 event_win_id = 0;
|
Uint32 event_win_id = 0;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
@@ -329,6 +409,9 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
case SDL_MOUSEWHEEL:
|
case SDL_MOUSEWHEEL:
|
||||||
event_win_id = e.wheel.windowID;
|
event_win_id = e.wheel.windowID;
|
||||||
break;
|
break;
|
||||||
|
case SDL_MOUSEMOTION:
|
||||||
|
event_win_id = e.motion.windowID;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -338,62 +421,70 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
break;
|
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.type == SDL_WINDOWEVENT) {
|
||||||
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||||
// Mark the window as dead; primary window close = quit
|
if (target) {
|
||||||
for (std::size_t i = 0; i < windows_.size(); ++i) {
|
if (target_idx == 0) {
|
||||||
if (SDL_GetWindowID(windows_[i]->window) == e.window.windowID) {
|
|
||||||
if (i == 0) {
|
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
windows_[i]->alive = false;
|
target->alive = false;
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||||
for (auto &ws: windows_) {
|
if (target) {
|
||||||
if (SDL_GetWindowID(ws->window) == e.window.windowID) {
|
target->width = e.window.data1;
|
||||||
ws->width = e.window.data1;
|
target->height = e.window.data2;
|
||||||
ws->height = e.window.data2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route input events to the correct window's input handler
|
// Route input events to the correct window's input handler
|
||||||
if (event_win_id != 0) {
|
if (target) {
|
||||||
// Primary window (index 0) uses the external editor &ed
|
target->input.ProcessSDLEvent(e);
|
||||||
if (windows_.size() > 0 &&
|
|
||||||
SDL_GetWindowID(windows_[0]->window) == event_win_id) {
|
|
||||||
windows_[0]->input.ProcessSDLEvent(e);
|
|
||||||
} else {
|
|
||||||
for (std::size_t i = 1; i < windows_.size(); ++i) {
|
|
||||||
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
|
|
||||||
windows_[i]->input.ProcessSDLEvent(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!running)
|
if (!running)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// --- Apply pending font change ---
|
// --- Apply pending font change (to all contexts) ---
|
||||||
{
|
{
|
||||||
std::string fname;
|
std::string fname;
|
||||||
float fsize = 0.0f;
|
float fsize = 0.0f;
|
||||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
||||||
if (!fname.empty() && fsize > 0.0f) {
|
if (!fname.empty() && fsize > 0.0f) {
|
||||||
|
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);
|
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Step each window ---
|
// --- Step each window ---
|
||||||
// We iterate by index because OpenNewWindow_ may append to windows_.
|
// We iterate by index because OpenNewWindow_ may append to windows_.
|
||||||
@@ -404,7 +495,12 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
|
|
||||||
Editor &wed = (wi == 0) ? ed : ws.editor;
|
Editor &wed = (wi == 0) ? ed : ws.editor;
|
||||||
|
|
||||||
|
// Shared buffer list may have been modified by another window.
|
||||||
|
wed.ValidateBufferIndex();
|
||||||
|
|
||||||
|
// Activate this window's GL and ImGui contexts
|
||||||
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||||
|
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||||
|
|
||||||
// Start a new ImGui frame
|
// Start a new ImGui frame
|
||||||
ImGui_ImplOpenGL3_NewFrame();
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
@@ -442,12 +538,6 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new-window request
|
|
||||||
if (wed.NewWindowRequested()) {
|
|
||||||
wed.SetNewWindowRequested(false);
|
|
||||||
OpenNewWindow_(ed); // always share primary editor's buffers
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wi == 0 && wed.QuitRequested()) {
|
if (wi == 0 && wed.QuitRequested()) {
|
||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
@@ -466,52 +556,40 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
SDL_GL_SwapWindow(ws.window);
|
SDL_GL_SwapWindow(ws.window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove dead secondary windows
|
// Remove dead secondary windows
|
||||||
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
||||||
if (!(*it)->alive) {
|
if (!(*it)->alive) {
|
||||||
SDL_GL_MakeCurrent((*it)->window, (*it)->gl_ctx);
|
DestroyWindowResources_(**it);
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
|
||||||
SDL_GL_DeleteContext((*it)->gl_ctx);
|
|
||||||
SDL_DestroyWindow((*it)->window);
|
|
||||||
it = windows_.erase(it);
|
it = windows_.erase(it);
|
||||||
// Restore primary context
|
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore primary context
|
||||||
|
if (!windows_.empty()) {
|
||||||
|
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||||
|
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
GUIFrontend::Shutdown()
|
GUIFrontend::Shutdown()
|
||||||
{
|
{
|
||||||
// Destroy secondary windows first
|
// Destroy all windows (secondary first, then primary)
|
||||||
for (std::size_t i = 1; i < windows_.size(); ++i) {
|
for (auto it = windows_.rbegin(); it != windows_.rend(); ++it) {
|
||||||
SDL_GL_MakeCurrent(windows_[i]->window, windows_[i]->gl_ctx);
|
DestroyWindowResources_(**it);
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
|
||||||
SDL_GL_DeleteContext(windows_[i]->gl_ctx);
|
|
||||||
SDL_DestroyWindow(windows_[i]->window);
|
|
||||||
}
|
|
||||||
windows_.resize(std::min(windows_.size(), std::size_t(1)));
|
|
||||||
|
|
||||||
// Destroy primary window
|
|
||||||
if (!windows_.empty()) {
|
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
|
||||||
}
|
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
|
||||||
ImGui_ImplSDL2_Shutdown();
|
|
||||||
ImGui::DestroyContext();
|
|
||||||
|
|
||||||
if (!windows_.empty()) {
|
|
||||||
if (windows_[0]->gl_ctx) {
|
|
||||||
SDL_GL_DeleteContext(windows_[0]->gl_ctx);
|
|
||||||
windows_[0]->gl_ctx = nullptr;
|
|
||||||
}
|
|
||||||
if (windows_[0]->window) {
|
|
||||||
SDL_DestroyWindow(windows_[0]->window);
|
|
||||||
windows_[0]->window = nullptr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
windows_.clear();
|
windows_.clear();
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
|
|
||||||
struct SDL_Window;
|
struct SDL_Window;
|
||||||
|
struct ImGuiContext;
|
||||||
typedef void *SDL_GLContext;
|
typedef void *SDL_GLContext;
|
||||||
|
|
||||||
class GUIFrontend final : public Frontend {
|
class GUIFrontend final : public Frontend {
|
||||||
@@ -28,10 +29,13 @@ public:
|
|||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Per-window state
|
// 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 {
|
struct WindowState {
|
||||||
SDL_Window *window = nullptr;
|
SDL_Window *window = nullptr;
|
||||||
SDL_GLContext gl_ctx = nullptr;
|
SDL_GLContext gl_ctx = nullptr;
|
||||||
|
ImGuiContext *imgui_ctx = nullptr;
|
||||||
ImGuiInputHandler input{};
|
ImGuiInputHandler input{};
|
||||||
ImGuiRenderer renderer{};
|
ImGuiRenderer renderer{};
|
||||||
Editor editor{};
|
Editor editor{};
|
||||||
@@ -44,6 +48,9 @@ private:
|
|||||||
// Returns false if window creation fails.
|
// Returns false if window creation fails.
|
||||||
bool OpenNewWindow_(Editor &primary);
|
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);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIConfig config_{};
|
GUIConfig config_{};
|
||||||
|
|||||||
@@ -76,19 +76,16 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
||||||
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
// - 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.
|
// - 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_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||||
|
|
||||||
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
||||||
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
// 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;
|
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||||
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
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_x = static_cast<float>(buf_coloffs) * space_w;
|
||||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||||
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
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
|
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
||||||
bool forced_scroll = false;
|
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_top = static_cast<long>(scroll_y / row_h);
|
||||||
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
||||||
|
|
||||||
// Check if rowoffs was programmatically changed this frame
|
// 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;
|
forced_scroll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user scrolled (not programmatic), update buffer offsets accordingly
|
// 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)) {
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||||
mbuf->Coloffs());
|
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)) {
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||||
@@ -142,11 +136,11 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update trackers for next frame
|
// Update trackers for next frame
|
||||||
prev_scroll_y = scroll_y;
|
prev_scroll_y_ = scroll_y;
|
||||||
prev_scroll_x = scroll_x;
|
prev_scroll_x_ = scroll_x;
|
||||||
}
|
}
|
||||||
prev_buf_rowoffs = buf_rowoffs;
|
prev_buf_rowoffs_ = buf_rowoffs;
|
||||||
prev_buf_coloffs = buf_coloffs;
|
prev_buf_coloffs_ = buf_coloffs;
|
||||||
// Cache current horizontal offset in rendered columns for click handling
|
// Cache current horizontal offset in rendered columns for click handling
|
||||||
const std::size_t coloffs_now = buf->Coloffs();
|
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_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
||||||
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 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> {
|
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
||||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||||
// Convert mouse pos to buffer row
|
// 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
|
// Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
mouse_selecting = true;
|
mouse_selecting_ = true;
|
||||||
auto [by, bx] = mouse_pos_to_buf();
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
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();
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
// If we are dragging (mouse moved while down), ensure mark is set to start selection
|
// If we are dragging (mouse moved while down), ensure mark is set to start selection
|
||||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
|
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
|
||||||
@@ -242,8 +236,8 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
}
|
}
|
||||||
if (mouse_selecting && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||||
mouse_selecting = false;
|
mouse_selecting_ = false;
|
||||||
}
|
}
|
||||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||||
// Capture the screen position before drawing the line
|
// Capture the screen position before drawing the line
|
||||||
|
|||||||
@@ -11,4 +11,13 @@ public:
|
|||||||
~ImGuiRenderer() override = default;
|
~ImGuiRenderer() override = default;
|
||||||
|
|
||||||
void Draw(Editor &ed) override;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user