From ac5d06804014bac2cfc29ca96078339e584cc3b2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 21 May 2026 14:32:55 -0500 Subject: [PATCH 1/2] qt: keep the resize overlay visible for the whole resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "cols × rows" overlay flashed and vanished almost instantly while resizing windows or split panes, unlike macOS/GTK which hold it for the duration of the resize. Two root causes: - Duration unit mismatch. ghostty_config_get returns a Duration via Duration.cval(), which is milliseconds. The handler named the value `durNs` and divided by 1e6, turning the 750ms default into 0 — so the hide timer fired on the next event-loop tick. Read it as ms. - The hide timer was only re-armed on a grid-size change, so it expired mid-drag whenever the grid was briefly stable. Re-arm it on every resize event; it now fires ~750ms after resizing actually stops. Also move the overlay from a child QLabel to direct painting in paintEvent (like the bell-flash border): a child widget composited over this fast-repainting surface gets covered / flickers during a resize. Co-Authored-By: claude-flow --- qt/src/GhosttySurface.cpp | 104 +++++++++++++++++++++++++++----------- qt/src/GhosttySurface.h | 12 ++++- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/qt/src/GhosttySurface.cpp b/qt/src/GhosttySurface.cpp index fd0a0f446..c1c074761 100644 --- a/qt/src/GhosttySurface.cpp +++ b/qt/src/GhosttySurface.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -333,6 +335,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 +489,93 @@ 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"); + const int durMs = + (durOk && durCfgMs > 0) ? static_cast(durCfgMs) : 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; From e6e96c78c47b0b7be993dba7f7d78692c04658be Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 21 May 2026 14:37:53 -0500 Subject: [PATCH 2/2] =?UTF-8?q?fix(audit):=20pass=201=20=E2=80=94=20clamp?= =?UTF-8?q?=20resize-overlay-duration=20before=20narrowing=20to=20int?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resize-overlay-duration is a Duration whose millisecond value (Duration.cval → asMilliseconds, a c_uint) can exceed INT_MAX. The unguarded static_cast would wrap to a negative interval, which QTimer::start() rejects — leaving the resize overlay stuck on screen. Clamp to INT_MAX before the cast. Co-Authored-By: claude-flow --- qt/src/GhosttySurface.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qt/src/GhosttySurface.cpp b/qt/src/GhosttySurface.cpp index c1c074761..2d7917cd2 100644 --- a/qt/src/GhosttySurface.cpp +++ b/qt/src/GhosttySurface.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -527,8 +528,14 @@ void GhosttySurface::showResizeOverlay() { // 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(durCfgMs) : 750; + (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);