From c2dfefd395160d77ab4447819a33beb10188f89f Mon Sep 17 00:00:00 2001
From: wheaney <42350981+wheaney@users.noreply.github.com>
Date: Fri, 19 Sep 2025 15:14:25 -0700
Subject: [PATCH] Add curved display support
---
kwin/src/breezydesktopconfig.kcfg | 5 ++
kwin/src/breezydesktopeffect.cpp | 7 ++
kwin/src/breezydesktopeffect.h | 4 +
kwin/src/kcm/breezydesktopeffectkcm.cpp | 2 +
kwin/src/kcm/breezydesktopeffectkcm.ui | 34 ++++++---
kwin/src/qml/BreezyDesktop.qml | 9 +--
kwin/src/qml/BreezyDesktopDisplay.qml | 98 ++++++++++++++++++++++++-
kwin/src/qml/Displays.qml | 2 +-
8 files changed, 139 insertions(+), 22 deletions(-)
diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg
index 0003e2d..99cfec4 100644
--- a/kwin/src/breezydesktopconfig.kcfg
+++ b/kwin/src/breezydesktopconfig.kcfg
@@ -86,5 +86,10 @@
Apply follow mode to all displays instead of only the focused display
+
+ false
+
+ Curve the displays around you
+
diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp
index dfd23d2..9ada276 100644
--- a/kwin/src/breezydesktopeffect.cpp
+++ b/kwin/src/breezydesktopeffect.cpp
@@ -229,6 +229,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(); }
+ bool curved = BreezyDesktopConfig::curvedDisplay();
+ if (m_curvedDisplay != curved) { m_curvedDisplay = curved; Q_EMIT curvedDisplayChanged(); }
+
// this one doesn't have a signal, just always assign it
m_allDisplaysFollowMode = BreezyDesktopConfig::allDisplaysFollowMode();
}
@@ -505,6 +508,10 @@ bool BreezyDesktopEffect::mirrorPhysicalDisplays() const {
return m_mirrorPhysicalDisplays;
}
+bool BreezyDesktopEffect::curvedDisplay() const {
+ return m_curvedDisplay;
+}
+
QList BreezyDesktopEffect::smoothFollowOrigin() const {
return m_smoothFollowOrigin;
}
diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h
index 049e453..ec9beda 100644
--- a/kwin/src/breezydesktopeffect.h
+++ b/kwin/src/breezydesktopeffect.h
@@ -44,6 +44,7 @@ namespace KWin
Q_PROPERTY(int antialiasingQuality READ antialiasingQuality NOTIFY antialiasingQualityChanged)
Q_PROPERTY(bool removeVirtualDisplaysOnDisable READ removeVirtualDisplaysOnDisable NOTIFY removeVirtualDisplaysOnDisableChanged)
Q_PROPERTY(bool mirrorPhysicalDisplays READ mirrorPhysicalDisplays NOTIFY mirrorPhysicalDisplaysChanged)
+ Q_PROPERTY(bool curvedDisplay READ curvedDisplay NOTIFY curvedDisplayChanged)
public:
@@ -87,6 +88,7 @@ namespace KWin
int antialiasingQuality() const;
bool removeVirtualDisplaysOnDisable() const;
bool mirrorPhysicalDisplays() const;
+ bool curvedDisplay() const;
void showCursor();
void hideCursor();
@@ -120,6 +122,7 @@ namespace KWin
void antialiasingQualityChanged();
void removeVirtualDisplaysOnDisableChanged();
void mirrorPhysicalDisplaysChanged();
+ void curvedDisplayChanged();
void cursorImageSourceChanged();
void cursorPosChanged();
@@ -167,6 +170,7 @@ namespace KWin
int m_antialiasingQuality = 3; // 0=None, 1=Medium, 2=High, 3=VeryHigh
bool m_removeVirtualDisplaysOnDisable = true;
bool m_mirrorPhysicalDisplays = false;
+ bool m_curvedDisplay = 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 6c44371..12ad2b8 100644
--- a/kwin/src/kcm/breezydesktopeffectkcm.cpp
+++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp
@@ -125,6 +125,7 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu
connect(ui.kcfg_MirrorPhysicalDisplays, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_RemoveVirtualDisplaysOnDisable, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_AllDisplaysFollowMode, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save);
+ connect(ui.kcfg_CurvedDisplay, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save);
connect(ui.EnableMultitap, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::updateMultitapEnabled);
connect(ui.SmoothFollowTrackYaw, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::updateSmoothFollowTrackYaw);
connect(ui.SmoothFollowTrackPitch, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::updateSmoothFollowTrackPitch);
@@ -257,6 +258,7 @@ void BreezyDesktopEffectConfig::updateUiFromConfig()
ui.kcfg_DisplayWrappingScheme->setCurrentIndex(BreezyDesktopConfig::self()->displayWrappingScheme());
ui.kcfg_AntialiasingQuality->setCurrentIndex(BreezyDesktopConfig::self()->antialiasingQuality());
ui.kcfg_MirrorPhysicalDisplays->setChecked(BreezyDesktopConfig::self()->mirrorPhysicalDisplays());
+ ui.kcfg_CurvedDisplay->setChecked(BreezyDesktopConfig::self()->curvedDisplay());
ui.kcfg_RemoveVirtualDisplaysOnDisable->setChecked(BreezyDesktopConfig::self()->removeVirtualDisplaysOnDisable());
ui.kcfg_AllDisplaysFollowMode->setChecked(BreezyDesktopConfig::self()->allDisplaysFollowMode());
ui.kcfg_ZoomOnFocusEnabled->setChecked(BreezyDesktopConfig::self()->zoomOnFocusEnabled());
diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui
index 74a2b1d..61a1ea9 100644
--- a/kwin/src/kcm/breezydesktopeffectkcm.ui
+++ b/kwin/src/kcm/breezydesktopeffectkcm.ui
@@ -73,6 +73,16 @@
-
+
+
+ Curved display
+
+
+ false
+
+
+
+ -
Follow mode
@@ -82,14 +92,14 @@
- -
+
-
Focused Display Distance:
- -
+
-
2
@@ -111,14 +121,14 @@
- -
+
-
All Displays Distance:
- -
+
-
2
@@ -140,14 +150,14 @@
- -
+
-
Display Spacing:
- -
+
-
Qt::Horizontal
@@ -157,14 +167,14 @@
- -
+
-
Follow threshold:
- -
+
-
Qt::Horizontal
@@ -186,7 +196,7 @@
- -
+
-
Add Virtual Display:
@@ -199,7 +209,7 @@
- -
+
-
false
@@ -232,7 +242,7 @@
- -
+
-
false
false
@@ -245,7 +255,7 @@
- -
+
-
diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml
index 6baf840..435d5fb 100644
--- a/kwin/src/qml/BreezyDesktop.qml
+++ b/kwin/src/qml/BreezyDesktop.qml
@@ -145,6 +145,7 @@ Node {
delegate: BreezyDesktopDisplay {
screen: breezyDesktop.screens[index]
monitorPlacement: breezyDesktop.monitorPlacements[index]
+ fovDetails: breezyDesktop.fovDetails
property real smoothFollowTransitionProgress: 0.0
property real monitorDistance: effect.allDisplaysDistance
@@ -158,14 +159,6 @@ Node {
return matrix;
}
- property vector3d screenScale: {
- const geometry = screen.geometry;
-
- // apparently the default model unit size is 100x100, so we scale it up to the screen size
- return Qt.vector3d(geometry.width / 100, geometry.height / 100, 1);
- }
-
- scale: screenScale
eulerRotation.y: screenRotationY
eulerRotation.x: screenRotationX
position: {
diff --git a/kwin/src/qml/BreezyDesktopDisplay.qml b/kwin/src/qml/BreezyDesktopDisplay.qml
index e192c11..0ab6763 100644
--- a/kwin/src/qml/BreezyDesktopDisplay.qml
+++ b/kwin/src/qml/BreezyDesktopDisplay.qml
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick3D
+import QtQuick3D.Helpers
Model {
id: display
@@ -7,12 +8,107 @@ Model {
required property QtObject screen
required property var monitorPlacement
required property int index
+ required property var fovDetails
property string cursorImageSource: effect.cursorImageSource
property size cursorImageSize: effect.cursorImageSize
property point cursorPos: effect.cursorPos
- source: "#Rectangle"
+ Displays {
+ id: displays
+ }
+
+ geometry: ProceduralMesh {
+ id: mesh
+
+ property var _meshArrays: generateMesh()
+ positions: _meshArrays.positions
+ uv0s: _meshArrays.uvs
+ indexes: _meshArrays.indices
+ primitiveMode: ProceduralMesh.TriangleStrip
+
+ function generateMesh() {
+ if (!display.fovDetails || !display.screen)
+ return { positions: [], uvs: [], indices: [] };
+
+ const fov = display.fovDetails;
+ const monitor = display.screen.geometry;
+
+ const conv = fov.curvedDisplay ? displays.fovConversionFns.curved
+ : displays.fovConversionFns.flat;
+
+ const horizontalWrap = fov.monitorWrappingScheme === 'horizontal';
+ const verticalWrap = fov.monitorWrappingScheme === 'vertical';
+
+ const sideEdgeDistance = conv.centerToFovEdgeDistance(
+ fov.completeScreenDistancePixels, fov.widthPixels);
+ const horizontalRadians = conv.lengthToRadians(
+ fov.defaultDistanceHorizontalRadians,
+ fov.widthPixels,
+ sideEdgeDistance,
+ monitor.width
+ );
+
+ const topEdgeDistance = conv.centerToFovEdgeDistance(
+ fov.completeScreenDistancePixels, fov.heightPixels);
+ const verticalRadians = conv.lengthToRadians(
+ fov.defaultDistanceVerticalRadians,
+ fov.heightPixels,
+ topEdgeDistance,
+ monitor.height
+ );
+
+ const positions = [];
+ const uvs = [];
+ const indices = [];
+
+ const radius = fov.completeScreenDistancePixels;
+ function vertexFor(s, t) {
+ let z = 0;
+
+ const xOffset = s - 0.5;
+ let x = xOffset * monitor.width;
+ if (fov.curvedDisplay && horizontalWrap) {
+ const xOffsetRadians = xOffset * horizontalRadians;
+ x = Math.sin(xOffsetRadians) * radius;
+ z = radius - Math.cos(xOffsetRadians) * radius;
+ }
+
+ const yOffset = t - 0.5;
+ let y = yOffset * monitor.height;
+ if (fov.curvedDisplay && verticalWrap) {
+ const yOffsetRadians = yOffset * verticalRadians;
+ y = Math.sin(yOffsetRadians) * radius;
+ z = radius - Math.cos(yOffsetRadians) * radius;
+ }
+
+ return { pos: Qt.vector3d(x, y, z), uv: Qt.vector2d(s, t) };
+ }
+
+ let segments = 1;
+ if (horizontalWrap) segments = conv.radiansToSegments(horizontalRadians);
+ if (verticalWrap) segments = conv.radiansToSegments(verticalRadians);
+ for (let i = 0; i <= segments; i++) {
+ const texFraction = i / segments;
+
+ // !verticalWrap also covers "flat" wrap scheme
+ const texX0 = !verticalWrap ? texFraction : 0;
+ const texX1 = !verticalWrap ? texFraction : 1;
+
+ const texY0 = verticalWrap ? texFraction : 1;
+ const texY1 = verticalWrap ? texFraction : 0;
+
+ let vtxB = vertexFor(texX0, texY0);
+ let vtxT = vertexFor(texX1, texY1);
+ positions.push(vtxB.pos);
+ positions.push(vtxT.pos);
+ uvs.push(vtxB.uv);
+ uvs.push(vtxT.uv);
+ }
+
+ return { positions: positions, uvs: uvs, indices: [] };
+ }
+ }
materials: [
CustomMaterial {
id: customMat
diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml
index e82b98d..b30a495 100644
--- a/kwin/src/qml/Displays.qml
+++ b/kwin/src/qml/Displays.qml
@@ -75,7 +75,7 @@ QtObject {
lensDistancePixels,
completeScreenDistancePixels,
monitorWrappingScheme: monitorWrappingScheme,
- curvedDisplay: false // or true
+ curvedDisplay: effect.curvedDisplay
};
}