#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 left of coloffs or // right of the viewport are skipped. s = static_cast(std::lower_bound(col_of.begin() + s, col_of.begin() + e, coloffs) - col_of.begin()); 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; } }