Add zoom on focus logic

This commit is contained in:
wheaney 2025-08-04 14:53:51 -07:00
parent 7fb48fe77d
commit d7120fbc2e
8 changed files with 225 additions and 65 deletions

View File

@ -5,11 +5,17 @@
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name="kwinrc"/>
<group name="Effect-breezy_desktop_effect">
<entry name="DisplayDistance" type="Double">
<entry name="FocusedDisplayDistance" type="Double">
<default>0.85</default>
<min>0.2</min>
<max>2.5</max>
<label>Focused Display Distance</label>
</entry>
<entry name="AllDisplaysDistance" type="Double">
<default>1.05</default>
<min>0.2</min>
<max>2.5</max>
<label>Display Distance</label>
<label>All Displays Distance</label>
</entry>
</group>
</kcfg>

View File

@ -123,7 +123,8 @@ BreezyDesktopEffect::BreezyDesktopEffect()
void BreezyDesktopEffect::reconfigure(ReconfigureFlags)
{
BreezyDesktopConfig::self()->read();
setDisplayDistance(BreezyDesktopConfig::displayDistance());
setFocusedDisplayDistance(BreezyDesktopConfig::focusedDisplayDistance());
setAllDisplaysDistance(BreezyDesktopConfig::allDisplaysDistance());
}
QVariantMap BreezyDesktopEffect::initialProperties(Output *screen)
@ -205,13 +206,24 @@ QList<quint32> BreezyDesktopEffect::displayResolution() const {
return m_displayResolution;
}
qreal BreezyDesktopEffect::displayDistance() const {
return m_displayDistance;
qreal BreezyDesktopEffect::focusedDisplayDistance() const {
return m_focusedDisplayDistance;
}
void BreezyDesktopEffect::setDisplayDistance(qreal distance) {
if (distance != m_displayDistance) {
m_displayDistance = std::clamp(distance, 0.2, 2.5);
void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) {
if (distance != m_focusedDisplayDistance) {
m_focusedDisplayDistance = std::clamp(distance, 0.2, 2.5);
Q_EMIT displayDistanceChanged();
}
}
qreal BreezyDesktopEffect::allDisplaysDistance() const {
return m_allDisplaysDistance;
}
void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) {
if (distance != m_allDisplaysDistance) {
m_allDisplaysDistance = std::clamp(distance, 0.2, 2.5);
Q_EMIT displayDistanceChanged();
}
}

View File

@ -22,7 +22,8 @@ namespace KWin
Q_PROPERTY(QPointF cursorPos READ cursorPos NOTIFY cursorPosChanged)
Q_PROPERTY(QList<qreal> lookAheadConfig READ lookAheadConfig NOTIFY devicePropertiesChanged)
Q_PROPERTY(QList<quint32> displayResolution READ displayResolution NOTIFY devicePropertiesChanged)
Q_PROPERTY(qreal displayDistance READ displayDistance NOTIFY displayDistanceChanged)
Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY displayDistanceChanged)
Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY displayDistanceChanged)
Q_PROPERTY(qreal diagonalFOV READ diagonalFOV NOTIFY devicePropertiesChanged)
Q_PROPERTY(qreal lensDistanceRatio READ lensDistanceRatio NOTIFY devicePropertiesChanged)
Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY devicePropertiesChanged)
@ -46,8 +47,10 @@ namespace KWin
bool imuResetState() const;
QList<qreal> lookAheadConfig() const;
QList<quint32> displayResolution() const;
qreal displayDistance() const;
void setDisplayDistance(qreal distance);
qreal focusedDisplayDistance() const;
void setFocusedDisplayDistance(qreal distance);
qreal allDisplaysDistance() const;
void setAllDisplaysDistance(qreal distance);
qreal diagonalFOV() const;
qreal lensDistanceRatio() const;
bool sbsEnabled() const;
@ -101,7 +104,8 @@ namespace KWin
QFileSystemWatcher *m_shmDirectoryWatcher = nullptr;
QPointF m_cursorPos;
QTimer *m_cursorUpdateTimer = nullptr;
qreal m_displayDistance = 1.05;
qreal m_focusedDisplayDistance = 0.85;
qreal m_allDisplaysDistance = 1.05;
};
} // namespace KWin

