Files
Panama/config/dot/quickshell/modules/settings/DisplayModePicker.qml
T

161 lines
6.1 KiB
QML

// The resolution list for one display.
//
// One line per resolution, not per mode: this panel reports 35 modes, most of
// which differ only in refresh-rate rounding, and a flat list of
// "[email protected]" strings is a wall of near-identical text rather than a
// choice. The rates live in their own row on the page, where changing only the
// rate does not mean going back through the resolution you already had.
//
// Each line carries the two facts that decide the choice: the shape of the
// picture, and whether this is the one the panel was built for.
//
// Applying is the page's job. Every per-display edit on the Displays page goes
// through one funnel so the whole record -- position, color, mirror state --
// rides the same keep-or-revert transaction; this reports which resolution was
// asked for and lets that funnel do the rest.
import QtQuick
import qs.config
Column {
id: root
property var monitor: null
property bool enabled: true
// The mode string for the resolution that was chosen, at the rate closest
// to the one in use.
signal picked(string mode)
spacing: 0
readonly property var grouped: {
if (!root.monitor)
return [];
const buckets = {};
const order = [];
for (const mode of root.monitor.modes) {
const key = mode.label;
if (!buckets[key]) {
buckets[key] = { label: key, width: mode.width, height: mode.height, rates: [] };
order.push(key);
}
buckets[key].rates.push(mode);
}
return order.map(key => buckets[key]);
}
// Modes arrive sorted by area, so the first is the largest the panel
// advertises -- which is the one it was built for.
readonly property string nativeLabel: root.grouped.length > 0 ? root.grouped[0].label : ""
function aspectLabel(width: int, height: int): string {
if (!width || !height)
return "";
const ratio = width / height;
const named = [
{ ratio: 1, label: "1:1" },
{ ratio: 5 / 4, label: "5:4" },
{ ratio: 4 / 3, label: "4:3" },
{ ratio: 3 / 2, label: "3:2" },
{ ratio: 16 / 10, label: "16:10" },
{ ratio: 16 / 9, label: "16:9" },
{ ratio: 21 / 9, label: "21:9" },
{ ratio: 32 / 9, label: "32:9" }
];
for (const entry of named) {
if (Math.abs(ratio - entry.ratio) < 0.05)
return entry.label;
}
const reduce = (a, b) => b === 0 ? a : reduce(b, a % b);
const divisor = reduce(width, height) || 1;
return `${Math.round(width / divisor)}:${Math.round(height / divisor)}`;
}
// The rate nearest the one in use, so changing the resolution does not
// quietly change the refresh rate as well when the panel offers the same
// one at the new size.
function modeFor(group: var): string {
const target = root.monitor ? root.monitor.refreshRate : 0;
const rates = group.rates.slice().sort((left, right) =>
Math.abs(left.refresh - target) - Math.abs(right.refresh - target)
|| right.refresh - left.refresh);
return rates.length > 0 ? rates[0].mode : "";
}
Repeater {
model: root.grouped
SettingRow {
id: resolution
required property var modelData
required property int index
readonly property bool isCurrent: !!root.monitor
&& root.monitor.width === resolution.modelData.width
&& root.monitor.height === resolution.modelData.height
readonly property bool isNative: resolution.modelData.label === root.nativeLabel
readonly property string tag: {
if (resolution.isCurrent && resolution.isNative)
return "Current · Native";
if (resolution.isCurrent)
return "Current";
return resolution.isNative ? "Native" : "";
}
width: parent.width
label: resolution.modelData.label
controlWidth: 170
divider: resolution.index < root.grouped.length - 1
opacity: root.enabled ? 1 : 0.45
activatable: root.enabled && !resolution.isCurrent
onActivated: {
const mode = root.modeFor(resolution.modelData);
if (mode !== "")
root.picked(mode);
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 10
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.aspectLabel(resolution.modelData.width, resolution.modelData.height)
color: Theme.fgMuted
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: resolution.tag !== ""
width: visible ? tagLabel.implicitWidth + 18 : 0
height: 21
radius: Theme.pillRadius
color: resolution.isCurrent
? Theme.alpha(Theme.accent, 0.1)
: Theme.alpha(Theme.fg, 0.06)
border.width: 1
border.color: resolution.isCurrent
? Theme.alpha(Theme.accent, 0.25)
: Theme.alpha(Theme.fg, 0.1)
Text {
id: tagLabel
anchors.centerIn: parent
text: resolution.tag
color: resolution.isCurrent ? Theme.accentAlt : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.weight: Font.DemiBold
}
}
}
}
}
}