Improve cursor interaction so that physical displays use native cursor rendering

This commit is contained in:
wheaney 2025-09-24 10:15:42 -07:00
parent 4a756d63a5
commit fc6858d535
4 changed files with 79 additions and 35 deletions

View File

@ -291,8 +291,6 @@ void BreezyDesktopEffect::activate()
// and doesn't allow for interaction with anything on the desktop. These two calls fix that.
effects->ungrabKeyboard();
effects->stopMouseInterception(this);
hideCursor();
}
void BreezyDesktopEffect::deactivate()
@ -392,6 +390,14 @@ bool BreezyDesktopEffect::isEnabled() const {
return m_enabled;
}
void BreezyDesktopEffect::setEffectTargetScreenIndex(int index) {
if (m_effectTargetScreenIndex != index) {
m_effectTargetScreenIndex = index;
invalidateEffectOnScreenGeometryCache();
evaluateCursorOnScreenState(m_cursorPos, m_cursorPos);
}
}
bool BreezyDesktopEffect::isZoomOnFocusEnabled() const {
return m_zoomOnFocusEnabled;
}
@ -572,7 +578,6 @@ bool BreezyDesktopEffect::checkParityByte(const char* data) {
return parityByte == parity;
}
// TODO - can this be something callable from the camera qml code, so it's pulled only when needed?
static qint64 lastConfigUpdate = 0;
static qint64 activatedAt = 0;
void BreezyDesktopEffect::updateImuRotation() {
@ -786,13 +791,19 @@ QPointF BreezyDesktopEffect::cursorPos() const
void BreezyDesktopEffect::showCursor()
{
if (!m_cursorHidden) return;
effects->showCursor();
m_cursorHidden = false;
}
void BreezyDesktopEffect::hideCursor()
{
if (m_cursorHidden) return;
updateCursorImage();
effects->hideCursor();
m_cursorHidden = true;
}
void BreezyDesktopEffect::updateCursorImage()
@ -810,6 +821,8 @@ void BreezyDesktopEffect::updateCursorImage()
m_cursorImageSource = QString();
m_cursorImageSize = QSize();
}
// Cursor size affects the expanded geometry margin; invalidate cache.
invalidateEffectOnScreenGeometryCache();
Q_EMIT cursorImageSourceChanged();
}
@ -819,11 +832,56 @@ void BreezyDesktopEffect::updateCursorPos()
const auto cursor = effects->cursorImage();
QPointF newPos = effects->cursorPos() - cursor.hotSpot();
if (m_cursorPos != newPos) {
const QPointF prevPos = m_cursorPos;
m_cursorPos = newPos;
Q_EMIT cursorPosChanged();
evaluateCursorOnScreenState(prevPos, m_cursorPos);
}
}
void BreezyDesktopEffect::evaluateCursorOnScreenState(const QPointF &prevPos, const QPointF &newPos)
{
if (!updateEffectOnScreenGeometryCache()) return;
const QPointF velocity = newPos - prevPos;
const QPointF predicted = newPos + velocity;
const bool onScreen =
m_effectOnScreenExpandedGeometry.contains(newPos.toPoint()) ||
m_effectOnScreenExpandedGeometry.contains(predicted.toPoint());
if (!m_cursorHidden && onScreen) {
hideCursor();
} else if (m_enabled && !m_imuResetState && m_cursorHidden && !onScreen) {
showCursor();
}
}
void BreezyDesktopEffect::invalidateEffectOnScreenGeometryCache()
{
m_effectOnScreenGeometryValid = false;
}
bool BreezyDesktopEffect::updateEffectOnScreenGeometryCache()
{
if (m_effectOnScreenGeometryValid)
return true;
if (m_effectTargetScreenIndex == -1)
return false;
Output *effectOnScreen = effects->screens().at(m_effectTargetScreenIndex);
if (!effectOnScreen)
return false;
const QRect geometry = effectOnScreen->geometry();
const int marginX = (m_cursorImageSize.width() > 0) ? m_cursorImageSize.width() : 10;
const int marginY = (m_cursorImageSize.height() > 0) ? m_cursorImageSize.height() : 10;
m_effectOnScreenExpandedGeometry = geometry.adjusted(-marginX, -marginY, marginX, marginY);
m_effectOnScreenGeometryValid = true;
return true;
}
void BreezyDesktopEffect::warpPointerToOutputCenter(Output *output)
{
if (!output) {
@ -832,6 +890,9 @@ void BreezyDesktopEffect::warpPointerToOutputCenter(Output *output)
const QRect geometry = output->geometry();
const QPointF center = geometry.center();
Cursors::self()->mouse()->setPos(center);
// When warping, we don't have a meaningful previous position; use center for both.
evaluateCursorOnScreenState(center, center);
}
void BreezyDesktopEffect::moveCursorToFocusedDisplay()

View File

@ -11,6 +11,7 @@
#include <QVariant>
#include <QVariantList>
#include <QHash>
#include <QRect>
namespace KWin
{
@ -18,6 +19,7 @@ namespace KWin
{
Q_OBJECT
Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY enabledStateChanged)
Q_PROPERTY(int effectTargetScreenIndex READ effectTargetScreenIndex WRITE setEffectTargetScreenIndex)
Q_PROPERTY(bool zoomOnFocusEnabled READ isZoomOnFocusEnabled WRITE setZoomOnFocusEnabled NOTIFY zoomOnFocusChanged)
Q_PROPERTY(int lookingAtScreenIndex READ lookingAtScreenIndex WRITE setLookingAtScreenIndex)
Q_PROPERTY(bool imuResetState READ imuResetState NOTIFY imuResetStateChanged)
@ -48,6 +50,7 @@ namespace KWin
Q_PROPERTY(bool curvedDisplay READ curvedDisplay NOTIFY curvedDisplayChanged)
Q_PROPERTY(bool curvedDisplaySupported READ curvedDisplaySupported WRITE setCurvedDisplaySupported NOTIFY curvedDisplaySupportedChanged)
public:
BreezyDesktopEffect();
@ -62,6 +65,8 @@ namespace KWin
QPointF cursorPos() const;
bool isEnabled() const;
int effectTargetScreenIndex() const { return m_effectTargetScreenIndex; }
void setEffectTargetScreenIndex(int index);
bool isZoomOnFocusEnabled() const;
void setZoomOnFocusEnabled(bool enabled);
int lookingAtScreenIndex() const { return m_lookingAtScreenIndex; }
@ -147,6 +152,9 @@ namespace KWin
void setSmoothFollowThreshold(float threshold);
void updateDriverSmoothFollowSettings();
void warpPointerToOutputCenter(Output *output);
void evaluateCursorOnScreenState(const QPointF &prevPos, const QPointF &newPos);
void invalidateEffectOnScreenGeometryCache();
bool updateEffectOnScreenGeometryCache();
QString m_cursorImageSource;
QSize m_cursorImageSize;
@ -154,6 +162,7 @@ namespace KWin
bool m_enabled = false;
bool m_zoomOnFocusEnabled = false;
int m_lookingAtScreenIndex = -1;
int m_effectTargetScreenIndex = -1;
bool m_imuResetState;
QList<QQuaternion> m_imuRotations;
quint32 m_imuTimeElapsedMs;
@ -169,6 +178,7 @@ namespace KWin
bool m_customBannerEnabled;
QFileSystemWatcher *m_shmFileWatcher = nullptr;
QFileSystemWatcher *m_shmDirectoryWatcher = nullptr;
bool m_cursorHidden = false;
QPointF m_cursorPos;
QTimer *m_cursorUpdateTimer = nullptr;
qreal m_focusedDisplayDistance = 0.85;
@ -186,6 +196,10 @@ namespace KWin
bool m_allDisplaysFollowMode = false;
bool m_focusedSmoothFollowEnabled = false;
// Cached geometry for on-screen cursor evaluation
QRect m_effectOnScreenExpandedGeometry;
bool m_effectOnScreenGeometryValid = false;
struct VirtualOutputInfo {
Output *output = nullptr;
QString id;

View File

@ -2,33 +2,15 @@ import QtQuick
Item {
id: singleDesktopView
property point cursorPos: effect.cursorPos
property bool supportsXR: false
property bool showCalibratingBanner: false
function cursorInBounds() {
const x = cursorPos.x
const y = cursorPos.y
const screenGeom = targetScreen.geometry
return x >= screenGeom.x &&
x < screenGeom.x + screenGeom.width &&
y >= screenGeom.y &&
y < screenGeom.y + screenGeom.height
}
DesktopView {
id: desktopViewComponent
screen: targetScreen
width: targetScreen.geometry.width
height: targetScreen.geometry.height
}
Image {
id: cursorImg
x: 0
y: 0
z: 9999 // ensure on top
}
Image {
source: effect.customBannerEnabled ? "custom_banner.png" : "calibrating.png"
@ -36,18 +18,4 @@ Item {
anchors.horizontalCenter: desktopViewComponent.horizontalCenter
anchors.bottom: desktopViewComponent.bottom
}
onCursorPosChanged: {
if (singleDesktopView.cursorInBounds()) {
const newX = effect.cursorPos.x - targetScreen.geometry.x
const newY = effect.cursorPos.y - targetScreen.geometry.y
const newSrc = effect.cursorImageSource
if (cursorImg.x !== newX) cursorImg.x = newX
if (cursorImg.y !== newY) cursorImg.y = newY
if (cursorImg.source !== newSrc) cursorImg.source = newSrc
if (!cursorImg.visible) cursorImg.visible = true
} else if (cursorImg.visible) {
cursorImg.visible = false
}
}
}

View File

@ -140,6 +140,7 @@ Item {
console.log(`Breezy - checking screen ${targetScreen.model}: ${targetScreenSupported} ${targetScreenIsVirtual} ${isEnabled} ${imuResetState}`);
const show3DView = targetScreenSupported && isEnabled && !imuResetState;
if (!targetScreenIsVirtual) viewLoader.sourceComponent = show3DView ? view3DComponent : desktopViewComponent;
if (targetScreenSupported) effect.effectTargetScreenIndex = KWinComponents.Workspace.screens.indexOf(targetScreen);
}
onImuResetStateChanged: {