The row cache rebuilt a string for every line of the file after each edit and had one production caller left (filetype detection in the ImGui frontend, which needs only the first line). Removed with it: rows_, nrows_, the dirty flag set on every edit, ensure_rows_cache and a mutex guarding only that cache. Tests read lines with GetLineString, Nrows or, where they compare every line, a test-only LinesForTests(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
690 lines
19 KiB
C++
690 lines
19 KiB
C++
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <string>
|
|
|
|
#include <imgui.h>
|
|
#include <SDL.h>
|
|
#include <SDL_opengl.h>
|
|
#include <backends/imgui_impl_opengl3.h>
|
|
#include <backends/imgui_impl_sdl2.h>
|
|
|
|
#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
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static void
|
|
apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
|
|
{
|
|
if (!b)
|
|
return;
|
|
|
|
// Auto-detect edit mode from file extension once per buffer so that
|
|
// manual toggles (C-k m / : mode) are not overridden every frame.
|
|
if (!b->EditModeDetected() && !b->Filename().empty())
|
|
b->SetEditMode(DetectEditMode(b->Filename()));
|
|
|
|
// If the user explicitly set syntax state via a command (:syntax on/off,
|
|
// :set filetype=...), leave it alone - otherwise this runs every frame
|
|
// and silently undoes a manual ":syntax off" on the very next frame.
|
|
if (b->SyntaxUserOverride())
|
|
return;
|
|
|
|
// Writing mode disables syntax; otherwise follow the global config.
|
|
if (cfg.syntax && b->GetEditMode() != EditMode::Writing) {
|
|
b->SetSyntaxEnabled(true);
|
|
b->EnsureHighlighter();
|
|
if (auto *eng = b->Highlighter()) {
|
|
if (!eng->HasHighlighter()) {
|
|
const std::string first_line = b->Nrows() > 0 ? b->GetLineString(0) : std::string();
|
|
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();
|
|
// Use average character width rather than "M" (the widest character)
|
|
// so that column count is reasonable for proportional fonts too.
|
|
float ch_w = ImGui::CalcTextSize("abcdefghijklmnopqrstuvwxyz").x / 26.0f;
|
|
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;
|
|
|
|
// 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<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);
|
|
}
|
|
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
|
|
|
|
// Create primary ImGui context
|
|
IMGUI_CHECKVERSION();
|
|
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;
|
|
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<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(), primary.Swap());
|
|
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;
|
|
}
|
|
|
|
|
|
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)) {
|
|
// 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;
|
|
case SDL_MOUSEMOTION:
|
|
event_win_id = e.motion.windowID;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
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) {
|
|
Editor &tgt_ed = (target_idx == 0) ? ed : target->editor;
|
|
if (tgt_ed.FilePickerVisible()) {
|
|
// Modal: don't let keystrokes fall through as edit commands
|
|
// to the buffer underneath. Escape closes the picker;
|
|
// everything else (navigation, filtering, double-click) is
|
|
// handled by the picker's own ImGui widgets via
|
|
// ImGui_ImplSDL2_ProcessEvent above.
|
|
if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) {
|
|
tgt_ed.SetFilePickerVisible(false);
|
|
}
|
|
} else {
|
|
target->input.ProcessSDLEvent(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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;
|
|
|
|
// 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);
|
|
ImGui::SetCurrentContext(ws.imgui_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<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
|
|
wed.ProcessPendingOpens();
|
|
|
|
// Ensure newly opened buffers get syntax + edit mode detection
|
|
apply_syntax_to_buffer(wed.CurrentBuffer(), config_);
|
|
|
|
// 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 if (mi.id == CommandId::FontZoomIn ||
|
|
mi.id == CommandId::FontZoomOut ||
|
|
mi.id == CommandId::FontZoomReset) {
|
|
auto &fr = kte::Fonts::FontRegistry::Instance();
|
|
float cur = fr.CurrentFontSize();
|
|
if (cur <= 0.0f) cur = config_.font_size;
|
|
float next = cur;
|
|
if (mi.id == CommandId::FontZoomIn)
|
|
next = std::min(cur + 2.0f, 72.0f);
|
|
else if (mi.id == CommandId::FontZoomOut)
|
|
next = std::max(cur - 2.0f, 8.0f);
|
|
else
|
|
next = config_.font_size; // reset to config default
|
|
if (next != cur)
|
|
fr.RequestLoadFont(fr.CurrentFontName(), next);
|
|
} 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;
|
|
}
|
|
|
|
// Switch font based on current buffer's edit mode (deferred to next frame)
|
|
{
|
|
Buffer *cur = wed.CurrentBuffer();
|
|
if (cur) {
|
|
auto &fr = kte::Fonts::FontRegistry::Instance();
|
|
const std::string &expected =
|
|
(cur->GetEditMode() == EditMode::Writing)
|
|
? config_.writing_font
|
|
: config_.code_font;
|
|
if (fr.CurrentFontName() != expected && fr.HasFont(expected)) {
|
|
float sz = fr.CurrentFontSize();
|
|
if (sz <= 0.0f) sz = config_.font_size;
|
|
fr.RequestLoadFont(expected, sz);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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
|
|
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
|
if (!(*it)->alive) {
|
|
DestroyWindowResources_(**it);
|
|
it = windows_.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
// Destroy all windows (secondary first, then primary)
|
|
for (auto it = windows_.rbegin(); it != windows_.rend(); ++it) {
|
|
DestroyWindowResources_(**it);
|
|
}
|
|
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;
|
|
}
|