#include #include #include #include #include #include #include #include #include #include #include #include "ImGuiFrontend.h" #include "Command.h" #include "Editor.h" #include "GUIConfig.h" #include "GUITheme.h" #include "fonts/Font.h" // embedded default font (DefaultFont) #include "fonts/FontRegistry.h" #include "fonts/IosevkaExtended.h" #include "syntax/HighlighterRegistry.h" #include "syntax/NullHighlighter.h" #ifndef KTE_FONT_SIZE #define KTE_FONT_SIZE 16.0f #endif 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; // 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; } // 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); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); // Compute desired window size from config Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; int init_w = 1280, init_h = 800; if (config_.fullscreen) { SDL_Rect usable{}; if (SDL_GetDisplayUsableBounds(0, &usable) == 0) { init_w = usable.w; init_h = usable.h; } #if !defined(__APPLE__) win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; #endif } else { 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); } init_w = std::max(320, w); init_h = std::max(200, h); } SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); SDL_Window *win = SDL_CreateWindow( "kge - kyle's graphical editor " KTE_VERSION_STR, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, init_w, init_h, win_flags); if (!win) { return false; } SDL_EnableScreenSaver(); #if defined(__APPLE__) if (config_.fullscreen) { SDL_Rect usable{}; if (SDL_GetDisplayUsableBounds(0, &usable) == 0) { SDL_SetWindowPosition(win, usable.x, usable.y); } } #endif 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); // vsync IMGUI_CHECKVERSION(); 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; 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); apply_syntax_to_buffer(ed.CurrentBuffer(), config_); if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx)) return false; if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) return false; // Cache initial window size int w, h; SDL_GetWindowSize(win, &w, &h); init_w = w; init_h = h; #if defined(__APPLE__) if (w > 1 && h > 1) { 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 kte::Fonts::InstallDefaultFonts(); 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)) { kte::Fonts::FontRegistry::Instance().LoadFont(n, s); } } // 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; } 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_WINDOWEVENT: 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; } 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; } } } } } 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); ImGui_ImplOpenGL3_DestroyFontsTexture(); ImGui_ImplOpenGL3_CreateFontsTexture(); } } } // --- 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; Editor &wed = (wi == 0) ? ed : ws.editor; SDL_GL_MakeCurrent(ws.window, ws.gl_ctx); // Start a new ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL2_NewFrame(ws.window); ImGui::NewFrame(); // 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 wed.ProcessPendingOpens(); // 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); } // 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; } } } 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 (!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(); } bool GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px) { const ImGuiIO &io = ImGui::GetIO(); io.Fonts->Clear(); ImFontConfig config; config.MergeMode = false; io.Fonts->AddFontFromMemoryCompressedTTF( kte::Fonts::DefaultFontData, kte::Fonts::DefaultFontSize, size_px, &config, io.Fonts->GetGlyphRangesDefault()); config.MergeMode = true; static const ImWchar extended_ranges[] = { 0x0370, 0x03FF, // Greek and Coptic 0x2200, 0x22FF, // Mathematical Operators 0, }; io.Fonts->AddFontFromMemoryCompressedTTF( kte::Fonts::IosevkaExtended::DefaultFontRegularCompressedData, kte::Fonts::IosevkaExtended::DefaultFontRegularCompressedSize, size_px, &config, extended_ranges); io.Fonts->Build(); return true; }