Rebuild Displays around the canvas, and let the transaction keep color

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 10:44:11 -04:00
parent 1f694b00b6
commit 9bc68ba358
29 changed files with 3065 additions and 388 deletions
@@ -1,13 +1,21 @@
// The resolution list for one display.
//
// Grouped by resolution with refresh rates beside it, rather than a flat list
// of "[email protected]" strings: this panel reports 35 modes, many of which
// differ only in refresh-rate rounding, and a flat list of those is a wall of
// near-identical text rather than a choice.
// 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, colour, 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
import qs.services
Column {
id: root
@@ -15,9 +23,10 @@ Column {
property var monitor: null
property bool enabled: true
// Emitted once a mode has been asked for, so a container can put the list
// away. Applying is still this component's job; closing is not.
signal picked
// 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: {
@@ -36,6 +45,44 @@ Column {
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
@@ -45,79 +92,66 @@ Column {
required property var modelData
required property int index
readonly property bool isCurrent: root.monitor
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
detail: resolution.isCurrent ? "Current resolution" : ""
controlWidth: Math.max(120, resolution.modelData.rates.length * 84)
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: 6
spacing: 10
Repeater {
model: resolution.modelData.rates
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 {
id: rate
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)
required property var modelData
readonly property bool selected: resolution.isCurrent
&& Displays.modeIsCurrent(root.monitor, rate.modelData)
implicitWidth: Math.max(74, rateCaption.implicitWidth + 22)
implicitHeight: 30
radius: 9
opacity: root.enabled ? 1 : 0.45
color: rate.selected ? "transparent" : Theme.alpha(Theme.fg, rateHover.hovered && root.enabled ? 0.11 : 0.06)
border.width: rate.selected ? 1 : 0
border.color: Theme.alpha(Theme.accent, 0.5)
Rectangle {
anchors.fill: parent
radius: parent.radius
visible: rate.selected
border.width: 0
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: Theme.alpha(Theme.accent, 0.28) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.accentSecondary, 0.28) }
}
}
Text {
id: rateCaption
anchors.centerIn: parent
text: rate.modelData.refreshLabel
color: rate.selected ? Theme.fg : Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
font.weight: rate.selected ? Font.DemiBold : Font.Normal
}
HoverHandler {
id: rateHover
enabled: root.enabled
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: root.enabled && !rate.selected
onTapped: {
Displays.apply(
root.monitor.name,
rate.modelData.mode,
Displays.nearestCleanScale(rate.modelData.mode, root.monitor.scale),
root.monitor.transform);
root.picked();
}
}
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
}
}
}