Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
VirtualScreen.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 fuddlesworth
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#pragma once
5
7
8#include <QHash>
9#include <QRect>
10#include <QRectF>
11#include <QSet>
12#include <QString>
13#include <QVector>
14
15#include <algorithm>
16#include <utility>
17
18namespace Phosphor::Screens {
19
28{
29 QString id;
31 QString displayName;
32 QRectF region;
33 int index = 0;
34
37 static constexpr qreal Tolerance = 1e-3;
38
42 QRect absoluteGeometry(const QRect& physicalGeometry) const
43 {
44 int left = physicalGeometry.x() + qRound(region.x() * physicalGeometry.width());
45 int top = physicalGeometry.y() + qRound(region.y() * physicalGeometry.height());
46 int right = physicalGeometry.x() + qRound((region.x() + region.width()) * physicalGeometry.width());
47 int bottom = physicalGeometry.y() + qRound((region.y() + region.height()) * physicalGeometry.height());
48 // Clamp to physical screen bounds to prevent tolerance overshoot.
49 right = qMin(right, physicalGeometry.x() + physicalGeometry.width());
50 bottom = qMin(bottom, physicalGeometry.y() + physicalGeometry.height());
51 // Prevent degenerate geometry when tolerance overshoot pushes left/top
52 // past right/bottom. Apply BEFORE the floor clamp so the floor clamp
53 // has the final word on minimum origin.
54 left = qMin(left, right - 1);
55 top = qMin(top, bottom - 1);
56 left = qMax(left, physicalGeometry.x());
57 top = qMax(top, physicalGeometry.y());
58 int w = qMax(1, right - left);
59 int h = qMax(1, bottom - top);
60 return QRect(left, top, w, h);
61 }
62
71 bool operator==(const VirtualScreenDef&) const = default;
72
76 bool approxEqual(const VirtualScreenDef& other) const
77 {
78 return id == other.id && physicalScreenId == other.physicalScreenId && displayName == other.displayName
79 && index == other.index && qAbs(region.x() - other.region.x()) < Tolerance
80 && qAbs(region.y() - other.region.y()) < Tolerance
81 && qAbs(region.width() - other.region.width()) < Tolerance
82 && qAbs(region.height() - other.region.height()) < Tolerance;
83 }
84
91 bool isValid() const
92 {
94 && region.x() >= -Tolerance && region.y() >= -Tolerance && region.width() > 0 && region.height() > 0
95 && region.x() + region.width() <= 1.0 + Tolerance && region.y() + region.height() <= 1.0 + Tolerance;
96 }
97
103 {
104 bool left = true;
105 bool top = true;
106 bool right = true;
107 bool bottom = true;
108 };
110 {
111 return {region.left() < Tolerance, region.top() < Tolerance, region.right() > (1.0 - Tolerance),
112 region.bottom() > (1.0 - Tolerance)};
113 }
114};
115
124{
126 QVector<VirtualScreenDef> screens;
127
128 bool hasSubdivisions() const
129 {
130 return screens.size() > 1;
131 }
132 bool isEmpty() const
133 {
134 return screens.isEmpty();
135 }
136
146 bool operator==(const VirtualScreenConfig&) const = default;
147
153 bool approxEqual(const VirtualScreenConfig& other) const
154 {
155 if (physicalScreenId != other.physicalScreenId || screens.size() != other.screens.size()) {
156 return false;
157 }
158 for (int i = 0; i < screens.size(); ++i) {
159 if (!screens[i].approxEqual(other.screens[i])) {
160 return false;
161 }
162 }
163 return true;
164 }
165
175 static bool isValid(const VirtualScreenConfig& cfg, const QString& expectedPhysicalScreenId,
176 int maxScreensPerPhysical, QString* error = nullptr)
177 {
178 auto fail = [&](const QString& msg) {
179 if (error)
180 *error = msg;
181 return false;
182 };
183
184 // Empty config = removal request. Always valid.
185 if (cfg.isEmpty()) {
186 return true;
187 }
188
189 if (!expectedPhysicalScreenId.isEmpty() && cfg.physicalScreenId != expectedPhysicalScreenId) {
190 return fail(QStringLiteral("config physicalScreenId '%1' does not match parameter '%2'")
191 .arg(cfg.physicalScreenId, expectedPhysicalScreenId));
192 }
193
194 if (cfg.screens.size() < 2) {
195 return fail(QStringLiteral("need at least 2 screens for subdivision, got %1").arg(cfg.screens.size()));
196 }
197
198 if (maxScreensPerPhysical > 0 && cfg.screens.size() > maxScreensPerPhysical) {
199 return fail(QStringLiteral("too many virtual screens %1 (max %2)")
200 .arg(cfg.screens.size())
201 .arg(maxScreensPerPhysical));
202 }
203
204 QSet<QString> seenIds;
205 QSet<int> seenIndices;
206 for (const auto& def : cfg.screens) {
207 if (!def.isValid()) {
208 return fail(QStringLiteral("invalid VirtualScreenDef id='%1' region=(%2,%3 %4x%5)")
209 .arg(def.id)
210 .arg(def.region.x())
211 .arg(def.region.y())
212 .arg(def.region.width())
213 .arg(def.region.height()));
214 }
215 if (def.physicalScreenId != cfg.physicalScreenId) {
216 return fail(QStringLiteral("def.physicalScreenId '%1' does not match config '%2' for def '%3'")
217 .arg(def.physicalScreenId, cfg.physicalScreenId, def.id));
218 }
219 const QString expectedId = PhosphorIdentity::VirtualScreenId::make(cfg.physicalScreenId, def.index);
220 if (def.id != expectedId) {
221 return fail(QStringLiteral("def.id '%1' does not match expected '%2' for index %3")
222 .arg(def.id, expectedId)
223 .arg(def.index));
224 }
225 if (seenIds.contains(def.id)) {
226 return fail(QStringLiteral("duplicate def.id %1").arg(def.id));
227 }
228 seenIds.insert(def.id);
229 if (seenIndices.contains(def.index)) {
230 return fail(QStringLiteral("duplicate def.index %1").arg(def.index));
231 }
232 seenIndices.insert(def.index);
233 }
234
235 // Pairwise overlap check (tolerance-aware).
236 for (int i = 0; i < cfg.screens.size(); ++i) {
237 for (int j = i + 1; j < cfg.screens.size(); ++j) {
238 const QRectF intersection = cfg.screens[i].region.intersected(cfg.screens[j].region);
239 if (intersection.width() > VirtualScreenDef::Tolerance
240 && intersection.height() > VirtualScreenDef::Tolerance) {
241 return fail(QStringLiteral("overlapping regions between '%1' and '%2'")
242 .arg(cfg.screens[i].id, cfg.screens[j].id));
243 }
244 }
245 }
246
247 // Total area must approximately cover the unit square.
248 qreal totalArea = 0.0;
249 for (const auto& def : cfg.screens) {
250 totalArea += def.region.width() * def.region.height();
251 }
252 const qreal lower = 1.0 - VirtualScreenDef::Tolerance;
253 const qreal upper = 1.0 + VirtualScreenDef::Tolerance;
254 if (totalArea < lower) {
255 return fail(QStringLiteral("insufficient coverage for '%1' — total area %2 < %3")
256 .arg(cfg.physicalScreenId)
257 .arg(totalArea)
258 .arg(lower));
259 }
260 if (totalArea > upper) {
261 return fail(QStringLiteral("excessive coverage for '%1' — total area %2 > %3")
262 .arg(cfg.physicalScreenId)
263 .arg(totalArea)
264 .arg(upper));
265 }
266
267 return true;
268 }
269
275 bool swapRegions(const QString& idA, const QString& idB)
276 {
277 if (idA == idB) {
278 return false;
279 }
280 int indexA = -1;
281 int indexB = -1;
282 for (int i = 0; i < screens.size(); ++i) {
283 if (screens[i].id == idA) {
284 indexA = i;
285 } else if (screens[i].id == idB) {
286 indexB = i;
287 }
288 }
289 if (indexA < 0 || indexB < 0) {
290 return false;
291 }
292 std::swap(screens[indexA].region, screens[indexB].region);
293 return true;
294 }
295
317 bool rotateRegions(const QVector<QString>& orderedIds, bool clockwise)
318 {
319 if (orderedIds.size() < 2) {
320 return false;
321 }
322 QSet<QString> seenIds;
323 seenIds.reserve(orderedIds.size());
324 QVector<int> defIndices;
325 defIndices.reserve(orderedIds.size());
326 for (const auto& id : orderedIds) {
327 if (seenIds.contains(id)) {
328 return false; // duplicate id in orderedIds
329 }
330 seenIds.insert(id);
331 int found = -1;
332 for (int i = 0; i < screens.size(); ++i) {
333 if (screens[i].id == id) {
334 found = i;
335 break;
336 }
337 }
338 if (found < 0) {
339 return false;
340 }
341 defIndices.append(found);
342 }
343 const int n = defIndices.size();
344 QVector<QRectF> regions;
345 regions.reserve(n);
346 for (int idx : defIndices) {
347 regions.append(screens[idx].region);
348 }
349 for (int i = 0; i < n; ++i) {
350 const int src = clockwise ? (i + 1) % n : (i - 1 + n) % n;
351 screens[defIndices[i]].region = regions[src];
352 }
353 return true;
354 }
355};
356
357} // namespace Phosphor::Screens
bool isVirtual(const QString &screenId)
Check if a screen ID is a virtual screen ID (contains "/vs:").
Definition VirtualScreenId.h:31
QString make(const QString &physicalScreenId, int index)
Construct a virtual screen ID from physical ID and index.
Definition VirtualScreenId.h:60
Definition IWindowTrackingService.h:26
Configuration for how a physical screen is subdivided into virtual screens.
Definition VirtualScreen.h:124
bool operator==(const VirtualScreenConfig &) const =default
Exact vector equality: order-sensitive (QVector::operator==) and delegates to VirtualScreenDef::opera...
bool swapRegions(const QString &idA, const QString &idB)
Swap the region fields of the two defs identified by idA and idB.
Definition VirtualScreen.h:275
static bool isValid(const VirtualScreenConfig &cfg, const QString &expectedPhysicalScreenId, int maxScreensPerPhysical, QString *error=nullptr)
Validate geometric and structural invariants for this config.
Definition VirtualScreen.h:175
bool hasSubdivisions() const
Definition VirtualScreen.h:128
QVector< VirtualScreenDef > screens
Definition VirtualScreen.h:126
bool rotateRegions(const QVector< QString > &orderedIds, bool clockwise)
Rotate the region fields through the defs identified by orderedIds.
Definition VirtualScreen.h:317
bool isEmpty() const
Definition VirtualScreen.h:132
QString physicalScreenId
Definition VirtualScreen.h:125
bool approxEqual(const VirtualScreenConfig &other) const
Tolerance-aware equivalent of operator==: compares defs pairwise using VirtualScreenDef::approxEqual.
Definition VirtualScreen.h:153
Check which edges of this virtual screen are at the physical screen boundary (vs internal edges share...
Definition VirtualScreen.h:103
bool left
Definition VirtualScreen.h:104
bool right
Definition VirtualScreen.h:106
bool bottom
Definition VirtualScreen.h:107
bool top
Definition VirtualScreen.h:105
Definition of a single virtual screen within a physical screen.
Definition VirtualScreen.h:28
QRect absoluteGeometry(const QRect &physicalGeometry) const
Compute absolute geometry from the physical screen's geometry.
Definition VirtualScreen.h:42
QString displayName
User-facing name, e.g. "Left", "Right".
Definition VirtualScreen.h:31
bool approxEqual(const VirtualScreenDef &other) const
Tolerance-aware comparison for change-detection paths that round through JSON.
Definition VirtualScreen.h:76
bool isValid() const
Check if the definition is valid: non-empty id, non-empty physicalScreenId, non-negative origin,...
Definition VirtualScreen.h:91
QString id
Full ID: "physicalId/vs:N".
Definition VirtualScreen.h:29
bool operator==(const VirtualScreenDef &) const =default
Exact bitwise equality across every field.
static constexpr qreal Tolerance
Shared tolerance for floating-point region comparisons.
Definition VirtualScreen.h:37
PhysicalEdges physicalEdges() const
Definition VirtualScreen.h:109
QRectF region
Relative geometry within physical screen (0-1)
Definition VirtualScreen.h:32
int index
Index within the physical screen's subdivision.
Definition VirtualScreen.h:33
QString physicalScreenId
Owning physical screen's stable EDID ID.
Definition VirtualScreen.h:30