View File

@ -23,14 +23,34 @@
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelDisplayDistance">
<widget class="QLabel" name="labelFocusedDisplayDistance">
<property name="text">
<string>Display Distance:</string>
<string>Focused Display Distance:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="kcfg_DisplayDistance">
<widget class="QDoubleSpinBox" name="kcfg_FocusedDisplayDistance">
<property name="minimum">
<double>0.2</double>
</property>
<property name="maximum">
<double>2.5</double>
</property>
<property name="singleStep">
<double>0.01</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelAllDisplaysDistance">
<property name="text">
<string>All Displays Distance:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="kcfg_AllDisplaysDistance">
<property name="minimum">
<double>0.2</double>
</property>

View File

@ -3,9 +3,14 @@ import QtQuick3D
Node {
id: breezyDesktop
property var viewportResolution: effect.displayResolution
property var screens: root.screens
property var fovDetails: root.fovDetails
property var monitorPlacements: root.monitorPlacements
property var imuRotations: effect.imuRotations
property int focusedMonitorIndex: -1
// x value for placing the viewport in the middle of all screens
property real screensXMid: {
@ -39,7 +44,6 @@ Node {
id: displays
}
Repeater3D {
model: screens.length
delegate: BreezyDesktopDisplay {
@ -60,13 +64,40 @@ Node {
eulerRotation.y: screenRotationY
eulerRotation.x: screenRotationX
position: {
// rotate about the Y (up) axis, to create a horizontal movement
const transform = Qt.matrix4x4();
transform.rotate(screenRotationY, Qt.vector3d(0, 1, 0));
transform.rotate(screenRotationX, Qt.vector3d(1, 0, 0));
// camera looks along the negative Z axis
return transform.times(Qt.vector3d(-monitorPlacement.centerNoRotate[1], monitorPlacement.centerNoRotate[2], -monitorPlacement.centerNoRotate[0]));
let positionVector = displays.nwuToEusVector(monitorPlacement.centerNoRotate);
if (focusedMonitorIndex === index) {
positionVector = positionVector.times(effect.focusedDisplayDistance / effect.allDisplaysDistance);
}
// position vector is only translated in flat directions, without rotations applied, so apply them here
const rotationMatrix = Qt.matrix4x4();
// only one of these should ever be non-zero, since we only rotate in the direction of the "wrap" preference
rotationMatrix.rotate(screenRotationY, Qt.vector3d(0, 1, 0));
rotationMatrix.rotate(screenRotationX, Qt.vector3d(1, 0, 0));
return rotationMatrix.times(positionVector);
}
}
}
FrameAnimation {
running: true
onTriggered: {
if (breezyDesktop.imuRotations && breezyDesktop.imuRotations.length > 0) {
const focusedIndex = displays.findFocusedMonitor(
displays.eusToNwuQuat(breezyDesktop.imuRotations[0]),
breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook),
breezyDesktop.focusedMonitorIndex,
false, // TODO smooth follow
breezyDesktop.fovDetails,
breezyDesktop.screens.map(screen => screen.geometry)
);
console.log(`\t\t\tBreezy - Next focused monitor index: ${focusedIndex}`);
if (focusedIndex !== breezyDesktop.focusedMonitorIndex)
breezyDesktop.focusedMonitorIndex = focusedIndex;
}
}
}

View File

@ -15,7 +15,6 @@ Item {
property real lensDistanceRatio: effect.lensDistanceRatio
property bool sbsEnabled: effect.sbsEnabled
property bool customBannerEnabled: effect.customBannerEnabled
property bool useImuRotation: true // Set to true to use XR rotation when available
implicitWidth: parent.width
implicitHeight: parent.height
@ -72,7 +71,7 @@ Item {
FrameAnimation {
running: true
onTriggered: {
if (root.useImuRotation && root.imuRotations && root.imuRotations.length > 0) {
if (root.imuRotations && root.imuRotations.length > 0) {
updateCamera(applyLookAhead(
root.imuRotations[0],
root.imuRotations[1],

View File

@ -1,7 +1,8 @@
import QtQuick
QtObject {
property real displayDistance: effect.displayDistance
readonly property real focusThreshold: 0.95 / 2.0
readonly property real unfocusThreshold: 1.1 / 2.0
// Converts degrees to radians
function degreeToRadian(degree) {
@ -12,6 +13,16 @@ QtObject {
return radian * 180 / Math.PI;
}
function nwuToEusVector(vector) {
// Converts NWU vector to EUS vector
return Qt.vector3d(-vector.y, vector.z, -vector.x);
}
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
function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
var flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2);
@ -24,10 +35,6 @@ QtObject {
}
}
function displayDistanceDefault() {
return displayDistance;
}
function actualWrapScheme(screens, viewportWidth, viewportHeight) {
const minX = Math.min(...screens.map(screen => screen.geometry.x));
const maxX = Math.max(...screens.map(screen => screen.geometry.x + screen.geometry.width));
@ -44,8 +51,8 @@ QtObject {
function fovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio) {
const aspect = viewportWidth / viewportHeight;
const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect);
const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / displayDistanceDefault());
const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / displayDistanceDefault());
const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / effect.allDisplaysDistance);
const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / effect.allDisplaysDistance);
// distance needed for the FOV-sized monitor to fill up the screen
const fullScreenDistance = viewportHeight / 2 / Math.tan(fovRadians.vertical / 2);
@ -106,26 +113,6 @@ QtObject {
}
})
// Applies a quaternion to a 3D vector
function applyQuaternionToVector(vector, quaternion) {
var t = [
2.0 * (quaternion[1] * vector[2] - quaternion[2] * vector[1]),
2.0 * (quaternion[2] * vector[0] - quaternion[0] * vector[2]),
2.0 * (quaternion[0] * vector[1] - quaternion[1] * vector[0])
];
return [
vector[0] + quaternion[3] * t[0] + quaternion[1] * t[2] - quaternion[2] * t[1],
vector[1] + quaternion[3] * t[1] + quaternion[2] * t[0] - quaternion[0] * t[2],
vector[2] + quaternion[3] * t[2] + quaternion[0] * t[1] - quaternion[1] * t[0]
];
}
// Normalizes a 3D vector
function normalizeVector(vector) {
var length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
return [vector[0] / length, vector[1] / length, vector[2] / length];
}
function monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels, lengthToRadianFn) {
var closestWrapPixel = monitorBeginPixel;
var closestWrap = cachedMonitorRadians[monitorBeginPixel];
@ -227,16 +214,16 @@ QtObject {
monitorPlacements.push({
originalIndex: originalIndex,
centerNoRotate: [
centerNoRotate: Qt.vector3d(
monitorCenterRadius,
0,
upCenterPixels
],
centerLook: normalizeVector([
),
centerLook: Qt.vector3d(
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
-monitorCenterRadius * Math.sin(monitorWrapDetails.center),
upCenterPixels
]),
).normalized(),
rotationAngleRadians: {
x: 0,
y: -monitorWrapDetails.center
@ -267,16 +254,16 @@ QtObject {
monitorPlacements.push({
originalIndex: originalIndex,
centerNoRotate: [
centerNoRotate: Qt.vector3d(
monitorCenterRadius,
westCenterPixels,
0
],
centerLook: normalizeVector([
),
centerLook: Qt.vector3d(
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
westCenterPixels,
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
]),
).normalized(),
rotationAngleRadians: {
x: -monitorWrapDetails.center,
y: 0
@ -295,16 +282,16 @@ QtObject {
monitorPlacements.push({
originalIndex: index,
centerNoRotate: [
centerNoRotate: Qt.vector3d(
fovDetails.completeScreenDistancePixels,
westCenterPixels,
upCenterPixels
],
centerLook: normalizeVector([
),
centerLook: Qt.vector3d(
fovDetails.completeScreenDistancePixels,
westCenterPixels,
upCenterPixels
]),
).normalized(),
rotationAngleRadians: {
x: 0,
y: 0
@ -313,7 +300,108 @@ QtObject {
});
}
// put them back in the original monitor order before returning
monitorPlacements.sort(function(a, b) { return a.originalIndex - b.originalIndex; });
return monitorPlacements;
}
// returns how far the look vector is from the center of the monitor, as a percentage of the monitor's width
function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) {
var monitorAspectRatio = monitorDetails.width / monitorDetails.height;
// weight the up distance by the aspect ratio
var vectorUpPixels = upAngleToLength(
fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels,
fovDetails.completeScreenDistancePixels,
monitorVector.z,
monitorVector.x
);
var upDeltaPixels = (lookUpPixels - vectorUpPixels) * monitorAspectRatio;
var vectorWestPixels = westAngleToLength(
fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels,
fovDetails.completeScreenDistancePixels,
monitorVector.y,
monitorVector.x
);
var westDeltaPixels = lookWestPixels - vectorWestPixels;
var totalDeltaPixels = Math.sqrt(upDeltaPixels * upDeltaPixels + westDeltaPixels * westDeltaPixels);
// threshold is a percentage of width, and height was already properly weighted
return totalDeltaPixels / monitorDetails.width;
}
function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) {
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
var upConversionFns = fovDetails.monitorWrappingScheme === "vertical" ? fovConversionFns.curved : fovConversionFns.flat;
var lookUpPixels = upConversionFns.angleToLength(
fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels,
fovDetails.completeScreenDistancePixels,
rotatedLookVector.z,
rotatedLookVector.x
);
var westConversionFns = fovDetails.monitorWrappingScheme === "horizontal" ? fovConversionFns.curved : fovConversionFns.flat;
var lookWestPixels = westConversionFns.angleToLength(
fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels,
fovDetails.completeScreenDistancePixels,
rotatedLookVector.y,
rotatedLookVector.x
);
var closestIndex = -1;
var closestDistance = Number.POSITIVE_INFINITY;
// Check current focused monitor first
if (currentFocusedIndex !== -1) {
var focusedDistance = getMonitorDistance(
fovDetails,
lookUpPixels,
lookWestPixels,
monitorVectors[currentFocusedIndex],
monitorsDetails[currentFocusedIndex],
upConversionFns.angleToLength,
westConversionFns.angleToLength
) * effect.focusedDisplayDistance / effect.allDisplaysDistance;
console.log(`\t\t\tBreezy - Focused monitor index: ${currentFocusedIndex}, distance: ${focusedDistance}`);
if (smoothFollowEnabled || focusedDistance < unfocusThreshold)
return currentFocusedIndex;
}
// Find the closest monitor
for (var i = 0; i < monitorVectors.length; ++i) {
if (i === currentFocusedIndex)
continue;
var distance = getMonitorDistance(
fovDetails,
lookUpPixels,
lookWestPixels,
monitorVectors[i],
monitorsDetails[i],
upConversionFns.angleToLength,
westConversionFns.angleToLength
);
console.log(`\t\t\tBreezy - Monitor index: ${i}, distance: ${distance}`);
if (distance < closestDistance) {
closestIndex = i;
closestDistance = distance;
}
}
if (smoothFollowEnabled || closestDistance < focusThreshold)
return closestIndex;
// Unfocus all displays
return -1;
}
}

View File

@ -33,8 +33,9 @@ Item {
id: displays
}
property var fovDetails: displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio)
property var monitorPlacements: {
const fovDetails = displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio);
const monitorSpacing = 0.0;
return displays.monitorsToPlacements(fovDetails, screens.map(screen => screen.geometry), monitorSpacing);
}
@ -58,7 +59,6 @@ Item {
PerspectiveCamera {
id: camera
frustumCullingEnabled: false
}
BreezyDesktop {