// Displays. // // Resolution, refresh rate, scale, and rotation, plus panel brightness and the // gaming display policy that was already here. // // Every geometry change goes through an apply-then-confirm countdown. This is // the one page where a wrong value can leave the screen unreadable or blank, // and no other control in the app can undo it once that happens. Confirming is // what writes the choice to the settings store; letting the countdown run // leaves nothing behind. import QtQuick import qs.config import qs.services import qs.widgets SettingsPage { id: root title: "Displays" lede: SystemSettings.monitorDescription || "Reading the active display…" property string selectedOutput: "" readonly property var monitor: Displays.monitorNamed(root.selectedOutput) ?? (Displays.monitors.length > 0 ? Displays.monitors[0] : null) readonly property string currentMode: root.monitor ? root.monitor.mode : "" function syncSelectedOutput(): void { if (!Displays.monitorNamed(root.selectedOutput)) root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : ""; } // Probing I2C for DDC-capable monitors takes on the order of a second, so // it runs when this page is opened rather than at shell startup. Monitors // do not appear while you are looking at a settings page, so once is enough. Component.onCompleted: { root.syncSelectedOutput(); if (!Brightness.scanned) Brightness.refresh(); } Connections { target: Displays function onMonitorsChanged(): void { root.syncSelectedOutput(); } } // The confirmation sits above everything, because while it is counting down // it is the only thing that matters on this page. header: Component { Rectangle { visible: Displays.awaitingConfirmation implicitHeight: visible ? confirmRow.implicitHeight + 28 : 0 radius: Theme.cardRadius color: Theme.mix(Theme.bgPanel, Theme.warn, 0.12) border.width: 1 border.color: Theme.alpha(Theme.warn, 0.4) Row { id: confirmRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: 16 spacing: 14 Column { width: parent.width - keepButton.width - revertButton.width - 28 anchors.verticalCenter: parent.verticalCenter spacing: 3 Text { width: parent.width text: "Keep this display setting?" color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.DemiBold } Text { width: parent.width text: "Reverting in " + Displays.secondsLeft + (Displays.secondsLeft === 1 ? " second" : " seconds") + " if you do nothing. If you cannot read this, just wait." color: Theme.fgDim font.family: Theme.fontFamily font.features: Theme.tabularFigures font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap } } SettingsButton { id: revertButton anchors.verticalCenter: parent.verticalCenter text: "Revert now" onClicked: Displays.revert() } SettingsButton { id: keepButton anchors.verticalCenter: parent.verticalCenter text: "Keep" enabled: Displays.canConfirm onClicked: Displays.confirm() } } } } SettingsCard { visible: Displays.monitors.length > 1 title: "Arrange displays" subtitle: "Drag the screens into place. The primary display anchors the desktop at 0,0." DisplayArrangement { width: parent.width displayService: Displays selectedOutput: root.selectedOutput interactionEnabled: !Displays.awaitingConfirmation && !Displays.busy onSelectionRequested: output => root.selectedOutput = output } } SettingsCard { visible: Displays.monitors.length > 1 title: "Connected display" subtitle: "Choose the output whose resolution, scale, and rotation you want to adjust." ChoiceGrid { width: parent.width label: "Display" options: Displays.monitors.map(monitor => ({ value: monitor.name, label: monitor.description || monitor.name })) current: root.monitor ? root.monitor.name : "" enabled: !Displays.awaitingConfirmation && !Displays.busy divider: false onPicked: value => root.selectedOutput = value } } SettingsCard { title: root.monitor ? root.monitor.name : (SystemSettings.monitorName || "Active display") subtitle: root.monitor ? `${root.monitor.description} · ${root.monitor.width} × ${root.monitor.height} at ${Math.round(root.monitor.refreshRate)} Hz · ${root.monitor.scale.toFixed(2)}× scale` : "Reading the active display…" TextRow { label: "Color mode" detail: "Wide-gamut SDR at 10-bit. Full-time HDR is left to the Hyprland config: it currently breaks screenshots, OBS, and the lock screen's blurred background." value: root.monitor ? `${root.monitor.colorPreset || "standard"} · ${root.monitor.currentFormat || "detecting format"}` : "Detecting" } TextRow { label: "Variable refresh" detail: root.monitor && root.monitor.vrr ? "Active on this output for current fullscreen content" : "This output is ready when game or video content requests it" value: root.monitor && root.monitor.vrr ? "Active" : "Standby" divider: Displays.isOverridden(root.monitor ? root.monitor.name : "") } ActionRow { visible: Displays.isOverridden(root.monitor ? root.monitor.name : "") label: "Using a custom display setting" detail: "Forget it to go back to the shipped resolution and scale" action: "Forget" divider: false onTriggered: Displays.forget(root.monitor.name) } } SettingsCard { visible: root.monitor !== null title: "Resolution" subtitle: "Applied straight away, then reverted automatically unless you confirm." PickerRow { id: modePicker label: "Resolution" detail: root.monitor ? root.monitor.width + " × " + root.monitor.height + " native" : "No display selected" value: root.monitor ? root.monitor.width + " × " + root.monitor.height : "" enabled: !Displays.awaitingConfirmation && !Displays.busy divider: false DisplayModePicker { width: parent.width monitor: root.monitor enabled: !Displays.awaitingConfirmation && !Displays.busy onPicked: modePicker.collapse() } } } SettingsCard { visible: root.monitor !== null title: "Scale and rotation" ChoiceGrid { width: parent.width label: "Scale" detail: "Fractional scales that do not divide the resolution into whole pixels are rejected by the compositor, so only clean ones are offered." options: Displays.scalesForMode(root.currentMode) .map(scale => ({ value: scale, label: scale.toFixed(2) + "×" })) current: root.monitor ? root.monitor.scale : 1 enabled: !Displays.awaitingConfirmation && !Displays.busy onPicked: value => root.applyWith({ scale: value }) } ChoiceGrid { width: parent.width label: "Rotation" options: Displays.transforms current: root.monitor ? root.monitor.transform : 0 enabled: !Displays.awaitingConfirmation && !Displays.busy divider: false onPicked: value => root.applyWith({ transform: value }) } } SettingsCard { title: "Night Light" subtitle: NightLight.active ? "On now, warming the display to reduce blue light." : "Warms the display in the evening to reduce blue light." ToggleRow { setting: "nightLightEnabled" } ToggleRow { setting: "nightLightAutomatic" } TimeOfDayRow { setting: "nightLightFrom" } TimeOfDayRow { setting: "nightLightTo" } SliderRow { setting: "nightLightTemperature"; divider: false } } // Panel brightness, over DDC/CI. // // This is hardware state rather than a stored preference: the monitor // remembers it, the bezel buttons change it behind Panama's back, and // writing it into settings.json would mean restoring a value the panel had // already moved on from. So there is no schema key here and no SliderRow -- // the rows read and write the display directly. SettingsCard { visible: Brightness.available || Brightness.lastError !== "" title: "Brightness" subtitle: Brightness.available ? "Sent to the monitor over DDC/CI, the same channel its buttons use." : Brightness.lastError Repeater { model: Brightness.displays SettingRow { id: brightnessRow required property var modelData required property int index label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector detail: modelData.connector ? modelData.connector + " · " + modelData.value + "%" : modelData.value + "%" divider: brightnessRow.index < Brightness.displays.length - 1 controlWidth: 190 ValueSlider { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right width: parent.width value: brightnessRow.modelData.value / 100 onMoved: v => Brightness.set(brightnessRow.modelData.bus, Math.round(v * 100)) } } } } SettingsCard { title: "Gaming display policy" subtitle: "Applied immediately and restored when the session starts." ToggleRow { setting: "autoHdr" } ChoiceRow { setting: "vrrPolicy" } ChoiceRow { setting: "directScanoutPolicy"; divider: false } } SettingsCard { visible: Displays.lastError !== "" title: "Display problem" subtitle: Displays.lastError } // Applies a change to one field, keeping the others at what is in effect. function applyWith(change: var): void { if (!root.monitor) return; const mode = change.mode ?? root.currentMode; const requestedScale = change.scale ?? root.monitor.scale; Displays.apply( root.monitor.name, mode, Displays.isScaleClean(mode, requestedScale) ? requestedScale : Displays.nearestCleanScale(mode, requestedScale), change.transform ?? root.monitor.transform); } }