diff --git a/AGENTS.md b/AGENTS.md index 8c8a6c3..30c5ae4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,9 @@ # kte -kte (Kyle's Text Editor) is a C++20 text editor with a terminal-first design (ncurses) and optional GUI frontends (ImGui via SDL2/OpenGL/Freetype, or Qt6); it uses a WordStar/VDE-style command model. Standards: none. The Metacircular service standards do not apply here. +kte (Kyle's Text Editor) is a C++20 text editor with a terminal-first design (ncurses) and an optional ImGui GUI frontend (SDL2/OpenGL/Freetype); it uses a WordStar/VDE-style command model. Standards: none. The Metacircular service standards do not apply here. ## Bootstrap (run once per checkout, idempotent) -nix develop (default shell; also provides `terminal` and `qt` devshells) or system SDL2/Freetype/ncurses if not using nix. +nix develop (default shell; also provides a `terminal` devshell) or system SDL2/Freetype/ncurses if not using nix. ## Gate (run before saying "done"; must exit 0) make gate # what it runs: configure (cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_GUI=ON -DBUILD_TESTS=ON) -> build (cmake --build build) -> test (./build/kte_tests) @@ -12,7 +12,7 @@ Also run clang-tidy on changed files via build/compile_commands.json (not part o ## Run Terminal editor: `./build/kte ` (always built). -GUI editor: `./build/kge ` (built when `-DBUILD_GUI=ON`; requires SDL2/OpenGL/Freetype, or Qt6 with `-DKTE_USE_QT=ON`). +GUI editor: `./build/kge ` (built when `-DBUILD_GUI=ON`; requires SDL2/OpenGL/Freetype). Docker (cross-platform Linux testing): `docker build -t kte-linux . && docker run --rm -v "$(pwd):/kte" kte-linux`. ## Sensors the harness watches @@ -42,7 +42,7 @@ None: kte builds and runs locally (or in the Docker/CI Linux image); it has no r Three-layer design with strict frontend independence: ``` -Frontend Layer (Terminal / ImGui / Qt / Test) +Frontend Layer (Terminal / ImGui / Test) InputHandler.h, Renderer.h, Frontend.h interfaces ↓ Command Layer @@ -69,7 +69,6 @@ Core Model Layer Each frontend implements three interfaces (`Frontend.h`, `InputHandler.h`, `Renderer.h`): - **Terminal**: ncurses-based (always built) - **ImGui**: SDL2+OpenGL+Freetype (built with `-DBUILD_GUI=ON`) -- **Qt**: Qt6 (built with `-DBUILD_GUI=ON -DKTE_USE_QT=ON`) - **Test**: Programmatic frontend for testing (always built, no UI deps) ### Key CMake Options @@ -77,7 +76,6 @@ Each frontend implements three interfaces (`Frontend.h`, `InputHandler.h`, `Rend | Flag | Default | Purpose | |------|---------|---------| | `BUILD_GUI` | ON | Build `kge` (ImGui GUI) | -| `KTE_USE_QT` | OFF | Use Qt6 instead of ImGui for GUI | | `BUILD_TESTS` | ON | Build test suite | | `ENABLE_ASAN` | OFF | AddressSanitizer | | `KTE_STATIC_LINK` | OFF | Static linking (Linux only) | diff --git a/CMakeLists.txt b/CMakeLists.txt index 19d7da6..ce7fe0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,6 @@ set(KTE_VERSION "1.12.0") # Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.") -set(KTE_USE_QT OFF CACHE BOOL "Build the QT frontend instead of ImGui.") set(BUILD_TESTS ON CACHE BOOL "Enable building test programs.") set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI") option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF) @@ -163,25 +162,13 @@ if (BUILD_GUI) set(GUI_SOURCES GUIConfig.cc ) - if (KTE_USE_QT) - find_package(Qt6 COMPONENTS Widgets REQUIRED) - set(GUI_SOURCES - ${GUI_SOURCES} - QtFrontend.cc - QtInputHandler.cc - QtRenderer.cc - ) - # Expose preprocessor switch so sources can exclude ImGui-specific code - add_compile_definitions(KTE_USE_QT) - else () - set(GUI_SOURCES - ${GUI_SOURCES} - ${FONT_SOURCES} - ImGuiFrontend.cc - ImGuiInputHandler.cc - ImGuiRenderer.cc - ) - endif () + set(GUI_SOURCES + ${GUI_SOURCES} + ${FONT_SOURCES} + ImGuiFrontend.cc + ImGuiInputHandler.cc + ImGuiRenderer.cc + ) endif () set(COMMON_SOURCES @@ -311,24 +298,15 @@ if (BUILD_GUI) GUIConfig.h ) - if (KTE_USE_QT) - set(GUI_HEADERS - ${GUI_HEADERS} - QtFrontend.h - QtInputHandler.h - QtRenderer.h - ) - else () - set(GUI_HEADERS - ${GUI_HEADERS} - ${THEME_HEADERS} - ${FONT_HEADERS} - ImGuiFrontend.h - ImGuiInputHandler.h - ImGuiRenderer.h - fonts/BerkeleyMono.h - ) - endif () + set(GUI_HEADERS + ${GUI_HEADERS} + ${THEME_HEADERS} + ${FONT_HEADERS} + ImGuiFrontend.h + ImGuiInputHandler.h + ImGuiRenderer.h + fonts/BerkeleyMono.h + ) endif () # kte (terminal-first) executable @@ -473,17 +451,10 @@ if (BUILD_GUI) ) target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE}) - if (KTE_USE_QT) - target_compile_definitions(kge PRIVATE KTE_USE_QT=1) - endif () if (KTE_UNDO_DEBUG) target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1) endif () - if (KTE_USE_QT) - target_link_libraries(kge ${CURSES_LIBRARIES} Qt6::Widgets) - else () - target_link_libraries(kge ${CURSES_LIBRARIES} imgui) - endif () + target_link_libraries(kge ${CURSES_LIBRARIES} imgui) # Static linking on Linux only (macOS does not support static linking of system libraries) if (NOT APPLE AND KTE_STATIC_LINK) @@ -565,7 +536,8 @@ if (BUILD_GUI) # Optional post-build bundle fixup (can also be run from scripts). # This provides a CMake target to run BundleUtilities' fixup_bundle on the - # built app, useful after macdeployqt to ensure non-Qt dylibs are internalized. + # built app, copying non-system dylibs (e.g. Homebrew SDL2/SDL3, freetype, + # pcre2) into the bundle so it runs on machines without them installed. if (APPLE AND TARGET kge) get_target_property(IS_BUNDLE kge MACOSX_BUNDLE) if (IS_BUNDLE) @@ -573,7 +545,7 @@ if (BUILD_GUI) COMMAND ${CMAKE_COMMAND} -DAPP_BUNDLE=${CMAKE_CURRENT_BINARY_DIR}/$.app -P ${CMAKE_CURRENT_LIST_DIR}/cmake/fix_bundle.cmake - COMMENT "Running fixup_bundle on kge.app to internalize non-Qt dylibs" + COMMENT "Running fixup_bundle on kge.app to internalize non-system dylibs" VERBATIM) add_dependencies(kge_fixup_bundle kge) endif () diff --git a/Command.cc b/Command.cc index 41c4798..490c654 100644 --- a/Command.cc +++ b/Command.cc @@ -24,27 +24,21 @@ #include "syntax/CppHighlighter.h" #ifdef KTE_BUILD_GUI # include "GUITheme.h" -# if !defined(KTE_USE_QT) -# include "fonts/FontRegistry.h" -# include "imgui.h" -# endif -# if defined(KTE_USE_QT) -# include -# include -# endif +# include "fonts/FontRegistry.h" +# include "imgui.h" #endif // Define cross-frontend theme change flags declared in GUITheme.h namespace kte { bool gThemeChangePending = false; std::string gThemeChangeRequest; -// Qt font change globals +// Font change request globals bool gFontChangePending = false; std::string gFontFamilyRequest; float gFontSizeRequest = 0.0f; std::string gCurrentFontFamily; float gCurrentFontSize = 0.0f; -// Request Qt visual font dialog +// Request a visual font dialog bool gFontDialogRequested = false; } @@ -1749,7 +1743,7 @@ cmd_set_option(CommandContext &ctx) // GUI theme cycling commands (available in GUI build; ImGui-only for now) -#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) +#if defined(KTE_BUILD_GUI) static bool cmd_theme_next(CommandContext &ctx) { @@ -1785,7 +1779,7 @@ cmd_theme_prev(CommandContext &ctx) // Theme set by name command -#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) +#if defined(KTE_BUILD_GUI) static bool cmd_theme_set_by_name(const CommandContext &ctx) { @@ -1829,41 +1823,15 @@ cmd_theme_set_by_name(const CommandContext &ctx) static bool cmd_theme_set_by_name(CommandContext &ctx) { -# if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT) - // Qt GUI build: schedule theme change for frontend - std::string name = ctx.arg; - // trim spaces - auto ltrim = [](std::string &s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { - return !std::isspace(ch); - })); - }; - auto rtrim = [](std::string &s) { - s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { - return !std::isspace(ch); - }).base(), s.end()); - }; - ltrim(name); - rtrim(name); - if (name.empty()) { - ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)"); - return true; - } - kte::gThemeChangeRequest = name; - kte::gThemeChangePending = true; - ctx.editor.SetStatus(std::string("Theme requested: ") + name); - return true; -# else (void) ctx; // No-op in terminal build return true; -# endif } #endif // Font set by name (GUI) -#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) +#if defined(KTE_BUILD_GUI) static bool cmd_font_set_by_name(const CommandContext &ctx) { @@ -1916,7 +1884,7 @@ cmd_font_set_by_name(const CommandContext &ctx) static bool cmd_font_set_by_name(CommandContext &ctx) { - // Qt build: queue font family change + // Non-GUI build: record the requested font family std::string name = ctx.arg; // trim auto ltrim = [](std::string &s) { @@ -1947,7 +1915,7 @@ cmd_font_set_by_name(CommandContext &ctx) // Font size set (GUI, ImGui-only for now) -#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) +#if defined(KTE_BUILD_GUI) static bool cmd_font_set_size(const CommandContext &ctx) { @@ -2006,7 +1974,7 @@ cmd_font_set_size(const CommandContext &ctx) static bool cmd_font_set_size(CommandContext &ctx) { - // Qt build: parse size and queue change + // Non-GUI build: parse size and record the request std::string a = ctx.arg; auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { @@ -2081,7 +2049,7 @@ cmd_toggle_edit_mode(const CommandContext &ctx) // Background set command (GUI, ImGui-only for now) -#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) +#if defined(KTE_BUILD_GUI) static bool cmd_background_set(const CommandContext &ctx) { @@ -2254,7 +2222,7 @@ cmd_visual_file_picker_toggle(const CommandContext &ctx) } -// GUI: request visual font picker (Qt frontend will consume flag) +// GUI: request visual font picker (sets a flag for the frontend) static bool cmd_visual_font_picker_toggle(const CommandContext &ctx) { @@ -2570,7 +2538,6 @@ cmd_insert_text(CommandContext &ctx) // Only special-case argument completion for certain commands if (cmd == "theme") { #if defined(KTE_BUILD_GUI) -# if !defined(KTE_USE_QT) std::vector cands; const auto ® = kte::ThemeRegistry(); for (const auto &t: reg) { @@ -2578,23 +2545,6 @@ cmd_insert_text(CommandContext &ctx) if (argprefix.empty() || n.rfind(argprefix, 0) == 0) cands.push_back(n); } -# else - // Qt: offer known theme names handled by ApplyQtThemeByName - static const char *qt_themes[] = { - "nord", - "solarized-dark", - "solarized-light", - "gruvbox-dark", - "gruvbox-light", - "eink" - }; - std::vector cands; - for (const char *t: qt_themes) { - std::string n(t); - if (argprefix.empty() || n.rfind(argprefix, 0) == 0) - cands.push_back(n); - } -# endif if (cands.empty()) { // no change } else if (cands.size() == 1) { @@ -2626,20 +2576,7 @@ cmd_insert_text(CommandContext &ctx) [](unsigned char c) { return (char) std::tolower(c); }); -#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT) - // Qt: complete against system font families - QStringList fams = QFontDatabase::families(); - for (const auto &fam: fams) { - std::string n = fam.toStdString(); - std::string nlower = n; - std::transform(nlower.begin(), nlower.end(), nlower.begin(), - [](unsigned char c) { - return (char) std::tolower(c); - }); - if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0) - cands.push_back(n); - } -#elif defined(KTE_BUILD_GUI) +#if defined(KTE_BUILD_GUI) // ImGui: complete against embedded font registry for (const auto &n : kte::Fonts::FontRegistry::Instance().FontNames()) { if (apfx_lower.empty() || n.rfind(apfx_lower, 0) == 0) diff --git a/GUITheme.h b/GUITheme.h index 77e94e3..8f6747a 100644 --- a/GUITheme.h +++ b/GUITheme.h @@ -12,292 +12,16 @@ namespace kte { extern bool gThemeChangePending; extern std::string gThemeChangeRequest; // raw user-provided name -// Qt GUI: cross-frontend font change hooks and current font state +// Font change request hooks and current font state (set by command handlers) extern bool gFontChangePending; extern std::string gFontFamilyRequest; // requested family (case-insensitive) extern float gFontSizeRequest; // <= 0 means keep size -extern std::string gCurrentFontFamily; // last applied family (Qt) -extern float gCurrentFontSize; // last applied size (Qt) -// Qt GUI: request to show a visual font dialog (set by command handler) +extern std::string gCurrentFontFamily; // last applied family +extern float gCurrentFontSize; // last applied size +// Request to show a visual font dialog (set by command handler) extern bool gFontDialogRequested; } -#if defined(KTE_USE_QT) -// Qt build: avoid hard dependency on ImGui headers/types. -// Provide a lightweight color vector matching ImVec4 fields used by renderers. -struct KteColor { - float x{0}, y{0}, z{0}, w{1}; -}; - -static inline KteColor -RGBA(unsigned int rgb, float a = 1.0f) -{ - const float r = static_cast((rgb >> 16) & 0xFF) / 255.0f; - const float g = static_cast((rgb >> 8) & 0xFF) / 255.0f; - const float b = static_cast(rgb & 0xFF) / 255.0f; - return {r, g, b, a}; -} - -namespace kte { -// Background mode selection for light/dark palettes -enum class BackgroundMode { Light, Dark }; - -// Global background mode; default to Dark to match prior defaults -static inline auto gBackgroundMode = BackgroundMode::Dark; - - -static inline void -SetBackgroundMode(const BackgroundMode m) -{ - gBackgroundMode = m; -} - - -static inline BackgroundMode -GetBackgroundMode() -{ - return gBackgroundMode; -} - - -// Minimal GUI palette for Qt builds. This mirrors the defaults used in the ImGui -// frontend (Nord-ish) and switches for light/dark background mode. -struct Palette { - KteColor bg; // editor background - KteColor fg; // default foreground text - KteColor sel_bg; // selection background - KteColor cur_bg; // cursor cell background - KteColor status_bg; // status bar background - KteColor status_fg; // status bar foreground -}; - -// Optional theme override (Qt): when set, GetPalette() will return this instead -// of the generic light/dark defaults. This allows honoring theme names in kge.ini. -static inline bool gPaletteOverride = false; -static inline Palette gOverridePalette{}; -static inline std::string gOverrideThemeName = ""; // lowercased name - -static inline Palette -GetPalette() -{ - const bool dark = (GetBackgroundMode() == BackgroundMode::Dark); - if (gPaletteOverride) { - return gOverridePalette; - } - if (dark) { - return Palette{ - /*bg*/ RGBA(0x1C1C1E), - /*fg*/ RGBA(0xDCDCDC), - /*sel_bg*/ RGBA(0xC8C800, 0.35f), - /*cur_bg*/ RGBA(0xC8C8FF, 0.50f), - /*status_bg*/ RGBA(0x28282C), - /*status_fg*/ RGBA(0xB4B48C) - }; - } else { - // Light palette tuned for readability - return Palette{ - /*bg*/ RGBA(0xFBFBFC), - /*fg*/ RGBA(0x30343A), - /*sel_bg*/ RGBA(0x268BD2, 0.22f), - /*cur_bg*/ RGBA(0x000000, 0.15f), - /*status_bg*/ RGBA(0xE6E8EA), - /*status_fg*/ RGBA(0x50555A) - }; - } -} - - -// A few named palettes to provide visible differences between themes in Qt. -// These are approximate and palette-based (no widget style changes like ImGuiStyle). -static inline Palette -NordDark() -{ - return { - /*bg*/RGBA(0x2E3440), /*fg*/RGBA(0xD8DEE9), /*sel_bg*/RGBA(0x88C0D0, 0.25f), - /*cur_bg*/RGBA(0x81A1C1, 0.35f), /*status_bg*/RGBA(0x3B4252), /*status_fg*/RGBA(0xE5E9F0) - }; -} - - -static inline Palette -NordLight() -{ - return { - /*bg*/RGBA(0xECEFF4), /*fg*/RGBA(0x2E3440), /*sel_bg*/RGBA(0x5E81AC, 0.22f), - /*cur_bg*/RGBA(0x000000, 0.12f), /*status_bg*/RGBA(0xE5E9F0), /*status_fg*/RGBA(0x4C566A) - }; -} - - -static inline Palette -SolarizedDark() -{ - return { - /*bg*/RGBA(0x002b36), /*fg*/RGBA(0x93a1a1), /*sel_bg*/RGBA(0x586e75, 0.40f), - /*cur_bg*/RGBA(0x657b83, 0.35f), /*status_bg*/RGBA(0x073642), /*status_fg*/RGBA(0xeee8d5) - }; -} - - -static inline Palette -SolarizedLight() -{ - return { - /*bg*/RGBA(0xfdf6e3), /*fg*/RGBA(0x586e75), /*sel_bg*/RGBA(0x268bd2, 0.25f), - /*cur_bg*/RGBA(0x000000, 0.10f), /*status_bg*/RGBA(0xeee8d5), /*status_fg*/RGBA(0x657b83) - }; -} - - -static inline Palette -GruvboxDark() -{ - return { - /*bg*/RGBA(0x282828), /*fg*/RGBA(0xebdbb2), /*sel_bg*/RGBA(0xd79921, 0.35f), - /*cur_bg*/RGBA(0x458588, 0.40f), /*status_bg*/RGBA(0x3c3836), /*status_fg*/RGBA(0xd5c4a1) - }; -} - - -static inline Palette -GruvboxLight() -{ - return { - /*bg*/RGBA(0xfbf1c7), /*fg*/RGBA(0x3c3836), /*sel_bg*/RGBA(0x076678, 0.22f), - /*cur_bg*/RGBA(0x000000, 0.10f), /*status_bg*/RGBA(0xebdbb2), /*status_fg*/RGBA(0x504945) - }; -} - - -static inline Palette -EInk() -{ - return { - /*bg*/RGBA(0xffffff), /*fg*/RGBA(0x000000), /*sel_bg*/RGBA(0x000000, 0.10f), - /*cur_bg*/RGBA(0x000000, 0.12f), /*status_bg*/RGBA(0x000000), /*status_fg*/RGBA(0xffffff) - }; -} - - -// Apply a Qt theme by name. Returns true on success. Name matching is case-insensitive and -// supports common aliases (e.g., "solarized-light" or "solarized light"). If the name conveys -// a background (light/dark), BackgroundMode is updated to keep SyntaxInk consistent. -static inline bool -ApplyQtThemeByName(std::string name) -{ - // normalize - std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { - return (char) std::tolower(c); - }); - auto has = [&](const std::string &s) { - return name.find(s) != std::string::npos; - }; - - if (name.empty() || name == "default" || name == "nord") { - // Choose variant by current background mode - if (GetBackgroundMode() == BackgroundMode::Dark) { - gOverridePalette = NordDark(); - } else { - gOverridePalette = NordLight(); - } - gPaletteOverride = true; - gOverrideThemeName = "nord"; - return true; - } - - if (has("solarized")) { - if (has("light")) { - SetBackgroundMode(BackgroundMode::Light); - gOverridePalette = SolarizedLight(); - } else if (has("dark")) { - SetBackgroundMode(BackgroundMode::Dark); - gOverridePalette = SolarizedDark(); - } else { - // pick from current background - gOverridePalette = (GetBackgroundMode() == BackgroundMode::Dark) - ? SolarizedDark() - : SolarizedLight(); - } - gPaletteOverride = true; - gOverrideThemeName = "solarized"; - return true; - } - - if (has("gruvbox")) { - if (has("light")) { - SetBackgroundMode(BackgroundMode::Light); - gOverridePalette = GruvboxLight(); - } else if (has("dark")) { - SetBackgroundMode(BackgroundMode::Dark); - gOverridePalette = GruvboxDark(); - } else { - gOverridePalette = (GetBackgroundMode() == BackgroundMode::Dark) - ? GruvboxDark() - : GruvboxLight(); - } - gPaletteOverride = true; - gOverrideThemeName = "gruvbox"; - return true; - } - - if (has("eink") || has("e-ink") || has("paper")) { - SetBackgroundMode(BackgroundMode::Light); - gOverridePalette = EInk(); - gPaletteOverride = true; - gOverrideThemeName = "eink"; - return true; - } - - // Unknown -> clear override so default light/dark applies; return false. - gPaletteOverride = false; - gOverrideThemeName.clear(); - return false; -} - - -// Minimal SyntaxInk mapping for Qt builds, returning KteColor -[[maybe_unused]] static KteColor -SyntaxInk(const TokenKind k) -{ - const bool dark = (GetBackgroundMode() == BackgroundMode::Dark); - const KteColor def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440); - switch (k) { - case TokenKind::Keyword: - return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC); - case TokenKind::Type: - return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A); - case TokenKind::String: - return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E); - case TokenKind::Char: - return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E); - case TokenKind::Comment: - return dark ? RGBA(0x616E88) : RGBA(0x7A869A); - case TokenKind::Number: - return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900); - case TokenKind::Preproc: - return dark ? RGBA(0xD08770) : RGBA(0xAF3A03); - case TokenKind::Constant: - return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F); - case TokenKind::Function: - return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4); - case TokenKind::Operator: - return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); // as the ImGui palette - case TokenKind::Punctuation: - return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); - case TokenKind::Identifier: - return def; - case TokenKind::Whitespace: - return def; - case TokenKind::Error: - return dark ? RGBA(0xBF616A) : RGBA(0xCC0000); - case TokenKind::Default: default: - return def; - } -} -} // namespace kte - -#else - #include #include #include @@ -1109,5 +833,3 @@ SyntaxInk(const TokenKind k) } } } // namespace kte - -#endif // KTE_USE_QT diff --git a/ImGuiInputHandler.cc b/ImGuiInputHandler.cc index 9119647..8e25e93 100644 --- a/ImGuiInputHandler.cc +++ b/ImGuiInputHandler.cc @@ -12,7 +12,7 @@ // Verbose k-prefix suffix logging for debugging macOS/SDL key translation // issues. Default to off; enable by defining IMGUI_IH_DEBUG=1 at compile -// time. Mirrors QtInputHandler.cc's QT_IH_DEBUG gate. +// time. #ifndef IMGUI_IH_DEBUG #define IMGUI_IH_DEBUG 0 #endif diff --git a/QtFrontend.cc b/QtFrontend.cc deleted file mode 100644 index e9ff502..0000000 --- a/QtFrontend.cc +++ /dev/null @@ -1,923 +0,0 @@ -#include "QtFrontend.h" -#include "SearchHighlight.h" -#include "TermWidth.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Editor.h" -#include "Command.h" -#include "Buffer.h" -#include "GUITheme.h" -#include "Highlight.h" - -namespace { -// Text layout, shared by painting and the editor's row/column count: they -// must agree, or the editor scrolls the cursor to a column the window does -// not show. -struct TextLayout { - int line_h; - qreal cell_w; // fractional: rounding it drifts over long lines - int ch_w; // cell_w rounded, for pixel-delta arithmetic - int status_h; - static constexpr int pad_l = 8, pad_t = 6, pad_r = 8, pad_b = 6; - - - explicit TextLayout(const QFont &f) - : line_h(std::max(1, QFontMetrics(f).height())), - cell_w(std::max(1.0, QFontMetricsF(f).horizontalAdvance(QStringLiteral(" ")))), - ch_w(std::max(1, qRound(cell_w))), - status_h(line_h + 6) {} - - - // Rows (text rows plus the status row, see Editor::ContentRows) and - // columns for a w x h window. - void SetEditorDimensions(Editor &ed, int w, int h) const - { - const int avail_w = std::max(0, w - pad_l - pad_r); - const int avail_h = std::max(0, h - pad_t - pad_b - status_h); - ed.SetDimensions(std::max(1, avail_h / line_h) + 1, - std::max(1, static_cast(avail_w / cell_w))); - } -}; - - -class MainWindow : public QWidget { -public: - explicit MainWindow(class QtInputHandler &ih, QWidget *parent = nullptr) - : QWidget(parent), input_(ih) - { - // Match ImGui window title format - setWindowTitle(QStringLiteral("kge - kyle's graphical editor ") - + QStringLiteral(KTE_VERSION_STR)); - resize(1280, 800); - setFocusPolicy(Qt::StrongFocus); - } - - - bool WasClosed() const - { - return closed_; - } - - - void SetEditor(Editor *ed) - { - ed_ = ed; - } - - - void SetFontFamilyAndSize(QString family, int px) - { - if (family.isEmpty()) - family = QStringLiteral("Brass Mono"); - if (px <= 0) - px = 18; - font_family_ = std::move(family); - font_px_ = px; - update(); - } - -protected: - void keyPressEvent(QKeyEvent *event) override - { - // Route to editor keymap; if handled, accept and stop propagation so - // Qt doesn't trigger any default widget shortcuts. - if (input_.ProcessKeyEvent(*event)) { - event->accept(); - return; - } - QWidget::keyPressEvent(event); - } - - - void paintEvent(QPaintEvent *event) override - { - Q_UNUSED(event); - QPainter p(this); - p.setRenderHint(QPainter::TextAntialiasing, true); - - // Colors from GUITheme palette (Qt branch) - auto to_qcolor = [](const KteColor &c) -> QColor { - int r = int(std::round(c.x * 255.0f)); - int g = int(std::round(c.y * 255.0f)); - int b = int(std::round(c.z * 255.0f)); - int a = int(std::round(c.w * 255.0f)); - return QColor(r, g, b, a); - }; - const auto pal = kte::GetPalette(); - const QColor bg = to_qcolor(pal.bg); - const QColor fg = to_qcolor(pal.fg); - const QColor sel_bg = to_qcolor(pal.sel_bg); - const QColor cur_bg = to_qcolor(pal.cur_bg); - const QColor status_bg = to_qcolor(pal.status_bg); - const QColor status_fg = to_qcolor(pal.status_fg); - - // Background - p.fillRect(rect(), bg); - - // Font/metrics (configured or defaults) - QFont f(font_family_, font_px_); - p.setFont(f); - QFontMetrics fm(f); - const TextLayout layout(f); - const int line_h = layout.line_h; - const qreal cell_w = layout.cell_w; - const int pad_l = TextLayout::pad_l; - const int pad_t = TextLayout::pad_t; - const int pad_r = TextLayout::pad_r; - const int pad_b = TextLayout::pad_b; - const int status_h = layout.status_h; - - // Content area (text viewport) - const QRect content_rect(pad_l, - pad_t, - width() - pad_l - pad_r, - height() - pad_t - pad_b - status_h); - - // Text viewport occupies all content area (no extra title row) - QRect viewport(content_rect.x(), content_rect.y(), content_rect.width(), content_rect.height()); - - // Draw buffer contents - if (ed_ && viewport.height() > 0 && viewport.width() > 0) { - const Buffer *buf = ed_->CurrentBuffer(); - if (buf) { - const std::size_t nrows = buf->Nrows(); - const std::size_t rowoffs = buf->Rowoffs(); - const std::size_t coloffs = buf->Coloffs(); - const std::size_t cy = buf->Cury(); - const std::size_t cx = buf->Curx(); - - // Visible line count - const int max_lines = (line_h > 0) ? (viewport.height() / line_h) : 0; - const std::size_t last_row = std::min( - nrows, rowoffs + std::max(0, max_lines)); - - - // Prepare painter clip to viewport - p.save(); - p.setClipRect(viewport); - - // Search matches: the pattern is compiled once per frame. - const kte::SearchHighlight search_hl(*ed_); - std::vector > hl_src_ranges; - - std::vector col_of; // per line, see DisplayColumns - // Left edge of display column col (fractional pixels). - auto col_x = [&](std::size_t col) -> qreal { - return viewport.x() + (static_cast(col) - static_cast(coloffs)) * cell_w; - }; - // Columns the viewport shows, plus one partly visible. - const std::size_t vis_cols = static_cast(viewport.width() / cell_w) + 1; - - // Iterate visible lines - for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) { - // Get line as string for regex/iterator usage and general string ops. - const std::string line = buf->GetLineString(i); - const int y = viewport.y() + static_cast(vis_idx) * line_h; - const int baseline = y + fm.ascent(); - - // Display column (cells, as the command layer's scrolling - // counts them) of each source byte. - kte::DisplayColumns(line, col_of); - auto src_to_rx_line = [&](std::size_t src_col) -> std::size_t { - return col_of[std::min(src_col, line.size())]; - }; - // Draw source bytes [s, e) (tabs expanded) at their column; - // the viewport clip hides what lies left of coloffs. ASCII runs - // are drawn whole; other characters one at a time at their - // cell, since a glyph from a fallback font can be wider than a - // cell and would push the rest of the run out of place. - auto draw_text_at = [&](std::size_t col, const char *d, std::size_t n) { - p.drawText(QPointF(col_x(col), baseline), QString::fromUtf8(d, static_cast(n))); - }; - auto draw_seg = [&](std::size_t s, std::size_t e, const QColor &color) { - // Only the visible columns: characters that end at or - // before coloffs, or start right of the viewport, are - // skipped. A tab or wide character straddling coloffs - // starts left of it; it is drawn at its own column and - // the clip cuts its left part. - const std::size_t first = s; - s = static_cast(std::lower_bound(col_of.begin() + s, col_of.begin() + e, - coloffs) - col_of.begin()); - if (s > first && col_of[s] > coloffs) { - // The character before s ends past coloffs: back up to - // its first byte. - --s; - while (s > first && (static_cast(line[s]) & 0xC0) == 0x80) - --s; - } - e = static_cast(std::lower_bound(col_of.begin() + s, col_of.begin() + e, - coloffs + vis_cols) - col_of.begin()); - if (e <= s) - return; - p.setPen(color); - std::string run; // ASCII, tabs expanded - std::size_t run_col = col_of[s]; - for (std::size_t k = s; k < e;) { - const unsigned char c = static_cast(line[k]); - if (c < 0x80) { - if (run.empty()) - run_col = col_of[k]; - if (c == '\t') - run.append(col_of[k + 1] - col_of[k], ' '); - else - run.push_back(static_cast(c)); - ++k; - continue; - } - if (!run.empty()) { - draw_text_at(run_col, run.data(), run.size()); - run.clear(); - } - std::size_t n = 1; // bytes sharing this character's column - while (k + n < e && col_of[k + n] == col_of[k] && - (static_cast(line[k + n]) & 0xC0) == 0x80) - ++n; - draw_text_at(col_of[k], line.data() + k, n); - k += n; - } - if (!run.empty()) - draw_text_at(run_col, run.data(), run.size()); - }; - - // Search-match background highlights first (under text) - if (search_hl.Active()) { - search_hl.Ranges(line, hl_src_ranges); - if (!hl_src_ranges.empty()) { - const bool has_current = - ed_->SearchMatchLen() > 0 && ed_->SearchMatchY() == i; - const std::size_t cur_x = has_current ? ed_->SearchMatchX() : 0; - const std::size_t cur_end = has_current - ? (ed_->SearchMatchX() + ed_->SearchMatchLen()) - : 0; - for (const auto &rg: hl_src_ranges) { - std::size_t sx = rg.first, ex = rg.second; - std::size_t rx_s = src_to_rx_line(sx); - std::size_t rx_e = src_to_rx_line(ex); - if (rx_e <= coloffs) - continue; // fully left of view - const int vx0 = qRound(col_x(std::max(rx_s, coloffs))); - const int vx1 = qRound(col_x(rx_e)); - QRect r(vx0, y, std::max(0, vx1 - vx0), line_h); - if (r.width() <= 0) - continue; - bool is_current = - has_current && sx == cur_x && ex == cur_end; - QColor col = is_current - ? QColor(255, 220, 120, 140) - : QColor(200, 200, 0, 90); - p.fillRect(r, col); - } - } - } - - // Selection background (if active on this line) - if (buf->MarkSet() && ( - i == buf->MarkCury() || i == cy || ( - i > std::min(buf->MarkCury(), cy) && i < std::max( - buf->MarkCury(), cy)))) { - std::size_t sx = 0, ex = 0; - if (buf->MarkCury() == i && cy == i) { - sx = std::min(buf->MarkCurx(), cx); - ex = std::max(buf->MarkCurx(), cx); - } else if (i == buf->MarkCury()) { - sx = buf->MarkCurx(); - ex = line.size(); - } else if (i == cy) { - sx = 0; - ex = cx; - } else { - sx = 0; - ex = line.size(); - } - std::size_t rx_s = src_to_rx_line(sx); - std::size_t rx_e = src_to_rx_line(ex); - if (rx_e > coloffs) { - const int vx0 = qRound(col_x(std::max(rx_s, coloffs))); - const int vx1 = qRound(col_x(rx_e)); - QRect sel_r(vx0, y, std::max(0, vx1 - vx0), line_h); - if (sel_r.width() > 0) - p.fillRect(sel_r, sel_bg); - } - } - - // Syntax-colored spans, the text between them in the default - // color (drawing only the spans dropped unhighlighted text such - // as operators). - if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()-> - HasHighlighter()) { - const kte::LineHighlight &lh = buf->Highlighter()->GetLine( - *buf, static_cast(i), buf->Version()); - std::vector spans; // clamped, ordered, whole characters - kte::SanitizeSpans(line, lh.spans, spans); - - auto colorFor = [](kte::TokenKind k) -> QColor { - // GUITheme provides colors via ImGui vector; avoid direct dependency types - const auto v = kte::SyntaxInk(k); - return QColor(int(v.x * 255.0f), int(v.y * 255.0f), - int(v.z * 255.0f), int(v.w * 255.0f)); - }; - - std::size_t pos = 0; - for (const auto &sp: spans) { - const std::size_t s = std::max(static_cast(sp.col_start), pos); - draw_seg(pos, s, fg); - draw_seg(s, static_cast(sp.col_end), colorFor(sp.kind)); - pos = std::max(pos, static_cast(sp.col_end)); - } - draw_seg(pos, line.size(), fg); - } else { - draw_seg(0, line.size(), fg); - } - - // Cursor indicator on current line - if (i == cy) { - const std::size_t rx_cur = src_to_rx_line(cx); - if (rx_cur >= coloffs) { - const int x0 = qRound(col_x(rx_cur)); - p.fillRect(QRect(x0, y, qRound(col_x(rx_cur + 1)) - x0, line_h), cur_bg); - } - } - } - - p.restore(); - } - } - - // Status bar - const int bar_y = height() - status_h; - QRect status_rect(0, bar_y, width(), status_h); - p.fillRect(status_rect, status_bg); - p.setPen(status_fg); - if (ed_) { - const int pad = 6; - const int left_x = status_rect.x() + pad; - const int right_x_max = status_rect.x() + status_rect.width() - pad; - const int baseline_y = bar_y + (status_h + fm.ascent() - fm.descent()) / 2; - - // If a prompt is active, mirror ImGui/TUI: show only the prompt across the bar - if (ed_->PromptActive()) { - std::string label = ed_->PromptLabel(); - std::string text = ed_->PromptText(); - - // Map $HOME to ~ for path prompts (Open/Save/Chdir) - auto kind = ed_->CurrentPromptKind(); - if (kind == Editor::PromptKind::OpenFile || - kind == Editor::PromptKind::SaveAs || - kind == Editor::PromptKind::Chdir) { - const char *home_c = std::getenv("HOME"); - if (home_c && *home_c) { - std::string home(home_c); - if (text.rfind(home, 0) == 0) { - std::string rest = text.substr(home.size()); - if (rest.empty()) - text = "~"; - else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\')) - text = std::string("~") + rest; - } - } - } - - std::string prefix; - if (kind == Editor::PromptKind::Command) - prefix = ": "; - else if (!label.empty()) - prefix = label + ": "; - - // Compose text and elide per behavior: - const int max_w = status_rect.width() - 2 * pad; - QString qprefix = QString::fromStdString(prefix); - QString qtext = QString::fromStdString(text); - int avail_w = std::max(0, max_w - fm.horizontalAdvance(qprefix)); - Qt::TextElideMode mode = Qt::ElideRight; - if (kind == Editor::PromptKind::OpenFile || - kind == Editor::PromptKind::SaveAs || - kind == Editor::PromptKind::Chdir) { - mode = Qt::ElideLeft; - } - QString shown = fm.elidedText(qtext, mode, avail_w); - p.drawText(left_x, baseline_y, qprefix + shown); - } else { - // Build left segment: app/version, buffer idx/total, filename [+dirty], line count - QString left; - left += QStringLiteral("kge "); - left += QStringLiteral(KTE_VERSION_STR); - - const Buffer *buf = ed_->CurrentBuffer(); - if (buf) { - // buffer index/total - std::size_t total = ed_->BufferCount(); - if (total > 0) { - std::size_t idx1 = ed_->CurrentBufferIndex() + 1; // 1-based - left += QStringLiteral(" ["); - left += QString::number(static_cast(idx1)); - left += QStringLiteral("/"); - left += QString::number(static_cast(total)); - left += QStringLiteral("] "); - } else { - left += QStringLiteral(" "); - } - - // buffer display name - std::string disp; - try { - disp = ed_->DisplayNameFor(*buf); - } catch (...) { - disp = buf->Filename(); - } - if (disp.empty()) - disp = "[No Name]"; - left += QString::fromStdString(disp); - if (buf->Dirty()) - left += QStringLiteral(" *"); - - // total lines suffix " L" - unsigned long lcount = static_cast(buf->Nrows()); - left += QStringLiteral(" "); - left += QString::number(static_cast(lcount)); - left += QStringLiteral("L"); - } - - // Build right segment: cursor and mark - QString right; - if (buf) { - int row1 = static_cast(buf->Cury()) + 1; - int col1 = static_cast(buf->Curx()) + 1; - bool have_mark = buf->MarkSet(); - int mrow1 = have_mark ? static_cast(buf->MarkCury()) + 1 : 0; - int mcol1 = have_mark ? static_cast(buf->MarkCurx()) + 1 : 0; - if (have_mark) - right = QString("%1,%2 | M: %3,%4").arg(row1).arg(col1).arg(mrow1).arg( - mcol1); - else - right = QString("%1,%2 | M: not set").arg(row1).arg(col1); - } - - // Middle message: status text - QString mid = QString::fromStdString(ed_->Status()); - - // Measure and layout - int left_w = fm.horizontalAdvance(left); - int right_w = fm.horizontalAdvance(right); - int lx = left_x; - int rx = std::max(left_x, right_x_max - right_w); - - // If overlap, elide left to make space for right - if (lx + left_w + pad > rx) { - int max_left_w = std::max(0, rx - lx - pad); - left = fm.elidedText(left, Qt::ElideRight, max_left_w); - left_w = fm.horizontalAdvance(left); - } - - // Draw left and right - p.drawText(lx, baseline_y, left); - if (!right.isEmpty()) - p.drawText(rx, baseline_y, right); - - // Middle message clipped between end of left and start of right - int mid_left = lx + left_w + pad; - int mid_right = std::max(mid_left, rx - pad); - int mid_w = std::max(0, mid_right - mid_left); - if (mid_w > 0 && !mid.isEmpty()) { - QString mid_show = fm.elidedText(mid, Qt::ElideRight, mid_w); - p.save(); - p.setClipRect(QRect(mid_left, bar_y, mid_w, status_h)); - p.drawText(mid_left, baseline_y, mid_show); - p.restore(); - } - } - } - } - - - void resizeEvent(QResizeEvent *event) override - { - QWidget::resizeEvent(event); - if (!ed_) - return; - // Update editor dimensions based on new size - QFont f(font_family_, font_px_); - TextLayout(f).SetEditorDimensions(*ed_, width(), height()); - } - - - void wheelEvent(QWheelEvent *event) override - { - if (!ed_) { - QWidget::wheelEvent(event); - return; - } - Buffer *buf = ed_->CurrentBuffer(); - if (!buf) { - QWidget::wheelEvent(event); - return; - } - - // Recompute metrics to map pixel deltas to rows/cols - const TextLayout layout(QFont(font_family_, font_px_)); - const int line_h = layout.line_h; - const int ch_w = layout.ch_w; - - // Determine scroll intent: use pixelDelta when available (trackpads), otherwise angleDelta - QPoint pixel = event->pixelDelta(); - QPoint angle = event->angleDelta(); - - double v_lines_delta = 0.0; - double h_cols_delta = 0.0; - - // Horizontal scroll with Shift or explicit horizontal delta - bool horiz_mode = (event->modifiers() & Qt::ShiftModifier) || (!pixel.isNull() && pixel.x() != 0) || ( - !angle.isNull() && angle.x() != 0); - - if (!pixel.isNull()) { - // Trackpad smooth scrolling (pixels) - v_lines_delta = -static_cast(pixel.y()) / std::max(1, line_h); - h_cols_delta = -static_cast(pixel.x()) / std::max(1, ch_w); - } else if (!angle.isNull()) { - // Mouse wheel: 120 units per notch; map one notch to 3 lines similar to ImGui UX - v_lines_delta = -static_cast(angle.y()) / 120.0 * 3.0; - // For horizontal wheels, each notch scrolls 8 columns - h_cols_delta = -static_cast(angle.x()) / 120.0 * 8.0; - } - - // Accumulate fractional deltas across events - v_scroll_accum_ += v_lines_delta; - h_scroll_accum_ += h_cols_delta; - - int d_rows = 0; - int d_cols = 0; - if (std::fabs(v_scroll_accum_) >= 1.0 && (!horiz_mode || std::fabs(v_scroll_accum_) > std::fabs( - h_scroll_accum_))) { - d_rows = static_cast(v_scroll_accum_); - v_scroll_accum_ -= d_rows; - } - if (std::fabs(h_scroll_accum_) >= 1.0 && (horiz_mode || std::fabs(h_scroll_accum_) >= std::fabs( - v_scroll_accum_))) { - d_cols = static_cast(h_scroll_accum_); - h_scroll_accum_ -= d_cols; - } - - if (d_rows != 0 || d_cols != 0) { - std::size_t new_rowoffs = buf->Rowoffs(); - std::size_t new_coloffs = buf->Coloffs(); - // Clamp vertical between 0 and last row (leaving at least one visible line) - if (d_rows != 0) { - long nr = static_cast(new_rowoffs) + d_rows; - if (nr < 0) - nr = 0; - const auto nrows = static_cast(buf->Nrows()); - if (nr > std::max(0L, nrows - 1)) - nr = std::max(0L, nrows - 1); - new_rowoffs = static_cast(nr); - } - if (d_cols != 0) { - long nc = static_cast(new_coloffs) + d_cols; - if (nc < 0) - nc = 0; - new_coloffs = static_cast(nc); - } - buf->SetOffsets(new_rowoffs, new_coloffs); - update(); - event->accept(); - return; - } - - QWidget::wheelEvent(event); - } - - - void closeEvent(QCloseEvent *event) override - { - closed_ = true; - QWidget::closeEvent(event); - } - -private: - QtInputHandler &input_; - bool closed_ = false; - Editor *ed_ = nullptr; - double v_scroll_accum_ = 0.0; - double h_scroll_accum_ = 0.0; - QString font_family_ = QStringLiteral("Brass Mono"); - int font_px_ = 18; -}; -} // namespace - -bool -GUIFrontend::Init(int &argc, char **argv, Editor &ed) -{ - app_ = new QApplication(argc, argv); - - window_ = new MainWindow(input_); - window_->show(); - // Ensure the window becomes the active, focused window so it receives key events - window_->activateWindow(); - window_->raise(); - window_->setFocus(Qt::OtherFocusReason); - - renderer_.Attach(window_); - input_.Attach(&ed); - if (auto *mw = dynamic_cast(window_)) - mw->SetEditor(&ed); - - // Load GUI configuration (kge.ini) and configure font for Qt - config_ = GUIConfig::Load(); - - // Apply background mode from config to match ImGui frontend behavior - if (config_.background == "light") - kte::SetBackgroundMode(kte::BackgroundMode::Light); - else - kte::SetBackgroundMode(kte::BackgroundMode::Dark); - - // Apply theme by name for Qt palette-based theming (maps to named palettes). - // If unknown, falls back to the generic light/dark palette. - (void) kte::ApplyQtThemeByName(config_.theme); - if (window_) - window_->update(); - - // Map GUIConfig font name to a system family (Qt uses installed fonts) - auto choose_family = [](const std::string &name) -> QString { - QString fam; - std::string n = name; - std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) { - return (char) std::tolower(c); - }); - if (n.empty() || n == "default" || n == "brassmono" || n == "brassmonocode") { - fam = QStringLiteral("Brass Mono"); - } else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") { - fam = QStringLiteral("JetBrains Mono"); - } else if (n == "iosevka") { - fam = QStringLiteral("Iosevka"); - } else if (n == "inconsolata" || n == "inconsolataex") { - fam = QStringLiteral("Inconsolata"); - } else if (n == "space" || n == "spacemono" || n == "space mono") { - fam = QStringLiteral("Space Mono"); - } else if (n == "go") { - fam = QStringLiteral("Go Mono"); - } else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") { - fam = QStringLiteral("IBM Plex Mono"); - } else if (n == "fira" || n == "fira code" || n == "fira-code") { - fam = QStringLiteral("Fira Code"); - } else if (!name.empty()) { - fam = QString::fromStdString(name); - } - - // Validate availability; choose a fallback if needed - const auto families = QFontDatabase::families(); - if (!fam.isEmpty() && families.contains(fam)) { - return fam; - } - // Preferred fallback chain on macOS; otherwise, try common monospace families - const QStringList fallbacks = { - QStringLiteral("Brass Mono"), - QStringLiteral("JetBrains Mono"), - QStringLiteral("SF Mono"), - QStringLiteral("Menlo"), - QStringLiteral("Monaco"), - QStringLiteral("Courier New"), - QStringLiteral("Courier"), - QStringLiteral("Monospace") - }; - for (const auto &fb: fallbacks) { - if (families.contains(fb)) - return fb; - } - // As a last resort, return the request (Qt will substitute) - return fam.isEmpty() ? QStringLiteral("Monospace") : fam; - }; - - QString family = choose_family(config_.font); - int px_size = (config_.font_size > 0.0f) ? (int) std::lround(config_.font_size) : 18; - if (auto *mw = dynamic_cast(window_)) { - mw->SetFontFamilyAndSize(family, px_size); - } - // Track current font in globals for command/status queries - kte::gCurrentFontFamily = family.toStdString(); - kte::gCurrentFontSize = static_cast(px_size); - - // Set initial dimensions based on font metrics - QFont f(family, px_size); - TextLayout(f).SetEditorDimensions(ed, window_->width(), window_->height()); - - return true; -} - - -void -GUIFrontend::Step(Editor &ed, bool &running) -{ - // Pump Qt events - if (app_) - app_->processEvents(); - - // Allow deferred opens (including swap recovery prompts) to run. - ed.ProcessPendingOpens(); - - // Drain input queue - for (;;) { - MappedInput mi; - if (!input_.Poll(mi)) - break; - if (mi.hasCommand) { - Execute(ed, mi.id, mi.arg, mi.count); - } - } - - if (ed.QuitRequested()) { - running = false; - } - - // --- Visual File Picker (Qt): invoked via CommandId::VisualFilePickerToggle --- - if (ed.FilePickerVisible()) { - QString startDir; - if (!ed.FilePickerDir().empty()) { - startDir = QString::fromStdString(ed.FilePickerDir()); - } - QFileDialog dlg(window_, QStringLiteral("Open File"), startDir); - dlg.setFileMode(QFileDialog::ExistingFile); - if (dlg.exec() == QDialog::Accepted) { - const QStringList files = dlg.selectedFiles(); - if (!files.isEmpty()) { - const QString fp = files.front(); - ed.RequestOpenFile(fp.toStdString()); - (void) ed.ProcessPendingOpens(); - // Update picker dir for next time - QFileInfo info(fp); - ed.SetFilePickerDir(info.dir().absolutePath().toStdString()); - } - } - // Close picker overlay regardless of outcome - ed.SetFilePickerVisible(false); - if (window_) - window_->update(); - } - - // Apply any queued theme change requests (from command handler) - if (kte::gThemeChangePending) { - if (!kte::gThemeChangeRequest.empty()) { - // Apply Qt palette theme by name; if unknown, keep current palette - (void) kte::ApplyQtThemeByName(kte::gThemeChangeRequest); - } - kte::gThemeChangePending = false; - kte::gThemeChangeRequest.clear(); - if (window_) - window_->update(); - } - - // Visual font picker request (Qt only) - if (kte::gFontDialogRequested) { - // Seed initial font from current or default - QFont seed; - if (!kte::gCurrentFontFamily.empty()) { - seed = QFont(QString::fromStdString(kte::gCurrentFontFamily), - (int) std::lround(kte::gCurrentFontSize > 0 ? kte::gCurrentFontSize : 18)); - } else { - seed = window_ ? window_->font() : QFont(); - } - bool ok = false; - const QFont chosen = QFontDialog::getFont(&ok, seed, window_, QStringLiteral("Choose Editor Font")); - if (ok) { - // Queue font change via existing hooks - kte::gFontFamilyRequest = chosen.family().toStdString(); - // Use pixel size if available, otherwise convert from point size approximately - int px = chosen.pixelSize(); - if (px <= 0) { - // Approximate points to pixels (96 DPI assumption); Qt will rasterize appropriately - px = (int) std::lround(chosen.pointSizeF() * 96.0 / 72.0); - if (px <= 0) - px = 18; - } - kte::gFontSizeRequest = static_cast(px); - kte::gFontChangePending = true; - } - kte::gFontDialogRequested = false; - if (window_) - window_->update(); - } - - // Apply any queued font change requests (Qt) - if (kte::gFontChangePending) { - // Derive target family - auto map_family = [](const std::string &name) -> QString { - std::string n = name; - std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) { - return (char) std::tolower(c); - }); - QString fam; - if (n == "brass" || n == "brassmono" || n == "brass mono") { - fam = QStringLiteral("Brass Mono"); - } else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") { - fam = QStringLiteral("JetBrains Mono"); - } else if (n == "iosevka") { - fam = QStringLiteral("Iosevka"); - } else if (n == "inconsolata" || n == "inconsolataex") { - fam = QStringLiteral("Inconsolata"); - } else if (n == "space" || n == "spacemono" || n == "space mono") { - fam = QStringLiteral("Space Mono"); - } else if (n == "go") { - fam = QStringLiteral("Go Mono"); - } else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") { - fam = QStringLiteral("IBM Plex Mono"); - } else if (n == "fira" || n == "fira code" || n == "fira-code") { - fam = QStringLiteral("Fira Code"); - } else if (!name.empty()) { - fam = QString::fromStdString(name); - } - // Validate availability; choose fallback if needed - const auto families = QFontDatabase::families(); - if (!fam.isEmpty() && families.contains(fam)) { - return fam; - } - // Fallback chain - const QStringList fallbacks = { - QStringLiteral("Brass Mono"), - QStringLiteral("JetBrains Mono"), - QStringLiteral("SF Mono"), - QStringLiteral("Menlo"), - QStringLiteral("Monaco"), - QStringLiteral("Courier New"), - QStringLiteral("Courier"), - QStringLiteral("Monospace") - }; - for (const auto &fb: fallbacks) { - if (families.contains(fb)) - return fb; - } - return fam.isEmpty() ? QStringLiteral("Monospace") : fam; - }; - - QString target_family; - if (!kte::gFontFamilyRequest.empty()) { - target_family = map_family(kte::gFontFamilyRequest); - } else if (!kte::gCurrentFontFamily.empty()) { - target_family = QString::fromStdString(kte::gCurrentFontFamily); - } - int target_px = 0; - if (kte::gFontSizeRequest > 0.0f) { - target_px = (int) std::lround(kte::gFontSizeRequest); - } else if (kte::gCurrentFontSize > 0.0f) { - target_px = (int) std::lround(kte::gCurrentFontSize); - } - if (target_px <= 0) - target_px = 18; - if (target_family.isEmpty()) - target_family = QStringLiteral("Monospace"); - - if (auto *mw = dynamic_cast(window_)) { - mw->SetFontFamilyAndSize(target_family, target_px); - } - // Update globals - kte::gCurrentFontFamily = target_family.toStdString(); - kte::gCurrentFontSize = static_cast(target_px); - // Reset requests - kte::gFontChangePending = false; - kte::gFontFamilyRequest.clear(); - kte::gFontSizeRequest = 0.0f; - - // Recompute editor dimensions to match new metrics - QFont f(target_family, target_px); - TextLayout(f).SetEditorDimensions(ed, window_ ? window_->width() : 0, - window_ ? window_->height() : 0); - - if (window_) - window_->update(); - } - - // Draw current frame (request repaint) - renderer_.Draw(ed); - - // Detect window close - if (auto *mw = dynamic_cast(window_)) { - if (mw->WasClosed()) { - running = false; - } - } -} - - -void -GUIFrontend::Shutdown() -{ - if (window_) { - window_->close(); - delete window_; - window_ = nullptr; - } - if (app_) { - delete app_; - app_ = nullptr; - } -} \ No newline at end of file diff --git a/QtFrontend.h b/QtFrontend.h deleted file mode 100644 index b11bd0b..0000000 --- a/QtFrontend.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * QtFrontend - couples QtInputHandler + QtRenderer and owns Qt lifecycle - */ -#pragma once - -#include "Frontend.h" -#include "GUIConfig.h" -#include "QtInputHandler.h" -#include "QtRenderer.h" - -class QApplication; -class QWidget; - -// Keep the public class name GUIFrontend to match main.cc selection logic. -class GUIFrontend final : public Frontend { -public: - GUIFrontend() = default; - - ~GUIFrontend() override = default; - - bool Init(int &argc, char **argv, Editor &ed) override; - - void Step(Editor &ed, bool &running) override; - - void Shutdown() override; - -private: - GUIConfig config_{}; - QtInputHandler input_{}; - QtRenderer renderer_{}; - - QApplication *app_ = nullptr; // owned - QWidget *window_ = nullptr; // owned - int width_ = 1280; - int height_ = 800; -}; diff --git a/QtInputHandler.cc b/QtInputHandler.cc deleted file mode 100644 index d839639..0000000 --- a/QtInputHandler.cc +++ /dev/null @@ -1,536 +0,0 @@ -// Refactor: route all key processing through command subsystem, mirroring ImGuiInputHandler - -#include "QtInputHandler.h" - -#include - -#include - -#include "Editor.h" -#include "KKeymap.h" - -// Temporary verbose logging to debug macOS Qt key translation issues -// Default to off; enable by defining QT_IH_DEBUG=1 at compile time when needed. -#ifndef QT_IH_DEBUG -#define QT_IH_DEBUG 0 -#endif - -#if QT_IH_DEBUG -#include -static const char * -mods_str(Qt::KeyboardModifiers m) -{ - static thread_local char buf[64]; - buf[0] = '\0'; - bool first = true; - auto add = [&](const char *s) { - if (!first) - std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "|"); - std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "%s", s); - first = false; - }; - if (m & Qt::ShiftModifier) - add("Shift"); - if (m & Qt::ControlModifier) - add("Ctrl"); - if (m & Qt::AltModifier) - add("Alt"); - if (m & Qt::MetaModifier) - add("Meta"); - if (first) - std::snprintf(buf, sizeof(buf), "none"); - return buf; -} -#define LOGF(...) std::fprintf(stderr, __VA_ARGS__) -#else -#define LOGF(...) ((void)0) -#endif - -static bool -IsPrintableQt(const QKeyEvent &e) -{ - // Printable if it yields non-empty text and no Ctrl/Meta modifier - if (e.modifiers() & (Qt::ControlModifier | Qt::MetaModifier)) - return false; - const QString t = e.text(); - return !t.isEmpty() && !t.at(0).isNull(); -} - - -static int -ToAsciiKey(const QKeyEvent &e) -{ - const QString t = e.text(); - if (!t.isEmpty()) { - const QChar c = t.at(0); - if (!c.isNull()) - return KLowerAscii(c.unicode()); - } - // When modifiers (like Control) are held, Qt::text() can be empty on macOS. - // Fall back to mapping common virtual keys to ASCII. - switch (e.key()) { - case Qt::Key_A: - return 'a'; - case Qt::Key_B: - return 'b'; - case Qt::Key_C: - return 'c'; - case Qt::Key_D: - return 'd'; - case Qt::Key_E: - return 'e'; - case Qt::Key_F: - return 'f'; - case Qt::Key_G: - return 'g'; - case Qt::Key_H: - return 'h'; - case Qt::Key_I: - return 'i'; - case Qt::Key_J: - return 'j'; - case Qt::Key_K: - return 'k'; - case Qt::Key_L: - return 'l'; - case Qt::Key_M: - return 'm'; - case Qt::Key_N: - return 'n'; - case Qt::Key_O: - return 'o'; - case Qt::Key_P: - return 'p'; - case Qt::Key_Q: - return 'q'; - case Qt::Key_R: - return 'r'; - case Qt::Key_S: - return 's'; - case Qt::Key_T: - return 't'; - case Qt::Key_U: - return 'u'; - case Qt::Key_V: - return 'v'; - case Qt::Key_W: - return 'w'; - case Qt::Key_X: - return 'x'; - case Qt::Key_Y: - return 'y'; - case Qt::Key_Z: - return 'z'; - case Qt::Key_0: - return '0'; - case Qt::Key_1: - return '1'; - case Qt::Key_2: - return '2'; - case Qt::Key_3: - return '3'; - case Qt::Key_4: - return '4'; - case Qt::Key_5: - return '5'; - case Qt::Key_6: - return '6'; - case Qt::Key_7: - return '7'; - case Qt::Key_8: - return '8'; - case Qt::Key_9: - return '9'; - case Qt::Key_Comma: - return ','; - case Qt::Key_Period: - return '.'; - case Qt::Key_Semicolon: - return ';'; - case Qt::Key_Apostrophe: - return '\''; - case Qt::Key_Minus: - return '-'; - case Qt::Key_Equal: - return '='; - case Qt::Key_Slash: - return '/'; - case Qt::Key_Backslash: - return '\\'; - case Qt::Key_BracketLeft: - return '['; - case Qt::Key_BracketRight: - return ']'; - case Qt::Key_QuoteLeft: - return '`'; - case Qt::Key_Space: - return ' '; - default: - break; - } - return 0; -} - - -// Case-preserving ASCII derivation for k-prefix handling where we need to -// distinguish between 'C' and 'c'. Falls back to virtual-key mapping if -// event text is unavailable (common when Control/Meta held on macOS). -static int -ToAsciiKeyPreserveCase(const QKeyEvent &e) -{ - const QString t = e.text(); - if (!t.isEmpty()) { - const QChar c = t.at(0); - if (!c.isNull()) - return c.unicode(); - } - // Fall back to virtual key mapping (letters as uppercase A..Z) - switch (e.key()) { - case Qt::Key_A: - return 'A'; - case Qt::Key_B: - return 'B'; - case Qt::Key_C: - return 'C'; - case Qt::Key_D: - return 'D'; - case Qt::Key_E: - return 'E'; - case Qt::Key_F: - return 'F'; - case Qt::Key_G: - return 'G'; - case Qt::Key_H: - return 'H'; - case Qt::Key_I: - return 'I'; - case Qt::Key_J: - return 'J'; - case Qt::Key_K: - return 'K'; - case Qt::Key_L: - return 'L'; - case Qt::Key_M: - return 'M'; - case Qt::Key_N: - return 'N'; - case Qt::Key_O: - return 'O'; - case Qt::Key_P: - return 'P'; - case Qt::Key_Q: - return 'Q'; - case Qt::Key_R: - return 'R'; - case Qt::Key_S: - return 'S'; - case Qt::Key_T: - return 'T'; - case Qt::Key_U: - return 'U'; - case Qt::Key_V: - return 'V'; - case Qt::Key_W: - return 'W'; - case Qt::Key_X: - return 'X'; - case Qt::Key_Y: - return 'Y'; - case Qt::Key_Z: - return 'Z'; - case Qt::Key_Comma: - return ','; - case Qt::Key_Period: - return '.'; - case Qt::Key_Semicolon: - return ';'; - case Qt::Key_Apostrophe: - return '\''; - case Qt::Key_Minus: - return '-'; - case Qt::Key_Equal: - return '='; - case Qt::Key_Slash: - return '/'; - case Qt::Key_Backslash: - return '\\'; - case Qt::Key_BracketLeft: - return '['; - case Qt::Key_BracketRight: - return ']'; - case Qt::Key_QuoteLeft: - return '`'; - case Qt::Key_Space: - return ' '; - default: - break; - } - return 0; -} - - -bool -QtInputHandler::ProcessKeyEvent(const QKeyEvent &e) -{ - const Qt::KeyboardModifiers mods = e.modifiers(); - LOGF("[QtIH] keyPress key=0x%X mods=%s text='%s' k_prefix=%d k_ctrl_pending=%d esc_meta=%d\n", - e.key(), mods_str(mods), e.text().toUtf8().constData(), (int)k_prefix_, (int)k_ctrl_pending_, - (int)esc_meta_); - - // Control-chord detection: only treat the physical Control key as control-like. - // Do NOT include Meta (Command) here so that ⌘-letter shortcuts do not fall into - // the Ctrl map (prevents ⌘-T being mistaken for C-t). - const bool ctrl_like = (mods & Qt::ControlModifier); - - // 1) Universal argument digits (when active), consume digits without enqueuing commands - if (ed_ && ed_ - - -> - UArg() != 0 - ) { - if (!(mods & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) { - if (e.key() >= Qt::Key_0 && e.key() <= Qt::Key_9) { - int d = e.key() - Qt::Key_0; - ed_->UArgDigit(d); - // request status refresh - std::lock_guard lk(mu_); - q_.push(MappedInput{true, CommandId::UArgStatus, std::string(), 0}); - LOGF("[QtIH] UArg digit %d -> enqueue UArgStatus\n", d); - return true; - } - } - } - - // 2) Enter k-prefix on C-k - if (ctrl_like && (e.key() == Qt::Key_K)) { - k_prefix_ = true; - k_ctrl_pending_ = false; - LOGF("[QtIH] Enter KPrefix\n"); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, CommandId::KPrefix, std::string(), 0}); - return true; - } - - // 3) If currently in k-prefix, resolve next key via KLookupKCommand - if (k_prefix_) { - // ESC/meta prefix should not interfere with k-suffix resolution - esc_meta_ = false; - // Support literal 'C' (uppercase) or '^' to indicate the next key is Ctrl-qualified. - // Use case-preserving derivation so that 'c' (lowercase) can still be a valid suffix - // like C-k c (BufferClose). - int ascii_raw = ToAsciiKeyPreserveCase(e); - if (ascii_raw == 'C' || ascii_raw == '^') { - k_ctrl_pending_ = true; - if (ed_) - ed_->SetStatus("C-k C _"); - LOGF("[QtIH] KPrefix: set k_ctrl_pending via '%c'\n", (ascii_raw == 'C') ? 'C' : '^'); - return true; // consume, wait for next key - } - int ascii_key = (ascii_raw != 0) ? ascii_raw : ToAsciiKey(e); - int lower = KLowerAscii(ascii_key); - // Only pass a control suffix for specific supported keys (d/x/q), - // matching ImGui behavior so that holding Ctrl during the suffix - // doesn't break other mappings like C-k c (BufferClose). - bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q'); - bool pass_ctrl = (ctrl_like || k_ctrl_pending_) && ctrl_suffix_supported; - k_ctrl_pending_ = false; // consume pending qualifier on any suffix - LOGF("[QtIH] KPrefix: ascii_key=%d lower=%d pass_ctrl=%d\n", ascii_key, lower, (int)pass_ctrl); - if (ascii_key != 0) { - CommandId id; - if (KLookupKCommand(ascii_key, pass_ctrl, id)) { - LOGF("[QtIH] KPrefix: mapped to command id=%d\n", (int)id); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, id, std::string(), 0}); - } else { - // Unknown k-command: notify - std::string a; - a.push_back(static_cast(ascii_key)); - LOGF("[QtIH] KPrefix: unknown command for '%c'\n", (char)ascii_key); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, CommandId::UnknownKCommand, a, 0}); - } - k_prefix_ = false; - return true; - } - // If not resolvable, consume and exit k-prefix - k_prefix_ = false; - LOGF("[QtIH] KPrefix: unresolved key; exiting prefix\n"); - return true; - } - - // 3.5) GUI shortcut: Command/Meta + T opens the visual font picker (Qt only). - // Require Meta present and Control NOT present so Ctrl-T never triggers this. - if ((mods & Qt::MetaModifier) && !(mods & Qt::ControlModifier) && e.key() == Qt::Key_T) { - LOGF("[QtIH] Meta/Super-T -> VisualFontPickerToggle\n"); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, CommandId::VisualFontPickerToggle, std::string(), 0}); - return true; - } - - // 4) ESC as Meta prefix (set state). Alt/Meta chord handled below directly. - if (e.key() == Qt::Key_Escape) { - esc_meta_ = true; - LOGF("[QtIH] ESC: set esc_meta\n"); - return true; // consumed - } - - // 5) Alt/Meta bindings (ESC f/b equivalent). Handle either Alt/Meta or pending esc_meta_ - // ESC/meta chords: on macOS, do NOT treat Meta as ESC; only Alt (Option) should trigger. -#if defined(__APPLE__) - if (esc_meta_ || (mods & Qt::AltModifier)) { -#else - if (esc_meta_ || (mods & (Qt::AltModifier | Qt::MetaModifier))) { - -#endif - int ascii_key = 0; - if (e.key() == Qt::Key_Backspace) { - ascii_key = KEY_BACKSPACE; - } else if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) { - ascii_key = 'a' + (e.key() - Qt::Key_A); - } else if (e.key() == Qt::Key_Comma) { - ascii_key = '<'; - } else if (e.key() == Qt::Key_Period) { - ascii_key = '>'; - } - // If still unknown, try deriving from text (covers digits, punctuation, locale) - if (ascii_key == 0) { - ascii_key = ToAsciiKey(e); - } - esc_meta_ = false; // one-shot regardless - if (ascii_key != 0) { - ascii_key = KLowerAscii(ascii_key); - CommandId id; - if (KLookupEscCommand(ascii_key, id)) { - LOGF("[QtIH] ESC/Meta: mapped '%d' -> id=%d\n", ascii_key, (int)id); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, id, std::string(), 0}); - return true; - } else { - // Report invalid ESC sequence just like ImGui path - LOGF("[QtIH] ESC/Meta: unknown command for ascii=%d\n", ascii_key); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, CommandId::UnknownEscCommand, std::string(), 0}); - return true; - } - } - // Nothing derivable: consume (ESC prefix cleared) and do not insert text - return true; - } - - // 6) Control-chord direct mappings (e.g., C-n/C-p/C-f/C-b...) - if (ctrl_like) { - // Universal argument handling: C-u starts collection; C-g cancels - if (e.key() == Qt::Key_U) { - if (ed_) - ed_->UArgStart(); - LOGF("[QtIH] Ctrl-chord: start universal argument\n"); - return true; - } - if (e.key() == Qt::Key_G) { - if (ed_) - ed_->UArgClear(); - k_ctrl_pending_ = false; - k_prefix_ = false; - LOGF("[QtIH] Ctrl-chord: cancel universal argument and k-prefix via C-g\n"); - // Fall through to map C-g to Refresh via ctrl map - } - if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) { - int ascii_key = 'a' + (e.key() - Qt::Key_A); - CommandId id; - if (KLookupCtrlCommand(ascii_key, id)) { - LOGF("[QtIH] Ctrl-chord: 'C-%c' -> id=%d\n", (char)ascii_key, (int)id); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, id, std::string(), 0}); - return true; - } - } - // If no mapping, continue to allow other keys below - } - - // 7) Special navigation/edit keys (match ImGui behavior) - { - CommandId id; - bool has = false; - switch (e.key()) { - case Qt::Key_Return: - case Qt::Key_Enter: - id = CommandId::Newline; - has = true; - break; - case Qt::Key_Backspace: - id = CommandId::Backspace; - has = true; - break; - case Qt::Key_Delete: - id = CommandId::DeleteChar; - has = true; - break; - case Qt::Key_Left: - id = CommandId::MoveLeft; - has = true; - break; - case Qt::Key_Right: - id = CommandId::MoveRight; - has = true; - break; - case Qt::Key_Up: - id = CommandId::MoveUp; - has = true; - break; - case Qt::Key_Down: - id = CommandId::MoveDown; - has = true; - break; - case Qt::Key_Home: - id = CommandId::MoveHome; - has = true; - break; - case Qt::Key_End: - id = CommandId::MoveEnd; - has = true; - break; - case Qt::Key_PageUp: - id = CommandId::PageUp; - has = true; - break; - case Qt::Key_PageDown: - id = CommandId::PageDown; - has = true; - break; - default: - break; - } - if (has) { - LOGF("[QtIH] Special key -> id=%d\n", (int)id); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, id, std::string(), 0}); - return true; - } - } - - // 8) Insert printable text - if (IsPrintableQt(e)) { - std::string s = e.text().toStdString(); - if (!s.empty()) { - LOGF("[QtIH] InsertText '%s'\n", s.c_str()); - std::lock_guard lk(mu_); - q_.push(MappedInput{true, CommandId::InsertText, s, 0}); - return true; - } - } - - LOGF("[QtIH] Unhandled key\n"); - return false; -} - - -bool -QtInputHandler::Poll(MappedInput &out) -{ - std::lock_guard lock(mu_); - if (q_.empty()) - return false; - out = q_.front(); - q_.pop(); - return true; -} diff --git a/QtInputHandler.h b/QtInputHandler.h deleted file mode 100644 index f5edc61..0000000 --- a/QtInputHandler.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * QtInputHandler - Qt-based input mapping for GUI mode - */ -#pragma once - -#include -#include - -#include "InputHandler.h" - -class QKeyEvent; - -class QtInputHandler final : public InputHandler { -public: - QtInputHandler() = default; - - ~QtInputHandler() override = default; - - - void Attach(Editor *ed) override - { - ed_ = ed; - } - - - // Translate a Qt key event to editor command and enqueue if applicable. - // Returns true if it produced a mapped command or consumed input. - bool ProcessKeyEvent(const QKeyEvent &e); - - bool Poll(MappedInput &out) override; - -private: - std::mutex mu_; - std::queue q_; - bool k_prefix_ = false; - bool k_ctrl_pending_ = false; // C-k C-… qualifier - bool esc_meta_ = false; // ESC-prefix for next key - bool suppress_text_input_once_ = false; // reserved (Qt sends text in keyPressEvent) - Editor *ed_ = nullptr; -}; diff --git a/QtRenderer.cc b/QtRenderer.cc deleted file mode 100644 index b513d4c..0000000 --- a/QtRenderer.cc +++ /dev/null @@ -1,76 +0,0 @@ -#include "QtRenderer.h" - -#include -#include -#include -#include -#include - -#include "Editor.h" - -namespace { -class EditorWidget : public QWidget { -public: - explicit EditorWidget(QWidget *parent = nullptr) : QWidget(parent) - { - setAttribute(Qt::WA_OpaquePaintEvent); - setFocusPolicy(Qt::StrongFocus); - } - - - void SetEditor(Editor *ed) - { - ed_ = ed; - } - -protected: - void paintEvent(QPaintEvent *event) override - { - Q_UNUSED(event); - QPainter p(this); - // Background - const QColor bg(28, 28, 30); - p.fillRect(rect(), bg); - - // Font and metrics - QFont f("JetBrains Mono", 13); - p.setFont(f); - QFontMetrics fm(f); - const int line_h = fm.height(); - - // Title - p.setPen(QColor(220, 220, 220)); - p.drawText(8, fm.ascent() + 4, QStringLiteral("kte (Qt frontend)")); - - // Status bar at bottom - const int bar_h = line_h + 6; // padding - const int bar_y = height() - bar_h; - QRect status_rect(0, bar_y, width(), bar_h); - p.fillRect(status_rect, QColor(40, 40, 44)); - p.setPen(QColor(180, 180, 140)); - if (ed_) { - const QString status = QString::fromStdString(ed_->Status()); - // draw at baseline within the bar - const int baseline = bar_y + 3 + fm.ascent(); - p.drawText(8, baseline, status); - } - } - -private: - Editor *ed_ = nullptr; -}; -} // namespace - -void -QtRenderer::Draw(Editor &ed) -{ - if (!widget_) - return; - - // If our widget is an EditorWidget, pass the editor pointer for painting - if (auto *ew = dynamic_cast(widget_)) { - ew->SetEditor(&ed); - } - // Request a repaint - widget_->update(); -} diff --git a/QtRenderer.h b/QtRenderer.h deleted file mode 100644 index 5657793..0000000 --- a/QtRenderer.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * QtRenderer - minimal Qt-based renderer - */ -#pragma once - -#include "Renderer.h" - -class QWidget; - -class QtRenderer final : public Renderer { -public: - QtRenderer() = default; - - ~QtRenderer() override = default; - - - void Attach(QWidget *widget) - { - widget_ = widget; - } - - - void Draw(Editor &ed) override; - -private: - QWidget *widget_ = nullptr; // not owned -}; diff --git a/REWRITE.md b/REWRITE.md index e8aba64..83988b2 100644 --- a/REWRITE.md +++ b/REWRITE.md @@ -938,7 +938,7 @@ pub enum CommandError { **Current C++ Implementation:** - `Frontend` trait with Init/Step/Shutdown lifecycle -- Three implementations: Terminal (ncurses), ImGui (SDL2/OpenGL), Qt +- Two implementations: Terminal (ncurses), ImGui (SDL2/OpenGL) - Step() does one iteration: poll input, dispatch commands, render - Each frontend owns InputHandler and Renderer instances @@ -986,7 +986,7 @@ pub enum FrontendError { - `InputHandler` trait with Poll() returning `MappedInput` - `MappedInput`: hasCommand bool, CommandId, arg string, count - Terminal: ncurses-based, translates keys to commands -- ImGui/Qt: event-based, handles text input differently +- ImGui: event-based, handles text input differently - Keymap lookups via `KKeymap` functions (KCommand, Ctrl, Esc) **Rust Design:** @@ -1075,7 +1075,7 @@ impl InputHandler for TerminalInputHandler { - `Renderer` trait with single Draw(Editor&) method - Terminal: ncurses-based, manual cursor positioning -- ImGui/Qt: retained mode GUI, draw calls within Begin/End blocks +- ImGui: immediate mode GUI, draw calls within Begin/End blocks - Syntax highlighting integrated via Buffer::Highlighter() **Rust Design:** @@ -1200,7 +1200,7 @@ impl Frontend for TerminalFrontend { #### 9.2 GUI Frontend (egui-based) -For GUI, consider replacing ImGui/Qt with `egui` (pure Rust, immediate +For GUI, consider replacing ImGui with `egui` (pure Rust, immediate mode): ```rust diff --git a/cmake/fix_bundle.cmake b/cmake/fix_bundle.cmake index d9b3ac9..48fe711 100644 --- a/cmake/fix_bundle.cmake +++ b/cmake/fix_bundle.cmake @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.15) -# Fix up a macOS .app bundle by copying non-Qt dylibs into +# Fix up a macOS .app bundle by copying non-system dylibs into # Contents/Frameworks and rewriting install names to use @rpath/@loader_path. # # Usage: @@ -29,9 +29,8 @@ set(DIRS ) # Note: We pass empty plugin list so fixup_bundle scans the executable and all -# libs it references recursively. Qt frameworks already live in the bundle after -# macdeployqt; this step is primarily for non-Qt dylibs (glib, icu, pcre2, zstd, -# dbus, etc.). +# libs it references recursively, copying every non-system dylib (e.g. Homebrew +# SDL2, freetype, pcre2) into Contents/Frameworks; SDL3 is handled below. # fixup_bundle often fails if copied libraries are read-only. # We also try to use the system install_name_tool and otool to avoid issues with Anaconda's version. # Note: BundleUtilities uses find_program(gp_otool "otool") internally, so we might need to set it differently. diff --git a/default.nix b/default.nix index 49b6fb1..bec15f8 100644 --- a/default.nix +++ b/default.nix @@ -8,13 +8,10 @@ SDL2, libGL, xorg, - kdePackages, - qt6Packages ? kdePackages.qt6Packages, installShellFiles, copyDesktopItems, makeDesktopItem, graphical ? false, - graphical-qt ? false, }: let cmakeContent = builtins.readFile ./CMakeLists.txt; @@ -25,7 +22,7 @@ let version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine); in stdenv.mkDerivation { - pname = if graphical then (if graphical-qt then "kge-qt" else "kge") else "kte"; + pname = if graphical then "kge" else "kte"; inherit version; src = lib.cleanSource ./.; @@ -36,8 +33,6 @@ stdenv.mkDerivation { installShellFiles ] ++ lib.optionals graphical [ copyDesktopItems - ] ++ lib.optionals graphical-qt [ - qt6Packages.wrapQtAppsHook ]; buildInputs = [ @@ -47,14 +42,10 @@ stdenv.mkDerivation { SDL2 libGL xorg.libX11 - ] ++ lib.optionals graphical-qt [ - kdePackages.qt6ct - qt6Packages.qtbase ]; cmakeFlags = [ "-DBUILD_GUI=${if graphical then "ON" else "OFF"}" - "-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}" "-DCMAKE_BUILD_TYPE=Debug" "-DKTE_STATIC_LINK=OFF" ]; @@ -65,7 +56,7 @@ stdenv.mkDerivation { desktopName = "kge"; genericName = "Text Editor"; comment = "kyle's graphical text editor"; - exec = if graphical-qt then "kge-qt %F" else "kge %F"; + exec = "kge %F"; icon = "kge"; terminal = false; categories = [ "Utility" "TextEditor" "Development" ]; @@ -91,11 +82,7 @@ stdenv.mkDerivation { installManPage ../docs/kte.1 ${lib.optionalString graphical '' - ${if graphical-qt then '' - cp kge $out/bin/kge-qt - '' else '' - cp kge $out/bin/kge - ''} + cp kge $out/bin/kge installManPage ../docs/kge.1 mkdir -p $out/share/icons/hicolor/256x256/apps @@ -108,6 +95,6 @@ stdenv.mkDerivation { meta = { description = "kyle's text editor" + lib.optionalString graphical " (graphical)"; platforms = lib.platforms.unix; - mainProgram = if graphical then (if graphical-qt then "kge-qt" else "kge") else "kte"; + mainProgram = if graphical then "kge" else "kte"; }; } diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 6c2e6f0..05951bb 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -20,7 +20,7 @@ kte follows a clean separation of concerns with three main layers: ``` ┌─────────────────────────────────────────┐ -│ Frontend Layer (Terminal/ImGui/Qt) │ +│ Frontend Layer (Terminal/ImGui) │ │ - TerminalFrontend / ImGuiFrontend │ │ - InputHandler + Renderer interfaces │ └─────────────────────────────────────────┘ @@ -154,7 +154,6 @@ Implementations: `TerminalInputHandler`, `TerminalRenderer`) - **ImGui**: Dear ImGui-based (`ImGuiFrontend`, `ImGuiInputHandler`, `ImGuiRenderer`) -- **Qt**: Qt-based (`QtFrontend`, `QtInputHandler`, `QtRenderer`) - **Test**: Programmatic testing (`TestFrontend`, `TestInputHandler`, `TestRenderer`) @@ -196,7 +195,6 @@ Large files that may need attention: into logical groups - `Swap.cc` (1300 lines) - Crash recovery system (migrated to direct PieceTable operations) -- `QtFrontend.cc` (985 lines) - Qt integration - `ImGuiRenderer.cc` (930 lines) - ImGui rendering - `PieceTable.cc` (800 lines) - Core data structure - `Buffer.cc` (763 lines) - Document model diff --git a/docs/plans/qt-frontend.md b/docs/plans/qt-frontend.md deleted file mode 100644 index af4184b..0000000 --- a/docs/plans/qt-frontend.md +++ /dev/null @@ -1,124 +0,0 @@ -Based on the project structure and the presence of files like -`imgui.ini`, `GUIFrontend.h`, and `TerminalFrontend.h`, here is an -analysis of the difficulty and challenges involved in adding a GTK or Qt -version of the GUI. - -### **Executive Summary: Difficulty Level - Moderate** - -The project is well-architected for this task. It already supports -multiple frontends (Terminal vs. GUI), meaning the "Core Logic" ( -Buffers, Syntax, Commands) is successfully decoupled from the "View" ( -Rendering/Input). However, the specific move from an **Immediate Mode** -GUI (likely Dear ImGui, implied by `imgui.ini` and standard naming -patterns) to a **Retained Mode** GUI (Qt/GTK) introduces specific -architectural frictions regarding the event loop and state management. - ---- - -### **1. Architectural Analysis** - -The existence of abstract interfaces—likely `Frontend`, `Renderer`, and -`InputHandler`—is the biggest asset here. - -* **Current State:** - * **Abstract Layer:** `Frontend.h`, `Renderer.h`, `InputHandler.h` - likely define the contract. - * **Implementations:** - * `Terminal*` files implement the TUI (likely ncurses or VT100). - * `GUI*` files (currently ImGui) implement the graphical - version. -* **The Path Forward:** - * You would create `QtFrontend`, `QtRenderer`, `QtInputHandler` (or - GTK equivalents). - * Because the core logic (`Editor.cc`, `Buffer.cc`) calls these - interfaces, you theoretically don't need to touch the core text - manipulation code. - -### **2. Key Challenges** - -#### **A. The Event Loop Inversion (Main Challenge)** - -* **Current (ImGui):** Typically, the application owns the loop: - `while (running) { HandleInput(); Update(); Render(); }`. The - application explicitly tells the GUI to draw every frame. -* **Target (Qt/GTK):** The framework owns the loop: `app.exec()` or - `gtk_main()`. The framework calls *you* when events happen. -* **Difficulty:** You will need to refactor `main.cc` or the entry point - to hand over control to the Qt/GTK application object. The Editor's " - tick" function might need to be connected to a timer or an idle event - in the new framework to ensure logic updates happen. - -#### **B. Rendering Paradigm: Canvas vs. Widgets** - -* **The "Easy" Way (Custom Canvas):** - * Implement the `QtRenderer` by subclassing `QWidget` and overriding - `paintEvent`. - * Use `QPainter` (or Cairo in GTK) to draw text, cursors, and - selections exactly where the `Renderer` interface says to. - * **Pros:** Keeps the code similar to the current ImGui/Terminal - renderers. - * **Cons:** You lose native accessibility and some native "feel" ( - scrolling physics, native text context menus). -* **The "Hard" Way (Native Widgets):** - * Trying to map an internal `Buffer` directly to a `QTextEdit` or - `GtkTextView`. - * **Difficulty:** This is usually very hard because the Editor core - likely manages its own cursor, selection, and syntax highlighting. - Syncing that internal state with a complex native widget often - leads to conflicts. - * **Recommendation:** Stick to the "Custom Canvas" approach (drawing - text manually on a surface) to preserve the custom editor - behavior (vim-like modes, specific syntax highlighting). - -#### **C. Input Handling** - -* **Challenge:** Mapping Qt/GTK key events to the internal `Keymap`. -* **Detail:** ImGui and Terminal libraries often provide raw scancodes - or simple chars. Qt/GTK provide complex Event objects. You will need a - translation layer in `QtInputHandler::keyPressEvent` that converts - `Qt::Key_Escape` -> `KKey::Escape` (or your internal equivalent). - -### **3. Portability of Assets** - -#### **Themes (Colors)** - -* **Feasibility:** High. -* **Approach:** `GUITheme.h` likely contains structs with RGB/Hex - values. Qt supports stylesheets (QSS) and GTK uses CSS. You can write - a converter that reads your current theme configuration and generates - a CSS string to apply to your window, or simply use the RGB values - directly in your custom `QPainter`/Cairo drawing logic. - -#### **Fonts** - -* **Feasibility:** Moderate. -* **Approach:** - * **ImGui:** Usually loads a TTF into a texture atlas. - * **Qt/GTK:** Uses the system font engine (Freetype/Pango). - * **Challenge:** You won't use the texture atlas anymore. You will - simply request a font family and size (e.g., - `QFont("JetBrains Mono", 12)`). You may need to ensure your custom - renderer calculates character width/height metrics correctly using - `QFontMetrics` (Qt) or `PangoLayout` (GTK) to align the grid - correctly. - -### **4. Summary Recommendation** - -If you proceed, **Qt** is generally considered easier to integrate with -C++ projects than GTK (which is C-based, though `gtkmm` exists). - -1. **Create a `QtFrontend`** class inheriting from `Frontend`. -2. **Create a `QtWindow`** class inheriting from `QWidget`. -3. **Implement `QtRenderer`** that holds a pointer to the `QtWindow`. - When the core calls `DrawText()`, `QtRenderer` should queue that - command or draw directly to the widget's paint buffer. -4. **Refactor `main.cc`** to instantiate `QApplication` instead of the - current manual loop. - ---- - -Note (2025-12): The Qt frontend defers all key processing to the -existing command subsystem and keymaps, mirroring the ImGui path. There -are no Qt-only keybindings; `QtInputHandler` translates Qt key events -into the shared keymap flow (C-k prefix, Ctrl chords, ESC/Meta, -universal-argument digits, printable insertion). \ No newline at end of file diff --git a/flake.nix b/flake.nix index 0290151..0cbf9a2 100644 --- a/flake.nix +++ b/flake.nix @@ -12,9 +12,8 @@ packages = eachSystem (system: rec { default = kte; full = kge; - kte = (pkgsFor system).callPackage ./default.nix { graphical = false; graphical-qt = false; }; - kge = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = false; }; - qt = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = true; }; + kte = (pkgsFor system).callPackage ./default.nix { graphical = false; }; + kge = (pkgsFor system).callPackage ./default.nix { graphical = true; }; }); devShells = eachSystem (system: @@ -27,10 +26,6 @@ terminal = pkgs.mkShell { inputsFrom = [ self.packages.${system}.kte ]; }; - qt = pkgs.mkShell { - inputsFrom = [ self.packages.${system}.qt ]; - packages = with pkgs; [ gdb valgrind ]; - }; } ); diff --git a/main.cc b/main.cc index 43ae724..92839bd 100644 --- a/main.cc +++ b/main.cc @@ -25,12 +25,8 @@ #include "ErrorHandler.h" #if defined(KTE_BUILD_GUI) -#if defined(KTE_USE_QT) -#include "QtFrontend.h" -#else #include "ImGuiFrontend.h" #endif -#endif #ifndef KTE_VERSION_STR diff --git a/make-app-release b/make-app-release index 57c2566..68d837c 100755 --- a/make-app-release +++ b/make-app-release @@ -14,19 +14,3 @@ zip -r kge.app.zip kge.app sha256sum kge.app.zip open . cd .. - -# Qt build disabled — ImGui frontend is the primary GUI. -# mkdir -p cmake-build-release-qt -# cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF -# -# cd cmake-build-release-qt -# make clean -# rm -fr kge.app* kge-qt.app* -# make -# mv -f kge.app kge-qt.app -# macdeployqt kge-qt.app -always-overwrite -verbose=3 -# cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake" -# zip -r kge-qt.app.zip kge-qt.app -# sha256sum kge-qt.app.zip -# open . -# cd ..