From d768e567274533ad451818e7dc017229c36a2d2c Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sun, 15 Mar 2026 13:19:04 -0700 Subject: [PATCH] Add multi-window support to GUI with shared buffer list and improved input handling - Introduced support for multiple windows, sharing the primary editor's buffer list. - Added `GUIFrontend::OpenNewWindow_` for creating secondary windows with independent dimensions and input handlers. - Redesigned `WindowState` to encapsulate per-window attributes (dimensions, renderer, input, etc.). - Updated input processing and command execution to route events based on active window, preserving window-level states. - Enhanced SDL2 and ImGui integration for proper context management across multiple windows. - Increased robustness by handling window closing, resizing, and cleanup of secondary windows without affecting the primary editor. - Updated documentation and key bindings for multi-window operations (e.g., Cmd+N / Ctrl+Shift+N). - Version updated to 1.8.0 to reflect the major GUI enhancement. --- CMakeLists.txt | 2 +- Command.cc | 19 +- Command.h | 2 + Editor.h | 29 ++- HelpText.cc | 15 +- ImGuiFrontend.cc | 512 ++++++++++++++++++++++++++++--------------- ImGuiFrontend.h | 30 ++- ImGuiInputHandler.cc | 14 +- 8 files changed, 429 insertions(+), 194 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e37c128..eb62721 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(kte) include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 20) -set(KTE_VERSION "1.7.1") +set(KTE_VERSION "1.8.0") # Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. diff --git a/Command.cc b/Command.cc index e816d57..21d7291 100644 --- a/Command.cc +++ b/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) { @@ -2254,10 +2262,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); @@ -5002,6 +5008,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 + }); } diff --git a/Command.h b/Command.h index 1a8d1e5..3d000a5 100644 --- a/Command.h +++ b/Command.h @@ -111,6 +111,8 @@ enum class CommandId { SetOption, // generic ":set key=value" (v1: filetype=) // 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, }; diff --git a/Editor.h b/Editor.h index 1ccae76..109ce8d 100644 --- a/Editor.h +++ b/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; @@ -570,13 +582,22 @@ public: // Direct access when needed (try to prefer methods above) [[nodiscard]] const std::vector &Buffers() const { - return buffers_; + return shared_buffers_ ? *shared_buffers_ : buffers_; } std::vector &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 *shared) + { + shared_buffers_ = shared; + curbuf_ = 0; } @@ -628,7 +649,8 @@ private: bool repeatable_ = false; // whether the next command is repeatable std::vector buffers_; - std::size_t curbuf_ = 0; // index into buffers_ + std::vector *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 swap_; @@ -639,6 +661,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 diff --git a/HelpText.cc b/HelpText.cc index 46a2d74..7fa4877 100644 --- a/HelpText.cc +++ b/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 Repeat 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" ); } \ No newline at end of file diff --git a/ImGuiFrontend.cc b/ImGuiFrontend.cc index b761485..0cd3f54 100644 --- a/ImGuiFrontend.cc +++ b/ImGuiFrontend.cc @@ -29,21 +29,86 @@ static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible) +// --------------------------------------------------------------------------- +// Helpers shared between Init and OpenNewWindow_ +// --------------------------------------------------------------------------- + +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(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()); + 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::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::max(1.0f, std::floor(avail_w / ch_w))); + + if (rows != ed.Rows() || cols != ed.Cols()) { + ed.SetDimensions(rows, cols); + } +} + + 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,61 +121,55 @@ 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(cfg.font_size); - int h = cfg.rows * static_cast(cfg.font_size * 1.2); - - // As a safety, clamp to display usable bounds if retrievable + int w = config_.columns * static_cast(config_.font_size); + int h = config_.rows * static_cast(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 IMGUI_CHECKVERSION(); @@ -121,94 +180,54 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed) 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(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()); - 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 +235,68 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed) } } + // Build primary WindowState + auto ws = std::make_unique(); + ws->window = win; + ws->gl_ctx = gl_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) +{ + SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); + + 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); + + // Secondary windows share the ImGui context already created in Init. + // We need to init the SDL2/OpenGL backends for this new window. + // ImGui_ImplSDL2 supports multiple windows via SDL_GetWindowID checks. + ImGui_ImplOpenGL3_Init(kGlslVersion); + + auto ws = std::make_unique(); + ws->window = win; + ws->gl_ctx = gl_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()); + ws->input.Attach(&ws->editor); + + windows_.push_back(std::move(ws)); + + // Restore primary GL context as current + SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); return true; } @@ -223,137 +304,216 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed) void GUIFrontend::Step(Editor &ed, bool &running) { + // --- Event processing --- 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; default: break; } - // Map input to commands - input_.ProcessSDLEvent(e); + + if (e.type == SDL_QUIT) { + running = false; + break; + } + + if (e.type == SDL_WINDOWEVENT) { + if (e.window.event == SDL_WINDOWEVENT_CLOSE) { + // Mark the window as dead; primary window close = quit + for (std::size_t i = 0; i < windows_.size(); ++i) { + if (SDL_GetWindowID(windows_[i]->window) == e.window.windowID) { + if (i == 0) { + running = false; + } else { + windows_[i]->alive = false; + } + break; + } + } + } else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + for (auto &ws: windows_) { + if (SDL_GetWindowID(ws->window) == e.window.windowID) { + ws->width = e.window.data1; + ws->height = e.window.data2; + break; + } + } + } + } + + // Route input events to the correct window's input handler + if (event_win_id != 0) { + // Primary window (index 0) uses the external editor &ed + 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; + } + } + } + } } - // Apply pending font change before starting a new frame + if (!running) + return; + + // --- Apply pending font change --- { 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(); } } } - // 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(width_); - float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast(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; + SDL_GL_MakeCurrent(ws.window, ws.gl_ctx); - // 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; + // Start a new ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL2_NewFrame(ws.window); + ImGui::NewFrame(); - // Visible content rows inside the scroll child - auto content_rows = static_cast(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; - - float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x); - std::size_t cols = static_cast(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(ws.width); + float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast(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()); + } + } } } + + // Handle new-window request + if (wed.NewWindowRequested()) { + wed.SetNewWindowRequested(false); + OpenNewWindow_(ed); // always share primary editor's buffers + } + + 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; + // Remove dead secondary windows + for (auto it = windows_.begin() + 1; it != windows_.end();) { + if (!(*it)->alive) { + SDL_GL_MakeCurrent((*it)->window, (*it)->gl_ctx); + ImGui_ImplOpenGL3_Shutdown(); + SDL_GL_DeleteContext((*it)->gl_ctx); + SDL_DestroyWindow((*it)->window); + it = windows_.erase(it); + // Restore primary context + SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); + } else { + ++it; + } } - - // No runtime font UI; always use embedded font. - - // 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_); } void GUIFrontend::Shutdown() { + // Destroy secondary windows first + for (std::size_t i = 1; i < windows_.size(); ++i) { + SDL_GL_MakeCurrent(windows_[i]->window, windows_[i]->gl_ctx); + 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 (gl_ctx_) { - SDL_GL_DeleteContext(gl_ctx_); - gl_ctx_ = nullptr; - } - if (window_) { - SDL_DestroyWindow(window_); - window_ = nullptr; + 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(); SDL_Quit(); } @@ -367,7 +527,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 +534,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 @@ -391,4 +549,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px) io.Fonts->Build(); return true; -} +} \ No newline at end of file diff --git a/ImGuiFrontend.h b/ImGuiFrontend.h index b4f3e12..e0a8a57 100644 --- a/ImGuiFrontend.h +++ b/ImGuiFrontend.h @@ -2,10 +2,14 @@ * GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle */ #pragma once +#include +#include + #include "Frontend.h" #include "GUIConfig.h" #include "ImGuiInputHandler.h" #include "ImGuiRenderer.h" +#include "Editor.h" struct SDL_Window; @@ -24,13 +28,25 @@ public: void Shutdown() override; private: + // Per-window state + struct WindowState { + SDL_Window *window = nullptr; + SDL_GLContext gl_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); + 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 > windows_; +}; \ No newline at end of file diff --git a/ImGuiInputHandler.cc b/ImGuiInputHandler.cc index 956c90e..2eabf31 100644 --- a/ImGuiInputHandler.cc +++ b/ImGuiInputHandler.cc @@ -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 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 )