From 775905bdab89ade47cbc6fc09c71ac463ee1a865 Mon Sep 17 00:00:00 2001 From: ntomsic Date: Sat, 23 May 2026 14:46:53 -0500 Subject: [PATCH] =?UTF-8?q?qt:=20phase=205=20=E2=80=94=20extract=20QuickTe?= =?UTF-8?q?rminal=20into=20qt/src/quickterm/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pulls the layer-shell setup (setupLayerShell — ~100 lines reading quick-terminal-keyboard-interactivity / -screen / -size / -position plus the LayerShellQt anchor wiring) and the windowOpacity fade in/out animation out of MainWindow into a free-function `quickterm::` namespace under qt/src/quickterm/. The animation's QPropertyAnimation is parented to the QWidget via a dynamic property, so it still dies with the window — no per-window member bookkeeping needed on MainWindow anymore. MainWindow keeps the m_quickTerminal flag (too many sites read it: undo gating, refreshChrome, applyWindowConfig, etc.) and three thin forwarders (makeQuickTerminal, animateQuickTerminalIn, animateQuickTerminalOut). The autohide branch in changeEvent is one line and stays inline. MainWindow.cpp drops ~150 lines plus six now-unused includes (QEasingCurve, QPropertyAnimation, QScreen, QWindow, QCursor's sibling, LayerShellQt/window.h). MainWindow.h drops the QPropertyAnimation forward decl and the m_quickTerminalAnim member. Co-Authored-By: claude-flow --- qt/CMakeLists.txt | 1 + qt/src/MainWindow.cpp | 172 +----------------------- qt/src/MainWindow.h | 9 +- qt/src/quickterm/QuickTerminal.cpp | 202 +++++++++++++++++++++++++++++ qt/src/quickterm/QuickTerminal.h | 34 +++++ 5 files changed, 244 insertions(+), 174 deletions(-) create mode 100644 qt/src/quickterm/QuickTerminal.cpp create mode 100644 qt/src/quickterm/QuickTerminal.h diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index 5371b0edf..cb79d1343 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -113,6 +113,7 @@ add_executable(ghastty src/InspectorWindow.cpp src/MainWindow.cpp src/OverlayScrollbar.cpp + src/quickterm/QuickTerminal.cpp src/SearchBar.cpp src/TabWidget.cpp src/undo/UndoStack.cpp diff --git a/qt/src/MainWindow.cpp b/qt/src/MainWindow.cpp index 13e47309e..6925733a8 100644 --- a/qt/src/MainWindow.cpp +++ b/qt/src/MainWindow.cpp @@ -25,15 +25,12 @@ #include #include #include -#include #include #include #include -#include #include #include #include -#include #include #include #include @@ -45,14 +42,12 @@ #include #include #include -#include - -#include #include "app/GhosttyApp.h" #include "config/Config.h" #include "CommandPalette.h" #include "GhosttySurface.h" +#include "quickterm/QuickTerminal.h" #include "TabWidget.h" #include "undo/UndoStack.h" #include "Util.h" @@ -765,170 +760,13 @@ MainWindow *MainWindow::makeQuickTerminal() { delete w; return nullptr; } - w->setupLayerShell(); - w->animateQuickTerminalIn(); + quickterm::setupLayerShell(w); + quickterm::animateIn(w); return w; } -// Read quick-terminal-animation-duration (seconds) and convert to ms. -static int quickTerminalAnimationMs() { - double secs = 0.2; // matches Config.zig default - config::get(&secs, "quick-terminal-animation-duration"); - // Clamp to a sane range so a misconfigured 0 or negative value - // doesn't make the window appear/disappear instantly without an - // animation, and a very large value doesn't lock the user out. - if (secs <= 0.0) return 0; - return std::clamp(static_cast(secs * 1000.0), 1, 1000); -} - -void MainWindow::animateQuickTerminalIn() { - setWindowOpacity(0.0); - show(); - raise(); - activateWindow(); - const int ms = quickTerminalAnimationMs(); - if (ms <= 0) { - setWindowOpacity(1.0); - return; - } - // Stop any running fade so toggling rapidly doesn't stack - // animations. The animation is parented to `this` so it dies - // with the window. - if (m_quickTerminalAnim) m_quickTerminalAnim->stop(); - else m_quickTerminalAnim = new QPropertyAnimation(this, "windowOpacity", this); - m_quickTerminalAnim->setDuration(ms); - m_quickTerminalAnim->setStartValue(0.0); - m_quickTerminalAnim->setEndValue(1.0); - m_quickTerminalAnim->setEasingCurve(QEasingCurve::OutCubic); - m_quickTerminalAnim->start(); -} - -void MainWindow::animateQuickTerminalOut() { - const int ms = quickTerminalAnimationMs(); - if (ms <= 0) { - hide(); - return; - } - if (m_quickTerminalAnim) m_quickTerminalAnim->stop(); - else m_quickTerminalAnim = new QPropertyAnimation(this, "windowOpacity", this); - m_quickTerminalAnim->setDuration(ms); - m_quickTerminalAnim->setStartValue(windowOpacity()); - m_quickTerminalAnim->setEndValue(0.0); - m_quickTerminalAnim->setEasingCurve(QEasingCurve::InCubic); - // Disconnect any previous handler before reconnecting; otherwise a - // toggle-out-then-in cycle accumulates handlers that all fire on - // the next out. - disconnect(m_quickTerminalAnim, &QPropertyAnimation::finished, - this, nullptr); - connect(m_quickTerminalAnim, &QPropertyAnimation::finished, this, - [this]() { hide(); }); - m_quickTerminalAnim->start(); -} - -void MainWindow::setupLayerShell() { - // LayerShellQt attaches to the native window; force it into being. - winId(); - QWindow *handle = windowHandle(); - if (!handle) return; - LayerShellQt::Window *ls = LayerShellQt::Window::get(handle); - if (!ls) { - // The Qt frontend targets Wayland exclusively (the project - // builds against LayerShellQt for the dropdown). If we can't - // get a layer-shell handle the platform isn't supported — log - // and bail rather than silently degrading to a non-functional - // regular window. - std::fprintf(stderr, - "[ghastty] LayerShellQt::Window::get returned null; " - "the quick terminal needs a Wayland session with " - "wlr-layer-shell support (e.g. KWin, sway).\n"); - return; - } - using LSW = LayerShellQt::Window; - - ls->setLayer(LSW::LayerTop); - const QString ki = config::string("quick-terminal-keyboard-interactivity"); - ls->setKeyboardInteractivity( - ki == QLatin1String("exclusive") ? LSW::KeyboardInteractivityExclusive - : ki == QLatin1String("none") ? LSW::KeyboardInteractivityNone - : LSW::KeyboardInteractivityOnDemand); - - // quick-terminal-screen: pick which output to anchor on. - // `main` → primary screen. - // `mouse` → the screen the pointer is currently on. - // `macos-menu-bar` → macOS-only; falls through to primary on - // Linux. - // LayerShellQt 6.6+ exposes setScreen(QScreen*) on the layer-shell - // window directly; the older setScreenConfiguration is deprecated. - // Pass null to fall back to the QWindow's screen (LayerShellQt's - // documented default when neither setScreen nor - // setWantsToBeOnActiveScreen is set). - const QString screenMode = config::string("quick-terminal-screen"); - QScreen *screen = nullptr; - if (screenMode == QLatin1String("mouse")) { - screen = QGuiApplication::screenAt(QCursor::pos()); - } else if (screenMode == QLatin1String("main") || - screenMode == QLatin1String("macos-menu-bar")) { - screen = QGuiApplication::primaryScreen(); - } - ls->setScreen(screen); - if (!screen) screen = handle->screen(); - - // quick-terminal-space-behavior (`remain` / `move`) is intentionally - // not read: macOS controls whether the dropdown follows the active - // Space or pins to the one it was opened on, but Wayland's - // wlr-layer-shell has no equivalent — the compositor always renders - // the surface on the active workspace (KWin behaviour), which - // corresponds to `move`. Achieving `remain` would need a - // per-workspace pin that no mainstream compositor exposes; honour - // by no-op and document. - - const QSize scr = screen ? screen->size() : QSize(1920, 1080); - - // quick-terminal-size: primary is the edge-perpendicular extent. - ghostty_config_quick_terminal_size_s qsz = {}; - config::get(&qsz, "quick-terminal-size"); - const auto toPx = [](const ghostty_quick_terminal_size_s &s, int dim, - int fallback) -> int { - switch (s.tag) { - case GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE: - return static_cast(s.value.percentage / 100.0f * dim); - case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS: - return static_cast(s.value.pixels); - default: - return fallback; - } - }; - - const QString pos = config::string("quick-terminal-position"); - LSW::Anchors anchors; - QSize size; - if (pos == QLatin1String("bottom")) { - anchors = LSW::Anchors(LSW::AnchorBottom) | LSW::AnchorLeft | - LSW::AnchorRight; - size = {scr.width(), toPx(qsz.primary, scr.height(), 400)}; - } else if (pos == QLatin1String("left")) { - anchors = LSW::Anchors(LSW::AnchorLeft) | LSW::AnchorTop | - LSW::AnchorBottom; - size = {toPx(qsz.primary, scr.width(), 400), scr.height()}; - } else if (pos == QLatin1String("right")) { - anchors = LSW::Anchors(LSW::AnchorRight) | LSW::AnchorTop | - LSW::AnchorBottom; - size = {toPx(qsz.primary, scr.width(), 400), scr.height()}; - } else if (pos == QLatin1String("center")) { - anchors = LSW::Anchors(LSW::AnchorNone); - size = {toPx(qsz.primary, scr.width(), 800), - toPx(qsz.secondary, scr.height(), 400)}; - } else { // top (the default) - anchors = LSW::Anchors(LSW::AnchorTop) | LSW::AnchorLeft | - LSW::AnchorRight; - size = {scr.width(), toPx(qsz.primary, scr.height(), 400)}; - } - ls->setAnchors(anchors); - // The layer-shell protocol takes the size from the underlying - // wl_surface (i.e. the QWindow's size); LayerShellQt has no - // setDesiredSize on this Qt branch. - resize(size); -} +void MainWindow::animateQuickTerminalIn() { quickterm::animateIn(this); } +void MainWindow::animateQuickTerminalOut() { quickterm::animateOut(this); } void MainWindow::changeEvent(QEvent *e) { // quick-terminal-autohide: fade out the dropdown when it loses diff --git a/qt/src/MainWindow.h b/qt/src/MainWindow.h index 6f61455f7..dbda595d0 100644 --- a/qt/src/MainWindow.h +++ b/qt/src/MainWindow.h @@ -14,7 +14,6 @@ class QMediaPlayer; class QShowEvent; class QSplitter; class TabWidget; -class QPropertyAnimation; class QTimer; class CommandPalette; class GhosttySurface; @@ -225,10 +224,6 @@ private: // compositor (see WindowBlur). void applyBlur(); - // Turn this window into a layer-shell dropdown anchored to a screen - // edge, per the `quick-terminal-*` config. Quick-terminal only. - void setupLayerShell(); - TabWidget *m_tabs = nullptr; QList m_surfaces; // every live surface in this window bool m_firstTabPending = true; // first tab is created on show() @@ -236,8 +231,8 @@ private: bool m_skipCloseConfirm = false; // close already confirmed elsewhere bool m_quickTerminal = false; // this is the dropdown quick terminal // Per-window opacity animation for the quick terminal (fade in/out - // using quick-terminal-animation-duration). Lazily created. - QPropertyAnimation *m_quickTerminalAnim = nullptr; + // using quick-terminal-animation-duration). Owned by quickterm/'s + // dynamic-property cache on this widget; cleared on widget delete. QSize m_defaultWindowSize; // for RESET_WINDOW_SIZE; from INITIAL_SIZE // Last cell size reported by libghostty for this window's surfaces // (CELL_SIZE action). Stored so future grid-snap resizing can use diff --git a/qt/src/quickterm/QuickTerminal.cpp b/qt/src/quickterm/QuickTerminal.cpp new file mode 100644 index 000000000..35bb1b182 --- /dev/null +++ b/qt/src/quickterm/QuickTerminal.cpp @@ -0,0 +1,202 @@ +#include "QuickTerminal.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../config/Config.h" +#include "ghostty.h" + +namespace quickterm { + +namespace { + +// Anim and toggle live on the QObject child tree of `window`, so +// they die with it. We keep the QPropertyAnimation as a dynamic +// property so callers don't need to thread it through. +constexpr const char *kAnimProperty = "_ghastty_qt_anim"; + +// Read quick-terminal-animation-duration (seconds) and convert to ms. +// Clamps to a sane range so a misconfigured 0/negative value doesn't +// make the window appear/disappear instantly without an animation, +// and a very large value doesn't lock the user out. +int animationMs() { + double secs = 0.2; // matches Config.zig default + config::get(&secs, "quick-terminal-animation-duration"); + if (secs <= 0.0) return 0; + return std::clamp(static_cast(secs * 1000.0), 1, 1000); +} + +// Lazily fetch (or build) the per-window opacity animation, parented +// to `window` so its lifetime tracks the widget's. +QPropertyAnimation *animFor(QWidget *window) { + auto *existing = window->property(kAnimProperty).value(); + if (existing) return existing; + auto *anim = new QPropertyAnimation(window, "windowOpacity", window); + window->setProperty(kAnimProperty, + QVariant::fromValue(anim)); + return anim; +} + +} // namespace + +void setupLayerShell(QWidget *window) { + // LayerShellQt attaches to the native window; force it into being. + window->winId(); + QWindow *handle = window->windowHandle(); + if (!handle) return; + LayerShellQt::Window *ls = LayerShellQt::Window::get(handle); + if (!ls) { + // The Qt frontend targets Wayland exclusively (the project + // builds against LayerShellQt for the dropdown). If we can't + // get a layer-shell handle the platform isn't supported — log + // and bail rather than silently degrading to a non-functional + // regular window. + std::fprintf(stderr, + "[ghastty] LayerShellQt::Window::get returned null; " + "the quick terminal needs a Wayland session with " + "wlr-layer-shell support (e.g. KWin, sway).\n"); + return; + } + using LSW = LayerShellQt::Window; + + ls->setLayer(LSW::LayerTop); + const QString ki = config::string("quick-terminal-keyboard-interactivity"); + ls->setKeyboardInteractivity( + ki == QLatin1String("exclusive") ? LSW::KeyboardInteractivityExclusive + : ki == QLatin1String("none") ? LSW::KeyboardInteractivityNone + : LSW::KeyboardInteractivityOnDemand); + + // quick-terminal-screen: pick which output to anchor on. + // `main` → primary screen. + // `mouse` → the screen the pointer is currently on. + // `macos-menu-bar` → macOS-only; falls through to primary on + // Linux. + // LayerShellQt 6.6+ exposes setScreen(QScreen*) on the layer-shell + // window directly; the older setScreenConfiguration is deprecated. + // Pass null to fall back to the QWindow's screen (LayerShellQt's + // documented default when neither setScreen nor + // setWantsToBeOnActiveScreen is set). + const QString screenMode = config::string("quick-terminal-screen"); + QScreen *screen = nullptr; + if (screenMode == QLatin1String("mouse")) { + screen = QGuiApplication::screenAt(QCursor::pos()); + } else if (screenMode == QLatin1String("main") || + screenMode == QLatin1String("macos-menu-bar")) { + screen = QGuiApplication::primaryScreen(); + } + ls->setScreen(screen); + if (!screen) screen = handle->screen(); + + // quick-terminal-space-behavior (`remain` / `move`) is intentionally + // not read: macOS controls whether the dropdown follows the active + // Space or pins to the one it was opened on, but Wayland's + // wlr-layer-shell has no equivalent — the compositor always renders + // the surface on the active workspace (KWin behaviour), which + // corresponds to `move`. Achieving `remain` would need a + // per-workspace pin that no mainstream compositor exposes; honour + // by no-op and document. + + const QSize scr = screen ? screen->size() : QSize(1920, 1080); + + // quick-terminal-size: primary is the edge-perpendicular extent. + ghostty_config_quick_terminal_size_s qsz = {}; + config::get(&qsz, "quick-terminal-size"); + const auto toPx = [](const ghostty_quick_terminal_size_s &s, int dim, + int fallback) -> int { + switch (s.tag) { + case GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE: + return static_cast(s.value.percentage / 100.0f * dim); + case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS: + return static_cast(s.value.pixels); + default: + return fallback; + } + }; + + const QString pos = config::string("quick-terminal-position"); + LSW::Anchors anchors; + QSize size; + if (pos == QLatin1String("bottom")) { + anchors = LSW::Anchors(LSW::AnchorBottom) | LSW::AnchorLeft | + LSW::AnchorRight; + size = {scr.width(), toPx(qsz.primary, scr.height(), 400)}; + } else if (pos == QLatin1String("left")) { + anchors = LSW::Anchors(LSW::AnchorLeft) | LSW::AnchorTop | + LSW::AnchorBottom; + size = {toPx(qsz.primary, scr.width(), 400), scr.height()}; + } else if (pos == QLatin1String("right")) { + anchors = LSW::Anchors(LSW::AnchorRight) | LSW::AnchorTop | + LSW::AnchorBottom; + size = {toPx(qsz.primary, scr.width(), 400), scr.height()}; + } else if (pos == QLatin1String("center")) { + anchors = LSW::Anchors(LSW::AnchorNone); + size = {toPx(qsz.primary, scr.width(), 800), + toPx(qsz.secondary, scr.height(), 400)}; + } else { // top (the default) + anchors = LSW::Anchors(LSW::AnchorTop) | LSW::AnchorLeft | + LSW::AnchorRight; + size = {scr.width(), toPx(qsz.primary, scr.height(), 400)}; + } + ls->setAnchors(anchors); + // The layer-shell protocol takes the size from the underlying + // wl_surface (i.e. the QWindow's size); LayerShellQt has no + // setDesiredSize on this Qt branch. + window->resize(size); +} + +void animateIn(QWidget *window) { + window->setWindowOpacity(0.0); + window->show(); + window->raise(); + window->activateWindow(); + const int ms = animationMs(); + if (ms <= 0) { + window->setWindowOpacity(1.0); + return; + } + // Stop any running fade so toggling rapidly doesn't stack + // animations. + QPropertyAnimation *anim = animFor(window); + anim->stop(); + anim->setDuration(ms); + anim->setStartValue(0.0); + anim->setEndValue(1.0); + anim->setEasingCurve(QEasingCurve::OutCubic); + anim->start(); +} + +void animateOut(QWidget *window) { + const int ms = animationMs(); + if (ms <= 0) { + window->hide(); + return; + } + QPropertyAnimation *anim = animFor(window); + anim->stop(); + anim->setDuration(ms); + anim->setStartValue(window->windowOpacity()); + anim->setEndValue(0.0); + anim->setEasingCurve(QEasingCurve::InCubic); + // Disconnect any previous handler before reconnecting; otherwise a + // toggle-out-then-in cycle accumulates handlers that all fire on + // the next out. + QObject::disconnect(anim, &QPropertyAnimation::finished, window, nullptr); + QObject::connect(anim, &QPropertyAnimation::finished, window, + [window]() { window->hide(); }); + anim->start(); +} + +} // namespace quickterm diff --git a/qt/src/quickterm/QuickTerminal.h b/qt/src/quickterm/QuickTerminal.h new file mode 100644 index 000000000..fa7f0596d --- /dev/null +++ b/qt/src/quickterm/QuickTerminal.h @@ -0,0 +1,34 @@ +#pragma once + +class QWidget; + +// Free functions that drive the dropdown quick-terminal window: a +// wlr-layer-shell anchored surface, faded in/out via windowOpacity. +// `window` is the QWidget hosting the layer-shell surface +// (MainWindow with m_quickTerminal == true). +// +// The animation is parented to `window`'s child tree, so it dies +// with the window. +namespace quickterm { + +// Configure the layer-shell anchor, screen, keyboard interactivity, +// and size from the `quick-terminal-*` config keys. Logs and bails +// when LayerShellQt isn't available — the Qt frontend is Wayland- +// only, so a missing layer-shell surface is a runtime configuration +// error, not a portability fallback. +void setupLayerShell(QWidget *window); + +// Fade the window in: opacity 0 → 1 over +// `quick-terminal-animation-duration` seconds. show()/raise()/ +// activateWindow() are called up front so the user gets the focus +// during the fade. A duration of 0 collapses to an immediate +// setWindowOpacity(1.0). +void animateIn(QWidget *window); + +// Fade the window out and hide() on completion. Disconnects any +// previous `finished` handler before reconnecting so a rapid +// in/out/in cycle doesn't pile up handlers that all fire on the +// next `out`. +void animateOut(QWidget *window); + +} // namespace quickterm