qt: paint the resize overlay directly, not as a child QLabel
The earlier "extend the timer on every resizeEvent" patch did not actually fix the flash, because the flash wasn't a timer issue — it was a paint-ordering race between the parent surface and the child QLabel. GhosttySurface::paintEvent draws the terminal FBO with QPainter::CompositionMode_Source, which fully replaces destination pixels in the painted rect. With a child QLabel sitting on top, Qt composites the child's paint into the same backing store, but only after the child's own paintEvent runs — and during a continuous resize the parent's frequent update() calls outpaced the child's paint, so the backing store was flushed once with the parent's blit already done but the child not yet repainted. That single-frame gap is the visible flash. Fix the root cause: drop the child QLabel entirely and paint the "cols × rows" plate inside GhosttySurface::paintEvent itself, in the same QPainter pass as the terminal blit. Now the overlay is atomic with the surface beneath it — no child-widget timing race possible. State is reduced to (m_resizeOverlayText, m_resizeOverlayUntilMs): showResizeOverlay updates both, calls update(); paintEvent calls paintResizeOverlay() which draws the plate when current time is before the deadline. A QTimer schedules a single update() at the deadline so the overlay disappears even if no further resize events arrive. cfgString is forward-declared because paintResizeOverlay uses it before its definition further down in the file. Co-Authored-By: claude-flow <ruv@ruv.net>pull/12846/head
parent
15b2b060e7
commit
af567feb83
|
|
@ -14,9 +14,11 @@
|
|||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QDateTime>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDropEvent>
|
||||
#include <QFocusEvent>
|
||||
#include <QFontMetrics>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QInputDialog>
|
||||
|
|
@ -308,6 +310,52 @@ void GhosttySurface::paintEvent(QPaintEvent *) {
|
|||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(QRectF(rect()).adjusted(1.5, 1.5, -1.5, -1.5));
|
||||
}
|
||||
|
||||
// Resize overlay (rendered here, not as a child widget, so it
|
||||
// can't race the Source-mode blit above mid-resize).
|
||||
paintResizeOverlay(painter);
|
||||
}
|
||||
|
||||
// Forward decl: cfgString is defined further down (alongside other
|
||||
// per-config-key helpers). We need it here for the resize-overlay
|
||||
// paint path.
|
||||
static QString cfgString(ghostty_config_t cfg, const char *key);
|
||||
|
||||
void GhosttySurface::paintResizeOverlay(QPainter &painter) {
|
||||
if (m_resizeOverlayText.isEmpty()) return;
|
||||
if (QDateTime::currentMSecsSinceEpoch() >= m_resizeOverlayUntilMs) return;
|
||||
if (!m_owner) return;
|
||||
|
||||
ghostty_config_t cfg = m_owner->config();
|
||||
const QString posCfg = cfgString(cfg, "resize-overlay-position");
|
||||
|
||||
// Layout the text in a rounded-rect plate, sized from the text's
|
||||
// bounding rect plus padding.
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
QFont f = painter.font();
|
||||
f.setPixelSize(13);
|
||||
painter.setFont(f);
|
||||
const QFontMetrics fm(f);
|
||||
const QRect textRect = fm.boundingRect(m_resizeOverlayText);
|
||||
const int padX = 10, padY = 4;
|
||||
const int w = textRect.width() + padX * 2;
|
||||
const int h = textRect.height() + padY * 2;
|
||||
|
||||
const int m = 8;
|
||||
int x = (width() - w) / 2;
|
||||
int y = (height() - h) / 2;
|
||||
if (posCfg.contains(QLatin1String("left"))) x = m;
|
||||
else if (posCfg.contains(QLatin1String("right"))) x = width() - w - m;
|
||||
if (posCfg.contains(QLatin1String("top"))) y = m;
|
||||
else if (posCfg.contains(QLatin1String("bottom"))) y = height() - h - m;
|
||||
|
||||
const QRectF plate(x, y, w, h);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(0, 0, 0, 191)); // 0.75 alpha
|
||||
painter.drawRoundedRect(plate, 4, 4);
|
||||
painter.setPen(QColor(0xf0, 0xf0, 0xf0));
|
||||
painter.drawText(plate, Qt::AlignCenter, m_resizeOverlayText);
|
||||
}
|
||||
|
||||
void GhosttySurface::flashBorder() {
|
||||
|
|
@ -422,26 +470,8 @@ void GhosttySurface::showResizeOverlay() {
|
|||
const QString mode = cfgString(cfg, "resize-overlay");
|
||||
if (mode == QLatin1String("never")) return;
|
||||
|
||||
// Reset the hide timer on EVERY resize event, not just on grid-size
|
||||
// boundaries. Without this, slow window drags only triggered the
|
||||
// overlay when the grid happened to step (e.g. crossing a cell
|
||||
// height), so the overlay flashed for ~750ms then disappeared even
|
||||
// though the user was still dragging. Reading the duration here
|
||||
// also picks up any config reload during a resize.
|
||||
unsigned long long durNs = 0;
|
||||
configGet(cfg, &durNs, "resize-overlay-duration");
|
||||
const int durMs = durNs ? static_cast<int>(durNs / 1000000ULL) : 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_resizeHideTimer->start(durMs);
|
||||
|
||||
// The overlay TEXT only changes when the grid steps. Suppress the
|
||||
// "after-first" mode's first-show, then track each grid step.
|
||||
// The "after-first" mode hides the overlay until the grid has
|
||||
// stepped at least once after the surface was created.
|
||||
const bool gridChanged =
|
||||
sz.columns != m_lastCols || sz.rows != m_lastRows;
|
||||
if (gridChanged) {
|
||||
|
|
@ -449,42 +479,39 @@ void GhosttySurface::showResizeOverlay() {
|
|||
m_lastCols = sz.columns;
|
||||
m_lastRows = sz.rows;
|
||||
m_firstGridSeen = true;
|
||||
m_resizeOverlayText =
|
||||
QStringLiteral("%1 × %2").arg(sz.columns).arg(sz.rows);
|
||||
if (mode == QLatin1String("after-first") && first) return;
|
||||
} else if (m_resizeOverlay && m_resizeOverlay->isVisible()) {
|
||||
// Overlay already visible with the right text; just reposition
|
||||
// for the new widget size and we're done.
|
||||
if (!m_resizeOverlay) return;
|
||||
repositionResizeOverlay();
|
||||
return;
|
||||
} else if (!m_firstGridSeen) {
|
||||
// Haven't seen a grid step yet AND no overlay is currently up.
|
||||
// Don't show stale 0×0 text.
|
||||
} else if (m_resizeOverlayText.isEmpty()) {
|
||||
// No grid step has happened yet AND no overlay text is cached —
|
||||
// nothing to display.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_resizeOverlay) m_resizeOverlay = makeOverlayLabel(this);
|
||||
m_resizeOverlay->setText(
|
||||
QStringLiteral("%1 × %2").arg(sz.columns).arg(sz.rows));
|
||||
m_resizeOverlay->adjustSize();
|
||||
repositionResizeOverlay();
|
||||
m_resizeOverlay->show();
|
||||
m_resizeOverlay->raise();
|
||||
}
|
||||
// Push the hide deadline forward on every resizeEvent so the
|
||||
// overlay stays visible until the user actually stops resizing.
|
||||
// Without this, slow drags between cell-boundary crossings would
|
||||
// let the timer fire mid-drag and flash the overlay off-on-off.
|
||||
unsigned long long durNs = 0;
|
||||
configGet(cfg, &durNs, "resize-overlay-duration");
|
||||
const int durMs = durNs ? static_cast<int>(durNs / 1000000ULL) : 750;
|
||||
m_resizeOverlayUntilMs =
|
||||
QDateTime::currentMSecsSinceEpoch() + durMs;
|
||||
|
||||
void GhosttySurface::repositionResizeOverlay() {
|
||||
if (!m_resizeOverlay || !m_owner) return;
|
||||
ghostty_config_t cfg = m_owner->config();
|
||||
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);
|
||||
// Schedule a paint at the deadline so the overlay disappears even
|
||||
// when no further resize events are arriving.
|
||||
if (!m_resizeHideTimer) {
|
||||
m_resizeHideTimer = new QTimer(this);
|
||||
m_resizeHideTimer->setSingleShot(true);
|
||||
connect(m_resizeHideTimer, &QTimer::timeout, this,
|
||||
[this]() { update(); });
|
||||
}
|
||||
m_resizeHideTimer->start(durMs);
|
||||
|
||||
// Repaint now so the overlay appears (or its text updates) on the
|
||||
// next frame. paintEvent reads m_resizeOverlayText and the
|
||||
// deadline; nothing else changes about the surface contents.
|
||||
update();
|
||||
}
|
||||
|
||||
void GhosttySurface::showChildExited(int exitCode) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class QOpenGLContext;
|
|||
class QOpenGLFramebufferObject;
|
||||
class QOpenGLShaderProgram;
|
||||
class QOpenGLVertexArrayObject;
|
||||
class QPainter;
|
||||
class OverlayScrollbar;
|
||||
|
||||
// One Ghostty terminal pane.
|
||||
|
|
@ -139,7 +140,10 @@ private:
|
|||
void flashScrollbar(); // reveal the overlay scrollbar, arm hide
|
||||
void buildExitOverlay(int exitCode);
|
||||
void showResizeOverlay(); // transient grid-size overlay on resize
|
||||
void repositionResizeOverlay(); // re-place overlay for current widget size
|
||||
// Paint the resize overlay (if visible) directly via the parent's
|
||||
// QPainter — done inside paintEvent so the overlay is atomic with
|
||||
// the terminal blit beneath it.
|
||||
void paintResizeOverlay(QPainter &painter);
|
||||
void layoutSearchBar(); // position the search bar at the top edge
|
||||
void sendKey(QKeyEvent *, ghostty_input_action_e action);
|
||||
void commitText(const QString &text);
|
||||
|
|
@ -183,8 +187,12 @@ private:
|
|||
QLabel *m_exitOverlay = nullptr; // "process exited" banner; lazily made
|
||||
QLabel *m_keySeqOverlay = nullptr; // pending keybind chord; 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
|
||||
// Resize overlay is painted directly inside paintEvent (not a child
|
||||
// QLabel) so it can't race the parent's CompositionMode_Source blit
|
||||
// mid-resize. Deadline-based: visible while now < m_resizeOverlayUntil.
|
||||
QString m_resizeOverlayText;
|
||||
qint64 m_resizeOverlayUntilMs = 0; // monotonic ms since epoch
|
||||
QTimer *m_resizeHideTimer = nullptr; // schedules a paint at hide-time
|
||||
bool m_firstGridSeen = false; // for `resize-overlay = after-first`
|
||||
int m_lastCols = 0; // last grid size, to detect changes
|
||||
int m_lastRows = 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue