mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-06 13:38:28 -04:00
11287459c3
Popouts sized from their content sometimes open too short and clip what
they render, then come back correct on the next open.
open() snapshots the surface geometry before setting shouldBeVisible,
and the onAligned{X,Y,Width,Height}Changed handlers are all gated on
that flag. Mapping contentWindow happens in between, and that's what
finally runs the layout Qt deferred while the window was hidden.
Content-sized popouts pick up their real height in that gap, so the
correction is dropped and the surface stays short. Re-snapshot once the
flag is set.
974 lines
41 KiB
QML
974 lines
41 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
readonly property var log: Log.scoped("DankPopoutStandalone")
|
|
|
|
property var popoutHandle: root
|
|
property string layerNamespace: "dms:popout"
|
|
property alias content: contentLoader.sourceComponent
|
|
property alias contentLoader: contentLoader
|
|
property Component overlayContent: null
|
|
property alias overlayLoader: overlayLoader
|
|
readonly property alias backgroundWindow: backgroundWindow
|
|
readonly property alias contentWindow: contentWindow
|
|
property real popupWidth: 400
|
|
property real popupHeight: 300
|
|
property real triggerX: 0
|
|
property real triggerY: 0
|
|
property real triggerWidth: 40
|
|
property string triggerSection: ""
|
|
property string positioning: "center"
|
|
property int animationDuration: Theme.popoutAnimationDuration
|
|
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
|
property real animationOffset: Theme.effectAnimOffset
|
|
property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
|
|
property list<real> animationExitCurve: Theme.variantPopoutExitCurve
|
|
property bool suspendShadowWhileResizing: false
|
|
property bool shouldBeVisible: false
|
|
property bool isClosing: false
|
|
property bool animationsEnabled: true
|
|
property bool hoverDismissEnabled: false
|
|
property bool hoverDismissSuspended: false
|
|
|
|
function cancelHoverDismiss() {
|
|
hoverDismissController.cancelPending();
|
|
}
|
|
|
|
function closeFromHoverDismiss() {
|
|
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
|
|
return;
|
|
if (popoutHandle?.closeFromHoverDismiss)
|
|
popoutHandle.closeFromHoverDismiss();
|
|
else
|
|
close();
|
|
}
|
|
|
|
property var customKeyboardFocus: null
|
|
property bool backgroundInteractive: true
|
|
property bool contentHandlesKeys: false
|
|
property bool fullHeightSurface: false
|
|
property bool _primeContent: false
|
|
property bool _contentWarm: false
|
|
property bool _resizeActive: false
|
|
property real _surfaceMarginLeft: 0
|
|
property real _surfaceMarginTop: 0
|
|
property real _surfaceW: 0
|
|
property real _surfaceH: 0
|
|
property real _surfaceBodyX: 0
|
|
property real _surfaceBodyY: 0
|
|
property real _surfaceBodyW: 0
|
|
property real _surfaceBodyH: 0
|
|
|
|
property real storedBarThickness: Theme.barHeight - 4
|
|
property real storedBarSpacing: 4
|
|
property var storedBarConfig: null
|
|
property bool triggerUsesOverlayLayer: false
|
|
property var adjacentBarInfo: ({
|
|
"topBar": 0,
|
|
"bottomBar": 0,
|
|
"leftBar": 0,
|
|
"rightBar": 0
|
|
})
|
|
property var screen: null
|
|
// Output the surface binds to; retargeted only in open() while hidden. A live
|
|
// screen change on a mapped surface loses the popup blur on niri.
|
|
property var surfaceScreen: null
|
|
readonly property bool frameGapStandaloneActive: CompositorService.frameConfiguredForScreen(screen) && !CompositorService.usesConnectedFrameChromeForScreen(screen)
|
|
readonly property bool fluidStandaloneActive: Theme.isDirectionalEffect
|
|
readonly property bool backgroundDismissWindowRequired: backgroundInteractive
|
|
readonly property bool backgroundWindowRequired: backgroundDismissWindowRequired || root.overlayContent !== null
|
|
readonly property bool _fullHeight: fullHeightSurface
|
|
readonly property var effectivePopoutLayer: LayerShell.fromEnv("DMS_POPOUT_LAYER", root.triggerUsesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
|
|
"allow": ["top", "overlay"],
|
|
"invalidLayer": WlrLayer.Top,
|
|
"label": "popouts"
|
|
})
|
|
|
|
function _frameGapMargin(side) {
|
|
return SettingsData.frameEdgeInsetForSide(screen, side) + Theme.popupDistance;
|
|
}
|
|
|
|
function _edgeClearance(side, popupGap, adjacentInset) {
|
|
if (frameGapStandaloneActive)
|
|
return Math.max(adjacentInset, _frameGapMargin(side));
|
|
return adjacentInset > 0 ? adjacentInset : popupGap;
|
|
}
|
|
|
|
readonly property real effectiveBarThickness: {
|
|
const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
|
|
return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
|
|
}
|
|
|
|
readonly property var barBounds: {
|
|
if (!screen)
|
|
return {
|
|
"x": 0,
|
|
"y": 0,
|
|
"width": 0,
|
|
"height": 0,
|
|
"wingSize": 0
|
|
};
|
|
return SettingsData.getBarBounds(screen, effectiveBarThickness, effectiveBarPosition, storedBarConfig);
|
|
}
|
|
|
|
readonly property real barX: barBounds.x
|
|
readonly property real barY: barBounds.y
|
|
readonly property real barWidth: barBounds.width
|
|
readonly property real barHeight: barBounds.height
|
|
readonly property real barWingSize: barBounds.wingSize
|
|
|
|
signal opened
|
|
signal popoutClosed
|
|
signal backgroundClicked
|
|
|
|
property int effectiveBarPosition: 0
|
|
property real effectiveBarBottomGap: 0
|
|
readonly property string autoBarShadowDirection: {
|
|
const section = triggerSection || "center";
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Top:
|
|
if (section === "left")
|
|
return "topLeft";
|
|
if (section === "right")
|
|
return "topRight";
|
|
return "top";
|
|
case SettingsData.Position.Bottom:
|
|
if (section === "left")
|
|
return "bottomLeft";
|
|
if (section === "right")
|
|
return "bottomRight";
|
|
return "bottom";
|
|
case SettingsData.Position.Left:
|
|
if (section === "left")
|
|
return "topLeft";
|
|
if (section === "right")
|
|
return "bottomLeft";
|
|
return "left";
|
|
case SettingsData.Position.Right:
|
|
if (section === "left")
|
|
return "topRight";
|
|
if (section === "right")
|
|
return "bottomRight";
|
|
return "right";
|
|
default:
|
|
return "top";
|
|
}
|
|
}
|
|
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
|
|
|
// Snapshot mask geometry to prevent background damage on bar updates
|
|
property real _frozenMaskX: 0
|
|
property real _frozenMaskY: 0
|
|
property real _frozenMaskWidth: 0
|
|
property real _frozenMaskHeight: 0
|
|
|
|
function setBarContext(position, bottomGap) {
|
|
effectiveBarPosition = position !== undefined ? position : 0;
|
|
effectiveBarBottomGap = bottomGap !== undefined ? bottomGap : 0;
|
|
}
|
|
|
|
function primeContent() {
|
|
_primeContent = true;
|
|
}
|
|
|
|
function clearPrimedContent() {
|
|
_primeContent = false;
|
|
}
|
|
|
|
function setTriggerPosition(x, y, width, section, targetScreen, barPosition, barThickness, barSpacing, barConfig) {
|
|
triggerX = x;
|
|
triggerY = y;
|
|
triggerWidth = width;
|
|
triggerSection = section;
|
|
screen = targetScreen;
|
|
|
|
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
|
|
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
|
|
storedBarConfig = barConfig;
|
|
|
|
const pos = barPosition !== undefined ? barPosition : 0;
|
|
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
|
|
|
|
adjacentBarInfo = SettingsData.getAdjacentBarInfo(targetScreen, pos, barConfig);
|
|
setBarContext(pos, bottomGap);
|
|
}
|
|
|
|
// Holds backgroundWindow.updatesEnabled true while the surface body is
|
|
// changing so the contentHoleRect mask carve-out tracks the popup body —
|
|
// otherwise clicks in newly-grown areas hit the bg window and dismiss.
|
|
// Debounced off ~250ms after the last change so a stable popup doesn't
|
|
// keep the bg window in active-update mode.
|
|
property bool _bgCommitWindow: false
|
|
|
|
// True only while the overlay is actually on screen, not merely declared.
|
|
readonly property bool _overlayActive: overlayLoader.item?.visible ?? false
|
|
|
|
Timer {
|
|
id: bgCommitSettleTimer
|
|
interval: 250
|
|
onTriggered: root._bgCommitWindow = false
|
|
}
|
|
|
|
// An idle layer surface won't commit the cleared blur region on auto-close, so the
|
|
// blur sticks; pulse updatesEnabled false->true to force a re-commit.
|
|
property bool _blurCommitSuppress: false
|
|
|
|
Timer {
|
|
id: blurCommitPulseTimer
|
|
interval: 16
|
|
onTriggered: root._blurCommitSuppress = false
|
|
}
|
|
|
|
function _pulseBlurCommit() {
|
|
if (!backgroundWindow.visible)
|
|
return;
|
|
_blurCommitSuppress = true;
|
|
blurCommitPulseTimer.restart();
|
|
// Land the off->on toggle on a commit even after _overlayActive has dropped.
|
|
_bgCommitWindow = true;
|
|
bgCommitSettleTimer.restart();
|
|
}
|
|
|
|
Connections {
|
|
target: overlayLoader.item
|
|
ignoreUnknownSignals: true
|
|
function onOverlayBlurActiveChanged() {
|
|
root._pulseBlurCommit();
|
|
}
|
|
}
|
|
|
|
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
|
const newX = Theme.snap(bodyX, dpr);
|
|
const newY = Theme.snap(bodyY, dpr);
|
|
const newW = Theme.snap(bodyW, dpr);
|
|
const newH = Theme.snap(bodyH, dpr);
|
|
const changed = newX !== _surfaceBodyX || newY !== _surfaceBodyY || newW !== _surfaceBodyW || newH !== _surfaceBodyH;
|
|
_surfaceBodyX = newX;
|
|
_surfaceBodyY = newY;
|
|
_surfaceBodyW = newW;
|
|
_surfaceBodyH = newH;
|
|
_surfaceMarginLeft = _surfaceBodyX - shadowBuffer;
|
|
_surfaceMarginTop = _surfaceBodyY - shadowBuffer;
|
|
_surfaceW = _surfaceBodyW + shadowBuffer * 2;
|
|
_surfaceH = _surfaceBodyH + shadowBuffer * 2;
|
|
if (changed && backgroundWindow.visible) {
|
|
_bgCommitWindow = true;
|
|
bgCommitSettleTimer.restart();
|
|
}
|
|
}
|
|
|
|
// Forces contentWindow to render a frame so Quickshell ships the updated
|
|
// WindowBlur region to the compositor. WindowBlur's property updates
|
|
// don't dirty the QML scene graph by themselves, so when the popup grows,
|
|
// shrinks, or closes without an animation running, the blur state can
|
|
// get stuck at its previous size. Called from the existing
|
|
// onAligned*Changed / onShouldBeVisibleChanged handlers.
|
|
function _kickBlurCommit() {
|
|
if (typeof contentWindow.update === "function")
|
|
contentWindow.update();
|
|
}
|
|
|
|
function _setSettledSurfaceGeometry() {
|
|
if (shouldBeVisible) {
|
|
_setSurfaceGeometry(alignedX, alignedY, alignedWidth, alignedHeight);
|
|
}
|
|
}
|
|
|
|
function _setAnimatedSurfaceEnvelope() {
|
|
if (!shouldBeVisible)
|
|
return;
|
|
if (_fullHeight) {
|
|
_setSettledSurfaceGeometry();
|
|
return;
|
|
}
|
|
|
|
const currentY = renderedAlignedY;
|
|
const currentBottom = renderedAlignedY + renderedAlignedHeight;
|
|
const targetY = alignedY;
|
|
const targetBottom = alignedY + alignedHeight;
|
|
const existingY = _surfaceBodyH > 0 ? _surfaceBodyY : currentY;
|
|
const existingBottom = _surfaceBodyH > 0 ? _surfaceBodyY + _surfaceBodyH : currentBottom;
|
|
const envelopeY = Math.min(currentY, targetY, existingY);
|
|
const envelopeBottom = Math.max(currentBottom, targetBottom, existingBottom);
|
|
_setSurfaceGeometry(alignedX, envelopeY, alignedWidth, Math.max(0, envelopeBottom - envelopeY));
|
|
surfaceSettleTimer.restart();
|
|
}
|
|
|
|
function updateSurfacePosition() {
|
|
_setSettledSurfaceGeometry();
|
|
}
|
|
|
|
onAlignedXChanged: {
|
|
if (shouldBeVisible)
|
|
_setAnimatedSurfaceEnvelope();
|
|
_kickBlurCommit();
|
|
}
|
|
|
|
onAlignedYChanged: {
|
|
if (shouldBeVisible)
|
|
_setAnimatedSurfaceEnvelope();
|
|
_kickBlurCommit();
|
|
}
|
|
|
|
onAlignedWidthChanged: {
|
|
if (shouldBeVisible)
|
|
_setAnimatedSurfaceEnvelope();
|
|
_kickBlurCommit();
|
|
}
|
|
|
|
function open() {
|
|
if (!screen)
|
|
return;
|
|
closeTimer.stop();
|
|
isClosing = false;
|
|
animationsEnabled = false;
|
|
_primeContent = true;
|
|
_contentWarm = true;
|
|
|
|
_frozenMaskX = maskX;
|
|
_frozenMaskY = maskY;
|
|
_frozenMaskWidth = maskWidth;
|
|
_frozenMaskHeight = maskHeight;
|
|
|
|
const screenChanged = surfaceScreen !== null && surfaceScreen !== screen;
|
|
if (screenChanged) {
|
|
// Unmap before retargeting so the surface is destroyed and recreated
|
|
// on the new output rather than live-migrated.
|
|
contentWindow.visible = false;
|
|
backgroundWindow.visible = false;
|
|
}
|
|
surfaceScreen = screen;
|
|
|
|
if (contentContainer && !shouldBeVisible) {
|
|
// Snap morph closed only on a fresh open; on screen-change re-open we stay at 1
|
|
// because shouldBeVisible doesn't change and won't drive morph back to 1.
|
|
morph.openProgress = 0;
|
|
}
|
|
|
|
_setSurfaceGeometry(alignedX, alignedY, alignedWidth, alignedHeight);
|
|
if (screenChanged) {
|
|
// Defer the show one event-loop tick. Qt coalesces a synchronous
|
|
// false→true visibility flip into a no-op, leaving WindowBlur committed
|
|
// to the previous screen's wl_surface. Splitting the flip across ticks
|
|
// forces a real surface destroy+create so BackgroundEffect.surfaceCreated
|
|
// fires and the blur region republishes on the new surface.
|
|
Qt.callLater(() => {
|
|
if (!root.shouldBeVisible)
|
|
return;
|
|
if (root.backgroundWindowRequired)
|
|
backgroundWindow.visible = true;
|
|
contentWindow.visible = true;
|
|
popoutBlur.kick();
|
|
_bgCommitWindow = true;
|
|
bgCommitSettleTimer.restart();
|
|
});
|
|
} else {
|
|
if (backgroundWindowRequired)
|
|
backgroundWindow.visible = true;
|
|
contentWindow.visible = true;
|
|
}
|
|
|
|
animationsEnabled = true;
|
|
shouldBeVisible = true;
|
|
// Content-sized popouts lay out when contentWindow maps, while the geometry
|
|
// handlers are still gated off. Re-snapshot so the surface isn't left short.
|
|
_setSettledSurfaceGeometry();
|
|
if (screen) {
|
|
PopoutManager.showPopout(popoutHandle);
|
|
opened();
|
|
}
|
|
}
|
|
|
|
function close() {
|
|
isClosing = true;
|
|
shouldBeVisible = false;
|
|
_primeContent = false;
|
|
PopoutManager.popoutChanged();
|
|
closeTimer.restart();
|
|
}
|
|
|
|
function toggle() {
|
|
shouldBeVisible ? close() : open();
|
|
}
|
|
|
|
Connections {
|
|
target: Quickshell
|
|
function onScreensChanged() {
|
|
if (!shouldBeVisible || !screen)
|
|
return;
|
|
const currentScreenName = screen.name;
|
|
let screenStillExists = false;
|
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
|
if (Quickshell.screens[i].name === currentScreenName) {
|
|
screenStillExists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!screenStillExists) {
|
|
close();
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: closeTimer
|
|
interval: Theme.variantCloseInterval(animationDuration)
|
|
onTriggered: {
|
|
if (!shouldBeVisible) {
|
|
contentWindow.visible = false;
|
|
backgroundWindow.visible = false;
|
|
isClosing = false;
|
|
PopoutManager.hidePopout(popoutHandle);
|
|
popoutClosed();
|
|
}
|
|
}
|
|
}
|
|
|
|
readonly property real screenWidth: screen ? screen.width : 0
|
|
readonly property real screenHeight: screen ? screen.height : 0
|
|
// devicePixelRatio rounds to integer under fractional scaling; use the real scale Qt renders at.
|
|
readonly property real dpr: screen ? (CompositorService.getScreenScale(screen) || screen.devicePixelRatio) : 1
|
|
|
|
readonly property var shadowLevel: Theme.elevationLevel3
|
|
readonly property real shadowFallbackOffset: 6
|
|
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
|
|
readonly property real shadowMotionPadding: fluidStandaloneActive ? 0 : Math.max(0, animationOffset)
|
|
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
|
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
|
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
|
property real renderedAlignedY: alignedY
|
|
property real renderedAlignedHeight: alignedHeight
|
|
readonly property bool renderedGeometryGrowing: alignedHeight >= renderedAlignedHeight
|
|
// Snap rendered geometry while the entrance morph runs so it doesn't ride a second animation.
|
|
readonly property bool _settlingToOpen: _fullHeight && shouldBeVisible && morphAnim.running
|
|
|
|
Behavior on renderedAlignedY {
|
|
enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible && !root._settlingToOpen
|
|
NumberAnimation {
|
|
duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing)
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
|
|
}
|
|
}
|
|
|
|
Behavior on renderedAlignedHeight {
|
|
enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible && !root._settlingToOpen
|
|
NumberAnimation {
|
|
duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing)
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
|
|
}
|
|
}
|
|
|
|
onAlignedHeightChanged: {
|
|
if (shouldBeVisible)
|
|
_setAnimatedSurfaceEnvelope();
|
|
_kickBlurCommit();
|
|
if (!suspendShadowWhileResizing || !shouldBeVisible)
|
|
return;
|
|
_resizeActive = true;
|
|
resizeSettleTimer.restart();
|
|
}
|
|
onShouldBeVisibleChanged: {
|
|
_kickBlurCommit();
|
|
if (!shouldBeVisible) {
|
|
_resizeActive = false;
|
|
resizeSettleTimer.stop();
|
|
}
|
|
}
|
|
onBackgroundWindowRequiredChanged: {
|
|
if (shouldBeVisible)
|
|
backgroundWindow.visible = backgroundWindowRequired;
|
|
}
|
|
|
|
Timer {
|
|
id: resizeSettleTimer
|
|
interval: 80
|
|
repeat: false
|
|
onTriggered: root._resizeActive = false
|
|
}
|
|
|
|
Timer {
|
|
id: surfaceSettleTimer
|
|
interval: Math.max(0, Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing) + 32)
|
|
repeat: false
|
|
onTriggered: root._setSettledSurfaceGeometry()
|
|
}
|
|
|
|
readonly property real alignedX: Theme.snap((() => {
|
|
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
|
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
|
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
|
const leftGap = _edgeClearance("left", popupGap, adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : 0);
|
|
const rightGap = _edgeClearance("right", popupGap, adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : 0);
|
|
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Left:
|
|
return Math.max(leftGap, Math.min(screenWidth - popupWidth - rightGap, triggerX));
|
|
case SettingsData.Position.Right:
|
|
return Math.max(leftGap, Math.min(screenWidth - popupWidth - rightGap, triggerX - popupWidth));
|
|
default:
|
|
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
|
|
const minX = leftGap;
|
|
const maxX = screenWidth - popupWidth - rightGap;
|
|
return Math.max(minX, Math.min(maxX, rawX));
|
|
}
|
|
})(), dpr)
|
|
|
|
readonly property real alignedY: Theme.snap((() => {
|
|
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
|
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
|
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
|
const topGap = _edgeClearance("top", popupGap, adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : 0);
|
|
const bottomGap = _edgeClearance("bottom", popupGap, adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : 0);
|
|
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Bottom:
|
|
return Math.max(topGap, Math.min(screenHeight - popupHeight - bottomGap, triggerY - popupHeight));
|
|
case SettingsData.Position.Top:
|
|
return Math.max(topGap, Math.min(screenHeight - popupHeight - bottomGap, triggerY));
|
|
default:
|
|
const rawY = triggerY - (popupHeight / 2);
|
|
const minY = topGap;
|
|
const maxY = screenHeight - popupHeight - bottomGap;
|
|
return Math.max(minY, Math.min(maxY, rawY));
|
|
}
|
|
})(), dpr)
|
|
|
|
readonly property real maskX: _dismissZone.x
|
|
readonly property real maskY: _dismissZone.y
|
|
readonly property real maskWidth: _dismissZone.width
|
|
readonly property real maskHeight: _dismissZone.height
|
|
|
|
DismissZone {
|
|
id: _dismissZone
|
|
barPosition: root.effectiveBarPosition
|
|
barX: root.barX
|
|
barY: root.barY
|
|
barWidth: root.barWidth
|
|
barHeight: root.barHeight
|
|
screenWidth: root.screenWidth
|
|
screenHeight: root.screenHeight
|
|
adjacentBarInfo: root.adjacentBarInfo
|
|
}
|
|
|
|
PanelWindow {
|
|
id: backgroundWindow
|
|
screen: root.surfaceScreen
|
|
visible: false
|
|
color: "transparent"
|
|
// Idle full-screen surface skips buffer updates, else it damages the whole screen
|
|
// every frame. Live while an overlay shows or _bgCommitWindow flushes a mask change.
|
|
updatesEnabled: !root._blurCommitSuppress && (root._overlayActive || root._bgCommitWindow)
|
|
|
|
WlrLayershell.namespace: root.layerNamespace + ":background"
|
|
WlrLayershell.layer: root.effectivePopoutLayer
|
|
WlrLayershell.exclusiveZone: -1
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
|
|
anchors {
|
|
top: true
|
|
left: true
|
|
right: true
|
|
bottom: true
|
|
}
|
|
|
|
mask: Region {
|
|
item: maskRect
|
|
Region {
|
|
item: contentHoleRect
|
|
intersection: Intersection.Subtract
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: maskRect
|
|
visible: false
|
|
color: "transparent"
|
|
x: root.backgroundDismissWindowRequired ? root._frozenMaskX : 0
|
|
y: root.backgroundDismissWindowRequired ? root._frozenMaskY : 0
|
|
width: (root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
|
|
height: (root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
|
|
}
|
|
|
|
Rectangle {
|
|
id: contentHoleRect
|
|
visible: false
|
|
color: "transparent"
|
|
x: root.backgroundDismissWindowRequired ? root._surfaceBodyX : 0
|
|
y: root.backgroundDismissWindowRequired ? root._surfaceBodyY : 0
|
|
width: (root.backgroundDismissWindowRequired && shouldBeVisible) ? root._surfaceBodyW : 0
|
|
height: (root.backgroundDismissWindowRequired && shouldBeVisible) ? root._surfaceBodyH : 0
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: false
|
|
enabled: root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: backgroundClicked()
|
|
}
|
|
|
|
Loader {
|
|
id: overlayLoader
|
|
anchors.fill: parent
|
|
active: root.overlayContent !== null && backgroundWindow.visible
|
|
sourceComponent: root.overlayContent
|
|
}
|
|
}
|
|
|
|
PanelWindow {
|
|
id: contentWindow
|
|
screen: root.surfaceScreen
|
|
visible: false
|
|
color: "transparent"
|
|
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
|
|
|
|
onVisibleChanged: {
|
|
if (visible || !Qt.inputMethod)
|
|
return;
|
|
Qt.inputMethod.hide();
|
|
Qt.inputMethod.reset();
|
|
}
|
|
|
|
PopoutHoverDismiss {
|
|
id: hoverDismissController
|
|
anchors.fill: parent
|
|
dismissEnabled: root.hoverDismissEnabled
|
|
dismissSuspended: root.hoverDismissSuspended
|
|
surfaceVisible: root.shouldBeVisible
|
|
globalOffsetX: root._surfaceMarginLeft
|
|
globalOffsetY: root._fullHeight ? 0 : root._surfaceMarginTop
|
|
onDismissRequested: root.closeFromHoverDismiss()
|
|
}
|
|
|
|
WindowBlur {
|
|
id: popoutBlur
|
|
targetWindow: contentWindow
|
|
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
|
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.08) * 1.6))
|
|
readonly property real visibleScale: s * op
|
|
readonly property bool revealClipActive: root.fluidStandaloneActive
|
|
|
|
// Blur tracks the surface's scaled rect, matching the connected backend
|
|
blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
|
blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
|
blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * visibleScale) : 0
|
|
blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * visibleScale) : 0
|
|
blurRadius: Theme.cornerRadius
|
|
clipEnabled: revealClipActive
|
|
clipX: contentContainer.x + contentContainer.revealX
|
|
clipY: contentContainer.y + contentContainer.revealY
|
|
clipWidth: root.shouldBeVisible ? contentContainer.revealWidth : 0
|
|
clipHeight: root.shouldBeVisible ? contentContainer.revealHeight : 0
|
|
}
|
|
|
|
WlrLayershell.namespace: root.layerNamespace
|
|
WlrLayershell.layer: root.effectivePopoutLayer
|
|
WlrLayershell.exclusiveZone: -1
|
|
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(shouldBeVisible, customKeyboardFocus)
|
|
|
|
anchors {
|
|
left: true
|
|
top: true
|
|
bottom: root._fullHeight
|
|
}
|
|
|
|
WlrLayershell.margins {
|
|
left: root._surfaceMarginLeft
|
|
top: root._fullHeight ? 0 : root._surfaceMarginTop
|
|
}
|
|
|
|
implicitWidth: root._surfaceW
|
|
implicitHeight: root._fullHeight ? 0 : root._surfaceH
|
|
|
|
mask: contentInputMask
|
|
|
|
Region {
|
|
id: contentInputMask
|
|
item: contentMaskRect
|
|
}
|
|
|
|
Item {
|
|
id: contentMaskRect
|
|
visible: false
|
|
x: contentContainer.x
|
|
y: contentContainer.y
|
|
width: contentWindow.closeVisualActive ? root.alignedWidth : 0
|
|
height: contentWindow.closeVisualActive ? root.renderedAlignedHeight : 0
|
|
}
|
|
|
|
Item {
|
|
id: contentContainer
|
|
x: shadowBuffer + root.alignedX - root._surfaceBodyX
|
|
y: root._fullHeight ? root.renderedAlignedY : shadowBuffer + root.renderedAlignedY - root._surfaceBodyY
|
|
width: root.alignedWidth
|
|
height: root.renderedAlignedHeight
|
|
|
|
readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
|
|
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
|
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
|
|
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
|
|
readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right"))
|
|
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
|
readonly property bool depthEffect: Theme.isDepthEffect
|
|
readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
|
|
readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
|
|
readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28)
|
|
readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0))
|
|
readonly property real offsetX: {
|
|
if (directionalEffect) {
|
|
if (barLeft)
|
|
return -directionalTravelX;
|
|
if (barRight)
|
|
return directionalTravelX;
|
|
if (barTop || barBottom)
|
|
return 0;
|
|
return sectionTilt * directionalTravelX * 0.2;
|
|
}
|
|
if (depthEffect) {
|
|
if (barLeft)
|
|
return -depthTravel;
|
|
if (barRight)
|
|
return depthTravel;
|
|
if (barTop || barBottom)
|
|
return 0;
|
|
return sectionTilt * depthTravel * 0.2;
|
|
}
|
|
return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0);
|
|
}
|
|
readonly property real offsetY: {
|
|
if (directionalEffect) {
|
|
if (barBottom)
|
|
return directionalTravelY;
|
|
if (barTop)
|
|
return -directionalTravelY;
|
|
if (barLeft || barRight)
|
|
return 0;
|
|
return directionalTravelY;
|
|
}
|
|
if (depthEffect) {
|
|
if (barBottom)
|
|
return depthTravel;
|
|
if (barTop)
|
|
return -depthTravel;
|
|
if (barLeft || barRight)
|
|
return 0;
|
|
return depthTravel;
|
|
}
|
|
return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0);
|
|
}
|
|
|
|
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
|
|
|
|
PopoutHoverBodyTracker {
|
|
controller: hoverDismissController
|
|
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
|
|
}
|
|
|
|
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
|
|
QtObject {
|
|
id: morph
|
|
property real openProgress: 0
|
|
onOpenProgressChanged: root._kickBlurCommit()
|
|
Behavior on openProgress {
|
|
enabled: root.animationsEnabled
|
|
NumberAnimation {
|
|
id: morphAnim
|
|
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
}
|
|
}
|
|
}
|
|
|
|
readonly property real animX: contentContainer.offsetX * (1 - morph.openProgress)
|
|
readonly property real animY: contentContainer.offsetY * (1 - morph.openProgress)
|
|
readonly property real scaleValue: contentContainer.computedScaleCollapsed + (1.0 - contentContainer.computedScaleCollapsed) * morph.openProgress
|
|
readonly property real clampedAnimX: Math.max(-width, Math.min(animX, width))
|
|
readonly property real clampedAnimY: Math.max(-height, Math.min(animY, height))
|
|
readonly property real revealWidth: {
|
|
if (!root.fluidStandaloneActive)
|
|
return width;
|
|
if (barLeft)
|
|
return Theme.snap(Math.max(0, width + clampedAnimX), root.dpr);
|
|
if (barRight)
|
|
return Theme.snap(Math.max(0, width - clampedAnimX), root.dpr);
|
|
return width;
|
|
}
|
|
readonly property real revealHeight: {
|
|
if (!root.fluidStandaloneActive)
|
|
return height;
|
|
if (barTop)
|
|
return Theme.snap(Math.max(0, height + clampedAnimY), root.dpr);
|
|
if (barBottom)
|
|
return Theme.snap(Math.max(0, height - clampedAnimY), root.dpr);
|
|
return height;
|
|
}
|
|
readonly property real revealX: root.fluidStandaloneActive && barRight ? Theme.snap(width - revealWidth, root.dpr) : 0
|
|
readonly property real revealY: root.fluidStandaloneActive && barBottom ? Theme.snap(height - revealHeight, root.dpr) : 0
|
|
|
|
Component.onCompleted: morph.openProgress = root.shouldBeVisible ? 1 : 0
|
|
|
|
Connections {
|
|
target: root
|
|
function onShouldBeVisibleChanged() {
|
|
morph.openProgress = root.shouldBeVisible ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: directionalClipMask
|
|
|
|
readonly property bool shouldClip: root.fluidStandaloneActive
|
|
|
|
clip: shouldClip
|
|
x: shouldClip ? contentContainer.revealX : 0
|
|
y: shouldClip ? contentContainer.revealY : 0
|
|
width: shouldClip ? contentContainer.revealWidth : parent.width
|
|
height: shouldClip ? contentContainer.revealHeight : parent.height
|
|
|
|
Item {
|
|
id: rollOutAdjuster
|
|
readonly property real baseWidth: contentContainer.width
|
|
readonly property real baseHeight: contentContainer.height
|
|
|
|
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
|
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
|
width: baseWidth
|
|
height: baseHeight
|
|
clip: false
|
|
|
|
ElevationShadow {
|
|
id: shadowSource
|
|
width: rollOutAdjuster.baseWidth
|
|
height: rollOutAdjuster.baseHeight
|
|
opacity: contentWrapper.publishedOpacity
|
|
scale: root.fluidStandaloneActive ? 1 : contentWrapper.scale
|
|
x: root.fluidStandaloneActive ? 0 : contentWrapper.x
|
|
y: root.fluidStandaloneActive ? 0 : contentWrapper.y
|
|
level: root.shadowLevel
|
|
direction: root.effectiveShadowDirection
|
|
fallbackOffset: root.shadowFallbackOffset
|
|
targetRadius: Theme.cornerRadius
|
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
|
}
|
|
|
|
Item {
|
|
id: contentWrapper
|
|
width: rollOutAdjuster.baseWidth
|
|
height: rollOutAdjuster.baseHeight
|
|
|
|
// publishedOpacity tracks Item.opacity on the GUI thread so consumers (WindowBlur,
|
|
// ElevationShadow, sibling rect) see interpolated values while the visual runs on
|
|
// the render thread via OpacityAnimator.
|
|
property bool _renderActive: Theme.isDirectionalEffect || shouldBeVisible
|
|
property real publishedOpacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
|
|
|
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
|
visible: _renderActive
|
|
scale: contentContainer.scaleValue
|
|
transformOrigin: Item.Center
|
|
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
|
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
|
|
|
layer.enabled: !Theme.isDirectionalEffect && _renderActive
|
|
layer.smooth: false
|
|
layer.textureSize: Qt.size(0, 0)
|
|
|
|
Behavior on opacity {
|
|
enabled: !Theme.isDirectionalEffect
|
|
NumberAnimation {
|
|
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
onRunningChanged: {
|
|
if (!running && !root.shouldBeVisible)
|
|
contentWrapper._renderActive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on publishedOpacity {
|
|
enabled: !Theme.isDirectionalEffect
|
|
NumberAnimation {
|
|
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root
|
|
function onShouldBeVisibleChanged() {
|
|
if (root.shouldBeVisible)
|
|
contentWrapper._renderActive = true;
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: contentWindow
|
|
function onVisibleChanged() {
|
|
// open() flips contentWindow.visible to rebind the layer surface to
|
|
// a new screen; don't deactivate the wrapper while still open.
|
|
if (!contentWindow.visible && !root.shouldBeVisible)
|
|
contentWrapper._renderActive = false;
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: contentLoader
|
|
anchors.fill: parent
|
|
// _contentWarm keeps the tree loaded across close for fast re-open; reclaimed by PopoutService on lock/idle.
|
|
active: root._primeContent || shouldBeVisible || contentWindow.visible || root._contentWarm
|
|
asynchronous: false
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: rollOutAdjuster.baseWidth
|
|
height: rollOutAdjuster.baseHeight
|
|
x: root.fluidStandaloneActive ? 0 : contentWrapper.x
|
|
y: root.fluidStandaloneActive ? 0 : contentWrapper.y
|
|
opacity: contentWrapper.publishedOpacity
|
|
scale: root.fluidStandaloneActive ? 1 : contentWrapper.scale
|
|
visible: contentWrapper.visible
|
|
radius: Theme.cornerRadius
|
|
color: "transparent"
|
|
border.color: BlurService.borderColor
|
|
border.width: BlurService.borderWidth
|
|
z: 100
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: focusHelper
|
|
parent: contentContainer
|
|
anchors.fill: parent
|
|
visible: !root.contentHandlesKeys
|
|
enabled: !root.contentHandlesKeys
|
|
focus: !root.contentHandlesKeys
|
|
Keys.onPressed: event => {
|
|
if (root.contentHandlesKeys)
|
|
return;
|
|
if (event.key === Qt.Key_Escape) {
|
|
close();
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|