mirror-ghostty/qt/src/quickterm/QuickTerminal.cpp

218 lines
8.4 KiB
C++

#include "QuickTerminal.h"
#include <algorithm>
#include <cstdio>
#include <QCursor>
#include <QEasingCurve>
#include <QGuiApplication>
#include <QPropertyAnimation>
#include <QScreen>
#include <QSize>
#include <QString>
#include <QStringLiteral>
#include <QWidget>
#include <QWindow>
#include <LayerShellQt/window.h>
#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. The "_q_"
// underscore-prefix space is reserved by Qt; any other prefix is
// fine and the dotted form keeps it visibly application-scoped.
constexpr const char *kAnimProperty = "ghastty.quickterm.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
// On read failure secs keeps the default; the success bit isn't
// load-bearing.
(void)config::get(&secs, "quick-terminal-animation-duration");
if (secs <= 0.0) return 0;
return std::clamp(static_cast<int>(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<QPropertyAnimation *>();
if (existing) return existing;
auto *anim = new QPropertyAnimation(window, "windowOpacity", window);
window->setProperty(kAnimProperty,
QVariant::fromValue<QPropertyAnimation *>(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);
// For sizing only — LayerShellQt already has the anchor screen above
// (or fell back to the QWindow's screen via setScreen(nullptr)). We
// need a non-null QScreen below to read its pixel dimensions.
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.
// On read failure qsz stays zero-initialized and toPx falls back to
// its `fallback` argument; the success bit isn't load-bearing.
ghostty_config_quick_terminal_size_s qsz = {};
(void)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<int>(s.value.percentage / 100.0f * dim);
case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS:
return static_cast<int>(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();
// animateOut leaves a `finished -> hide()` handler attached to the
// shared animation object. If a fade-out was interrupted by this
// fade-in (rapid out/in cycle), the leftover handler would fire at
// the end of the in-fade and silently hide the just-revealed
// window — clear it before starting.
QObject::disconnect(anim, &QPropertyAnimation::finished, window, nullptr);
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