Rebuild Displays around the canvas, and let the transaction keep color
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -1,3 +1,16 @@
|
||||
// Where the displays are, drawn to scale.
|
||||
//
|
||||
// The hero of the Displays page, and it renders whatever is connected --
|
||||
// including one display. A laptop used to open this page on a picker offering a
|
||||
// list of one and never saw the canvas at all, which made the page's clearest
|
||||
// surface the one thing a single-display machine could not have. Solo, the
|
||||
// display is drawn centred and dragging goes away: there is nothing to arrange
|
||||
// it against.
|
||||
//
|
||||
// A mirrored display has no position of its own. The compositor puts it on top
|
||||
// of the display it mirrors, so the canvas stacks it there and says what it is
|
||||
// mirroring, rather than drawing it wherever its stale coordinates point.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.widgets
|
||||
@@ -17,6 +30,34 @@ Item {
|
||||
readonly property var canvasData: DisplayLayout.canvasRects(
|
||||
root.draftLayout, canvas.width, canvas.height, 18)
|
||||
|
||||
readonly property bool solo: root.draftLayout.length <= 1
|
||||
readonly property bool draggable: root.interactionEnabled && !root.solo
|
||||
|
||||
// What is drawn, which is not quite what the layout resolved to: a single
|
||||
// display scaled to fill the whole canvas reads as a wall rather than as a
|
||||
// screen on a desk, so it is drawn at a size that leaves it room to sit in.
|
||||
readonly property real soloFactor: 0.62
|
||||
readonly property var tiles: {
|
||||
const rects = root.canvasData.rects;
|
||||
if (rects.length !== 1)
|
||||
return rects;
|
||||
const rect = rects[0];
|
||||
return [Object.assign({}, rect, {
|
||||
x: rect.x + rect.width * (1 - root.soloFactor) / 2,
|
||||
y: rect.y + rect.height * (1 - root.soloFactor) / 2,
|
||||
width: rect.width * root.soloFactor,
|
||||
height: rect.height * root.soloFactor
|
||||
})];
|
||||
}
|
||||
|
||||
readonly property string hint: {
|
||||
if (root.solo)
|
||||
return "One display connected — plug in another to arrange";
|
||||
return root.width >= 600
|
||||
? "Drag to arrange · arrows move 10 px · Shift moves 100 px"
|
||||
: "Drag or use the arrow keys";
|
||||
}
|
||||
|
||||
function copied(layout): var {
|
||||
return (layout || []).map(record => Object.assign({}, record));
|
||||
}
|
||||
@@ -55,10 +96,13 @@ Item {
|
||||
return root.applyDraft();
|
||||
}
|
||||
|
||||
// The layout as it resolved, not as it is drawn: this is what the
|
||||
// arrangement math produced, which is the thing worth asserting about.
|
||||
function canvasSnapshot(): var {
|
||||
return {
|
||||
bounds: root.canvasData.bounds,
|
||||
scale: root.canvasData.scale,
|
||||
draggable: root.draggable,
|
||||
rects: root.canvasData.rects.map(record => Object.assign({}, record))
|
||||
};
|
||||
}
|
||||
@@ -82,28 +126,79 @@ Item {
|
||||
Rectangle {
|
||||
id: canvas
|
||||
width: parent.width
|
||||
height: root.width >= 620 ? 232 : 190
|
||||
radius: Theme.cardRadius
|
||||
height: root.width >= 620 ? 250 : 200
|
||||
radius: Theme.cardRadius + 2
|
||||
color: Theme.alpha(Theme.bgDark, 0.76)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.07)
|
||||
clip: true
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: parent.radius
|
||||
opacity: 0.36
|
||||
z: 2
|
||||
}
|
||||
|
||||
// Light from above, so the screens read as objects standing on a
|
||||
// surface rather than as boxes floating in a field.
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: parent.height * 0.55
|
||||
border.width: 0
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Theme.alpha(Theme.accent, 0.05) }
|
||||
GradientStop { position: 1.0; color: Theme.alpha(Theme.accent, 0.0) }
|
||||
}
|
||||
}
|
||||
|
||||
// A restrained coordinate field makes the topology feel like a
|
||||
// precision instrument without turning it into a technical graph.
|
||||
Repeater {
|
||||
model: 4
|
||||
model: Math.max(1, Math.ceil(canvas.height / 44))
|
||||
Rectangle {
|
||||
required property int index
|
||||
y: (index + 1) * canvas.height / 5
|
||||
y: (index + 1) * 44
|
||||
width: canvas.width
|
||||
height: 1
|
||||
color: Theme.alpha(Theme.fg, 0.025)
|
||||
color: Theme.alpha(Theme.fg, 0.03)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.canvasData.rects
|
||||
model: Math.max(1, Math.ceil(canvas.width / 44))
|
||||
Rectangle {
|
||||
required property int index
|
||||
x: (index + 1) * 44
|
||||
width: 1
|
||||
height: canvas.height
|
||||
color: Theme.alpha(Theme.fg, 0.03)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: parent.width * 0.1
|
||||
anchors.rightMargin: parent.width * 0.1
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 26
|
||||
height: 1
|
||||
border.width: 0
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: Theme.alpha(Theme.fg, 0.0) }
|
||||
GradientStop { position: 0.5; color: Theme.alpha(Theme.fg, 0.14) }
|
||||
GradientStop { position: 1.0; color: Theme.alpha(Theme.fg, 0.0) }
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.tiles
|
||||
|
||||
Rectangle {
|
||||
id: tile
|
||||
@@ -113,26 +208,44 @@ Item {
|
||||
readonly property var draft: root.draftLayout.find(
|
||||
record => record.name === modelData.name)
|
||||
|
||||
// The rect carries the mirror flag; the draft record is the
|
||||
// fallback for a layout that has not been through
|
||||
// canvasRects yet.
|
||||
readonly property string mirrorOf: {
|
||||
if (tile.modelData.mirrorOf)
|
||||
return String(tile.modelData.mirrorOf);
|
||||
return tile.draft && tile.draft.mirrorOf ? String(tile.draft.mirrorOf) : "";
|
||||
}
|
||||
readonly property bool mirrored: tile.mirrorOf !== ""
|
||||
readonly property bool tileDraggable: root.draggable && !tile.mirrored
|
||||
|
||||
x: modelData.x
|
||||
y: modelData.y
|
||||
z: tile.mirrored ? 1 : 0
|
||||
width: Math.max(64, modelData.width)
|
||||
height: Math.max(48, modelData.height)
|
||||
radius: 11
|
||||
color: tile.selected
|
||||
? Theme.alpha(Theme.bgHighlight, 0.92)
|
||||
: Theme.alpha(Theme.bgPanel, hover.hovered ? 0.94 : 0.78)
|
||||
: Theme.alpha(Theme.bgPanel, hover.hovered ? 0.94 : 0.82)
|
||||
border.width: tile.selected || activeFocus ? 2 : 1
|
||||
border.color: activeFocus
|
||||
? Theme.accentSecondary
|
||||
: (tile.selected ? Theme.accent : Theme.alpha(Theme.fg, 0.14))
|
||||
: (tile.selected ? Theme.accent : Theme.alpha(Theme.fg, 0.18))
|
||||
opacity: root.interactionEnabled ? 1 : 0.5
|
||||
activeFocusOnTab: root.interactionEnabled
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: "Move " + tile.modelData.name
|
||||
Accessible.description: tile.draft && tile.draft.primary
|
||||
? "Primary display. Drag or use the arrow keys to move it."
|
||||
: "Drag or use the arrow keys to move this display."
|
||||
Accessible.description: {
|
||||
if (tile.mirrored)
|
||||
return "Mirroring " + tile.mirrorOf + ". Its position is chosen by the compositor.";
|
||||
if (!tile.tileDraggable)
|
||||
return "The only connected display. There is nothing to arrange it against.";
|
||||
return tile.draft && tile.draft.primary
|
||||
? "Primary display. Drag or use the arrow keys to move it."
|
||||
: "Drag or use the arrow keys to move this display.";
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
@@ -147,6 +260,21 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// The strip a bar would occupy, so the top of the picture is
|
||||
// the top of the tile without anything having to say so.
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 5
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
height: 3
|
||||
radius: 2
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.14)
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -165,9 +293,13 @@ Item {
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: tile.draft
|
||||
? `${Math.round(tile.draft.width / tile.draft.scale)} × ${Math.round(tile.draft.height / tile.draft.scale)}`
|
||||
: ""
|
||||
text: {
|
||||
if (!tile.draft)
|
||||
return "";
|
||||
const size = DisplayLayout.logicalSize(tile.draft);
|
||||
const caption = `${Math.round(size.width)} × ${Math.round(size.height)}`;
|
||||
return tile.width >= 150 ? caption + " points" : caption;
|
||||
}
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
@@ -176,21 +308,38 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Text {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 7
|
||||
width: primaryText.implicitWidth + 12
|
||||
height: 20
|
||||
radius: Theme.pillRadius
|
||||
anchors.topMargin: 7
|
||||
anchors.rightMargin: 9
|
||||
visible: tile.draft && tile.draft.primary
|
||||
color: Theme.alpha(Theme.accent, 0.2)
|
||||
text: "★"
|
||||
color: Theme.warn
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
|
||||
Accessible.role: Accessible.StaticText
|
||||
Accessible.name: "Primary display"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 7
|
||||
visible: tile.mirrored
|
||||
width: visible ? mirrorLabel.implicitWidth + 14 : 0
|
||||
height: 19
|
||||
radius: Theme.pillRadius
|
||||
color: Theme.alpha(Theme.cyan, 0.1)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.cyan, 0.3)
|
||||
|
||||
Text {
|
||||
id: primaryText
|
||||
id: mirrorLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Primary"
|
||||
color: Theme.accentAlt
|
||||
text: "Mirrors " + tile.mirrorOf
|
||||
color: Theme.cyan
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
|
||||
font.weight: Font.DemiBold
|
||||
@@ -200,7 +349,7 @@ Item {
|
||||
HoverHandler {
|
||||
id: hover
|
||||
enabled: root.interactionEnabled
|
||||
cursorShape: Qt.OpenHandCursor
|
||||
cursorShape: tile.tileDraggable ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
@@ -214,7 +363,7 @@ Item {
|
||||
DragHandler {
|
||||
id: drag
|
||||
target: null
|
||||
enabled: root.interactionEnabled
|
||||
enabled: tile.tileDraggable
|
||||
property real initialX: 0
|
||||
property real initialY: 0
|
||||
property bool moved: false
|
||||
@@ -250,7 +399,7 @@ Item {
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (!root.interactionEnabled)
|
||||
if (!tile.tileDraggable)
|
||||
return;
|
||||
const step = event.modifiers & Qt.ShiftModifier ? 100 : 10;
|
||||
let handled = true;
|
||||
@@ -278,19 +427,6 @@ Item {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
width: Math.max(0, parent.width - identifyButton.width
|
||||
- primaryButton.width - applyButton.width - 24)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.width >= 600
|
||||
? "Drag to arrange · arrows move 10 px · Shift moves 100 px"
|
||||
: "Drag or use the arrow keys"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
id: identifyButton
|
||||
text: "Identify"
|
||||
@@ -301,17 +437,21 @@ Item {
|
||||
SettingsButton {
|
||||
id: primaryButton
|
||||
text: "Make primary"
|
||||
enabled: root.interactionEnabled && root.selectedOutput !== ""
|
||||
enabled: root.interactionEnabled && !root.solo && root.selectedOutput !== ""
|
||||
&& !root.draftLayout.find(record =>
|
||||
record.name === root.selectedOutput)?.primary
|
||||
onClicked: root.makePrimary(root.selectedOutput)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
id: applyButton
|
||||
text: "Apply"
|
||||
enabled: root.interactionEnabled
|
||||
onClicked: root.applyDraft()
|
||||
Text {
|
||||
width: Math.max(0, parent.width - identifyButton.width - primaryButton.width - 16)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: root.hint
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user