From e96ccc4f0b2fe4475c67077f1824b46d3940b7c7 Mon Sep 17 00:00:00 2001 From: Wayne Heaney <42350981+wheaney@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:08:56 -0800 Subject: [PATCH] Add 6DoF support for KWin (#146), v2.6.3 --- VERSION | 2 +- kwin/src/breezydesktopconfig.kcfg | 28 ++++- kwin/src/breezydesktopeffect.cpp | 50 ++++++-- kwin/src/breezydesktopeffect.h | 15 ++- kwin/src/kcm/breezydesktopeffectkcm.cpp | 109 ++++++++++++++++- kwin/src/kcm/breezydesktopeffectkcm.h | 6 + kwin/src/kcm/breezydesktopeffectkcm.ui | 122 ++++++++++++++----- kwin/src/kcm/labeledslider.h | 86 +++++++++++-- kwin/src/qml/BreezyDesktop.qml | 10 +- kwin/src/qml/BreezyDesktopDisplay.qml | 5 +- kwin/src/qml/CameraController.qml | 20 ++- kwin/src/qml/CurvableDisplayMesh.qml | 27 ++--- kwin/src/qml/DesktopView.qml | 2 +- kwin/src/qml/Displays.qml | 155 ++++++++++++++++-------- kwin/src/qml/main.qml | 51 ++++++-- modules/XRLinuxDriver | 2 +- ui/modules/PyXRLinuxDriverIPC | 2 +- 17 files changed, 540 insertions(+), 152 deletions(-) diff --git a/VERSION b/VERSION index 097a15a..ec1cf33 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.6.2 +2.6.3 diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg index 99cfec4..2535499 100644 --- a/kwin/src/breezydesktopconfig.kcfg +++ b/kwin/src/breezydesktopconfig.kcfg @@ -6,17 +6,22 @@ - 85 - 20 - 230 + 22 + 10 + 250 - 105 - 20 - 230 + 25 + 10 + 250 + + cm + + Units for displaying distances in the KCM UI. Valid values: "cm" or "in". + false @@ -29,6 +34,13 @@ How far apart the displays are visually (not logically) + + 97 + 10 + 250 + + Scale the displays in XR (percentage) + 0 -250 @@ -91,5 +103,9 @@ Curve the displays around you + + + false + diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index d0f3a64..c1256e8 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -157,9 +157,9 @@ BreezyDesktopEffect::BreezyDesktopEffect() !m_shmFileWatcher->files().contains(DataView::SHM_PATH) )) { m_shmFileWatcher->removePath(DataView::SHM_PATH); - disconnect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePoseOrientation); + disconnect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePose); m_shmFileWatcher->addPath(DataView::SHM_PATH); - connect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePoseOrientation); + connect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePose); } }; @@ -173,7 +173,7 @@ BreezyDesktopEffect::BreezyDesktopEffect() m_watchdogTimer->setInterval(1000); connect(m_watchdogTimer, &QTimer::timeout, this, [this]() { if (!m_enabled) return; - this->updatePoseOrientation(); + this->updatePose(); }); m_watchdogTimer->start(); @@ -243,6 +243,7 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags) setFocusedDisplayDistance(BreezyDesktopConfig::focusedDisplayDistance() / 100.0f); setAllDisplaysDistance(BreezyDesktopConfig::allDisplaysDistance() / 100.0f); setDisplaySpacing(BreezyDesktopConfig::displaySpacing() / 1000.0f); + setDisplaySize(BreezyDesktopConfig::displaySize() / 100.0f); setZoomOnFocusEnabled(BreezyDesktopConfig::zoomOnFocusEnabled()); setSmoothFollowThreshold(BreezyDesktopConfig::smoothFollowThreshold()); @@ -262,6 +263,9 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags) if (m_removeVirtualDisplaysOnDisable != removeVD) { m_removeVirtualDisplaysOnDisable = removeVD; Q_EMIT removeVirtualDisplaysOnDisableChanged(); } if (m_mirrorPhysicalDisplays != mirrorPhysicalDisplays) { m_mirrorPhysicalDisplays = mirrorPhysicalDisplays; Q_EMIT mirrorPhysicalDisplaysChanged(); } + const bool developerMode = BreezyDesktopConfig::developerMode(); + if (m_developerMode != developerMode) { m_developerMode = developerMode; Q_EMIT developerModeChanged(); } + bool curved = BreezyDesktopConfig::curvedDisplay() && m_curvedDisplaySupported; if (m_curvedDisplay != curved) { m_curvedDisplay = curved; Q_EMIT curvedDisplayChanged(); } @@ -269,6 +273,11 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags) m_allDisplaysFollowMode = BreezyDesktopConfig::allDisplaysFollowMode(); } +bool BreezyDesktopEffect::developerMode() const +{ + return m_developerMode; +} + QVariantMap BreezyDesktopEffect::initialProperties(Output *screen) { return QVariantMap{ @@ -460,6 +469,10 @@ quint64 BreezyDesktopEffect::poseTimestamp() const { return m_poseTimestamp; } +bool BreezyDesktopEffect::poseHasPosition() const { + return m_poseHasPosition; +} + QList BreezyDesktopEffect::lookAheadConfig() const { return m_lookAheadConfig; } @@ -485,7 +498,7 @@ qreal BreezyDesktopEffect::focusedDisplayDistance() const { void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) { if (distance != m_focusedDisplayDistance) { - m_focusedDisplayDistance = std::clamp(distance, 0.2, m_allDisplaysDistance); + m_focusedDisplayDistance = std::clamp(distance, 0.1, m_allDisplaysDistance); Q_EMIT focusedDisplayDistanceChanged(); if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings(); @@ -498,7 +511,7 @@ qreal BreezyDesktopEffect::allDisplaysDistance() const { void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) { if (distance != m_allDisplaysDistance) { - qreal min = m_zoomOnFocusEnabled ? m_focusedDisplayDistance : 0.2; + qreal min = m_zoomOnFocusEnabled ? m_focusedDisplayDistance : 0.1; m_allDisplaysDistance = std::clamp(distance, min, 2.5); Q_EMIT allDisplaysDistanceChanged(); } @@ -515,6 +528,18 @@ void BreezyDesktopEffect::setDisplaySpacing(qreal spacing) { } } +qreal BreezyDesktopEffect::displaySize() const { + return m_displaySize; +} + +void BreezyDesktopEffect::setDisplaySize(qreal size) { + const qreal clamped = std::clamp(size, 0.1, 4.0); + if (!qFuzzyCompare(clamped, m_displaySize)) { + m_displaySize = clamped; + Q_EMIT displaySizeChanged(); + } +} + qreal BreezyDesktopEffect::displayHorizontalOffset() const { return m_displayHorizontalOffset; } @@ -604,7 +629,7 @@ bool BreezyDesktopEffect::checkParityByte(const char* data) { static qint64 lastConfigUpdate = 0; static qint64 activatedAt = 0; -void BreezyDesktopEffect::updatePoseOrientation() { +void BreezyDesktopEffect::updatePose() { // Reentrancy guard: if an update is already in progress, skip bool expected = false; if (!m_poseUpdateInProgress.compare_exchange_strong(expected, true)) { @@ -697,7 +722,16 @@ void BreezyDesktopEffect::updatePoseOrientation() { << "diagonalFOV:" << m_diagonalFOV; activate(); m_enabled = true; + m_poseHasPosition = false; + auto driverStateOpt = XRDriverIPC::instance().retrieveDriverState(); + if (driverStateOpt) { + QJsonObject driverState = driverStateOpt.value(); + if (driverState.contains(QStringLiteral("connected_device_pose_has_position"))) { + m_poseHasPosition = driverState.value(QStringLiteral("connected_device_pose_has_position")).toBool(); + } + } Q_EMIT enabledStateChanged(); + Q_EMIT poseHasPositionChanged(); activatedAt = currentTimeMs; } @@ -788,7 +822,7 @@ void BreezyDesktopEffect::setSmoothFollowThreshold(float threshold) { } void BreezyDesktopEffect::updateDriverSmoothFollowSettings() { - qreal adjustedDistance = m_focusedDisplayDistance; + qreal adjustedDistance = m_focusedDisplayDistance / (m_displaySize * m_allDisplaysDistance); if (m_lookingAtScreenIndex != -1 && !m_displayResolution.isEmpty()) { // Adjust display distance by relative monitor size compared to the FOV monitor @@ -803,7 +837,7 @@ void BreezyDesktopEffect::updateDriverSmoothFollowSettings() { const qreal ratioH = static_cast(focusedSize.height()) / fovH; const qreal focusedMonitorSizeAdjustment = std::max(ratioW, ratioH); - adjustedDistance = m_focusedDisplayDistance / focusedMonitorSizeAdjustment; + adjustedDistance /= focusedMonitorSizeAdjustment; } } diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index 328a67d..9be5552 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -25,6 +25,7 @@ namespace KWin Q_PROPERTY(bool zoomOnFocusEnabled READ isZoomOnFocusEnabled WRITE setZoomOnFocusEnabled NOTIFY zoomOnFocusChanged) Q_PROPERTY(int lookingAtScreenIndex READ lookingAtScreenIndex WRITE setLookingAtScreenIndex) Q_PROPERTY(bool poseResetState READ poseResetState NOTIFY poseResetStateChanged) + Q_PROPERTY(bool poseHasPosition READ poseHasPosition NOTIFY poseResetStateChanged) Q_PROPERTY(QList poseOrientations READ poseOrientations) Q_PROPERTY(QVector3D posePosition READ posePosition) Q_PROPERTY(quint32 poseTimeElapsedMs READ poseTimeElapsedMs) @@ -38,6 +39,7 @@ namespace KWin Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY focusedDisplayDistanceChanged) Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY allDisplaysDistanceChanged) Q_PROPERTY(qreal displaySpacing READ displaySpacing NOTIFY displaySpacingChanged) + Q_PROPERTY(qreal displaySize READ displaySize NOTIFY displaySizeChanged) Q_PROPERTY(qreal displayHorizontalOffset READ displayHorizontalOffset NOTIFY displayOffsetChanged) Q_PROPERTY(qreal displayVerticalOffset READ displayVerticalOffset NOTIFY displayOffsetChanged) Q_PROPERTY(int displayWrappingScheme READ displayWrappingScheme NOTIFY displayWrappingSchemeChanged) @@ -52,6 +54,7 @@ namespace KWin Q_PROPERTY(bool mirrorPhysicalDisplays READ mirrorPhysicalDisplays NOTIFY mirrorPhysicalDisplaysChanged) Q_PROPERTY(bool curvedDisplay READ curvedDisplay NOTIFY curvedDisplayChanged) Q_PROPERTY(bool curvedDisplaySupported READ curvedDisplaySupported WRITE setCurvedDisplaySupported NOTIFY curvedDisplaySupportedChanged) + Q_PROPERTY(bool developerMode READ developerMode NOTIFY developerModeChanged) public: @@ -79,6 +82,7 @@ namespace KWin quint32 poseTimeElapsedMs() const; quint64 poseTimestamp() const; bool poseResetState() const; + bool poseHasPosition() const; QList lookAheadConfig() const; qreal lookAheadOverride() const; void setLookAheadOverride(qreal override); @@ -89,6 +93,8 @@ namespace KWin void setAllDisplaysDistance(qreal distance); qreal displaySpacing() const; void setDisplaySpacing(qreal spacing); + qreal displaySize() const; + void setDisplaySize(qreal size); qreal displayHorizontalOffset() const; qreal displayVerticalOffset() const; int displayWrappingScheme() const; @@ -102,6 +108,7 @@ namespace KWin bool removeVirtualDisplaysOnDisable() const; bool mirrorPhysicalDisplays() const; bool curvedDisplay() const; + bool developerMode() const; void setCurvedDisplaySupported(bool supported); void showCursor(); @@ -114,7 +121,7 @@ namespace KWin void disableDriver(); void toggle(); void addVirtualDisplay(QSize size); - void updatePoseOrientation(); + void updatePose(); void updateCursorImage(); void updateCursorPos(); QVariantList listVirtualDisplays() const; @@ -127,11 +134,13 @@ namespace KWin void focusedDisplayDistanceChanged(); void allDisplaysDistanceChanged(); void displaySpacingChanged(); + void displaySizeChanged(); void displayOffsetChanged(); void displayWrappingSchemeChanged(); void enabledStateChanged(); void zoomOnFocusChanged(); void poseResetStateChanged(); + void poseHasPositionChanged(); void sbsEnabledChanged(); void smoothFollowEnabledChanged(); void devicePropertiesChanged(); @@ -140,6 +149,7 @@ namespace KWin void mirrorPhysicalDisplaysChanged(); void curvedDisplayChanged(); void curvedDisplaySupportedChanged(); + void developerModeChanged(); void cursorImageSourceChanged(); void cursorPosChanged(); @@ -168,6 +178,7 @@ namespace KWin int m_lookingAtScreenIndex = -1; int m_effectTargetScreenIndex = -1; bool m_poseResetState; + bool m_poseHasPosition = false; QList m_poseOrientations; QVector3D m_posePosition; quint32 m_poseTimeElapsedMs; @@ -191,6 +202,7 @@ namespace KWin qreal m_focusedDisplayDistance = 0.85; qreal m_allDisplaysDistance = 1.05; qreal m_displaySpacing = 0.0; + qreal m_displaySize = 1.0; qreal m_displayHorizontalOffset = 0.0; qreal m_displayVerticalOffset = 0.0; int m_displayWrappingScheme = 0; // 0=auto,1=horizontal,2=vertical,3=flat @@ -199,6 +211,7 @@ namespace KWin bool m_mirrorPhysicalDisplays = false; bool m_curvedDisplay = false; bool m_curvedDisplaySupported = false; + bool m_developerMode = false; float m_smoothFollowThreshold = 1.0f; bool m_allDisplaysFollowMode = false; bool m_focusedSmoothFollowEnabled = false; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index b629f5b..39ea26c 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -48,6 +48,9 @@ #include #include #include +#include +#include +#include #include #include @@ -201,6 +204,27 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu ui.setupUi(widget()); addConfig(BreezyDesktopConfig::self(), widget()); + // Advanced tab: measurement units selector (stored as "cm" or "in") + if (ui.comboMeasurementUnits) { + ui.comboMeasurementUnits->clear(); + ui.comboMeasurementUnits->addItem(i18n("Centimeters (cm)"), QStringLiteral("cm")); + ui.comboMeasurementUnits->addItem(i18n("Inches (in)"), QStringLiteral("in")); + + { + QSignalBlocker b(ui.comboMeasurementUnits); + const QString saved = KConfigGroup(BreezyDesktopConfig::self()->sharedConfig(), QLatin1String(EFFECT_GROUP)) + .readEntry(QStringLiteral("measurement_units"), QStringLiteral("cm")); + const int idx = ui.comboMeasurementUnits->findData(saved); + ui.comboMeasurementUnits->setCurrentIndex(idx >= 0 ? idx : 0); + } + + connect(ui.comboMeasurementUnits, qOverload(&QComboBox::currentIndexChanged), this, [this](int) { + if (m_updatingFromConfig) return; + applyDistanceLabelFormatters(); + save(); + }); + } + // One-time check if the KWin effect backend is actually loaded. If not, disable UI early. checkEffectLoaded(); @@ -330,6 +354,7 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu connect(ui.kcfg_ZoomOnFocusEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_DisplaySize, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_SmoothFollowThreshold, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplayHorizontalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); @@ -393,6 +418,8 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu lookAheadOverrideSlider->setValueText(-1, i18n("Default")); } + applyDistanceLabelFormatters(); + renderVirtualDisplays(dbusListVirtualDisplays()); m_virtualDisplayPollTimer.setInterval(15000); @@ -430,6 +457,14 @@ void BreezyDesktopEffectConfig::save() m_updatingFromConfig = true; updateConfigFromUi(); BreezyDesktopConfig::self()->save(); + + // Store measurement_units explicitly (snake_case key) without depending on KConfigXT accessor naming. + { + KConfigGroup grp(BreezyDesktopConfig::self()->sharedConfig(), QLatin1String(EFFECT_GROUP)); + grp.writeEntry(QStringLiteral("measurement_units"), measurementUnitsFromUi()); + grp.sync(); + } + KCModule::save(); ui.kcfg_FocusedDisplayDistance->setEnabled( ui.kcfg_ZoomOnFocusEnabled->isChecked() || ui.SmoothFollowEnabled->isChecked()); @@ -456,6 +491,7 @@ void BreezyDesktopEffectConfig::updateUiFromConfig() { ui.kcfg_FocusedDisplayDistance->setValue(BreezyDesktopConfig::self()->focusedDisplayDistance()); ui.kcfg_AllDisplaysDistance->setValue(BreezyDesktopConfig::self()->allDisplaysDistance()); + ui.kcfg_DisplaySize->setValue(BreezyDesktopConfig::self()->displaySize()); ui.kcfg_DisplaySpacing->setValue(BreezyDesktopConfig::self()->displaySpacing()); ui.kcfg_DisplayHorizontalOffset->setValue(BreezyDesktopConfig::self()->displayHorizontalOffset()); ui.kcfg_DisplayVerticalOffset->setValue(BreezyDesktopConfig::self()->displayVerticalOffset()); @@ -470,11 +506,29 @@ void BreezyDesktopEffectConfig::updateUiFromConfig() ui.kcfg_FocusedDisplayDistance->setEnabled( ui.kcfg_ZoomOnFocusEnabled->isChecked() || ui.SmoothFollowEnabled->isChecked()); ui.kcfg_SmoothFollowThreshold->setValue(BreezyDesktopConfig::self()->smoothFollowThreshold()); + + if (ui.comboMeasurementUnits) { + QSignalBlocker b(ui.comboMeasurementUnits); + const QString saved = KConfigGroup(BreezyDesktopConfig::self()->sharedConfig(), QLatin1String(EFFECT_GROUP)) + .readEntry(QStringLiteral("measurement_units"), QStringLiteral("cm")); + const int idx = ui.comboMeasurementUnits->findData(saved); + ui.comboMeasurementUnits->setCurrentIndex(idx >= 0 ? idx : 0); + } + + applyDistanceLabelFormatters(); } void BreezyDesktopEffectConfig::updateUiFromDefaultConfig() { ui.shortcutsEditor->allDefault(); + + if (ui.comboMeasurementUnits) { + QSignalBlocker b(ui.comboMeasurementUnits); + const int idx = ui.comboMeasurementUnits->findData(QStringLiteral("cm")); + ui.comboMeasurementUnits->setCurrentIndex(idx >= 0 ? idx : 0); + } + + applyDistanceLabelFormatters(); } void BreezyDesktopEffectConfig::updateUnmanagedState() @@ -492,7 +546,7 @@ void BreezyDesktopEffectConfig::checkEffectLoaded() { QPalette pal = warn->palette(); pal.setColor(QPalette::WindowText, QColor(Qt::red)); warn->setPalette(pal); - warn->setText(tr("The Breezy Desktop KWin effect is not loaded. Please log out and back in to enable it.")); + warn->setText(tr("The Breezy Desktop KWin effect is disabled or not loaded. Please check the Desktop Effects dialog. Otherwise, log out and back in to enable it.")); warn->setVisible(true); } } @@ -646,6 +700,11 @@ void BreezyDesktopEffectConfig::pollDriverState() auto stateJson = stateJsonOpt.value(); m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString(); m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString(); + m_connectedDeviceFullDistanceCm = stateJson.value(QStringLiteral("connected_device_full_distance_cm")).toDouble(0.0); + m_connectedDeviceFullSizeCm = stateJson.value(QStringLiteral("connected_device_full_size_cm")).toDouble(0.0); + m_connectedDevicePoseHasPosition = stateJson.value(QStringLiteral("connected_device_pose_has_position")).toBool(false); + + applyDistanceLabelFormatters(); const bool smoothFollow = smoothFollowEnabled(stateJsonOpt); if (ui.SmoothFollowEnabled->isChecked() != smoothFollow) { @@ -709,6 +768,54 @@ void BreezyDesktopEffectConfig::pollDriverState() m_driverStateInitialized = true; } +QString BreezyDesktopEffectConfig::measurementUnitsFromUi() const +{ + if (!ui.comboMeasurementUnits) return QStringLiteral("cm"); + const QString v = ui.comboMeasurementUnits->currentData().toString(); + if (v == QLatin1String("in")) return QStringLiteral("in"); + return QStringLiteral("cm"); +} + +void BreezyDesktopEffectConfig::applyDistanceLabelFormatters() +{ + auto *focused = ui.kcfg_FocusedDisplayDistance; + auto *all = ui.kcfg_AllDisplaysDistance; + if (!focused || !all) { + return; + } + + // Only apply the unit conversion labels when the driver reports positional tracking. + if (!m_connectedDevicePoseHasPosition) { + focused->clearValueToDisplayStringFn(); + all->clearValueToDisplayStringFn(); + focused->setValueUnitsSuffix(QString()); + all->setValueUnitsSuffix(QString()); + return; + } + + const double fullCm = static_cast(m_connectedDeviceFullDistanceCm); + const QString units = measurementUnitsFromUi(); + const QLocale loc; + + // Units should appear only in the floating value bubble, not on tick labels. + focused->setValueUnitsSuffix(units); + all->setValueUnitsSuffix(units); + + LabeledSlider::ValueToDisplayStringFn fn = [fullCm, units, loc](int raw) -> QString { + if (fullCm <= 0.0) return QString(); + const double ratio = static_cast(raw) / 100.0; // slider uses a 2-decimal fixed-point scale + const double cm = ratio * fullCm; + if (units == QLatin1String("in")) { + const double inches = cm / 2.54; + return loc.toString(inches, 'f', 1); + } + return loc.toString(cm, 'f', 0); + }; + + focused->setValueToDisplayStringFn(fn); + all->setValueToDisplayStringFn(fn); +} + double BreezyDesktopEffectConfig::neckSaverHorizontalMultiplier(std::optional configJsonOpt) { if (!configJsonOpt) return 1.0; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h index bc64832..9e1cb90 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.h +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -28,6 +28,9 @@ public Q_SLOTS: void defaults() override; private: + QString measurementUnitsFromUi() const; + void applyDistanceLabelFormatters(); + void updateDriverEnabled(); void updateMultitapEnabled(); void updateSmoothFollowEnabled(); @@ -73,6 +76,9 @@ private: int m_smoothFollowThreshold = 1; QString m_connectedDeviceBrand; QString m_connectedDeviceModel; + float m_connectedDeviceFullDistanceCm = 0.0; + float m_connectedDeviceFullSizeCm = 0.0; + bool m_connectedDevicePoseHasPosition = false; QTimer m_statePollTimer; // periodic driver state polling QTimer m_virtualDisplayPollTimer; // periodic virtual display list polling bool m_licenseLoading = false; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index 372b818..451c275 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -52,7 +52,16 @@ &General - + + + + 0 + 0 + 0 + 0 + 12 + 6 + XR Effect enabled @@ -61,8 +70,8 @@ false - - + + Zoom on Focus @@ -71,8 +80,8 @@ false - - + + Curved display @@ -81,8 +90,8 @@ false - - + + Follow mode @@ -91,15 +100,18 @@ false - - + + + + + Focused Display Distance: - + 2 @@ -108,10 +120,10 @@ QSlider::NoTicks - 5 + 15 - 25 + 50 Qt::Horizontal @@ -121,14 +133,14 @@ - + All Displays Distance: - + 2 @@ -137,10 +149,10 @@ QSlider::NoTicks - 5 + 15 - 25 + 50 Qt::Horizontal @@ -150,14 +162,43 @@ - + + + + Display Size: + + + + + + + 2 + + + QSlider::NoTicks + + + 15 + + + 50 + + + Qt::Horizontal + + + true + + + + Display Spacing: - + Qt::Horizontal @@ -167,14 +208,14 @@ - + Follow threshold: - + Qt::Horizontal @@ -196,7 +237,7 @@ - + Add Virtual Display: @@ -209,7 +250,7 @@ - + false @@ -272,7 +313,7 @@ - + false false @@ -285,15 +326,22 @@ - - - - - 0 - 0 - - - + + + + + &Shortcuts + + + + + + + 0 + 0 + + + @@ -605,6 +653,16 @@ + + + + Measurement units: + + + + + + diff --git a/kwin/src/kcm/labeledslider.h b/kwin/src/kcm/labeledslider.h index d08190b..7491223 100644 --- a/kwin/src/kcm/labeledslider.h +++ b/kwin/src/kcm/labeledslider.h @@ -5,6 +5,7 @@ #include #include #include // for std::max +#include #include #include #include @@ -32,7 +33,12 @@ class LabeledSlider : public QSlider { // tickStartOffset: starting offset for label positions relative to minimum(). // Example: minimum=0, tickInterval=20, tickStartOffset=10 -> labels at 10,30,50,... Q_PROPERTY(int tickStartOffset READ tickStartOffset WRITE setTickStartOffset) + // Optional units suffix shown ONLY in the floating value bubble. + // Tick labels never include units. + Q_PROPERTY(QString valueUnitsSuffix READ valueUnitsSuffix WRITE setValueUnitsSuffix) public: + using ValueToDisplayStringFn = std::function; + explicit LabeledSlider(QWidget *parent = nullptr) : QSlider(Qt::Horizontal, parent) { @@ -74,6 +80,32 @@ public: QMap valueTexts() const { return m_valueTexts; } + QString valueUnitsSuffix() const { return m_valueUnitsSuffix; } + void setValueUnitsSuffix(const QString &suffix) { + if (m_valueUnitsSuffix == suffix) return; + m_valueUnitsSuffix = suffix; + updateGeometry(); + update(); + } + + // Optional custom formatter for displayed values. + // If set, it is consulted for values without an explicit setValueText() override. + // Returning a null QString (QString()) falls back to the built-in formatting. + void setValueToDisplayStringFn(ValueToDisplayStringFn fn) { + m_valueToDisplayStringFn = std::move(fn); + updateGeometry(); + update(); + } + + void clearValueToDisplayStringFn() { + if (!m_valueToDisplayStringFn) return; + m_valueToDisplayStringFn = nullptr; + updateGeometry(); + update(); + } + + bool hasValueToDisplayStringFn() const { return static_cast(m_valueToDisplayStringFn); } + int decimalShift() const { return m_decimalShift; } void setDecimalShift(int shift) { // clamp to sensible range @@ -141,7 +173,7 @@ protected: optPos.sliderValue = v; QRect handleAtVal = style()->subControlRect(QStyle::CC_Slider, &optPos, QStyle::SC_SliderHandle, this); int x = handleAtVal.center().x(); - QString text = valueToDisplayString(v); + QString text = valueToDisplayString(v, /*forValueBubble=*/false); int halfW = fm.horizontalAdvance(text) / 2; QRect r(x - halfW, baselineY - fm.ascent(), fm.horizontalAdvance(text), fm.height()); p.drawText(r, Qt::AlignCenter, text); @@ -152,7 +184,7 @@ protected: if (m_showValueBubble) { // Handle rect const QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - QString valText = valueToDisplayString(value()); + QString valText = valueToDisplayString(value(), /*forValueBubble=*/true); QFontMetrics fm(font()); QRect textRect = fm.boundingRect(valText); textRect.adjust(-6, -4, 6, 4); @@ -187,29 +219,57 @@ protected: } private: - QString valueToDisplayString(int raw) const { + QString valueToDisplayString(int raw, bool forValueBubble) const { // Use custom text if provided for this exact integer value auto it = m_valueTexts.constFind(raw); if (it != m_valueTexts.constEnd()) { return *it; } - if (m_decimalShift == 0) { - return QString::number(raw); + + auto decimalShiftString = [this](int r) -> QString { + if (m_decimalShift == 0) { + return QString::number(r); + } + int divisor = 1; + for (int i = 0; i < m_decimalShift; ++i) divisor *= 10; + int whole = r / divisor; + int frac = std::abs(r % divisor); + QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0')); + QString result = QString::number(std::abs(whole)) + QLatin1Char('.') + fracStr; + if (r < 0) result.prepend(QLatin1Char('-')); + return result; + }; + + QString base; + if (m_valueToDisplayStringFn) { + base = m_valueToDisplayStringFn(raw); } - int divisor = 1; - for (int i = 0; i < m_decimalShift; ++i) divisor *= 10; - int whole = raw / divisor; - int frac = std::abs(raw % divisor); - QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0')); - QString result = QString::number(std::abs(whole)) + QLatin1Char('.') + fracStr; - if (raw < 0) result.prepend(QLatin1Char('-')); - return result; + if (base.isNull()) { + base = decimalShiftString(raw); + } + + if (!forValueBubble) { + return base; + } + + const QString suffix = m_valueUnitsSuffix.trimmed(); + if (suffix.isEmpty() || base.isEmpty()) { + return base; + } + // Avoid double-appending if caller included units in the formatter. + const QString baseTrimmed = base.trimmed(); + if (baseTrimmed.endsWith(suffix) || baseTrimmed.endsWith(QLatin1Char(' ') + suffix)) { + return base; + } + return baseTrimmed + QLatin1Char(' ') + suffix; } bool m_showValueBubble = true; int m_decimalShift = 0; // display-only decimal shift int m_tickStartOffset = 0; // label positions start offset relative to minimum QMap m_valueTexts; // optional text overrides for specific values + ValueToDisplayStringFn m_valueToDisplayStringFn; // optional custom formatter + QString m_valueUnitsSuffix; // shown only in the value bubble private: int labelInterval() const { int ti = tickInterval(); diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 5cf65ab..38bbe27 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -8,6 +8,7 @@ Node { property var viewportResolution: effect.displayResolution property bool smoothFollowEnabled: effect.smoothFollowEnabled required property var screens + required property var sizeAdjustedScreens required property var fovDetails required property var monitorPlacements property int focusedMonitorIndex: -1 @@ -28,14 +29,16 @@ Node { function updateFocus(smoothFollowEnabledChanged = false) { const orientations = smoothFollowEnabled ? effect.smoothFollowOrigin : effect.poseOrientations; if (orientations && orientations.length > 0) { + const posePosition = effect.posePosition.times(breezyDesktop.fovDetails.fullScreenDistancePixels); let focusedIndex = -1; const lookingAtIndex = displays.findFocusedMonitor( displays.eusToNwuQuat(orientations[0]), + displays.eusToNwuVector(posePosition), breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook), breezyDesktop.focusedMonitorIndex, smoothFollowEnabled, breezyDesktop.fovDetails, - breezyDesktop.screens.map(screen => screen.geometry) + breezyDesktop.sizeAdjustedScreens.map(screen => screen.geometry) ); if (breezyDesktop.lookingAtMonitorIndex !== lookingAtIndex) { @@ -158,6 +161,7 @@ Node { model: breezyDesktop.screens.length delegate: BreezyDesktopDisplay { screen: breezyDesktop.screens[index] + sizeAdjustedScreen: breezyDesktop.sizeAdjustedScreens[index] monitorPlacement: breezyDesktop.monitorPlacements[index] fovDetails: breezyDesktop.fovDetails @@ -175,7 +179,9 @@ Node { // only for the Rectangle geometry fallback property vector3d rectangleFallbackScale: { - const geometry = screen.geometry; + if (!sizeAdjustedScreen) return Qt.vector3d(1, 1, 1); + + const geometry = sizeAdjustedScreen.geometry; // default geometry unit size is 100x100, so we scale it up to the screen size return Qt.vector3d(geometry.width / 100, geometry.height / 100, 1); diff --git a/kwin/src/qml/BreezyDesktopDisplay.qml b/kwin/src/qml/BreezyDesktopDisplay.qml index e4ce5d7..64c52eb 100644 --- a/kwin/src/qml/BreezyDesktopDisplay.qml +++ b/kwin/src/qml/BreezyDesktopDisplay.qml @@ -4,7 +4,8 @@ import QtQuick3D Model { id: display - required property QtObject screen + required property var screen + required property var sizeAdjustedScreen required property var monitorPlacement required property int index required property var fovDetails @@ -27,7 +28,7 @@ Model { if (component.status === Component.Ready) { const mesh = component.createObject(display, { fovDetails: Qt.binding(() => display.fovDetails), - monitorGeometry: Qt.binding(() => display.screen ? display.screen.geometry : null), + monitorGeometry: Qt.binding(() => display.sizeAdjustedScreen ? display.sizeAdjustedScreen.geometry : null), fovConversionFns: Qt.binding(() => displays.fovConversionFns) }); if (mesh) { diff --git a/kwin/src/qml/CameraController.qml b/kwin/src/qml/CameraController.qml index 886116c..780ddcd 100644 --- a/kwin/src/qml/CameraController.qml +++ b/kwin/src/qml/CameraController.qml @@ -17,10 +17,12 @@ Item { property bool customBannerEnabled: effect.customBannerEnabled property bool smoothFollowEnabled: effect.smoothFollowEnabled property real lookAheadScanlineMs: effect.lookAheadConfig[2] - property var crossFovs: displays.diagonalToCrossFOVs( + property var fovLengths: displays.diagonalToCrossFOVs( displays.degreeToRadian(effect.diagonalFOV), aspectRatio ); + property real fovHalfVerticalTangent: fovLengths.heightUnitDistance / 2.0; + property real fovHalfHorizontalTangent: fovLengths.widthUnitDistance / 2.0; // if true, then smoothFollowEnabled just cleared and the orientation data is slerping back, // continue to use the origin data for the duration of the Timer @@ -58,7 +60,13 @@ Item { effect.lookAheadOverride ) ); - camera.position = position.times(fovDetails.completeScreenDistancePixels).plus(orientations[0].times(Qt.vector3d(0, 0, -fovDetails.lensDistancePixels))); + let lensVector = Qt.vector3d(0, 0, -fovDetails.lensDistancePixels); + + // if we only have 3DoF, account for a bit of positional change based on orientation, + // don't do this for 6DoF to prevent doubling the positional movement due to rotation + if (!effect.poseHasPosition) lensVector = orientations[0].times(lensVector); + + camera.position = position.times(fovDetails.fullScreenDistancePixels).plus(lensVector); } // how far to look ahead is how old the pose data is plus a constant that is either the default for this device or an override @@ -84,7 +92,7 @@ Item { } function buildPerspectiveMatrix() { - const f = 1.0 / crossFovs.verticalTangent; + const f = 1.0 / fovHalfVerticalTangent; const nf = 1.0 / (clipNear - clipFar); const m00 = f / aspectRatio; const m11 = f; @@ -102,13 +110,13 @@ Item { function applyRollingShutterShear(rates) { // Convert to maximum shift at bottom of frame - const maxDxNdc = (rates.yaw * lookAheadScanlineMs) / crossFovs.horizontalTangent; - const maxDyNdc = -(rates.pitch * lookAheadScanlineMs) / crossFovs.verticalTangent; + const maxDxNdc = (rates.yaw * lookAheadScanlineMs) / fovHalfHorizontalTangent; + const maxDyNdc = -(rates.pitch * lookAheadScanlineMs) / fovHalfVerticalTangent; let shx = maxDxNdc / 2.0; let shy = maxDyNdc / 2.0; - const f = 1.0 / crossFovs.verticalTangent; + const f = 1.0 / fovHalfVerticalTangent; const nf = 1.0 / (clipNear - clipFar); const m00 = f / aspectRatio; const m11 = f; diff --git a/kwin/src/qml/CurvableDisplayMesh.qml b/kwin/src/qml/CurvableDisplayMesh.qml index 28f33cd..24652e4 100644 --- a/kwin/src/qml/CurvableDisplayMesh.qml +++ b/kwin/src/qml/CurvableDisplayMesh.qml @@ -26,27 +26,26 @@ ProceduralMesh { const fov = mesh.fovDetails; const monitor = mesh.monitorGeometry; - const conv = fov.curvedDisplay ? mesh.fovConversionFns.curved - : mesh.fovConversionFns.flat; - const horizontalWrap = fov.monitorWrappingScheme === 'horizontal'; - const verticalWrap = fov.monitorWrappingScheme === 'vertical'; + const horizontalConversions = horizontalWrap && fov.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat; - const sideEdgeDistance = conv.centerToFovEdgeDistance( - fov.completeScreenDistancePixels, fov.widthPixels); - const horizontalRadians = conv.lengthToRadians( + const sideEdgeDistancePixels = horizontalConversions.centerToFovEdgeDistance( + fov.completeScreenDistancePixels, fov.sizeAdjustedWidthPixels); + const horizontalRadians = horizontalConversions.lengthToRadians( fov.defaultDistanceHorizontalRadians, fov.widthPixels, - sideEdgeDistance, + sideEdgeDistancePixels, monitor.width ); - const topEdgeDistance = conv.centerToFovEdgeDistance( - fov.completeScreenDistancePixels, fov.heightPixels); - const verticalRadians = conv.lengthToRadians( + const verticalWrap = fov.monitorWrappingScheme === 'vertical'; + const verticalConversions = verticalWrap && fov.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat; + const topEdgeDistancePixels = verticalConversions.centerToFovEdgeDistance( + fov.completeScreenDistancePixels, fov.sizeAdjustedHeightPixels); + const verticalRadians = verticalConversions.lengthToRadians( fov.defaultDistanceVerticalRadians, fov.heightPixels, - topEdgeDistance, + topEdgeDistancePixels, monitor.height ); @@ -78,8 +77,8 @@ ProceduralMesh { } let segments = 1; - if (horizontalWrap) segments = conv.radiansToSegments(horizontalRadians); - if (verticalWrap) segments = conv.radiansToSegments(verticalRadians); + if (horizontalWrap) segments = horizontalConversions.radiansToSegments(horizontalRadians); + if (verticalWrap) segments = verticalConversions.radiansToSegments(verticalRadians); for (let i = 0; i <= segments; i++) { const texFraction = i / segments; diff --git a/kwin/src/qml/DesktopView.qml b/kwin/src/qml/DesktopView.qml index 10d9940..19a9d2e 100644 --- a/kwin/src/qml/DesktopView.qml +++ b/kwin/src/qml/DesktopView.qml @@ -4,7 +4,7 @@ import org.kde.kwin as KWinComponents Item { id: desktopView - required property QtObject screen + required property var screen function overlapsScreen(win, screenGeom) { if (!win) return false diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml index be2a3d6..db9904f 100644 --- a/kwin/src/qml/Displays.qml +++ b/kwin/src/qml/Displays.qml @@ -18,22 +18,35 @@ QtObject { return Qt.vector3d(-vector.y, vector.z, -vector.x); } + function eusToNwuVector(vector) { + // Converts EUS vector to NWU vector + return Qt.vector3d(-vector.z, -vector.x, vector.y); + } + function eusToNwuQuat(quaternion) { // Converts EUS quaternion to NWU quaternion return Qt.quaternion(quaternion.scalar, -quaternion.z, -quaternion.x, quaternion.y); } - // Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOVs + // Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOV measurements function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { - var diagonalTangent = Math.tan(diagonalFOVRadians / 2); - var verticalTangent = diagonalTangent / Math.sqrt(1 + aspectRatio * aspectRatio); - var horizontalTangent = verticalTangent * aspectRatio; + // first convert from a spherical FOV to a diagonal FOV on a flat plane at a unit distance of 1.0 + const diagonalLengthUnitDistance = 2 * Math.tan(diagonalFOVRadians / 2); + + // then convert to flat plane horizontal and vertical FOVs + const heightUnitDistance = diagonalLengthUnitDistance / Math.sqrt(1 + aspectRatio * aspectRatio); + const widthUnitDistance = heightUnitDistance * aspectRatio; + return { - diagonal: diagonalFOVRadians, - horizontal: 2 * Math.atan(horizontalTangent), - horizontalTangent: horizontalTangent, - vertical: 2 * Math.atan(verticalTangent), - verticalTangent: verticalTangent + // then convert back to spherical FOV + diagonalRadians: diagonalFOVRadians, + horizontalRadians: 2 * Math.atan(widthUnitDistance / 2), + verticalRadians: 2 * Math.atan(heightUnitDistance / 2), + + // flat values are relative to a unit distance of 1.0 + diagonalLengthUnitDistance, + widthUnitDistance, + heightUnitDistance } } @@ -50,33 +63,55 @@ QtObject { } } - function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice) { + function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice, distanceAdjustedSize) { const aspect = viewportWidth / viewportHeight; - const crossFovs = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); - const defaultDistanceVerticalRadians = 2 * Math.atan(crossFovs.verticalTangent / defaultDisplayDistance); - const defaultDistanceHorizontalRadians = 2 * Math.atan(crossFovs.horizontalTangent / defaultDisplayDistance); - - // distance needed for the FOV-sized monitor to fill up the screen - const fullScreenDistance = viewportHeight / (2 * crossFovs.verticalTangent); - const lensDistancePixels = fullScreenDistance / (1.0 - lensDistanceRatio) - fullScreenDistance; - - // distance of a display at the default (most zoomed out) distance, plus the lens distance constant - const lensToScreenDistance = viewportHeight / (2 * Math.tan(defaultDistanceVerticalRadians / 2)); - const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels; + const fovLengths = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight); if (wrappingChoice === 1) monitorWrappingScheme = 'horizontal'; else if (wrappingChoice === 2) monitorWrappingScheme = 'vertical'; else if (wrappingChoice === 3) monitorWrappingScheme = 'flat'; + const lensDistanceComplement = 1.0 - lensDistanceRatio; + const lensDistanceFactor = (1.0 / lensDistanceComplement) - 1.0; + const horizontalConversions = effect.curvedDisplay && monitorWrappingScheme === 'horizontal' ? fovConversionFns.curved : fovConversionFns.flat; + const verticalConversions = effect.curvedDisplay && monitorWrappingScheme === 'vertical' ? fovConversionFns.curved : fovConversionFns.flat; + + const defaultDistanceVerticalRadians = verticalConversions.fovRadiansAtDistance( + fovLengths.verticalRadians, + fovLengths.heightUnitDistance, + defaultDisplayDistance + ); + const defaultDistanceHorizontalRadians = horizontalConversions.fovRadiansAtDistance( + fovLengths.horizontalRadians, + fovLengths.widthUnitDistance, + defaultDisplayDistance + ); + + // distance needed for the FOV-sized monitor to fill up the screen, as measured from the lenses + const lensToUnitDistancePixels = viewportWidth / fovLengths.widthUnitDistance; + + // distance from pivot point to lens + const lensDistancePixels = lensToUnitDistancePixels * lensDistanceFactor; + + // distance from pivot point to full screen (monitor at unit distance from lens) + const fullScreenDistancePixels = lensToUnitDistancePixels + lensDistancePixels; + + // distance of a display at the default (most zoomed out) distance from the pivot point + const completeScreenDistancePixels = fullScreenDistancePixels * defaultDisplayDistance; + return { widthPixels: viewportWidth, + distanceAdjustedSize, + sizeAdjustedWidthPixels: viewportWidth * distanceAdjustedSize, heightPixels: viewportHeight, + sizeAdjustedHeightPixels: viewportHeight * distanceAdjustedSize, defaultDistanceVerticalRadians, defaultDistanceHorizontalRadians, lensDistancePixels, + fullScreenDistancePixels, completeScreenDistancePixels, - monitorWrappingScheme: monitorWrappingScheme, + monitorWrappingScheme, curvedDisplay: effect.curvedDisplay }; } @@ -99,6 +134,9 @@ QtObject { angleToLength: function(fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) { return toAngleOpposite / toAngleAdjacent * screenDistance; }, + fovRadiansAtDistance: function(fovRadians, unitLength, newScreenDistance) { + return 2 * Math.atan(unitLength / 2 / newScreenDistance); + }, radiansToSegments: function(screenRadians) { return 1; } }, curved: { @@ -114,6 +152,9 @@ QtObject { angleToLength: function(fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) { return fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent); }, + fovRadiansAtDistance: function(fovRadians, unitLength, newScreenDistance) { + return fovRadians / newScreenDistance; + }, radiansToSegments: function(screenRadians) { return Math.ceil(screenRadians * segmentsPerRadian); } @@ -198,8 +239,12 @@ QtObject { var conversionFns = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat; if (fovDetails.monitorWrappingScheme === 'horizontal') { - var sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.widthPixels); - var monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; + // monitors wrap around us horizontally + + var sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedWidthPixels); + var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels; + + // targetWidth is assumed to aleady be size adjusted var lengthToRadianFn = function(targetWidth) { return conversionFns.lengthToRadians( fovDetails.defaultDistanceHorizontalRadians, @@ -209,14 +254,14 @@ QtObject { ); }; - cachedMonitorRadians[0] = -fovDetails.defaultDistanceHorizontalRadians / 2; + cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedWidthPixels) / 2; horizontalMonitorSort(monitorDetailsList).forEach(function(obj) { var monitorDetails = obj.monitorDetails; var originalIndex = obj.originalIndex; var monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.x, monitorDetails.width, lengthToRadianFn); var monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(sideEdgeRadius, monitorDetails.width); - var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; - var upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; + var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels; + var upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2; var upCenterPixels = upTopPixels - upCenterOffsetPixels; monitorPlacements.push({ @@ -231,7 +276,7 @@ QtObject { monitorCenterRadius * Math.cos(monitorWrapDetails.center), -monitorCenterRadius * Math.sin(monitorWrapDetails.center), upCenterPixels - ).normalized(), + ), rotationAngleRadians: { x: 0, y: -monitorWrapDetails.center @@ -239,8 +284,8 @@ QtObject { }); }); } else if (fovDetails.monitorWrappingScheme === 'vertical') { - var topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.heightPixels); - var monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels; + var topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedHeightPixels); + var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedHeightPixels; var lengthToRadianFn = function(targetHeight) { return conversionFns.lengthToRadians( fovDetails.defaultDistanceVerticalRadians, @@ -250,14 +295,14 @@ QtObject { ); }; - cachedMonitorRadians[0] = -fovDetails.defaultDistanceVerticalRadians / 2; + cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedHeightPixels) / 2; verticalMonitorSort(monitorDetailsList).forEach(function(obj) { var monitorDetails = obj.monitorDetails; var originalIndex = obj.originalIndex; var monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.y, monitorDetails.height, lengthToRadianFn); var monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(topEdgeRadius, monitorDetails.height); - var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; - var westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2; + var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels; + var westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2; var westCenterPixels = westLeftPixels - westCenterOffsetPixels; monitorPlacements.push({ @@ -272,7 +317,7 @@ QtObject { monitorCenterRadius * Math.cos(monitorWrapDetails.center), westCenterPixels, -monitorCenterRadius * Math.sin(monitorWrapDetails.center) - ).normalized(), + ), rotationAngleRadians: { x: -monitorWrapDetails.center, y: 0 @@ -280,12 +325,12 @@ QtObject { }); }); } else { - var monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; + var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels; monitorDetailsList.forEach(function(monitorDetails, index) { - var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; - var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; - var westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2; - var upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; + var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels; + var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels; + var westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2; + var upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2; var westCenterPixels = westLeftPixels - westCenterOffsetPixels; var upCenterPixels = upTopPixels - upCenterOffsetPixels; @@ -301,7 +346,7 @@ QtObject { fovDetails.completeScreenDistancePixels, westCenterPixels, upCenterPixels - ).normalized(), + ), rotationAngleRadians: { x: 0, y: 0 @@ -318,35 +363,41 @@ QtObject { // returns how far the look vector is from the center of the monitor, as a percentage of the monitor's dimensions function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) { + // since the monitor vector has been modified to be relative to the lens position, we need to calculate its distance from the lens + // we need to adjust all angle-based lengths based on new vector distance + const monitorDistance = monitorVector.length(); + const distanceAdjustment = monitorDistance / fovDetails.completeScreenDistancePixels; + var vectorUpPixels = upAngleToLength( fovDetails.defaultDistanceVerticalRadians, fovDetails.heightPixels, - fovDetails.completeScreenDistancePixels, + monitorDistance, monitorVector.z, monitorVector.x - ); - var upPercentage = Math.abs(lookUpPixels - vectorUpPixels) / monitorDetails.height; + ) * distanceAdjustment; + var upPercentage = Math.abs(lookUpPixels * distanceAdjustment - vectorUpPixels) / monitorDetails.height; var vectorWestPixels = westAngleToLength( fovDetails.defaultDistanceHorizontalRadians, fovDetails.widthPixels, - fovDetails.completeScreenDistancePixels, + monitorDistance, monitorVector.y, monitorVector.x - ); - var westPercentage = Math.abs(lookWestPixels - vectorWestPixels) / monitorDetails.width; + ) * distanceAdjustment; + var westPercentage = Math.abs(lookWestPixels * distanceAdjustment - vectorWestPixels) / monitorDetails.width; // how close we are to any edge is the largest of the two percentages return Math.max(upPercentage, westPercentage); } - function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) { + function findFocusedMonitor(quaternion, position, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) { if (currentFocusedIndex !== -1 && smoothFollowEnabled) return currentFocusedIndex; var lookVector = Qt.vector3d(1.0, 0.0, 0.0); // NWU vector pointing to the center of the screen var rotatedLookVector = quaternion.times(lookVector); - // Use curved or flat conversion functions depending on wrapping scheme + // TODO - right now we're using the curved functions to figure out distances even for flat monitors + // because it will account for the monitors facing towards us, but this will lose some accuracy var upConversionFns = fovDetails.monitorWrappingScheme === "vertical" ? fovConversionFns.curved : fovConversionFns.flat; var lookUpPixels = upConversionFns.angleToLength( fovDetails.defaultDistanceVerticalRadians, @@ -364,13 +415,17 @@ QtObject { rotatedLookVector.x ); + function vectorRelativeToPosition(vector) { + return vector.minus(position); + } + // Check current focused monitor first if (currentFocusedIndex !== -1) { var focusedDistance = getMonitorDistance( fovDetails, lookUpPixels, lookWestPixels, - monitorVectors[currentFocusedIndex], + vectorRelativeToPosition(monitorVectors[currentFocusedIndex]), monitorsDetails[currentFocusedIndex], upConversionFns.angleToLength, westConversionFns.angleToLength @@ -390,7 +445,7 @@ QtObject { fovDetails, lookUpPixels, lookWestPixels, - monitorVectors[i], + vectorRelativeToPosition(monitorVectors[i]), monitorsDetails[i], upConversionFns.angleToLength, westConversionFns.angleToLength diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 2682d4a..bf957f0 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -30,22 +30,45 @@ Item { property real viewportDiagonalFOVDegrees: effect.diagonalFOV property var viewportResolution: effect.displayResolution property bool mirrorPhysicalDisplays: effect.mirrorPhysicalDisplays + property bool developerMode: effect.developerMode property var screens: KWinComponents.Workspace.screens.filter(function(screen) { - return mirrorPhysicalDisplays || screen.name.includes("BreezyDesktop") || supportedModels.some(model => screen.model.includes(model)); + return developerMode || mirrorPhysicalDisplays || screen.name.includes("BreezyDesktop") || supportedModels.some(model => screen.model.includes(model)); }) + property real distanceAdjustedSize: (effect.allDisplaysDistance - effect.lensDistanceRatio) * effect.displaySize + property var sizeAdjustedScreens: screens.map(function(screen) { + const sizeComplement = (1.0 - distanceAdjustedSize) / 2.0; + const sizeViewportOffsetX = sizeComplement * viewportResolution[0]; + const sizeViewportOffsetY = sizeComplement * viewportResolution[1]; + return { + geometry: { + x: screen.geometry.x * distanceAdjustedSize + sizeViewportOffsetX, + y: screen.geometry.y * distanceAdjustedSize + sizeViewportOffsetY, + width: screen.geometry.width * distanceAdjustedSize, + height: screen.geometry.height * distanceAdjustedSize + }, + name: screen.name, + model: screen.model + }; + }) + property var sizeAdjustedViewport: { + return { + width: viewportResolution[0] * distanceAdjustedSize, + height: viewportResolution[1] * distanceAdjustedSize + }; + } // x value for placing the viewport in the middle of all screens property real screensXMid: { let xMin = Number.MAX_VALUE; let xMax = Number.MIN_VALUE; - for (let i = 0; i < screens.length; i++) { - const geometry = screens[i].geometry; + for (let i = 0; i < sizeAdjustedScreens.length; i++) { + const geometry = sizeAdjustedScreens[i].geometry; xMin = Math.min(xMin, geometry.x); xMax = Math.max(xMax, geometry.x + geometry.width); } - return (xMin + xMax) / 2 - (viewportResolution[0] / 2); + return (xMin + xMax) / 2 - (sizeAdjustedViewport.width / 2); } // y value for placing the viewport in the middle of all screens @@ -53,13 +76,13 @@ Item { let yMin = Number.MAX_VALUE; let yMax = Number.MIN_VALUE; - for (let i = 0; i < screens.length; i++) { - const geometry = screens[i].geometry; + for (let i = 0; i < sizeAdjustedScreens.length; i++) { + const geometry = sizeAdjustedScreens[i].geometry; yMin = Math.min(yMin, geometry.y); yMax = Math.max(yMax, geometry.y + geometry.height); } - return (yMin + yMax) / 2 - (viewportResolution[1] / 2); + return (yMin + yMax) / 2 - (sizeAdjustedViewport.height / 2); } Displays { @@ -67,19 +90,20 @@ Item { } property var fovDetails: displays.buildFovDetails( - screens, + sizeAdjustedScreens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio, effect.allDisplaysDistance, - effect.displayWrappingScheme + effect.displayWrappingScheme, + distanceAdjustedSize ) property var monitorPlacements: { - const dx = effect.displayHorizontalOffset * viewportResolution[0]; - const dy = effect.displayVerticalOffset * viewportResolution[1]; - const adjustedGeometries = screens.map(screen => { + const dx = effect.displayHorizontalOffset * sizeAdjustedViewport.width; + const dy = effect.displayVerticalOffset * sizeAdjustedViewport.height; + const adjustedGeometries = sizeAdjustedScreens.map(screen => { const g = screen.geometry; return { x: g.x - screensXMid + dx, @@ -91,7 +115,7 @@ Item { return displays.monitorsToPlacements(fovDetails, adjustedGeometries, effect.displaySpacing); } - property bool targetScreenSupported: supportedModels.some(model => root.targetScreen.model.includes(model)) + property bool targetScreenSupported: developerMode || supportedModels.some(model => root.targetScreen.model.includes(model)) property bool targetScreenIsVirtual: targetScreen.name.includes("BreezyDesktop") property bool poseResetState: effect.poseResetState property bool isEnabled: effect.isEnabled @@ -122,6 +146,7 @@ Item { BreezyDesktop { id: breezyDesktop screens: root.screens + sizeAdjustedScreens: root.sizeAdjustedScreens fovDetails: root.fovDetails monitorPlacements: root.monitorPlacements } diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index 7cf5924..b74587d 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit 7cf5924771b01bccf6d8d8ab0a9a38d3aa37babb +Subproject commit b74587dfc17e85c63ee03fdbdd8f2541400f454f diff --git a/ui/modules/PyXRLinuxDriverIPC b/ui/modules/PyXRLinuxDriverIPC index 1655c2b..44657a5 160000 --- a/ui/modules/PyXRLinuxDriverIPC +++ b/ui/modules/PyXRLinuxDriverIPC @@ -1 +1 @@ -Subproject commit 1655c2bb03a4e75bf2c9a9815a96b71342396fbd +Subproject commit 44657a5de6532ac0ec1a4ce6d3ccd73e2c87fdc6