Build the Panama Hyprland desktop
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
// Selection mode: a GNOME-style rubber band over the frozen screen.
|
||||
//
|
||||
// Behaviours copied from GNOME 42: the overlay opens with a sensible centred
|
||||
// selection already in place (so the shutter is immediately useful), dragging
|
||||
// inside moves it, dragging a corner resizes from the opposite corner, and
|
||||
// dragging anywhere else starts a fresh band.
|
||||
//
|
||||
// A single MouseArea owns all of it. Nested MouseAreas for the handles would
|
||||
// fight the move/create area for the press, and the hit priorities are much
|
||||
// easier to reason about in one place.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Anything smaller than this on release is treated as a stray click and the
|
||||
// previous selection is restored.
|
||||
readonly property real minSize: 12
|
||||
// Pointer distance from a corner that counts as grabbing its handle.
|
||||
readonly property real handleGrab: 24
|
||||
readonly property real handleSize: 14
|
||||
|
||||
property real selX: 0
|
||||
property real selY: 0
|
||||
property real selW: 0
|
||||
property real selH: 0
|
||||
|
||||
readonly property bool hasSelection: root.selW >= root.minSize && root.selH >= root.minSize
|
||||
readonly property rect selection: Qt.rect(root.selX, root.selY, root.selW, root.selH)
|
||||
|
||||
// Double-clicking inside the band captures, like confirming a crop.
|
||||
signal committed
|
||||
|
||||
// "none" | "new" | "move" | "nw" | "ne" | "sw" | "se"
|
||||
property string _grab: "none"
|
||||
property real _anchorX: 0
|
||||
property real _anchorY: 0
|
||||
property real _pressX: 0
|
||||
property real _pressY: 0
|
||||
property real _origX: 0
|
||||
property real _origY: 0
|
||||
property rect _prev: Qt.rect(0, 0, 0, 0)
|
||||
|
||||
onWidthChanged: root._resetIfEmpty()
|
||||
onHeightChanged: root._resetIfEmpty()
|
||||
Component.onCompleted: root._resetIfEmpty()
|
||||
|
||||
function _resetIfEmpty(): void {
|
||||
if (root.selW === 0 && root.selH === 0)
|
||||
root.resetSelection();
|
||||
}
|
||||
|
||||
// Centred, roughly the middle half of the screen.
|
||||
function resetSelection(): void {
|
||||
if (root.width <= 0 || root.height <= 0)
|
||||
return;
|
||||
root.selW = Math.round(root.width * 0.55);
|
||||
root.selH = Math.round(root.height * 0.55);
|
||||
root.selX = Math.round((root.width - root.selW) / 2);
|
||||
root.selY = Math.round((root.height - root.selH) / 2);
|
||||
}
|
||||
|
||||
function _clampX(v: real): real {
|
||||
return Math.max(0, Math.min(root.width, v));
|
||||
}
|
||||
function _clampY(v: real): real {
|
||||
return Math.max(0, Math.min(root.height, v));
|
||||
}
|
||||
|
||||
function _near(a: real, b: real): bool {
|
||||
return Math.abs(a - b) <= root.handleGrab;
|
||||
}
|
||||
|
||||
function _hitTest(px: real, py: real): string {
|
||||
const x2 = root.selX + root.selW;
|
||||
const y2 = root.selY + root.selH;
|
||||
if (root.hasSelection) {
|
||||
if (root._near(px, root.selX) && root._near(py, root.selY))
|
||||
return "nw";
|
||||
if (root._near(px, x2) && root._near(py, root.selY))
|
||||
return "ne";
|
||||
if (root._near(px, root.selX) && root._near(py, y2))
|
||||
return "sw";
|
||||
if (root._near(px, x2) && root._near(py, y2))
|
||||
return "se";
|
||||
if (px > root.selX && px < x2 && py > root.selY && py < y2)
|
||||
return "move";
|
||||
}
|
||||
return "new";
|
||||
}
|
||||
|
||||
function _cursorFor(m: string): int {
|
||||
switch (m) {
|
||||
case "nw":
|
||||
case "se":
|
||||
return Qt.SizeFDiagCursor;
|
||||
case "ne":
|
||||
case "sw":
|
||||
return Qt.SizeBDiagCursor;
|
||||
case "move":
|
||||
return Qt.SizeAllCursor;
|
||||
default:
|
||||
return Qt.CrossCursor;
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the band from a fixed anchor corner and the live pointer.
|
||||
function _dragTo(px: real, py: real): void {
|
||||
const cx = root._clampX(px);
|
||||
const cy = root._clampY(py);
|
||||
root.selX = Math.min(root._anchorX, cx);
|
||||
root.selY = Math.min(root._anchorY, cy);
|
||||
root.selW = Math.abs(cx - root._anchorX);
|
||||
root.selH = Math.abs(cy - root._anchorY);
|
||||
}
|
||||
|
||||
Spotlight {
|
||||
anchors.fill: parent
|
||||
holeVisible: root.hasSelection
|
||||
hole: root.selection
|
||||
}
|
||||
|
||||
Frame {
|
||||
x: root.selX
|
||||
y: root.selY
|
||||
width: root.selW
|
||||
height: root.selH
|
||||
visible: root.hasSelection
|
||||
strokeWidth: 2
|
||||
strokeColor: Theme.accent
|
||||
}
|
||||
|
||||
// ── Corner handles ──────────────────────────────────────────────────────
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
gx: 0,
|
||||
gy: 0
|
||||
},
|
||||
{
|
||||
gx: 1,
|
||||
gy: 0
|
||||
},
|
||||
{
|
||||
gx: 0,
|
||||
gy: 1
|
||||
},
|
||||
{
|
||||
gx: 1,
|
||||
gy: 1
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
|
||||
visible: root.hasSelection
|
||||
width: root.handleSize
|
||||
height: root.handleSize
|
||||
radius: width / 2
|
||||
border.width: 0
|
||||
color: Theme.accent
|
||||
x: root.selX + modelData.gx * root.selW - width / 2
|
||||
y: root.selY + modelData.gy * root.selH - height / 2
|
||||
|
||||
// A dark core keeps the handle readable on top of a light photo.
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 6
|
||||
height: width
|
||||
radius: width / 2
|
||||
border.width: 0
|
||||
color: Theme.bgDark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Live size readout ───────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
id: readout
|
||||
visible: root.hasSelection
|
||||
implicitWidth: readoutText.implicitWidth + 24
|
||||
implicitHeight: 34
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
||||
|
||||
x: Math.max(0, Math.min(root.width - width, root.selX + (root.selW - width) / 2))
|
||||
// Inside the band when there is room, otherwise tucked just above it.
|
||||
y: root.selH > 110 ? root.selY + (root.selH - height) / 2 : Math.max(0, root.selY - height - 10)
|
||||
|
||||
Text {
|
||||
id: readoutText
|
||||
anchors.centerIn: parent
|
||||
text: Math.round(root.selW) + " × " + Math.round(root.selH)
|
||||
color: Theme.fg
|
||||
// Tabular figures so the readout doesn't twitch while dragging.
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: area
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root._cursorFor(root._grab !== "none" ? root._grab : root._hitTest(area.mouseX, area.mouseY))
|
||||
|
||||
onPressed: event => {
|
||||
root._prev = root.selection;
|
||||
root._grab = root._hitTest(event.x, event.y);
|
||||
root._pressX = event.x;
|
||||
root._pressY = event.y;
|
||||
root._origX = root.selX;
|
||||
root._origY = root.selY;
|
||||
|
||||
const x2 = root.selX + root.selW;
|
||||
const y2 = root.selY + root.selH;
|
||||
switch (root._grab) {
|
||||
case "nw":
|
||||
root._anchorX = x2;
|
||||
root._anchorY = y2;
|
||||
break;
|
||||
case "ne":
|
||||
root._anchorX = root.selX;
|
||||
root._anchorY = y2;
|
||||
break;
|
||||
case "sw":
|
||||
root._anchorX = x2;
|
||||
root._anchorY = root.selY;
|
||||
break;
|
||||
case "se":
|
||||
root._anchorX = root.selX;
|
||||
root._anchorY = root.selY;
|
||||
break;
|
||||
case "new":
|
||||
root._anchorX = root._clampX(event.x);
|
||||
root._anchorY = root._clampY(event.y);
|
||||
root.selX = root._anchorX;
|
||||
root.selY = root._anchorY;
|
||||
root.selW = 0;
|
||||
root.selH = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: event => {
|
||||
if (root._grab === "none")
|
||||
return;
|
||||
if (root._grab === "move") {
|
||||
root.selX = Math.max(0, Math.min(root.width - root.selW, root._origX + (event.x - root._pressX)));
|
||||
root.selY = Math.max(0, Math.min(root.height - root.selH, root._origY + (event.y - root._pressY)));
|
||||
} else {
|
||||
root._dragTo(event.x, event.y);
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
// A plain click outside the band would otherwise wipe the selection
|
||||
// down to nothing; treat it as "no change" instead.
|
||||
if (root._grab !== "move" && !root.hasSelection) {
|
||||
root.selX = root._prev.x;
|
||||
root.selY = root._prev.y;
|
||||
root.selW = root._prev.width;
|
||||
root.selH = root._prev.height;
|
||||
}
|
||||
root._grab = "none";
|
||||
}
|
||||
|
||||
onDoubleClicked: event => {
|
||||
if (root.hasSelection && event.x > root.selX && event.x < root.selX + root.selW && event.y > root.selY && event.y < root.selY + root.selH)
|
||||
root.committed();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user