diff --git a/qt/src/GhosttySurface.cpp b/qt/src/GhosttySurface.cpp index fd0a0f446..2d7917cd2 100644 --- a/qt/src/GhosttySurface.cpp +++ b/qt/src/GhosttySurface.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -333,6 +336,9 @@ void GhosttySurface::paintEvent(QPaintEvent *) { painter.setBrush(Qt::NoBrush); painter.drawRect(QRectF(rect()).adjusted(1.5, 1.5, -1.5, -1.5)); } + + // Transient "cols × rows" overlay, on top of everything else. + paintResizeOverlay(painter); } void GhosttySurface::flashBorder() { @@ -484,52 +490,99 @@ void GhosttySurface::layoutSearchBar() { width() - m_searchBar->width() - OverlayScrollbar::kWidth - 8, 8); } +// Called from resizeEvent for every size change. The overlay is drawn +// in paintEvent (see m_resizeOverlayVisible there) rather than as a +// child QLabel: a child widget composited over this surface gets +// covered / flickers while the surface repaints rapidly during a +// resize. Here we just refresh the text and (re)arm the hide timer on +// EVERY resize event, so the overlay stays up for the whole drag and +// only fades once resizing actually stops. void GhosttySurface::showResizeOverlay() { if (!m_surface || !m_owner) return; const ghostty_surface_size_s sz = ghostty_surface_size(m_surface); - // Only a grid-size change is a "resize" worth announcing. - if (sz.columns == m_lastCols && sz.rows == m_lastRows) return; - m_lastCols = sz.columns; - m_lastRows = sz.rows; ghostty_config_t cfg = m_owner->config(); const QString mode = cfgString(cfg, "resize-overlay"); - const bool first = !m_firstGridSeen; - m_firstGridSeen = true; if (mode == QLatin1String("never")) return; - if (mode == QLatin1String("after-first") && first) return; - if (!m_resizeOverlay) m_resizeOverlay = makeOverlayLabel(this); - m_resizeOverlay->setText( - QStringLiteral("%1 × %2").arg(sz.columns).arg(sz.rows)); - m_resizeOverlay->adjustSize(); + if (sz.columns != m_lastCols || sz.rows != m_lastRows) { + const bool first = !m_firstGridSeen; + m_lastCols = sz.columns; + m_lastRows = sz.rows; + m_firstGridSeen = true; + // `after-first`: stay silent for the surface's very first grid. + if (mode == QLatin1String("after-first") && first) return; + m_resizeOverlayText = + QStringLiteral("%1 × %2").arg(sz.columns).arg(sz.rows); + } + // Nothing to announce yet (a pixel-only resize before the first grid, + // or `after-first` still waiting on the surface's initial grid). + if (m_resizeOverlayText.isEmpty()) return; - // resize-overlay-position: center / {top,bottom}-{left,center,right}. - const QString pos = cfgString(cfg, "resize-overlay-position"); - const int m = 8; - int x = (width() - m_resizeOverlay->width()) / 2; - int y = (height() - m_resizeOverlay->height()) / 2; - if (pos.contains(QLatin1String("left"))) x = m; - else if (pos.contains(QLatin1String("right"))) - x = width() - m_resizeOverlay->width() - m; - if (pos.contains(QLatin1String("top"))) y = m; - else if (pos.contains(QLatin1String("bottom"))) - y = height() - m_resizeOverlay->height() - m; - m_resizeOverlay->move(x, y); - m_resizeOverlay->show(); - m_resizeOverlay->raise(); + m_resizeOverlayVisible = true; - unsigned long long durNs = 0; - configGet(cfg, &durNs, "resize-overlay-duration"); - const int durMs = durNs ? static_cast(durNs / 1000000ULL) : 750; + // ghostty_config_get returns a Duration through Duration.cval(), + // which is MILLISECONDS — use it as-is. Dividing by 1e6 here (the + // value was misnamed "durNs") turned the 750ms default into 0, so + // the hide timer fired on the next event-loop tick and the overlay + // vanished the instant it appeared. + unsigned long long durCfgMs = 0; + const bool durOk = configGet(cfg, &durCfgMs, "resize-overlay-duration"); + // Clamp before narrowing: a Duration's millisecond value can exceed + // INT_MAX, and a wrapped negative int would make QTimer::start() + // reject the interval, leaving the overlay stuck on screen. + const int durMs = + (durOk && durCfgMs > 0) + ? static_cast(std::min( + durCfgMs, std::numeric_limits::max())) + : 750; if (!m_resizeHideTimer) { m_resizeHideTimer = new QTimer(this); m_resizeHideTimer->setSingleShot(true); connect(m_resizeHideTimer, &QTimer::timeout, this, [this]() { - if (m_resizeOverlay) m_resizeOverlay->hide(); + m_resizeOverlayVisible = false; + update(); }); } m_resizeHideTimer->start(durMs); + update(); +} + +// Draw the transient "cols × rows" overlay onto the current frame. +// Called from paintEvent so the overlay is composited in the same pass +// as the terminal image — it cannot be covered or flicker. +void GhosttySurface::paintResizeOverlay(QPainter &painter) { + if (!m_resizeOverlayVisible || m_resizeOverlayText.isEmpty()) return; + + QFont f = font(); + f.setPixelSize(13); + const QFontMetrics fm(f); + const int padX = 10, padY = 4; + const QSize ts = fm.size(Qt::TextSingleLine, m_resizeOverlayText); + const qreal boxW = ts.width() + 2 * padX; + const qreal boxH = ts.height() + 2 * padY; + + // resize-overlay-position: center / {top,bottom}-{left,center,right}. + const QString pos = + m_owner ? cfgString(m_owner->config(), "resize-overlay-position") + : QString(); + const qreal m = 8; + qreal x = (width() - boxW) / 2; + qreal y = (height() - boxH) / 2; + if (pos.contains(QLatin1String("left"))) x = m; + else if (pos.contains(QLatin1String("right"))) x = width() - boxW - m; + if (pos.contains(QLatin1String("top"))) y = m; + else if (pos.contains(QLatin1String("bottom"))) y = height() - boxH - m; + + const QRectF box(x, y, boxW, boxH); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(0, 0, 0, 191)); // rgba(0,0,0,0.75) + painter.drawRoundedRect(box, 4, 4); + painter.setFont(f); + painter.setPen(QColor(0xf0, 0xf0, 0xf0)); + painter.drawText(box, Qt::AlignCenter, m_resizeOverlayText); } void GhosttySurface::showChildExited(int exitCode) { diff --git a/qt/src/GhosttySurface.h b/qt/src/GhosttySurface.h index ca6aff220..d84d3ff7d 100644 --- a/qt/src/GhosttySurface.h +++ b/qt/src/GhosttySurface.h @@ -25,6 +25,7 @@ class QOpenGLContext; class QOpenGLFramebufferObject; class QOpenGLShaderProgram; class QOpenGLVertexArrayObject; +class QPainter; class OverlayScrollbar; // One Ghostty terminal pane. @@ -161,6 +162,7 @@ private: void flashScrollbar(); // reveal the overlay scrollbar, arm hide void buildExitOverlay(int exitCode); void showResizeOverlay(); // transient grid-size overlay on resize + void paintResizeOverlay(QPainter &painter); // draws ^ in paintEvent void layoutSearchBar(); // position the search bar at the top edge void sendKey(QKeyEvent *, ghostty_input_action_e action); void commitText(const QString &text); @@ -205,8 +207,14 @@ private: QLabel *m_keySeqOverlay = nullptr; // pending keybind chord; lazily made QLabel *m_linkOverlay = nullptr; // MOUSE_OVER_LINK URL hint; lazily made QStringList m_keySeq; // accumulated pending chords - QLabel *m_resizeOverlay = nullptr; // transient "cols x rows"; lazily made - QTimer *m_resizeHideTimer = nullptr; // auto-hides m_resizeOverlay + // The transient "cols × rows" overlay is painted directly in + // paintEvent (not a child widget) so it is part of the terminal frame + // and cannot be covered or flicker while the surface repaints during + // a resize. m_resizeHideTimer clears m_resizeOverlayVisible when the + // resize stops; m_resizeOverlayText is the text to draw. + QTimer *m_resizeHideTimer = nullptr; + QString m_resizeOverlayText; + bool m_resizeOverlayVisible = false; bool m_firstGridSeen = false; // for `resize-overlay = after-first` int m_lastCols = 0; // last grid size, to detect changes int m_lastRows = 0;