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. // and doesn't allow for interaction with anything on the desktop. These two calls fix that.
effects->ungrabKeyboard(); effects->ungrabKeyboard();
effects->stopMouseInterception(this); effects->stopMouseInterception(this);
hideCursor();
} }
void BreezyDesktopEffect::deactivate() void BreezyDesktopEffect::deactivate()
@ -392,6 +390,14 @@ bool BreezyDesktopEffect::isEnabled() const {
return m_enabled; 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 { bool BreezyDesktopEffect::isZoomOnFocusEnabled() const {
return m_zoomOnFocusEnabled; return m_zoomOnFocusEnabled;
} }
@ -572,7 +578,6 @@ bool BreezyDesktopEffect::checkParityByte(const char* data) {
return parityByte == parity; 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 lastConfigUpdate = 0;
static qint64 activatedAt = 0; static qint64 activatedAt = 0;
void BreezyDesktopEffect::updateImuRotation() { void BreezyDesktopEffect::updateImuRotation() {
@ -786,13 +791,19 @@ QPointF BreezyDesktopEffect::cursorPos() const
void BreezyDesktopEffect::showCursor() void BreezyDesktopEffect::showCursor()
{ {
if (!m_cursorHidden) return;
effects->showCursor(); effects->showCursor();
m_cursorHidden = false;
} }
void BreezyDesktopEffect::hideCursor() void BreezyDesktopEffect::hideCursor()
{ {
if (m_cursorHidden) return;
updateCursorImage(); updateCursorImage();
effects->hideCursor(); effects->hideCursor();
m_cursorHidden = true;
} }
void BreezyDesktopEffect::updateCursorImage() void BreezyDesktopEffect::updateCursorImage()
@ -810,6 +821,8 @@ void BreezyDesktopEffect::updateCursorImage()
m_cursorImageSource = QString(); m_cursorImageSource = QString();
m_cursorImageSize = QSize(); m_cursorImageSize = QSize();
} }
// Cursor size affects the expanded geometry margin; invalidate cache.
invalidateEffectOnScreenGeometryCache();
Q_EMIT cursorImageSourceChanged(); Q_EMIT cursorImageSourceChanged();
} }
@ -819,11 +832,56 @@ void BreezyDesktopEffect::updateCursorPos()
const auto cursor = effects->cursorImage(); const auto cursor = effects->cursorImage();
QPointF newPos = effects->cursorPos() - cursor.hotSpot(); QPointF newPos = effects->cursorPos() - cursor.hotSpot();
if (m_cursorPos != newPos) { if (m_cursorPos != newPos) {
const QPointF prevPos = m_cursorPos;
m_cursorPos = newPos; m_cursorPos = newPos;
Q_EMIT cursorPosChanged(); 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) void BreezyDesktopEffect::warpPointerToOutputCenter(Output *output)
{ {
if (!output) { if (!output) {
@ -832,6 +890,9 @@ void BreezyDesktopEffect::warpPointerToOutputCenter(Output *output)
const QRect geometry = output->geometry(); const QRect geometry = output->geometry();
const QPointF center = geometry.center(); const QPointF center = geometry.center();
Cursors::self()->mouse()->setPos(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() void BreezyDesktopEffect::moveCursorToFocusedDisplay()

View File

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

View File

@ -2,33 +2,15 @@ import QtQuick
Item { Item {
id: singleDesktopView id: singleDesktopView
property point cursorPos: effect.cursorPos
property bool supportsXR: false property bool supportsXR: false
property bool showCalibratingBanner: 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 { DesktopView {
id: desktopViewComponent id: desktopViewComponent
screen: targetScreen screen: targetScreen
width: targetScreen.geometry.width width: targetScreen.geometry.width
height: targetScreen.geometry.height height: targetScreen.geometry.height
} }
Image {
id: cursorImg
x: 0
y: 0
z: 9999 // ensure on top
}
Image { Image {
source: effect.customBannerEnabled ? "custom_banner.png" : "calibrating.png" source: effect.customBannerEnabled ? "custom_banner.png" : "calibrating.png"
@ -36,18 +18,4 @@ Item {
anchors.horizontalCenter: desktopViewComponent.horizontalCenter anchors.horizontalCenter: desktopViewComponent.horizontalCenter
anchors.bottom: desktopViewComponent.bottom 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}`); console.log(`Breezy - checking screen ${targetScreen.model}: ${targetScreenSupported} ${targetScreenIsVirtual} ${isEnabled} ${imuResetState}`);
const show3DView = targetScreenSupported && isEnabled && !imuResetState; const show3DView = targetScreenSupported && isEnabled && !imuResetState;
if (!targetScreenIsVirtual) viewLoader.sourceComponent = show3DView ? view3DComponent : desktopViewComponent; if (!targetScreenIsVirtual) viewLoader.sourceComponent = show3DView ? view3DComponent : desktopViewComponent;
if (targetScreenSupported) effect.effectTargetScreenIndex = KWinComponents.Workspace.screens.indexOf(targetScreen);
} }
onImuResetStateChanged: { onImuResetStateChanged: {