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