mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-29 14:37:47 -04:00
- Hyprland & Mango users will benefit most while niri gains solid QOL updates - New icon picker in dot settings - Dragging, flinging and opening the dot now run on one static surface so they stay smooth on every compositor Port 2.0
315 lines
12 KiB
QML
315 lines
12 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import qs.Common
|
|
import qs.Services
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
required property string barId
|
|
|
|
readonly property bool free: true
|
|
readonly property bool dot: body.dotMode
|
|
readonly property var barConfig: SettingsData.getBarConfig(root.barId)
|
|
readonly property var islandController: body.islandController
|
|
readonly property int launcherResultCount: body.launcherResultCount
|
|
readonly property int hostOriginX: body.hostOriginX
|
|
readonly property int hostOriginY: body.hostOriginY
|
|
readonly property real dotSize: body.dotSize
|
|
readonly property real compactWidth: body.islandController.compactTarget.width
|
|
readonly property real compactHeight: body.islandController.compactTarget.height
|
|
readonly property real idleScale: Math.max(0.2, Math.min(1, body.setting("islandFreeIdleScale")))
|
|
readonly property real idleOpacity: Math.max(0.05, Math.min(1, body.setting("islandFreeIdleOpacity")))
|
|
readonly property int idleDelay: root.dot ? body.setting("islandFreeIdleDelay") : 0
|
|
readonly property real screenWidth: root.screen?.width ?? 1920
|
|
readonly property real screenHeight: root.screen?.height ?? 1080
|
|
readonly property string positionKey: IslandHostRegistry.key(root.screen?.name, root.barId)
|
|
readonly property var storedAnchor: SessionData.islandFreePositions[root.positionKey] ?? null
|
|
readonly property point storedPoint: Qt.point(root.clampAnchorX((typeof root.storedAnchor?.x === "number" ? root.storedAnchor.x : 0.5) * root.screenWidth), root.clampAnchorY((typeof root.storedAnchor?.y === "number" ? root.storedAnchor.y : 0.5) * root.screenHeight))
|
|
readonly property real anchorX: root.anchor.x
|
|
readonly property real anchorY: root.anchor.y
|
|
// Sized from the visual's size alone, so a fractional spring position never resizes the surface.
|
|
readonly property int surfaceX: Math.floor(body.currentVisualX - root.hitPadding - body.shadowPadding)
|
|
readonly property int surfaceY: Math.floor(body.currentVisualY - root.hitPadding - body.shadowPadding)
|
|
readonly property int surfaceWidth: Math.ceil(body.currentVisualWidth + 2 * (root.hitPadding + body.shadowPadding)) + 1
|
|
readonly property int surfaceHeight: Math.ceil(body.currentVisualHeight + 2 * (root.hitPadding + body.shadowPadding)) + 1
|
|
|
|
// Gestures and morphs run on a surface lifted to the whole output: per-frame margin commits skew pointer coordinates and relayout Hyprland.
|
|
property bool lifting: false
|
|
property bool liftForced: false
|
|
property point liftOrigin
|
|
// True from the unlift until the tight configure lands: a re-grab in that window still reads the output size.
|
|
property bool sizeStale: false
|
|
readonly property bool outputSized: root.width >= root.screenWidth - 2 && root.height >= root.screenHeight - 2
|
|
// Negative far margins grow the surface in place; the origin flips only once the buffer spans the output.
|
|
readonly property bool lifted: root.lifting && (root.liftForced || (!root.sizeStale && root.outputSized))
|
|
readonly property int frameLeft: root.lifted ? 0 : root.lifting ? root.liftOrigin.x : root.surfaceX
|
|
readonly property int frameTop: root.lifted ? 0 : root.lifting ? root.liftOrigin.y : root.surfaceY
|
|
readonly property int frameRight: root.lifting ? -root.frameLeft : Math.round(root.screenWidth) - root.surfaceX - root.surfaceWidth
|
|
readonly property int frameBottom: root.lifting ? -root.frameTop : Math.round(root.screenHeight) - root.surfaceY - root.surfaceHeight
|
|
|
|
readonly property bool expanded: body.expanded
|
|
readonly property bool pointerInside: body.islandController.pointerInside
|
|
readonly property string activeActivity: body.islandController.activeActivity
|
|
readonly property bool motionRunning: body.motionRunning
|
|
// A shrunk nub keeps the full dot's hit area and surface, so idling and waking never resize the window.
|
|
readonly property real hitPadding: root.dot ? Math.max(0, (root.dotSize - body.currentVisualWidth) / 2) : 0
|
|
// Blur regions are not antialiased and a small circle shows the jagged rim, so blur waits for the morph.
|
|
readonly property bool blurWanted: !root.dot || Math.min(body.currentVisualWidth, body.currentVisualHeight) > root.dotSize * 1.5
|
|
|
|
property point anchor
|
|
property bool dragging: false
|
|
property bool flinging: false
|
|
// sheetOut spans the expand through the collapse spring, so the whole morph draws into one static buffer.
|
|
readonly property bool liftWanted: root.dragging || root.flinging || body.sheetOut
|
|
property bool idle: false
|
|
|
|
function anchorLimit(size, extent) {
|
|
const half = size / 2;
|
|
const low = body.freeMargin + half;
|
|
const high = extent - body.freeMargin - half;
|
|
return high <= low ? [extent / 2, extent / 2] : [low, high];
|
|
}
|
|
|
|
function clampAnchorX(value) {
|
|
const range = root.anchorLimit(root.compactWidth, root.screenWidth);
|
|
return Math.max(range[0], Math.min(value, range[1]));
|
|
}
|
|
|
|
function clampAnchorY(value) {
|
|
const range = root.anchorLimit(root.compactHeight, root.screenHeight);
|
|
return Math.max(range[0], Math.min(value, range[1]));
|
|
}
|
|
|
|
function storeAnchor() {
|
|
SessionData.setIslandFreePosition(root.positionKey, root.anchor.x / root.screenWidth, root.anchor.y / root.screenHeight);
|
|
}
|
|
|
|
function moveTo(x, y) {
|
|
root.anchor = Qt.point(root.clampAnchorX(x), root.clampAnchorY(y));
|
|
root.storeAnchor();
|
|
root.wake();
|
|
}
|
|
|
|
function beginLift() {
|
|
if (root.lifting)
|
|
return;
|
|
root.liftOrigin = Qt.point(root.frameLeft, root.frameTop);
|
|
root.liftForced = false;
|
|
root.lifting = true;
|
|
liftTimeout.restart();
|
|
}
|
|
|
|
function wake() {
|
|
root.idle = false;
|
|
if (root.idleDelay > 0)
|
|
idleTimer.restart();
|
|
else
|
|
idleTimer.stop();
|
|
}
|
|
|
|
function requestKeyboardFocus() {
|
|
body.requestKeyboardFocus();
|
|
}
|
|
|
|
function containsGlobalPoint(gx, gy, padding) {
|
|
return body.containsGlobalPoint(gx, gy, padding);
|
|
}
|
|
|
|
onStoredPointChanged: {
|
|
if (!root.liftWanted)
|
|
root.anchor = root.storedPoint;
|
|
}
|
|
// Stored once the fling settles; a save mid-gesture stalls the fling's first frame.
|
|
onLiftWantedChanged: {
|
|
if (root.liftWanted) {
|
|
root.beginLift();
|
|
return;
|
|
}
|
|
root.sizeStale = root.outputSized;
|
|
root.lifting = false;
|
|
liftTimeout.stop();
|
|
root.storeAnchor();
|
|
}
|
|
onOutputSizedChanged: {
|
|
if (!root.outputSized)
|
|
root.sizeStale = false;
|
|
}
|
|
onLiftedChanged: {
|
|
if (root.lifted)
|
|
liftTimeout.stop();
|
|
}
|
|
onMotionRunningChanged: {
|
|
if (!root.motionRunning)
|
|
root.flinging = false;
|
|
}
|
|
onPointerInsideChanged: root.wake()
|
|
onExpandedChanged: root.wake()
|
|
onActiveActivityChanged: root.wake()
|
|
onIdleDelayChanged: root.wake()
|
|
|
|
color: "transparent"
|
|
exclusiveZone: -1
|
|
WlrLayershell.namespace: "dms:dankisland"
|
|
WlrLayershell.layer: LayerShell.fromEnv("DMS_DANKISLAND_LAYER", body.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top)
|
|
WlrLayershell.keyboardFocus: islandChrome.keyboardFocusPolicy
|
|
BackgroundEffect.blurRegion: root.blurWanted && BlurService.enabled && BlurService.available && body.surfaceOpacity > 0 && body.surfaceOpacity < 1 ? surfaceBlurRegion : null
|
|
|
|
Region {
|
|
id: surfaceBlurRegion
|
|
|
|
readonly property real left: body.x + body.currentVisualX
|
|
readonly property real top: body.y + body.currentVisualY
|
|
|
|
x: Math.ceil(left) + 1
|
|
y: Math.ceil(top) + 1
|
|
width: Math.max(0, Math.floor(left + body.currentVisualWidth) - 1 - x)
|
|
height: Math.max(0, Math.floor(top + body.currentVisualHeight) - 1 - y)
|
|
radius: body.currentSurfaceRadius > 0 ? Math.ceil(body.currentSurfaceRadius) + 1 : 0
|
|
}
|
|
|
|
// All four edges are anchored so the margins alone place and size the surface, letting a lift grow it past the output.
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
WlrLayershell.margins {
|
|
left: root.frameLeft
|
|
top: root.frameTop
|
|
right: root.frameRight
|
|
bottom: root.frameBottom
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
root.wake();
|
|
KeyboardFocus.registerBarWindow(root);
|
|
}
|
|
Component.onDestruction: KeyboardFocus.unregisterBarWindow(root)
|
|
|
|
IslandHostChrome {
|
|
id: islandChrome
|
|
|
|
window: root
|
|
extraWindows: [dismissWindow]
|
|
host: body
|
|
}
|
|
|
|
Timer {
|
|
id: idleTimer
|
|
|
|
interval: root.idleDelay
|
|
onTriggered: {
|
|
if (root.expanded || root.pointerInside || hitHover.hovered || root.liftWanted || body.islandController.transientActive)
|
|
return;
|
|
root.idle = true;
|
|
}
|
|
}
|
|
|
|
// A compositor that never sizes the lifted surface would leave every drag frozen.
|
|
Timer {
|
|
id: liftTimeout
|
|
|
|
interval: 250
|
|
onTriggered: root.liftForced = true
|
|
}
|
|
|
|
// A quick stroke outruns the dot's rectangle between frames and the pointer leave ends the drag, so a drag takes input everywhere.
|
|
mask: root.dragging && root.lifted ? null : hitRegion
|
|
Region {
|
|
id: hitRegion
|
|
|
|
item: body.inputSuspended ? null : hitTarget
|
|
}
|
|
Item {
|
|
id: hitTarget
|
|
|
|
x: body.currentVisualX - root.hitPadding - root.frameLeft
|
|
y: body.currentVisualY - root.hitPadding - root.frameTop
|
|
width: body.currentVisualWidth + 2 * root.hitPadding
|
|
height: body.currentVisualHeight + 2 * root.hitPadding
|
|
|
|
// The idle ring around a shrunk nub takes clicks and drags, so hovering it wakes the dot too.
|
|
HoverHandler {
|
|
id: hitHover
|
|
|
|
onHoveredChanged: root.wake()
|
|
}
|
|
}
|
|
|
|
// Catches outside clicks; the lifted surface's own mask stays the island rectangle.
|
|
PanelWindow {
|
|
id: dismissWindow
|
|
screen: root.screen
|
|
visible: root.expanded && !body.inputSuspended
|
|
color: "transparent"
|
|
exclusiveZone: -1
|
|
WlrLayershell.namespace: "dms:dankisland-dismiss"
|
|
WlrLayershell.layer: root.WlrLayershell.layer
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
// The frozen spring envelope, so this full-output surface is not re-committed every frame of a morph.
|
|
mask: Region {
|
|
width: dismissWindow.width
|
|
height: dismissWindow.height
|
|
Region {
|
|
readonly property Item envelope: body.inputMaskItem
|
|
|
|
x: dismissWindow.visible ? envelope.x : 0
|
|
y: dismissWindow.visible ? envelope.y : 0
|
|
width: dismissWindow.visible ? envelope.width : 0
|
|
height: dismissWindow.visible ? envelope.height : 0
|
|
intersection: Intersection.Subtract
|
|
}
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: body.islandController.requestCollapse()
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: !root.expanded && !body.inputSuspended
|
|
onClicked: {
|
|
SettingsData.recordBarInteraction(root.screen, root.barId);
|
|
body.islandController.requestToggle(true);
|
|
}
|
|
}
|
|
|
|
IslandBarHost {
|
|
id: body
|
|
|
|
x: -root.frameLeft
|
|
y: -root.frameTop
|
|
width: root.screenWidth
|
|
height: root.screenHeight
|
|
originOffsetX: x
|
|
originOffsetY: y
|
|
barConfig: root.barConfig
|
|
screen: root.screen
|
|
hostWindow: root
|
|
hostLifted: root.lifted
|
|
barId: root.barId
|
|
anchor: root.anchor
|
|
anchorSnaps: root.dragging
|
|
freeScale: root.idle ? root.idleScale : 1
|
|
freeOpacity: root.idle ? root.idleOpacity : 1
|
|
}
|
|
|
|
IslandFreeDragArea {
|
|
hostWindow: root
|
|
surface: body.surface
|
|
enabled: !root.expanded && !body.inputSuspended
|
|
}
|
|
}
|