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.
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.7.1")
|
set(KTE_VERSION "1.8.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.
|
||||||
|
|||||||
19
Command.cc
19
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
|
static bool
|
||||||
cmd_center_on_cursor(CommandContext &ctx)
|
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) {
|
auto populate_from_text = [](Buffer &b, const std::string &text) {
|
||||||
// Clear existing rows
|
// Clear existing content
|
||||||
while (b.Nrows() > 0) {
|
b.replace_all_bytes("");
|
||||||
b.delete_row(0);
|
|
||||||
}
|
|
||||||
// Parse text and insert rows
|
// Parse text and insert rows
|
||||||
std::string line;
|
std::string line;
|
||||||
line.reserve(128);
|
line.reserve(128);
|
||||||
@@ -5002,6 +5008,11 @@ InstallDefaultCommands()
|
|||||||
CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor,
|
CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor,
|
||||||
false, false
|
false, false
|
||||||
});
|
});
|
||||||
|
// GUI: new window
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
|
||||||
|
false, false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ enum class CommandId {
|
|||||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||||
// Viewport control
|
// Viewport control
|
||||||
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
||||||
|
// GUI: open a new editor window sharing the same buffer list
|
||||||
|
NewWindow,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
29
Editor.h
29
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)
|
void SetQuitConfirmPending(bool on)
|
||||||
{
|
{
|
||||||
quit_confirm_pending_ = on;
|
quit_confirm_pending_ = on;
|
||||||
@@ -570,13 +582,22 @@ public:
|
|||||||
// Direct access when needed (try to prefer methods above)
|
// Direct access when needed (try to prefer methods above)
|
||||||
[[nodiscard]] const std::vector<Buffer> &Buffers() const
|
[[nodiscard]] const std::vector<Buffer> &Buffers() const
|
||||||
{
|
{
|
||||||
return buffers_;
|
return shared_buffers_ ? *shared_buffers_ : buffers_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::vector<Buffer> &Buffers()
|
std::vector<Buffer> &Buffers()
|
||||||
{
|
{
|
||||||
return buffers_;
|
return shared_buffers_ ? *shared_buffers_ : buffers_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Share another editor's buffer list. When set, this editor operates on
|
||||||
|
// the provided vector instead of its own. Pass nullptr to detach.
|
||||||
|
void SetSharedBuffers(std::vector<Buffer> *shared)
|
||||||
|
{
|
||||||
|
shared_buffers_ = shared;
|
||||||
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -628,7 +649,8 @@ private:
|
|||||||
bool repeatable_ = false; // whether the next command is repeatable
|
bool repeatable_ = false; // whether the next command is repeatable
|
||||||
|
|
||||||
std::vector<Buffer> buffers_;
|
std::vector<Buffer> buffers_;
|
||||||
std::size_t curbuf_ = 0; // index into buffers_
|
std::vector<Buffer> *shared_buffers_ = nullptr; // if set, use this instead of buffers_
|
||||||
|
std::size_t curbuf_ = 0; // index into buffers_
|
||||||
|
|
||||||
// Swap journaling manager (lifetime = editor)
|
// Swap journaling manager (lifetime = editor)
|
||||||
std::unique_ptr<kte::SwapManager> swap_;
|
std::unique_ptr<kte::SwapManager> swap_;
|
||||||
@@ -639,6 +661,7 @@ private:
|
|||||||
|
|
||||||
// Quit state
|
// Quit state
|
||||||
bool quit_requested_ = false;
|
bool quit_requested_ = false;
|
||||||
|
bool new_window_requested_ = false;
|
||||||
bool quit_confirm_pending_ = false;
|
bool quit_confirm_pending_ = false;
|
||||||
bool close_confirm_pending_ = false; // awaiting y/N to save-before-close
|
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
|
bool close_after_save_ = false; // if true, close buffer after successful Save/SaveAs
|
||||||
|
|||||||
15
HelpText.cc
15
HelpText.cc
@@ -27,6 +27,7 @@ HelpText::Text()
|
|||||||
" C-k SPACE Toggle mark\n"
|
" C-k SPACE Toggle mark\n"
|
||||||
" C-k C-d Kill entire line\n"
|
" C-k C-d Kill entire line\n"
|
||||||
" C-k C-q Quit now (no confirm)\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 C-x Save and quit\n"
|
||||||
" C-k a Mark start of file, jump to end\n"
|
" C-k a Mark start of file, jump to end\n"
|
||||||
" C-k b Switch buffer\n"
|
" C-k b Switch buffer\n"
|
||||||
@@ -63,6 +64,10 @@ HelpText::Text()
|
|||||||
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
||||||
" ESC q Reflow paragraph\n"
|
" ESC q Reflow paragraph\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
"Universal argument:\n"
|
||||||
|
" C-u Begin repeat count (then type digits); C-u alone multiplies by 4\n"
|
||||||
|
" C-u N <cmd> Repeat <cmd> N times (e.g., C-u 8 C-f moves right 8 chars)\n"
|
||||||
|
"\n"
|
||||||
"Control keys:\n"
|
"Control keys:\n"
|
||||||
" C-a C-e Line start / end\n"
|
" C-a C-e Line start / end\n"
|
||||||
" C-b C-f Move left / right\n"
|
" C-b C-f Move left / right\n"
|
||||||
@@ -74,12 +79,20 @@ HelpText::Text()
|
|||||||
" C-t Regex search & replace\n"
|
" C-t Regex search & replace\n"
|
||||||
" C-h Search & replace\n"
|
" C-h Search & replace\n"
|
||||||
" C-l / C-g Refresh / Cancel\n"
|
" C-l / C-g Refresh / Cancel\n"
|
||||||
" C-u [digits] Universal argument (repeat count)\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"GUI appearance (command prompt):\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"
|
" : 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"
|
" : 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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
512
ImGuiFrontend.cc
512
ImGuiFrontend.cc
@@ -29,21 +29,86 @@
|
|||||||
|
|
||||||
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_
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void
|
||||||
|
apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
|
||||||
|
{
|
||||||
|
if (!b)
|
||||||
|
return;
|
||||||
|
if (cfg.syntax) {
|
||||||
|
b->SetSyntaxEnabled(true);
|
||||||
|
b->EnsureHighlighter();
|
||||||
|
if (auto *eng = b->Highlighter()) {
|
||||||
|
if (!eng->HasHighlighter()) {
|
||||||
|
std::string first_line;
|
||||||
|
const auto &rows = b->Rows();
|
||||||
|
if (!rows.empty())
|
||||||
|
first_line = static_cast<std::string>(rows[0]);
|
||||||
|
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||||
|
b->Filename(), first_line);
|
||||||
|
if (!ft.empty()) {
|
||||||
|
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||||
|
b->SetFiletype(ft);
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
} else {
|
||||||
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
b->SetFiletype("");
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b->SetSyntaxEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Update editor logical rows/cols from current ImGui metrics for a given display size.
|
||||||
|
static void
|
||||||
|
update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
|
||||||
|
{
|
||||||
|
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||||
|
float ch_w = ImGui::CalcTextSize("M").x;
|
||||||
|
if (row_h <= 0.0f)
|
||||||
|
row_h = 16.0f;
|
||||||
|
if (ch_w <= 0.0f)
|
||||||
|
ch_w = 8.0f;
|
||||||
|
|
||||||
|
const float pad_x = 6.0f;
|
||||||
|
const float pad_y = 6.0f;
|
||||||
|
|
||||||
|
float wanted_bar_h = ImGui::GetFrameHeight();
|
||||||
|
float total_avail_h = std::max(0.0f, disp_h - 2.0f * pad_y);
|
||||||
|
float actual_avail_h = std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h;
|
||||||
|
|
||||||
|
auto content_rows = static_cast<std::size_t>(std::max(0.0f, std::floor(actual_avail_h / row_h)));
|
||||||
|
std::size_t rows = content_rows + 1;
|
||||||
|
|
||||||
|
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
||||||
|
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||||
|
|
||||||
|
if (rows != ed.Rows() || cols != ed.Cols()) {
|
||||||
|
ed.SetDimensions(rows, cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||||
{
|
{
|
||||||
(void) argc;
|
(void) argc;
|
||||||
(void) argv;
|
(void) argv;
|
||||||
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
|
||||||
input_.Attach(&ed);
|
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
||||||
// editor dimensions will be initialized during the first Step() frame
|
config_ = GUIConfig::Load();
|
||||||
|
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
|
||||||
GUIConfig cfg = GUIConfig::Load();
|
|
||||||
|
|
||||||
// GL attributes for core profile
|
// GL attributes for core profile
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
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_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
|
// Compute desired window size from config
|
||||||
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;
|
||||||
|
|
||||||
if (cfg.fullscreen) {
|
int init_w = 1280, init_h = 800;
|
||||||
// "Fullscreen": fill the usable bounds of the primary display.
|
if (config_.fullscreen) {
|
||||||
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
width_ = usable.w;
|
init_w = usable.w;
|
||||||
height_ = usable.h;
|
init_h = usable.h;
|
||||||
}
|
}
|
||||||
#if !defined(__APPLE__)
|
#if !defined(__APPLE__)
|
||||||
// Non-macOS: desktop fullscreen uses the current display resolution.
|
|
||||||
win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
int w = config_.columns * static_cast<int>(config_.font_size);
|
||||||
int w = cfg.columns * static_cast<int>(cfg.font_size);
|
int h = config_.rows * static_cast<int>(config_.font_size * 1.2);
|
||||||
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
|
|
||||||
|
|
||||||
// As a safety, clamp to display usable bounds if retrievable
|
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
w = std::min(w, usable.w);
|
w = std::min(w, usable.w);
|
||||||
h = std::min(h, usable.h);
|
h = std::min(h, usable.h);
|
||||||
}
|
}
|
||||||
width_ = std::max(320, w);
|
init_w = std::max(320, w);
|
||||||
height_ = std::max(200, h);
|
init_h = std::max(200, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||||
window_ = SDL_CreateWindow(
|
SDL_Window *win = SDL_CreateWindow(
|
||||||
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
||||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
width_, height_,
|
init_w, init_h,
|
||||||
win_flags);
|
win_flags);
|
||||||
if (!window_) {
|
if (!win) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
// macOS: when "fullscreen" is requested, position the window at the
|
if (config_.fullscreen) {
|
||||||
// top-left of the usable display area to mimic fullscreen while keeping
|
|
||||||
// the system menu bar visible.
|
|
||||||
if (cfg.fullscreen) {
|
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
SDL_SetWindowPosition(win, usable.x, usable.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
gl_ctx_ = SDL_GL_CreateContext(window_);
|
SDL_GLContext gl_ctx = SDL_GL_CreateContext(win);
|
||||||
if (!gl_ctx_)
|
if (!gl_ctx) {
|
||||||
|
SDL_DestroyWindow(win);
|
||||||
return false;
|
return false;
|
||||||
SDL_GL_MakeCurrent(window_, gl_ctx_);
|
}
|
||||||
|
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1); // vsync
|
SDL_GL_SetSwapInterval(1); // vsync
|
||||||
|
|
||||||
IMGUI_CHECKVERSION();
|
IMGUI_CHECKVERSION();
|
||||||
@@ -121,94 +180,54 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
if (const char *home = std::getenv("HOME")) {
|
if (const char *home = std::getenv("HOME")) {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
fs::path config_dir = fs::path(home) / ".config" / "kte";
|
fs::path config_dir = fs::path(home) / ".config" / "kte";
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!fs::exists(config_dir)) {
|
if (!fs::exists(config_dir)) {
|
||||||
fs::create_directories(config_dir, ec);
|
fs::create_directories(config_dir, ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs::exists(config_dir)) {
|
if (fs::exists(config_dir)) {
|
||||||
static std::string ini_path = (config_dir / "imgui.ini").string();
|
static std::string ini_path = (config_dir / "imgui.ini").string();
|
||||||
io.IniFilename = ini_path.c_str();
|
io.IniFilename = ini_path.c_str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||||
ImGui::StyleColorsDark();
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
if (config_.background == "light")
|
||||||
if (cfg.background == "light")
|
|
||||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
else
|
else
|
||||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
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
|
apply_syntax_to_buffer(ed.CurrentBuffer(), config_);
|
||||||
if (Buffer *b = ed.CurrentBuffer()) {
|
|
||||||
if (cfg.syntax) {
|
|
||||||
b->SetSyntaxEnabled(true);
|
|
||||||
// Ensure a highlighter is available if possible
|
|
||||||
b->EnsureHighlighter();
|
|
||||||
if (auto *eng = b->Highlighter()) {
|
|
||||||
if (!eng->HasHighlighter()) {
|
|
||||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
|
||||||
std::string first_line;
|
|
||||||
const auto &rows = b->Rows();
|
|
||||||
if (!rows.empty())
|
|
||||||
first_line = static_cast<std::string>(rows[0]);
|
|
||||||
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
|
||||||
b->Filename(), first_line);
|
|
||||||
if (!ft.empty()) {
|
|
||||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
|
||||||
b->SetFiletype(ft);
|
|
||||||
eng->InvalidateFrom(0);
|
|
||||||
} else {
|
|
||||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
|
||||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
|
||||||
b->SetFiletype("");
|
|
||||||
eng->InvalidateFrom(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
b->SetSyntaxEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx))
|
||||||
return false;
|
return false;
|
||||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
||||||
return false;
|
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;
|
int w, h;
|
||||||
SDL_GetWindowSize(window_, &w, &h);
|
SDL_GetWindowSize(win, &w, &h);
|
||||||
width_ = w;
|
init_w = w;
|
||||||
height_ = h;
|
init_h = h;
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
#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) {
|
if (w > 1 && h > 1) {
|
||||||
SDL_SetWindowSize(window_, w - 1, h - 1);
|
SDL_SetWindowSize(win, w - 1, h - 1);
|
||||||
SDL_SetWindowSize(window_, w, h);
|
SDL_SetWindowSize(win, w, h);
|
||||||
// Update cached size in case backend reports immediately
|
SDL_GetWindowSize(win, &w, &h);
|
||||||
SDL_GetWindowSize(window_, &w, &h);
|
init_w = w;
|
||||||
width_ = w;
|
init_h = h;
|
||||||
height_ = h;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Install embedded fonts into registry and load configured font
|
// Install embedded fonts
|
||||||
kte::Fonts::InstallDefaultFonts();
|
kte::Fonts::InstallDefaultFonts();
|
||||||
// Initialize font atlas using configured font name and size; fallback to embedded default helper
|
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
|
||||||
if (!kte::Fonts::FontRegistry::Instance().LoadFont(cfg.font, (float) cfg.font_size)) {
|
LoadGuiFont_(nullptr, (float) config_.font_size);
|
||||||
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) config_.font_size);
|
||||||
// Record defaults in registry so subsequent size changes have a base
|
|
||||||
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) cfg.font_size);
|
|
||||||
std::string n;
|
std::string n;
|
||||||
float s = 0.0f;
|
float s = 0.0f;
|
||||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(n, s)) {
|
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<WindowState>();
|
||||||
|
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<WindowState>();
|
||||||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,137 +304,216 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
void
|
void
|
||||||
GUIFrontend::Step(Editor &ed, bool &running)
|
GUIFrontend::Step(Editor &ed, bool &running)
|
||||||
{
|
{
|
||||||
|
// --- Event processing ---
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
while (SDL_PollEvent(&e)) {
|
while (SDL_PollEvent(&e)) {
|
||||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||||
|
|
||||||
|
// Determine which window this event belongs to
|
||||||
|
Uint32 event_win_id = 0;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case SDL_QUIT:
|
|
||||||
running = false;
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
event_win_id = e.window.windowID;
|
||||||
width_ = e.window.data1;
|
break;
|
||||||
height_ = e.window.data2;
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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;
|
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) {
|
||||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||||
// Recreate backend font texture
|
|
||||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new ImGui frame BEFORE processing commands so dimensions are correct
|
// --- Step each window ---
|
||||||
ImGui_ImplOpenGL3_NewFrame();
|
// We iterate by index because OpenNewWindow_ may append to windows_.
|
||||||
ImGui_ImplSDL2_NewFrame(window_);
|
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
|
||||||
ImGui::NewFrame();
|
WindowState &ws = *windows_[wi];
|
||||||
|
if (!ws.alive)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Update editor logical rows/cols using current ImGui metrics and display size
|
Editor &wed = (wi == 0) ? ed : ws.editor;
|
||||||
{
|
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
|
||||||
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
|
||||||
float ch_w = ImGui::CalcTextSize("M").x;
|
|
||||||
if (row_h <= 0.0f)
|
|
||||||
row_h = 16.0f;
|
|
||||||
if (ch_w <= 0.0f)
|
|
||||||
ch_w = 8.0f;
|
|
||||||
// Prefer ImGui IO display size; fall back to cached SDL window size
|
|
||||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
|
|
||||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
|
|
||||||
|
|
||||||
// Account for the GUI window padding and the status bar height used in ImGuiRenderer.
|
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||||
const float pad_x = 6.0f;
|
|
||||||
const float pad_y = 6.0f;
|
|
||||||
|
|
||||||
// Use the same logic as ImGuiRenderer for available height and status bar reservation.
|
// Start a new ImGui frame
|
||||||
float wanted_bar_h = ImGui::GetFrameHeight();
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
float total_avail_h = std::max(0.0f, disp_h - 2.0f * pad_y);
|
ImGui_ImplSDL2_NewFrame(ws.window);
|
||||||
float actual_avail_h = std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h;
|
ImGui::NewFrame();
|
||||||
|
|
||||||
// Visible content rows inside the scroll child
|
// Update editor dimensions
|
||||||
auto content_rows = static_cast<std::size_t>(std::max(0.0f, std::floor(actual_avail_h / row_h)));
|
{
|
||||||
// Editor::Rows includes the status line; add 1 back for it.
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
std::size_t rows = content_rows + 1;
|
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);
|
||||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
update_editor_dimensions(wed, disp_w, disp_h);
|
||||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
|
||||||
|
|
||||||
// Only update if changed to avoid churn
|
|
||||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
|
||||||
ed.SetDimensions(rows, cols);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Allow deferred opens (including swap recovery prompts) to run.
|
// Allow deferred opens
|
||||||
ed.ProcessPendingOpens();
|
wed.ProcessPendingOpens();
|
||||||
|
|
||||||
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
|
// Drain input queue
|
||||||
for (;;) {
|
for (;;) {
|
||||||
MappedInput mi;
|
MappedInput mi;
|
||||||
if (!input_.Poll(mi))
|
if (!ws.input.Poll(mi))
|
||||||
break;
|
break;
|
||||||
if (mi.hasCommand) {
|
if (mi.hasCommand) {
|
||||||
// Track kill ring before and after to sync GUI clipboard when it changes
|
if (mi.id == CommandId::NewWindow) {
|
||||||
const std::string before = ed.KillRingHead();
|
// Open a new window; handled after this loop
|
||||||
Execute(ed, mi.id, mi.arg, mi.count);
|
wed.SetNewWindowRequested(true);
|
||||||
const std::string after = ed.KillRingHead();
|
} else {
|
||||||
if (after != before && !after.empty()) {
|
const std::string before = wed.KillRingHead();
|
||||||
// Update the system clipboard to mirror the kill ring head in GUI
|
Execute(wed, mi.id, mi.arg, mi.count);
|
||||||
SDL_SetClipboardText(after.c_str());
|
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()) {
|
// Remove dead secondary windows
|
||||||
running = false;
|
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
|
void
|
||||||
GUIFrontend::Shutdown()
|
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_ImplOpenGL3_Shutdown();
|
||||||
ImGui_ImplSDL2_Shutdown();
|
ImGui_ImplSDL2_Shutdown();
|
||||||
ImGui::DestroyContext();
|
ImGui::DestroyContext();
|
||||||
|
|
||||||
if (gl_ctx_) {
|
if (!windows_.empty()) {
|
||||||
SDL_GL_DeleteContext(gl_ctx_);
|
if (windows_[0]->gl_ctx) {
|
||||||
gl_ctx_ = nullptr;
|
SDL_GL_DeleteContext(windows_[0]->gl_ctx);
|
||||||
}
|
windows_[0]->gl_ctx = nullptr;
|
||||||
if (window_) {
|
}
|
||||||
SDL_DestroyWindow(window_);
|
if (windows_[0]->window) {
|
||||||
window_ = nullptr;
|
SDL_DestroyWindow(windows_[0]->window);
|
||||||
|
windows_[0]->window = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
windows_.clear();
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +527,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
|||||||
ImFontConfig config;
|
ImFontConfig config;
|
||||||
config.MergeMode = false;
|
config.MergeMode = false;
|
||||||
|
|
||||||
// Load Basic Latin + Latin Supplement
|
|
||||||
io.Fonts->AddFontFromMemoryCompressedTTF(
|
io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||||
kte::Fonts::DefaultFontData,
|
kte::Fonts::DefaultFontData,
|
||||||
kte::Fonts::DefaultFontSize,
|
kte::Fonts::DefaultFontSize,
|
||||||
@@ -375,7 +534,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
|||||||
&config,
|
&config,
|
||||||
io.Fonts->GetGlyphRangesDefault());
|
io.Fonts->GetGlyphRangesDefault());
|
||||||
|
|
||||||
// Merge Greek and Mathematical symbols from IosevkaExtended
|
|
||||||
config.MergeMode = true;
|
config.MergeMode = true;
|
||||||
static const ImWchar extended_ranges[] = {
|
static const ImWchar extended_ranges[] = {
|
||||||
0x0370, 0x03FF, // Greek and Coptic
|
0x0370, 0x03FF, // Greek and Coptic
|
||||||
@@ -391,4 +549,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
|||||||
|
|
||||||
io.Fonts->Build();
|
io.Fonts->Build();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,14 @@
|
|||||||
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
#include "ImGuiInputHandler.h"
|
#include "ImGuiInputHandler.h"
|
||||||
#include "ImGuiRenderer.h"
|
#include "ImGuiRenderer.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
|
||||||
struct SDL_Window;
|
struct SDL_Window;
|
||||||
@@ -24,13 +28,25 @@ public:
|
|||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
|
||||||
private:
|
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);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIConfig config_{};
|
GUIConfig config_{};
|
||||||
ImGuiInputHandler input_{};
|
// Primary window (index 0 in windows_); created during Init.
|
||||||
ImGuiRenderer renderer_{};
|
std::vector<std::unique_ptr<WindowState> > windows_;
|
||||||
SDL_Window *window_ = nullptr;
|
};
|
||||||
SDL_GLContext gl_ctx_ = nullptr;
|
|
||||||
int width_ = 1280;
|
|
||||||
int height_ = 800;
|
|
||||||
};
|
|
||||||
@@ -337,6 +337,18 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
||||||
const SDL_Keycode key = e.key.keysym.sym;
|
const SDL_Keycode key = e.key.keysym.sym;
|
||||||
|
|
||||||
|
// New window: Cmd+N (macOS) or Ctrl+Shift+N (Linux/Windows)
|
||||||
|
{
|
||||||
|
const bool gui_n = (mods & KMOD_GUI) && !(mods & KMOD_CTRL) && (key == SDLK_n);
|
||||||
|
const bool ctrl_sn = (mods & KMOD_CTRL) && (mods & KMOD_SHIFT) && (key == SDLK_n);
|
||||||
|
if (gui_n || ctrl_sn) {
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, CommandId::NewWindow, std::string(), 0});
|
||||||
|
suppress_text_input_once_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
|
// 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.
|
// 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)) {
|
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
|
||||||
@@ -446,7 +458,7 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
if (ed_ &&ed_
|
if (ed_ &&ed_
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
->
|
->
|
||||||
UArg() != 0
|
UArg() != 0
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user