Stage 3 and 4 of docs/superpowers/plans/2026-08-17-panama-cohesion.md. Add SettingsPage plus ToggleRow, SliderRow, ChoiceRow, ActionRow, and TextRow. A row names a schema key and needs nothing else: label, detail, range, and unit come from PreferenceSchema, and writes go through SystemSettings.commitPreference, which routes compositor-backed keys through apply-and-verify and local keys straight to the store. The page scaffold that was copy-pasted eleven times is now one component. Rebuild Appearance around a live preview of the real desktop, scaled by the ratio between the preview and the actual monitor so a 10px gap on a 4500px display looks as small as it is. Rebuild Desktop & Dock and Input & Shortcuts on the shared rows, replacing the read-only text that stood in for controls that were merely expensive to add. Generate the shortcut list from hyprctl binds. The page held a hand-typed nineteen entries against a real keymap of a hundred and thirteen; it could not show the rest and went stale whenever a bind changed. Every bind now carries its own description -- backfilled for the twenty-nine that lacked one -- and keybinds-contract.sh fails if any bind lacks one, since undescribed binds are dropped from the page. Make Restore defaults span every store Panama owns. Resetting only the schema store left the Home accessory arrangement customised while claiming to restore defaults, which is worse than no reset because it is silent. Done through HomePreferences' existing public aliases rather than a new API. Four defects found while building: cursor:inactive_timeout is answered by getoption as float, not int. A wrong readAs does not fail loudly; it makes every write to that key look rejected, and the user saw an error for a change that worked. schema-hypr-shape-contract.sh now checks all 23 mapped options against the running compositor. The Settings window is tiled, so implicitWidth is only a hint and rows must survive roughly 400px. SliderRow stacks its control under the label below 520px. Binding an anchor to undefined to switch layouts does not reliably release it. Both row layouts are positioned explicitly. Concurrent compositor writes are queued and merged rather than refused. The startup replay of every compositor-backed preference routinely overlaps a UI change, and refusing left the store and the compositor disagreeing. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
215 lines
7.6 KiB
QML
215 lines
7.6 KiB
QML
// A miniature of the real desktop that redraws as you change Appearance.
|
|
//
|
|
// The point is that "outer gaps 24" and "radius 9" mean nothing as numbers. This
|
|
// shows two tiled windows at the settings currently in effect: the focused one
|
|
// wearing the prism border and its glow, the unfocused one carrying the
|
|
// inactive-opacity setting, both inside real gaps at a real corner radius.
|
|
//
|
|
// Geometry is scaled by the ratio between this preview's width and the actual
|
|
// monitor's, so proportions are honest rather than decorative -- a 10px gap on
|
|
// a 4500px display genuinely is almost invisible, and the preview says so.
|
|
//
|
|
// Everything here is driven by bindings on DesktopPreferences, so the preview
|
|
// shows what is actually stored and applied. It has no state of its own and
|
|
// nothing animates while idle.
|
|
|
|
import QtQuick
|
|
import QtQuick.Effects
|
|
import qs.config
|
|
import qs.services
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
implicitHeight: 232
|
|
radius: Theme.cardRadius
|
|
clip: true
|
|
border.width: 1
|
|
border.color: Theme.alpha(Theme.fg, 0.09)
|
|
|
|
// Falls back to a sane width if the monitor has not been read yet, so the
|
|
// preview is never wrong by a factor of ten on first paint.
|
|
readonly property real monitorWidth: SystemSettings.monitorWidth > 0 ? SystemSettings.monitorWidth : 3000
|
|
readonly property real scale: width > 0 ? width / root.monitorWidth : 0.25
|
|
|
|
readonly property int gapsIn: DesktopPreferences.get("gapsIn")
|
|
readonly property int gapsOut: DesktopPreferences.get("gapsOut")
|
|
readonly property int borderSize: DesktopPreferences.get("borderSize")
|
|
readonly property int rounding: DesktopPreferences.get("windowRounding")
|
|
readonly property real inactiveOpacity: DesktopPreferences.get("inactiveOpacity")
|
|
readonly property bool shadowOn: DesktopPreferences.get("shadowEnabled")
|
|
readonly property int shadowRange: DesktopPreferences.get("shadowRange")
|
|
readonly property bool glowOn: DesktopPreferences.get("glowEnabled")
|
|
readonly property int glowRange: DesktopPreferences.get("glowRange")
|
|
|
|
// Scaled geometry, floored so a small-but-nonzero setting stays visible
|
|
// rather than rounding away to nothing.
|
|
function px(value: real): real {
|
|
return value <= 0 ? 0 : Math.max(1, value * root.scale);
|
|
}
|
|
|
|
// Stands in for the wallpaper. Static: an animated gradient here would
|
|
// repaint forever behind a settings page.
|
|
gradient: Gradient {
|
|
orientation: Gradient.Vertical
|
|
GradientStop { position: 0.0; color: "#2b3050" }
|
|
GradientStop { position: 1.0; color: "#241f33" }
|
|
}
|
|
|
|
// The bar, so the preview reads as this desktop and not a generic one.
|
|
Rectangle {
|
|
id: miniBar
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 20
|
|
color: Theme.alpha(Theme.bgDark, 0.4)
|
|
border.width: 0
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 9
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 4
|
|
|
|
Repeater {
|
|
model: 3
|
|
Rectangle {
|
|
required property int index
|
|
width: index === 0 ? 12 : 5
|
|
height: 5
|
|
radius: 2.5
|
|
border.width: 0
|
|
color: index === 0 ? Theme.accent : Theme.alpha(Theme.fg, 0.3)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "Panama"
|
|
color: Theme.alpha(Theme.fg, 0.45)
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: 9
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: tiles
|
|
anchors.top: miniBar.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
anchors.margins: root.px(root.gapsOut)
|
|
spacing: root.px(root.gapsIn) * 2
|
|
|
|
MiniWindow {
|
|
width: (tiles.width - tiles.spacing) / 2
|
|
height: tiles.height
|
|
focused: true
|
|
}
|
|
|
|
MiniWindow {
|
|
width: (tiles.width - tiles.spacing) / 2
|
|
height: tiles.height
|
|
focused: false
|
|
}
|
|
}
|
|
|
|
component MiniWindow: Item {
|
|
id: win
|
|
|
|
required property bool focused
|
|
|
|
// The gradient border is drawn as a filled rounded rect with the window
|
|
// body inset on top of it: QML's Rectangle border takes a colour, not a
|
|
// gradient, and the prism border is the whole signature here.
|
|
Rectangle {
|
|
id: frame
|
|
anchors.fill: parent
|
|
radius: root.px(root.rounding)
|
|
border.width: 0
|
|
visible: win.focused && root.borderSize > 0
|
|
|
|
gradient: Gradient {
|
|
orientation: Gradient.Horizontal
|
|
GradientStop { position: 0.0; color: Theme.accent }
|
|
GradientStop { position: 1.0; color: Theme.accentSecondary }
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: body
|
|
anchors.fill: parent
|
|
anchors.margins: win.focused ? root.px(root.borderSize) : 0
|
|
radius: Math.max(0, root.px(root.rounding) - (win.focused ? root.px(root.borderSize) : 0))
|
|
color: Theme.alpha(Theme.bgDark, 0.92)
|
|
opacity: win.focused ? 1.0 : root.inactiveOpacity
|
|
border.width: win.focused ? 0 : 1
|
|
border.color: Theme.alpha(Theme.gutter, 0.6)
|
|
clip: true
|
|
|
|
Rectangle {
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 13
|
|
color: Theme.alpha(Theme.fg, 0.05)
|
|
border.width: 0
|
|
}
|
|
|
|
Column {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 22
|
|
anchors.margins: 10
|
|
spacing: 5
|
|
|
|
Repeater {
|
|
model: [1.0, 0.74, 0.52]
|
|
Rectangle {
|
|
required property real modelData
|
|
width: parent.width * modelData
|
|
height: 4
|
|
radius: 2
|
|
border.width: 0
|
|
color: Theme.alpha(Theme.fg, win.focused ? 0.2 : 0.13)
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.bottom: parent.bottom
|
|
anchors.margins: 8
|
|
text: win.focused ? "focused" : "unfocused"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: 9
|
|
}
|
|
}
|
|
|
|
// Shadow and glow both come from MultiEffect. Only the focused window
|
|
// gets the glow, matching looks.lua where glow.color_inactive is fully
|
|
// transparent.
|
|
MultiEffect {
|
|
anchors.fill: body
|
|
source: body
|
|
visible: root.shadowOn || (win.focused && root.glowOn)
|
|
z: -1
|
|
|
|
shadowEnabled: true
|
|
shadowColor: win.focused && root.glowOn
|
|
? Theme.alpha(Theme.accent, 0.5)
|
|
: Theme.alpha("#15161e", 0.85)
|
|
shadowBlur: win.focused && root.glowOn
|
|
? Math.min(1.0, root.px(root.glowRange) / 12)
|
|
: Math.min(1.0, root.px(root.shadowRange) / 24)
|
|
shadowVerticalOffset: win.focused && root.glowOn ? 0 : root.px(4)
|
|
shadowHorizontalOffset: 0
|
|
}
|
|
}
|
|
}
|