mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-18 09:14:01 -04:00
2193 lines
99 KiB
QML
2193 lines
99 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.DCommon.Widgets
|
|
import qs.Widgets
|
|
import "../Common/ConnectedSurfaceGeometry.js" as SurfaceGeometry
|
|
import "../Common/FluidGeometry.js" as FluidGeometry
|
|
|
|
Item {
|
|
id: root
|
|
readonly property var log: Log.scoped("DPopoutHost")
|
|
|
|
required property var popoutHandle
|
|
property bool connected: true
|
|
readonly property bool fluidMotionEnabled: Theme.isFluidEffect && (!connected || frameOwnsConnectedChrome)
|
|
property bool _fluidMotionActive: false
|
|
property string layerNamespace: popoutHandle.layerNamespace
|
|
property alias content: contentLoader.sourceComponent
|
|
property alias contentLoader: contentLoader
|
|
property Component overlayContent: popoutHandle.overlayContent
|
|
property alias overlayLoader: overlayLoader
|
|
readonly property var backgroundWindow: connected ? contentWindow : (backgroundLayer.item?.window ?? null)
|
|
readonly property alias contentWindow: contentWindow
|
|
property real popupWidth: popoutHandle.popupWidth
|
|
property real popupHeight: popoutHandle.popupHeight
|
|
property list<real> resizeCurve: popoutHandle.resizeCurve
|
|
property int resizeDuration: popoutHandle.resizeDuration
|
|
property bool resizeMotion: popoutHandle.resizeMotion
|
|
property bool resizing: popoutHandle.resizing
|
|
property real bottomOverflow: popoutHandle.bottomOverflow
|
|
property real topOverflow: popoutHandle.topOverflow
|
|
property real inputMargin: popoutHandle.inputMargin
|
|
property real triggerX: popoutHandle.triggerX
|
|
property real triggerY: popoutHandle.triggerY
|
|
property real triggerWidth: popoutHandle.triggerWidth
|
|
property string triggerSection: popoutHandle.triggerSection
|
|
property string positioning: popoutHandle.positioning
|
|
property int animationDuration: popoutHandle.animationDuration
|
|
property real animationScaleCollapsed: popoutHandle.animationScaleCollapsed
|
|
property real animationOffset: popoutHandle.animationOffset
|
|
property list<real> animationEnterCurve: popoutHandle.animationEnterCurve
|
|
property list<real> animationExitCurve: popoutHandle.animationExitCurve
|
|
property bool suspendShadowWhileResizing: popoutHandle.suspendShadowWhileResizing
|
|
property bool shouldBeVisible: false
|
|
property bool _presentPending: false
|
|
property bool _surfaceFrameReady: false
|
|
property var customKeyboardFocus: popoutHandle.customKeyboardFocus
|
|
property bool backgroundInteractive: popoutHandle.backgroundInteractive
|
|
property bool contentHandlesKeys: popoutHandle.contentHandlesKeys
|
|
property bool fullHeightSurface: popoutHandle.fullHeightSurface
|
|
property real minimumSurfaceWidth: popoutHandle.minimumSurfaceWidth
|
|
property bool _primeContent: false
|
|
property bool _contentWarm: false
|
|
property bool _backgroundWarm: false
|
|
property bool _contentRenderActive: Theme.isDirectionalEffect || shouldBeVisible
|
|
// Keyboard focus grabbed one tick after emerge starts, to avoid stalling first frames.
|
|
property bool _keyboardReady: false
|
|
readonly property bool contentActive: focusHelper.Window.active
|
|
property bool _resizeActive: false
|
|
property real _chromeAnimTravelX: 1
|
|
property real _chromeAnimTravelY: 1
|
|
property bool _fullSyncQueued: false
|
|
property bool _publishedBodyValid: false
|
|
property real _publishedBodyX: 0
|
|
property real _publishedBodyY: 0
|
|
property real _publishedBodyW: 0
|
|
property real _publishedBodyH: 0
|
|
property real _surfaceX: 0
|
|
property real _surfaceY: 0
|
|
property real _surfaceW: 0
|
|
property real _surfaceH: 0
|
|
property bool _anchorEndX: false
|
|
property bool _anchorEndY: false
|
|
property var _pendingSurfaceX: null
|
|
property var _pendingSurfaceY: null
|
|
property real _surfaceBodyX: 0
|
|
property real _surfaceBodyY: 0
|
|
property real _surfaceBodyW: 0
|
|
property real _surfaceBodyH: 0
|
|
|
|
property real storedBarThickness: popoutHandle.storedBarThickness
|
|
property real storedBarSpacing: popoutHandle.storedBarSpacing
|
|
property var storedBarConfig: popoutHandle.storedBarConfig
|
|
property bool triggerUsesOverlayLayer: popoutHandle.triggerUsesOverlayLayer
|
|
property var adjacentBarInfo: popoutHandle.adjacentBarInfo
|
|
property var screen: popoutHandle.screen
|
|
property var surfaceScreen: null
|
|
readonly property bool frameGapStandaloneActive: CompositorService.frameConfiguredForScreen(screen) && !CompositorService.usesConnectedFrameChromeForScreen(screen)
|
|
readonly property bool directionalRevealActive: Theme.isDirectionalEffect
|
|
readonly property var effectivePopoutLayer: LayerShell.fromEnv("DMS_POPOUT_LAYER", root.triggerUsesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
|
|
"allow": ["top", "overlay"],
|
|
"invalidLayer": WlrLayer.Top,
|
|
"label": "popouts"
|
|
})
|
|
|
|
readonly property real effectiveBarThickness: {
|
|
if (root.usesConnectedSurfaceChrome)
|
|
return Math.max(0, storedBarThickness);
|
|
return Theme.barThickness(storedBarConfig?.innerPadding ?? 2, dpr) + storedBarSpacing;
|
|
}
|
|
|
|
readonly property var barBounds: {
|
|
const dockBounds = connected ? popoutHandle?.inlineDockBounds : null;
|
|
if (dockBounds)
|
|
return {
|
|
x: dockBounds.x,
|
|
y: dockBounds.y,
|
|
width: dockBounds.width,
|
|
height: dockBounds.height,
|
|
wingSize: 0
|
|
};
|
|
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 closeAnimationFinished
|
|
signal backgroundClicked
|
|
|
|
Timer {
|
|
id: _syncTimer
|
|
interval: 0
|
|
onTriggered: root._flushSync()
|
|
}
|
|
|
|
ConnectedSurfaceLease {
|
|
id: chromeLease
|
|
claimPrefix: root.layerNamespace
|
|
screenName: root.screen ? root.screen.name : ""
|
|
enabled: root.frameOwnsConnectedChrome
|
|
active: contentWindow.visible || root.shouldBeVisible
|
|
presented: contentWindow.visible || root.shouldBeVisible
|
|
renewTokenOnRecovery: false
|
|
slot: "popout"
|
|
exclusive: true
|
|
requirePresentedState: true
|
|
isCurrentOwner: name => PopoutManager.isCurrentPopout(root.popoutHandle, name)
|
|
onClaimIdChanged: root._resetPublishedBody()
|
|
onRecoveryRequested: {
|
|
root._resetPublishedBody();
|
|
root._queueFullSync();
|
|
}
|
|
}
|
|
|
|
property var _lastOpenedScreen: null
|
|
readonly property var _openScreen: connected ? _lastOpenedScreen : surfaceScreen
|
|
property bool isClosing: false
|
|
|
|
property int effectiveBarPosition: popoutHandle.effectiveBarPosition
|
|
property real effectiveBarBottomGap: popoutHandle.effectiveBarBottomGap
|
|
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
|
|
|
|
function primeContent() {
|
|
_primeContent = true;
|
|
}
|
|
|
|
function clearPrimedContent() {
|
|
_primeContent = false;
|
|
}
|
|
|
|
function warmContent() {
|
|
_backgroundWarm = true;
|
|
_contentWarm = true;
|
|
}
|
|
|
|
function _captureChromeAnimTravel() {
|
|
_chromeAnimTravelX = Math.max(1, Math.abs(contentContainer.offsetX));
|
|
_chromeAnimTravelY = Math.max(1, Math.abs(contentContainer.offsetY));
|
|
}
|
|
|
|
function _connectedChromeAnimX() {
|
|
const barSide = contentContainer.connectedBarSide;
|
|
if (barSide !== "left" && barSide !== "right")
|
|
return contentContainer.animX;
|
|
|
|
const extent = Math.max(0, root.pubBodyW);
|
|
const progress = Math.min(1, Math.abs(contentContainer.animX) / Math.max(1, _chromeAnimTravelX));
|
|
const offset = Theme.snap(extent * progress, root.dpr);
|
|
return contentContainer.animX < 0 ? -offset : offset;
|
|
}
|
|
|
|
function _connectedChromeAnimY() {
|
|
const barSide = contentContainer.connectedBarSide;
|
|
if (barSide !== "top" && barSide !== "bottom")
|
|
return contentContainer.animY;
|
|
|
|
const extent = Math.max(0, root.pubBodyH);
|
|
const progress = Math.min(1, Math.abs(contentContainer.animY) / Math.max(1, _chromeAnimTravelY));
|
|
const offset = Theme.snap(extent * progress, root.dpr);
|
|
return contentContainer.animY < 0 ? -offset : offset;
|
|
}
|
|
|
|
function _connectedChromeState(visibleOverride) {
|
|
// Track intent rather than the transient wl_surface state during remap.
|
|
const visible = visibleOverride !== undefined ? !!visibleOverride : (contentWindow.visible || root.shouldBeVisible);
|
|
const presented = contentWindow.visible || root.shouldBeVisible;
|
|
const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open"));
|
|
const bodyX = Theme.snap(root.pubBodyX, root.dpr);
|
|
const bodyY = Theme.snap(root.pubBodyY, root.dpr);
|
|
const bodyW = Theme.snap(root.pubBodyW, root.dpr);
|
|
const bodyH = Theme.snap(root.pubBodyH, root.dpr);
|
|
const bodyRect = {
|
|
"x": bodyX,
|
|
"y": bodyY,
|
|
"width": bodyW,
|
|
"height": bodyH
|
|
};
|
|
const animationOffset = {
|
|
"x": _connectedChromeAnimX(),
|
|
"y": _connectedChromeAnimY()
|
|
};
|
|
return {
|
|
"kind": "popout",
|
|
"screenName": root.screen ? root.screen.name : "",
|
|
"phase": phase,
|
|
"visible": visible,
|
|
"presented": presented,
|
|
"layer": root.effectivePopoutLayer === WlrLayer.Overlay ? "overlay" : "top",
|
|
"barSide": contentContainer.connectedBarSide,
|
|
"bodyRect": bodyRect,
|
|
"animationOffset": animationOffset,
|
|
"scale": 1,
|
|
"opacity": Theme.connectedSurfaceColor.a,
|
|
"bodyX": bodyX,
|
|
"bodyY": bodyY,
|
|
"bodyW": bodyW,
|
|
"bodyH": bodyH,
|
|
"animX": animationOffset.x,
|
|
"animY": animationOffset.y,
|
|
"screen": root.screen ? root.screen.name : "",
|
|
"omitStartConnector": root._closeGapOmitStartConnector(),
|
|
"omitEndConnector": root._closeGapOmitEndConnector(),
|
|
"overflowTop": Theme.snap(root.topOverflow, root.dpr),
|
|
"overflowBottom": Theme.snap(root.bottomOverflow, root.dpr)
|
|
};
|
|
}
|
|
|
|
function _publishConnectedChromeState(forceClaim, visibleOverride) {
|
|
if (!root.frameOwnsConnectedChrome || !root.screen)
|
|
return false;
|
|
const state = _connectedChromeState(visibleOverride);
|
|
const published = chromeLease.publish(state, !!forceClaim);
|
|
if (published)
|
|
_rememberPublishedBody(state.bodyX, state.bodyY, state.bodyW, state.bodyH);
|
|
return published;
|
|
}
|
|
|
|
function _releaseConnectedChromeState() {
|
|
_resetPublishedBody();
|
|
chromeLease.release();
|
|
}
|
|
|
|
function _claimConnectedChrome() {
|
|
if (!root.frameOwnsConnectedChrome) {
|
|
chromeLease.release();
|
|
return;
|
|
}
|
|
chromeLease.beginClaim();
|
|
_publishConnectedChromeState(true, true);
|
|
}
|
|
|
|
function _syncPopoutChromeState() {
|
|
if (!root.frameOwnsConnectedChrome) {
|
|
_releaseConnectedChromeState();
|
|
return;
|
|
}
|
|
if (!root.screen) {
|
|
_releaseConnectedChromeState();
|
|
return;
|
|
}
|
|
if (!contentWindow.visible && !shouldBeVisible)
|
|
return;
|
|
_publishConnectedChromeState(false);
|
|
}
|
|
|
|
function _syncPopoutAnim(axis) {
|
|
if (!root.frameOwnsConnectedChrome || !chromeLease.claimId)
|
|
return;
|
|
if (!contentWindow.visible && !shouldBeVisible)
|
|
return;
|
|
const barSide = contentContainer.connectedBarSide;
|
|
const syncX = axis === "x" && (barSide === "left" || barSide === "right");
|
|
const syncY = axis === "y" && (barSide === "top" || barSide === "bottom");
|
|
if (!syncX && !syncY)
|
|
return;
|
|
chromeLease.updateAnim(syncX ? _connectedChromeAnimX() : undefined, syncY ? _connectedChromeAnimY() : undefined);
|
|
}
|
|
|
|
function _syncPopoutBody() {
|
|
if (!root.frameOwnsConnectedChrome || !chromeLease.claimId)
|
|
return;
|
|
if (!contentWindow.visible && !shouldBeVisible)
|
|
return;
|
|
const body = root.morphTravelEnabled ? root._boundTravel(travelSpring.value) : root.travelTarget;
|
|
const bodyX = Theme.snap(body.x, root.dpr);
|
|
const bodyY = Theme.snap(body.y, root.dpr);
|
|
const bodyW = Theme.snap(body.width, root.dpr);
|
|
const bodyH = Theme.snap(body.height, root.dpr);
|
|
if (_publishedBodyValid && _publishedBodyX === bodyX && _publishedBodyY === bodyY && _publishedBodyW === bodyW && _publishedBodyH === bodyH)
|
|
return;
|
|
if (chromeLease.updateBody(bodyX, bodyY, bodyW, bodyH))
|
|
_rememberPublishedBody(bodyX, bodyY, bodyW, bodyH);
|
|
if (morph.running)
|
|
_syncPopoutAnim(contentContainer.barLeft || contentContainer.barRight ? "x" : "y");
|
|
}
|
|
|
|
function _rememberPublishedBody(bodyX, bodyY, bodyW, bodyH) {
|
|
_publishedBodyX = bodyX;
|
|
_publishedBodyY = bodyY;
|
|
_publishedBodyW = bodyW;
|
|
_publishedBodyH = bodyH;
|
|
_publishedBodyValid = true;
|
|
}
|
|
|
|
function _resetPublishedBody() {
|
|
_publishedBodyValid = false;
|
|
}
|
|
|
|
function _queueFullSync() {
|
|
if (!connected)
|
|
return;
|
|
_fullSyncQueued = true;
|
|
if (!_syncTimer.running)
|
|
_syncTimer.restart();
|
|
}
|
|
function _flushSync() {
|
|
if (!_fullSyncQueued)
|
|
return;
|
|
_fullSyncQueued = false;
|
|
_syncPopoutChromeState();
|
|
}
|
|
|
|
readonly property bool settingsConnectedFrameModeActive: SettingsData.connectedFrameModeActive
|
|
readonly property bool settingsFrameCloseGaps: SettingsData.frameCloseGaps
|
|
readonly property var connectedSurfaceDescriptors: ConnectedModeState.surfaceDescriptors
|
|
|
|
function _syncAlignedGeometry() {
|
|
if (connected) {
|
|
_queueFullSync();
|
|
return;
|
|
}
|
|
_setAnimatedSurfaceEnvelope();
|
|
}
|
|
|
|
onAlignedXChanged: _syncAlignedGeometry()
|
|
onAlignedYChanged: {
|
|
_retargetBody();
|
|
_syncAlignedGeometry();
|
|
}
|
|
onAlignedWidthChanged: {
|
|
_retargetBody();
|
|
_syncAlignedGeometry();
|
|
}
|
|
onConnectedChanged: {
|
|
if (!connected)
|
|
_setAnimatedSurfaceEnvelope();
|
|
_surfaceSwitching = contentWindow.visible && Math.abs(contentWindow.width - (connected ? screenWidth : _surfaceW)) > 1;
|
|
}
|
|
property bool _surfaceSwitching: false
|
|
readonly property real _contentWindowWidth: contentWindow.width
|
|
readonly property real _contentWindowHeight: contentWindow.height
|
|
on_ContentWindowWidthChanged: {
|
|
_surfaceSwitching = false;
|
|
_flushPendingSurface("x", false);
|
|
}
|
|
on_ContentWindowHeightChanged: _flushPendingSurface("y", false)
|
|
onResizingChanged: {
|
|
if (!resizing)
|
|
backgroundLayer.item?.surfaceMoved();
|
|
}
|
|
onScreenChanged: {
|
|
if (!connected)
|
|
return;
|
|
_resetPublishedBody();
|
|
_queueFullSync();
|
|
}
|
|
onEffectiveBarPositionChanged: {
|
|
if (!connected)
|
|
return;
|
|
_queueFullSync();
|
|
}
|
|
onFrameOwnsConnectedChromeChanged: {
|
|
if (!connected)
|
|
return;
|
|
_syncPopoutChromeState();
|
|
}
|
|
|
|
onSurfaceBodyWidthChanged: {
|
|
if (connected)
|
|
return;
|
|
_setAnimatedSurfaceEnvelope();
|
|
}
|
|
|
|
Timer {
|
|
id: surfaceStepTimerX
|
|
interval: 100
|
|
onTriggered: root._flushPendingSurface("x", true)
|
|
}
|
|
|
|
Timer {
|
|
id: surfaceStepTimerY
|
|
interval: 100
|
|
onTriggered: root._flushPendingSurface("y", true)
|
|
}
|
|
|
|
onSettingsConnectedFrameModeActiveChanged: {
|
|
if (!connected)
|
|
return;
|
|
if (frameOwnsConnectedChrome) {
|
|
if ((contentWindow.visible || shouldBeVisible) && screen && PopoutManager.isCurrentPopout(popoutHandle, screen.name))
|
|
_publishConnectedChromeState(true);
|
|
} else {
|
|
_releaseConnectedChromeState();
|
|
}
|
|
}
|
|
onSettingsFrameCloseGapsChanged: {
|
|
if (!connected)
|
|
return;
|
|
_syncPopoutChromeState();
|
|
}
|
|
onTopOverflowChanged: {
|
|
if (!connected)
|
|
return;
|
|
_syncPopoutChromeState();
|
|
}
|
|
onBottomOverflowChanged: {
|
|
if (!connected)
|
|
return;
|
|
_syncPopoutChromeState();
|
|
}
|
|
onConnectedSurfaceDescriptorsChanged: {
|
|
if (!connected)
|
|
return;
|
|
chromeLease.checkRecovery();
|
|
}
|
|
|
|
Connections {
|
|
target: PopoutManager
|
|
enabled: root.connected
|
|
function onPopoutChanged() {
|
|
chromeLease.requestRecovery();
|
|
}
|
|
}
|
|
|
|
property bool _overStackedModal: false
|
|
readonly property bool frameOwnsConnectedChrome: connected && !_overStackedModal && effectivePopoutLayer === WlrLayer.Top && CompositorService.canShareConnectedFrameChromeForScreen(root.screen)
|
|
readonly property bool usesConnectedSurfaceChrome: connected && Theme.isConnectedEffect
|
|
readonly property bool usesLocalConnectedSurfaceChrome: usesConnectedSurfaceChrome && !frameOwnsConnectedChrome
|
|
|
|
property bool animationsEnabled: true
|
|
property bool hoverDismissEnabled: popoutHandle.hoverDismissEnabled
|
|
property bool hoverDismissSuspended: popoutHandle.effectiveHoverDismissSuspended
|
|
|
|
function cancelHoverDismiss() {
|
|
hoverDismissController.cancelPending();
|
|
}
|
|
|
|
function closeFromHoverDismiss() {
|
|
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
|
|
return;
|
|
if (popoutHandle?.closeFromHoverDismiss)
|
|
popoutHandle.closeFromHoverDismiss();
|
|
else
|
|
close();
|
|
}
|
|
|
|
DeferredAction {
|
|
id: showSurfaceAction
|
|
onTriggered: {
|
|
if (!root.screen || !(root.shouldBeVisible || root._presentPending))
|
|
return;
|
|
root.surfaceScreen = root.screen;
|
|
if (backgroundLayer.item)
|
|
backgroundLayer.item.show();
|
|
contentWindow.visible = true;
|
|
chromeLoader.item.blur.kick();
|
|
root._setSettledSurfaceGeometry();
|
|
}
|
|
}
|
|
|
|
function _canPreserveMotion() {
|
|
if (!contentWindow.visible || _openScreen !== screen || _supersededClose)
|
|
return false;
|
|
return !frameOwnsConnectedChrome || ConnectedModeState.hasSurfaceOwner(root.screen.name, "popout", chromeLease.claimId);
|
|
}
|
|
|
|
function open() {
|
|
if (!screen)
|
|
return;
|
|
if (!contentWindow.visible || _openScreen !== screen)
|
|
_overStackedModal = ModalManager.hasStackedModal(screen.name);
|
|
const preserveMotion = _canPreserveMotion();
|
|
_resetPublishedBody();
|
|
closeTimer.stop();
|
|
isClosing = false;
|
|
_snapBody();
|
|
if (!preserveMotion) {
|
|
animationsEnabled = false;
|
|
_fluidMotionActive = fluidMotionEnabled;
|
|
}
|
|
_primeContent = true;
|
|
_contentWarm = true;
|
|
_supersededClose = false;
|
|
|
|
if (_openScreen !== null && _openScreen !== screen)
|
|
contentWindow.visible = false;
|
|
if (!contentWindow.visible) {
|
|
_anchorEndX = surfaceBodyX + surfaceBodyWidth / 2 > screenWidth / 2;
|
|
_anchorEndY = !fullHeightSurface && alignedY + alignedHeight / 2 > screenHeight / 2;
|
|
}
|
|
_lastOpenedScreen = screen;
|
|
if (connected)
|
|
PopoutManager.showPopout(popoutHandle);
|
|
|
|
if (!shouldBeVisible && !preserveMotion)
|
|
morph.retarget(0);
|
|
_captureChromeAnimTravel();
|
|
|
|
if (!preserveMotion)
|
|
_beginMorphTravel();
|
|
|
|
if (morphTravelEnabled && !preserveMotion)
|
|
morph.retarget(1);
|
|
|
|
_claimConnectedChrome();
|
|
_setSurfaceGeometry(surfaceBodyX, alignedY, surfaceBodyWidth, alignedHeight);
|
|
|
|
if (!contentWindow.visible)
|
|
showSurfaceAction.schedule();
|
|
|
|
animationsEnabled = true;
|
|
if (!connected && !_surfaceFrameReady) {
|
|
_presentPending = true;
|
|
return;
|
|
}
|
|
_present();
|
|
}
|
|
|
|
function _present() {
|
|
_presentPending = false;
|
|
shouldBeVisible = true;
|
|
_setSettledSurfaceGeometry();
|
|
if (morphTravelEnabled)
|
|
_retargetMorphTravel();
|
|
Qt.callLater(() => {
|
|
if (root.shouldBeVisible)
|
|
root._keyboardReady = true;
|
|
});
|
|
if (!connected)
|
|
PopoutManager.showPopout(popoutHandle);
|
|
opened();
|
|
}
|
|
|
|
on_SurfaceFrameReadyChanged: {
|
|
if (_surfaceFrameReady && _presentPending)
|
|
_present();
|
|
}
|
|
|
|
function close() {
|
|
showSurfaceAction.cancel();
|
|
if (!contentWindow.visible || _presentPending) {
|
|
instantClose();
|
|
return;
|
|
}
|
|
if (!connected && _fluidMotionActive && (SettingsData.reduceMotion || animationDuration <= 0)) {
|
|
instantClose();
|
|
return;
|
|
}
|
|
if (_supersededClose && morphTravelEnabled)
|
|
_freezeMorphTravel();
|
|
_resetPublishedBody();
|
|
isClosing = true;
|
|
shouldBeVisible = false;
|
|
_keyboardReady = false;
|
|
_primeContent = false;
|
|
PopoutManager.popoutChanged();
|
|
_retractFluidTravel();
|
|
if (!isClosing)
|
|
return;
|
|
closeTimer.restart();
|
|
}
|
|
|
|
function _finishClose() {
|
|
if (shouldBeVisible || !isClosing)
|
|
return;
|
|
closeTimer.stop();
|
|
contentWindow.visible = false;
|
|
morph.snapTo(0);
|
|
_endMorphTravel();
|
|
_fluidMotionActive = false;
|
|
isClosing = false;
|
|
_snapBody();
|
|
PopoutManager.hidePopout(popoutHandle);
|
|
popoutClosed();
|
|
}
|
|
|
|
function instantClose() {
|
|
showSurfaceAction.cancel();
|
|
_presentPending = false;
|
|
closeTimer.stop();
|
|
_endMorphTravel();
|
|
_resetPublishedBody();
|
|
animationsEnabled = false;
|
|
isClosing = false;
|
|
shouldBeVisible = false;
|
|
_keyboardReady = false;
|
|
_primeContent = false;
|
|
contentWindow.visible = false;
|
|
_fluidMotionActive = false;
|
|
_snapBody();
|
|
PopoutManager.hidePopout(popoutHandle);
|
|
popoutClosed();
|
|
Qt.callLater(() => animationsEnabled = true);
|
|
}
|
|
|
|
function toggle() {
|
|
shouldBeVisible ? close() : open();
|
|
}
|
|
|
|
readonly property var quickshellScreens: Quickshell.screens
|
|
|
|
onQuickshellScreensChanged: {
|
|
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();
|
|
} else {
|
|
_queueFullSync();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: closeTimer
|
|
interval: Math.max(Theme.variantCloseInterval(root.animationDuration), morph.settleDurationMs + 32, root._fluidMotionActive ? travelSpring.settleDurationMs * 2 + 32 : 0)
|
|
onTriggered: root._finishClose()
|
|
}
|
|
|
|
Component.onDestruction: _releaseConnectedChromeState()
|
|
|
|
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 bool closeFrameGapsActive: SettingsData.frameCloseGaps && frameOwnsConnectedChrome
|
|
readonly property real frameInset: {
|
|
if (!root.frameOwnsConnectedChrome)
|
|
return 0;
|
|
const ft = SettingsData.frameThickness;
|
|
const fr = SettingsData.frameRounding;
|
|
const ccr = Theme.connectedCornerRadius;
|
|
return Math.max(ft * 4, ft + ccr * 2, fr);
|
|
}
|
|
|
|
function _popupGapValue() {
|
|
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
|
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
|
const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
|
return root.usesConnectedSurfaceChrome ? 0 : rawPopupGap;
|
|
}
|
|
|
|
function _frameEdgeInset(side) {
|
|
if (!root.frameOwnsConnectedChrome)
|
|
return 0;
|
|
return Math.max(0, SettingsData.frameEdgeReservation(root.screen, side));
|
|
}
|
|
|
|
function _edgeGapFor(side, popupGap) {
|
|
const dockInset = popoutHandle?.dockClearance?.(side, popupGap) ?? 0;
|
|
if (root.closeFrameGapsActive)
|
|
return Math.max(popupGap, _frameEdgeInset(side), dockInset);
|
|
return Math.max(popupGap, frameInset, dockInset);
|
|
}
|
|
|
|
function _frameGapMargin(side) {
|
|
return SettingsData.frameEdgeInsetForSide(screen, side) + Theme.popupDistance;
|
|
}
|
|
|
|
function _edgeClearance(side, popupGap, adjacentInset) {
|
|
const dockInset = popoutHandle?.dockClearance?.(side, popupGap) ?? 0;
|
|
if (frameGapStandaloneActive)
|
|
return Math.max(adjacentInset, _frameGapMargin(side), dockInset);
|
|
return Math.max(adjacentInset > 0 ? adjacentInset : popupGap, dockInset);
|
|
}
|
|
|
|
function _sideAdjacentClearance(side) {
|
|
switch (side) {
|
|
case "left":
|
|
return adjacentBarClearance(adjacentBarInfo.leftBar);
|
|
case "right":
|
|
return adjacentBarClearance(adjacentBarInfo.rightBar);
|
|
case "top":
|
|
return adjacentBarClearance(adjacentBarInfo.topBar);
|
|
case "bottom":
|
|
return adjacentBarClearance(adjacentBarInfo.bottomBar);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function _nearFrameBound(value, bound) {
|
|
return Math.abs(value - bound) <= Math.max(1, Theme.hairline(root.dpr) * 2);
|
|
}
|
|
|
|
// Snap positions within connector radius flush to the frame edge (avoids pinched arcs).
|
|
function _snapNearFrameBound(value, minBound, maxBound, minIsFrame, maxIsFrame) {
|
|
if (!root.usesConnectedSurfaceChrome || !root.closeFrameGapsActive)
|
|
return value;
|
|
const snapDist = Theme.connectedCornerRadius;
|
|
if (maxIsFrame && value < maxBound && maxBound - value < snapDist && maxBound - value <= value - minBound)
|
|
return maxBound;
|
|
if (minIsFrame && value > minBound && value - minBound < snapDist)
|
|
return minBound;
|
|
return value;
|
|
}
|
|
|
|
function _closeGapClampedToFrameSide(side) {
|
|
if (!root.closeFrameGapsActive)
|
|
return false;
|
|
const popupGap = _popupGapValue();
|
|
const edgeGap = _edgeGapFor(side, popupGap);
|
|
const adjacentGap = _sideAdjacentClearance(side);
|
|
if (edgeGap < adjacentGap - Math.max(1, Theme.hairline(root.dpr) * 2))
|
|
return false;
|
|
|
|
switch (side) {
|
|
case "left":
|
|
return _nearFrameBound(root.alignedX, edgeGap);
|
|
case "right":
|
|
return _nearFrameBound(root.alignedX, screenWidth - popupWidth - edgeGap);
|
|
case "top":
|
|
return _nearFrameBound(root.alignedY, edgeGap);
|
|
case "bottom":
|
|
return _nearFrameBound(root.alignedY, screenHeight - popupHeight - edgeGap);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function _closeGapOmitStartConnector() {
|
|
const side = contentContainer.connectedBarSide;
|
|
if (side === "top" || side === "bottom")
|
|
return _closeGapClampedToFrameSide("left");
|
|
return _closeGapClampedToFrameSide("top");
|
|
}
|
|
|
|
function _closeGapOmitEndConnector() {
|
|
const side = contentContainer.connectedBarSide;
|
|
if (side === "top" || side === "bottom")
|
|
return _closeGapClampedToFrameSide("right");
|
|
return _closeGapClampedToFrameSide("bottom");
|
|
}
|
|
|
|
readonly property var shadowLevel: Theme.elevationLevel2
|
|
readonly property real shadowFallbackOffset: Theme.spacingXS
|
|
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
|
|
readonly property real shadowMotionPadding: directionalRevealActive ? 0 : Math.max(0, animationOffset)
|
|
readonly property real shadowBuffer: Theme.snap(Math.max(popoutHandle.surfacePadding, shadowRenderPadding + shadowMotionPadding), dpr)
|
|
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
|
readonly property real alignedHeight: Theme.pxCeil(popupHeight, dpr)
|
|
readonly property real surfaceBodyWidth: Math.max(alignedWidth, Theme.px(Math.min(minimumSurfaceWidth, screenWidth), dpr))
|
|
readonly property real surfaceBodyX: Theme.snap(_standaloneAlignedXFor(surfaceBodyWidth), dpr)
|
|
readonly property real _surfaceOriginX: connected ? 0 : _surfaceX
|
|
readonly property real _surfaceOriginY: connected || fullHeightSurface ? 0 : _surfaceY
|
|
readonly property var _geometrySpringParams: Theme.springPreset("default", root.animationDuration)
|
|
|
|
readonly property real renderedAlignedX: alignedXFor(renderedAlignedWidth)
|
|
property real renderedAlignedY: alignedY
|
|
property real renderedAlignedWidth: alignedWidth
|
|
property real renderedAlignedHeight: alignedHeight
|
|
// Snap rendered geometry while the entrance morph runs so it doesn't ride a second animation.
|
|
readonly property bool _settlingToOpen: shouldBeVisible && (morph.running || travelSpring.running)
|
|
readonly property bool _geometryMotion: animationsEnabled && !SettingsData.reduceMotion && resizeDuration > 0 && contentWindow.visible && shouldBeVisible && !_settlingToOpen && !resizing
|
|
property real _bodyGlide: 1
|
|
property bool _bodyBatch: false
|
|
property real _bodyFromY: 0
|
|
property real _bodyFromWidth: 0
|
|
property real _bodyFromHeight: 0
|
|
|
|
NumberAnimation {
|
|
id: bodyGlide
|
|
target: root
|
|
property: "_bodyGlide"
|
|
from: 0
|
|
to: 1
|
|
duration: root.resizeDuration
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: root.resizeCurve
|
|
}
|
|
|
|
on_BodyGlideChanged: _setBody(_bodyFromY + (alignedY - _bodyFromY) * _bodyGlide, resizeMotion ? _bodyFromWidth + (alignedWidth - _bodyFromWidth) * _bodyGlide : renderedAlignedWidth, _bodyFromHeight + (alignedHeight - _bodyFromHeight) * _bodyGlide)
|
|
|
|
// One notification per body change: travelTarget would otherwise fire once per component write.
|
|
function _setBody(y, width, height) {
|
|
if (renderedAlignedY === y && renderedAlignedWidth === width && renderedAlignedHeight === height)
|
|
return;
|
|
_bodyBatch = true;
|
|
renderedAlignedY = y;
|
|
renderedAlignedWidth = width;
|
|
renderedAlignedHeight = height;
|
|
_bodyBatch = false;
|
|
_bodyChanged();
|
|
}
|
|
|
|
function _bodyChanged() {
|
|
if (_bodyBatch)
|
|
return;
|
|
if (morphTravelEnabled && shouldBeVisible && !_supersededClose)
|
|
_retargetMorphTravel();
|
|
if (connected)
|
|
_syncPopoutBody();
|
|
}
|
|
|
|
on_GeometryMotionChanged: {
|
|
if (_geometryMotion)
|
|
return;
|
|
if (isClosing) {
|
|
bodyGlide.stop();
|
|
return;
|
|
}
|
|
_snapBody();
|
|
}
|
|
|
|
function _snapBody() {
|
|
bodyGlide.stop();
|
|
_setBody(alignedY, alignedWidth, alignedHeight);
|
|
}
|
|
|
|
function _retargetBody() {
|
|
if (isClosing)
|
|
return;
|
|
if (!resizeMotion)
|
|
_setBody(renderedAlignedY, alignedWidth, renderedAlignedHeight);
|
|
if (!_geometryMotion) {
|
|
_snapBody();
|
|
return;
|
|
}
|
|
if (renderedAlignedY === alignedY && renderedAlignedWidth === alignedWidth && renderedAlignedHeight === alignedHeight) {
|
|
bodyGlide.stop();
|
|
return;
|
|
}
|
|
_bodyFromY = renderedAlignedY;
|
|
_bodyFromWidth = renderedAlignedWidth;
|
|
_bodyFromHeight = renderedAlignedHeight;
|
|
bodyGlide.restart();
|
|
}
|
|
|
|
property bool morphTravelEnabled: false
|
|
property bool _travelFromHandoff: false
|
|
readonly property string _travelSpringPreset: _travelFromHandoff ? "fast" : "default"
|
|
readonly property rect travelTarget: Qt.rect(renderedAlignedX, renderedAlignedY, renderedAlignedWidth, renderedAlignedHeight)
|
|
readonly property point _fluidOrigin: connected ? Qt.point(0, 0) : Qt.point(renderedAlignedX, renderedAlignedY)
|
|
readonly property rect fluidTarget: Qt.rect(renderedAlignedX - _fluidOrigin.x, renderedAlignedY - _fluidOrigin.y, renderedAlignedWidth, renderedAlignedHeight)
|
|
readonly property rect fluidCollapsedRect: !_fluidMotionActive ? fluidTarget : FluidGeometry.collapsed(fluidTarget, contentContainer.connectedBarSide, contentContainer.barTop || contentContainer.barBottom ? triggerX + triggerWidth / 2 - _fluidOrigin.x : triggerY - _fluidOrigin.y, triggerWidth)
|
|
|
|
RectSpringMotion {
|
|
id: travelSpring
|
|
enabled: root.morphTravelEnabled && root.animationsEnabled
|
|
reducedMotion: root.animationDuration <= 0 || SettingsData.reduceMotion
|
|
positionEpsilon: 0.25 / root.dpr
|
|
velocityEpsilon: positionEpsilon * damping / Math.max(0.001, 2 * mass)
|
|
stiffness: Theme.springPreset(root._travelSpringPreset, root.animationDuration).stiffness
|
|
damping: Theme.springPreset(root._travelSpringPreset, root.animationDuration).damping
|
|
onValueChanged: root._syncPopoutBody()
|
|
onRunningChanged: root._finishMorphTravel()
|
|
}
|
|
|
|
function _boundTravel(rect) {
|
|
return _fluidMotionActive && !_travelFromHandoff ? FluidGeometry.limitOvershoot(rect, travelTarget, Theme.fluidOvershootLimit) : rect;
|
|
}
|
|
readonly property rect travelBody: morphTravelEnabled ? _boundTravel(travelSpring.value) : travelTarget
|
|
readonly property real pubBodyX: travelBody.x
|
|
readonly property real pubBodyY: travelBody.y
|
|
readonly property real pubBodyW: travelBody.width
|
|
readonly property real pubBodyH: travelBody.height
|
|
readonly property real fluidContentOpacity: !_fluidMotionActive ? 1 : FluidGeometry.contentOpacity(travelBody, travelTarget)
|
|
|
|
onTravelTargetChanged: _bodyChanged()
|
|
|
|
function _retargetMorphTravel() {
|
|
travelSpring.retarget(_fluidMotionActive && !shouldBeVisible ? fluidCollapsedRect : travelTarget);
|
|
_finishMorphTravel();
|
|
}
|
|
|
|
function _finishMorphTravel() {
|
|
if (!morphTravelEnabled || travelSpring.running || _supersededClose)
|
|
return;
|
|
if (!travelSpring.isSettled(travelSpring.value, travelSpring.velocity))
|
|
return;
|
|
if (shouldBeVisible) {
|
|
_endMorphTravel();
|
|
return;
|
|
}
|
|
if (_fluidMotionActive)
|
|
_finishClose();
|
|
}
|
|
|
|
function _beginMorphTravel() {
|
|
morphTravelEnabled = false;
|
|
_travelFromHandoff = false;
|
|
if (!root.frameOwnsConnectedChrome || !root.screen)
|
|
return;
|
|
const handoff = root.hoverDismissEnabled ? ConnectedModeState.takePopoutMotion(root.screen.name, contentContainer.connectedBarSide) : null;
|
|
if (!handoff && !_fluidMotionActive)
|
|
return;
|
|
if (handoff && (handoff.rect.width <= 0 || handoff.rect.height <= 0))
|
|
return;
|
|
_travelFromHandoff = !!handoff;
|
|
travelSpring.snapTo(handoff ? handoff.rect : fluidCollapsedRect, handoff?.velocity);
|
|
morphTravelEnabled = true;
|
|
}
|
|
|
|
function _retractFluidTravel() {
|
|
if (!connected || !_fluidMotionActive || _supersededClose)
|
|
return;
|
|
if (!morphTravelEnabled) {
|
|
travelSpring.snapTo(travelTarget);
|
|
morphTravelEnabled = true;
|
|
}
|
|
_retargetMorphTravel();
|
|
}
|
|
|
|
function _freezeMorphTravel() {
|
|
travelSpring.snapTo(travelSpring.value);
|
|
_syncPopoutBody();
|
|
}
|
|
|
|
function _endMorphTravel() {
|
|
morphTravelEnabled = false;
|
|
}
|
|
|
|
property bool _supersededClose: false
|
|
property real _supersededContentX: 0
|
|
property real _supersededContentY: 0
|
|
property real _supersededContentScale: 1
|
|
|
|
function beginSupersededClose() {
|
|
if (!frameOwnsConnectedChrome || _supersededClose || !contentWindow.visible || !root.screen)
|
|
return;
|
|
if ((!hoverDismissEnabled && !isClosing) || !ConnectedModeState.hasSurfaceOwner(root.screen.name, "popout", chromeLease.claimId))
|
|
return;
|
|
const body = SurfaceGeometry.animatedBodyRect(_connectedChromeState(), root.dpr);
|
|
const traveling = morphTravelEnabled && !morph.running;
|
|
const rect = traveling ? root._boundTravel(travelSpring.value) : Qt.rect(body.x, body.y, Math.max(1, body.width), Math.max(1, body.height));
|
|
const velocity = traveling ? travelSpring.velocity : Qt.vector4d(0, 0, 0, 0);
|
|
const contentWrapper = chromeLoader.item.contentWrapper;
|
|
ConnectedModeState.savePopoutMotion(chromeLease.claimId, root.screen.name, contentContainer.connectedBarSide, rect, velocity);
|
|
_supersededContentX = contentContainer.x + contentWrapper.x - rect.x;
|
|
_supersededContentY = contentContainer.y + contentWrapper.y - rect.y;
|
|
_supersededContentScale = contentWrapper.scale;
|
|
_supersededClose = true;
|
|
travelSpring.snapTo(rect);
|
|
morphTravelEnabled = true;
|
|
morph.snapTo(1);
|
|
_syncPopoutBody();
|
|
}
|
|
|
|
readonly property real connectedAnchorX: {
|
|
if (!root.usesConnectedSurfaceChrome)
|
|
return triggerX;
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Left:
|
|
return barX + barWidth;
|
|
case SettingsData.Position.Right:
|
|
return barX;
|
|
default:
|
|
return triggerX;
|
|
}
|
|
}
|
|
readonly property real connectedAnchorY: {
|
|
if (!root.usesConnectedSurfaceChrome)
|
|
return triggerY;
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Top:
|
|
return barY + barHeight;
|
|
case SettingsData.Position.Bottom:
|
|
return barY;
|
|
default:
|
|
return triggerY;
|
|
}
|
|
}
|
|
|
|
function adjacentBarClearance(exclusion) {
|
|
if (exclusion <= 0)
|
|
return 0;
|
|
if (!root.usesConnectedSurfaceChrome)
|
|
return exclusion;
|
|
return exclusion + Theme.connectedCornerRadius * 2;
|
|
}
|
|
|
|
onAlignedHeightChanged: {
|
|
_retargetBody();
|
|
if (suspendShadowWhileResizing && shouldBeVisible) {
|
|
_resizeActive = true;
|
|
resizeSettleTimer.restart();
|
|
}
|
|
_syncAlignedGeometry();
|
|
}
|
|
onShouldBeVisibleChanged: {
|
|
if (shouldBeVisible) {
|
|
_contentRenderActive = true;
|
|
return;
|
|
}
|
|
_resizeActive = false;
|
|
resizeSettleTimer.stop();
|
|
}
|
|
on_ContentRenderActiveChanged: {
|
|
if (!_contentRenderActive && isClosing && !shouldBeVisible)
|
|
closeAnimationFinished();
|
|
}
|
|
|
|
Timer {
|
|
id: resizeSettleTimer
|
|
interval: 80
|
|
repeat: false
|
|
onTriggered: root._resizeActive = false
|
|
}
|
|
|
|
Timer {
|
|
id: surfaceSettleTimer
|
|
interval: Math.max(0, root.resizeDuration + 32)
|
|
repeat: false
|
|
onTriggered: root._setSettledSurfaceGeometry()
|
|
}
|
|
|
|
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
|
if (connected)
|
|
return;
|
|
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;
|
|
_requestSurfaceAxis("x", newX - shadowBuffer, newW + shadowBuffer * 2);
|
|
if (!fullHeightSurface)
|
|
_requestSurfaceAxis("y", newY - shadowBuffer, newH + shadowBuffer * 2);
|
|
if (changed && !resizing && backgroundLayer.item)
|
|
backgroundLayer.item.surfaceMoved();
|
|
}
|
|
|
|
// Compositors place the old buffer at the new origin until the configure round trip lands, and niri
|
|
// clamps sizes to the output. Anchoring the far edge lets the surface grow with its origin fixed, so
|
|
// the origin only moves once the buffer already spans the target; shrinks keep the body covered anyway.
|
|
function _requestSurfaceAxis(axis, start, size) {
|
|
const x = axis === "x";
|
|
const pendingProp = x ? "_pendingSurfaceX" : "_pendingSurfaceY";
|
|
const timer = x ? surfaceStepTimerX : surfaceStepTimerY;
|
|
root[pendingProp] = null;
|
|
timer.stop();
|
|
const currentStart = x ? _surfaceX : _surfaceY;
|
|
const actualSize = x ? contentWindow.width : contentWindow.height;
|
|
const growSize = (x ? _anchorEndX : _anchorEndY) ? size : Math.min(size, (x ? screenWidth : screenHeight) - currentStart);
|
|
if (!contentWindow.visible || start >= currentStart || growSize === actualSize) {
|
|
_applySurfaceAxis(axis, start, size);
|
|
return;
|
|
}
|
|
root[pendingProp] = {
|
|
"start": start,
|
|
"size": size
|
|
};
|
|
_applySurfaceAxis(axis, currentStart, size);
|
|
timer.restart();
|
|
}
|
|
|
|
function _applySurfaceAxis(axis, start, size) {
|
|
if (axis === "x") {
|
|
_surfaceX = start;
|
|
_surfaceW = size;
|
|
return;
|
|
}
|
|
_surfaceY = start;
|
|
_surfaceH = size;
|
|
}
|
|
|
|
function _flushPendingSurface(axis, timedOut) {
|
|
const x = axis === "x";
|
|
const pendingProp = x ? "_pendingSurfaceX" : "_pendingSurfaceY";
|
|
const pending = root[pendingProp];
|
|
if (!pending)
|
|
return;
|
|
const actualSize = x ? contentWindow.width : contentWindow.height;
|
|
const reachedEdge = (x ? _surfaceX : _surfaceY) + actualSize >= (x ? screenWidth : screenHeight);
|
|
if (!timedOut && pending.size !== actualSize && !reachedEdge)
|
|
return;
|
|
root[pendingProp] = null;
|
|
(x ? surfaceStepTimerX : surfaceStepTimerY).stop();
|
|
_applySurfaceAxis(axis, pending.start, pending.size);
|
|
}
|
|
|
|
function _setSettledSurfaceGeometry() {
|
|
if (connected || !shouldBeVisible || surfaceScreen !== screen)
|
|
return;
|
|
_setSurfaceGeometry(surfaceBodyX, alignedY, surfaceBodyWidth, alignedHeight);
|
|
}
|
|
|
|
function _setAnimatedSurfaceEnvelope() {
|
|
if (connected || !shouldBeVisible || surfaceScreen !== screen)
|
|
return;
|
|
const currentX = renderedAlignedX;
|
|
const currentRight = renderedAlignedX + renderedAlignedWidth;
|
|
// Handlers run before sibling bindings settle, so derive the target from the inputs.
|
|
const targetWidth = Math.max(Theme.px(popupWidth, dpr), Theme.px(Math.min(minimumSurfaceWidth, screenWidth), dpr));
|
|
const targetX = Theme.snap(_standaloneAlignedXFor(targetWidth), dpr);
|
|
const targetRight = targetX + targetWidth;
|
|
const envelopeX = Math.min(currentX, targetX);
|
|
const envelopeWidth = Math.max(0, Math.max(currentRight, targetRight) - envelopeX);
|
|
if (fullHeightSurface) {
|
|
_setSurfaceGeometry(envelopeX, alignedY, envelopeWidth, alignedHeight);
|
|
surfaceSettleTimer.restart();
|
|
return;
|
|
}
|
|
|
|
const currentY = renderedAlignedY;
|
|
const currentBottom = renderedAlignedY + renderedAlignedHeight;
|
|
const targetY = alignedY;
|
|
const targetBottom = alignedY + alignedHeight;
|
|
const envelopeY = Math.min(currentY, targetY);
|
|
const envelopeBottom = Math.max(currentBottom, targetBottom);
|
|
_setSurfaceGeometry(envelopeX, envelopeY, envelopeWidth, Math.max(0, envelopeBottom - envelopeY));
|
|
surfaceSettleTimer.restart();
|
|
}
|
|
|
|
function updateSurfacePosition() {
|
|
_setSettledSurfaceGeometry();
|
|
}
|
|
|
|
function _connectedAlignedXFor(width) {
|
|
const popupGap = _popupGapValue();
|
|
const edgeGapLeft = _edgeGapFor("left", popupGap);
|
|
const edgeGapRight = _edgeGapFor("right", popupGap);
|
|
const anchorX = root.usesConnectedSurfaceChrome ? connectedAnchorX : triggerX;
|
|
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Left:
|
|
return Math.max(popupGap, Math.min(screenWidth - width - edgeGapRight, anchorX));
|
|
case SettingsData.Position.Right:
|
|
return Math.max(edgeGapLeft, Math.min(screenWidth - width - popupGap, anchorX - width));
|
|
default:
|
|
const rawX = triggerX + (triggerWidth / 2) - (width / 2);
|
|
const clearLeft = adjacentBarClearance(adjacentBarInfo.leftBar);
|
|
const clearRight = adjacentBarClearance(adjacentBarInfo.rightBar);
|
|
const minX = Math.max(edgeGapLeft, clearLeft);
|
|
const maxX = screenWidth - width - Math.max(edgeGapRight, clearRight);
|
|
return _snapNearFrameBound(Math.max(minX, Math.min(maxX, rawX)), minX, maxX, edgeGapLeft >= clearLeft, edgeGapRight >= clearRight);
|
|
}
|
|
}
|
|
|
|
function _connectedAlignedY(overTop, overBottom) {
|
|
const popupGap = _popupGapValue();
|
|
const edgeGapTop = _edgeGapFor("top", popupGap);
|
|
const edgeGapBottom = _edgeGapFor("bottom", popupGap);
|
|
const anchorY = root.usesConnectedSurfaceChrome ? connectedAnchorY : triggerY;
|
|
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Bottom:
|
|
return Math.max(edgeGapTop + overTop, Math.min(screenHeight - popupHeight - overBottom - popupGap, anchorY - popupHeight - overBottom));
|
|
case SettingsData.Position.Top:
|
|
return Math.max(popupGap + overTop, Math.min(screenHeight - popupHeight - overBottom - edgeGapBottom, anchorY + overTop));
|
|
default:
|
|
const rawY = triggerY - (popupHeight / 2);
|
|
const clearTop = adjacentBarClearance(adjacentBarInfo.topBar);
|
|
const clearBottom = adjacentBarClearance(adjacentBarInfo.bottomBar);
|
|
const minY = Math.max(edgeGapTop, clearTop) + overTop;
|
|
const maxY = screenHeight - popupHeight - overBottom - Math.max(edgeGapBottom, clearBottom);
|
|
return _snapNearFrameBound(Math.max(minY, Math.min(maxY, rawY)), minY, maxY, edgeGapTop >= clearTop, edgeGapBottom >= clearBottom);
|
|
}
|
|
}
|
|
|
|
function _standalonePopupGap() {
|
|
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
|
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
|
return useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
|
}
|
|
|
|
function _standaloneAlignedXFor(width) {
|
|
const popupGap = _standalonePopupGap();
|
|
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 - width - rightGap, triggerX));
|
|
case SettingsData.Position.Right:
|
|
return Math.max(leftGap, Math.min(screenWidth - width - rightGap, triggerX - width));
|
|
default:
|
|
const rawX = triggerX + (triggerWidth / 2) - (width / 2);
|
|
const minX = leftGap;
|
|
const maxX = screenWidth - width - rightGap;
|
|
return Math.max(minX, Math.min(maxX, rawX));
|
|
}
|
|
}
|
|
|
|
function _standaloneAlignedY(overTop, overBottom) {
|
|
const popupGap = _standalonePopupGap();
|
|
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 + overTop, Math.min(screenHeight - popupHeight - overBottom - bottomGap, triggerY - popupHeight - overBottom));
|
|
case SettingsData.Position.Top:
|
|
return Math.max(topGap + overTop, Math.min(screenHeight - popupHeight - overBottom - bottomGap, triggerY + overTop));
|
|
default:
|
|
const rawY = triggerY - (popupHeight / 2);
|
|
const minY = topGap + overTop;
|
|
const maxY = screenHeight - popupHeight - overBottom - bottomGap;
|
|
return Math.max(minY, Math.min(maxY, rawY));
|
|
}
|
|
}
|
|
|
|
function alignedXFor(width) {
|
|
return Theme.snap(connected ? _connectedAlignedXFor(width) : _standaloneAlignedXFor(width), dpr);
|
|
}
|
|
|
|
readonly property real alignedX: alignedXFor(popupWidth)
|
|
|
|
function alignedYFor(overTop, overBottom) {
|
|
return Theme.snap(connected ? _connectedAlignedY(overTop, overBottom) : _standaloneAlignedY(overTop, overBottom), dpr);
|
|
}
|
|
|
|
readonly property real alignedY: alignedYFor(topOverflow, bottomOverflow)
|
|
|
|
function _maxBodyWidthFor(startGap, endGap, anchorX) {
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Left:
|
|
return screenWidth - anchorX - endGap;
|
|
case SettingsData.Position.Right:
|
|
return anchorX - startGap;
|
|
default:
|
|
return screenWidth - startGap - endGap;
|
|
}
|
|
}
|
|
|
|
function _maxBodyHeightFor(startGap, endGap, anchorY) {
|
|
switch (effectiveBarPosition) {
|
|
case SettingsData.Position.Top:
|
|
return screenHeight - anchorY - endGap;
|
|
case SettingsData.Position.Bottom:
|
|
return anchorY - startGap;
|
|
default:
|
|
return screenHeight - startGap - endGap;
|
|
}
|
|
}
|
|
|
|
readonly property real maxBodyWidth: {
|
|
if (connected) {
|
|
const popupGap = _popupGapValue();
|
|
const startGap = Math.max(_edgeGapFor("left", popupGap), adjacentBarClearance(adjacentBarInfo.leftBar));
|
|
const endGap = Math.max(_edgeGapFor("right", popupGap), adjacentBarClearance(adjacentBarInfo.rightBar));
|
|
return Math.max(0, _maxBodyWidthFor(startGap, endGap, usesConnectedSurfaceChrome ? connectedAnchorX : triggerX));
|
|
}
|
|
const popupGap = _standalonePopupGap();
|
|
const startGap = _edgeClearance("left", popupGap, Math.max(0, adjacentBarInfo.leftBar));
|
|
const endGap = _edgeClearance("right", popupGap, Math.max(0, adjacentBarInfo.rightBar));
|
|
return Math.max(0, _maxBodyWidthFor(startGap, endGap, triggerX));
|
|
}
|
|
|
|
readonly property real maxBodyHeight: {
|
|
if (connected) {
|
|
const popupGap = _popupGapValue();
|
|
const startGap = Math.max(_edgeGapFor("top", popupGap), adjacentBarClearance(adjacentBarInfo.topBar));
|
|
const endGap = Math.max(_edgeGapFor("bottom", popupGap), adjacentBarClearance(adjacentBarInfo.bottomBar));
|
|
return Math.max(0, _maxBodyHeightFor(startGap, endGap, usesConnectedSurfaceChrome ? connectedAnchorY : triggerY));
|
|
}
|
|
const popupGap = _standalonePopupGap();
|
|
const startGap = _edgeClearance("top", popupGap, Math.max(0, adjacentBarInfo.topBar));
|
|
const endGap = _edgeClearance("bottom", popupGap, Math.max(0, adjacentBarInfo.bottomBar));
|
|
return Math.max(0, _maxBodyHeightFor(startGap, endGap, triggerY));
|
|
}
|
|
|
|
readonly property vector4d surfaceCornerRadii: chromeLoader.item?.surfaceCornerRadii ?? Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius)
|
|
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
|
|
}
|
|
|
|
Loader {
|
|
id: backgroundLayer
|
|
active: !root.connected
|
|
sourceComponent: Item {
|
|
id: background
|
|
|
|
readonly property alias window: backgroundWindow
|
|
readonly property bool dismissRequired: root.backgroundInteractive
|
|
readonly property bool required: dismissRequired || root.overlayContent !== null
|
|
readonly property bool overlayActive: overlayLoader.item?.visible ?? false
|
|
// 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
|
|
// Keeps the contentHoleRect carve-out tracking the body so clicks in newly grown areas do not dismiss.
|
|
property bool commitWindow: false
|
|
// An idle layer surface won't commit the cleared blur region on auto-close; pulse updatesEnabled to force it.
|
|
property bool blurCommitSuppress: false
|
|
|
|
function show() {
|
|
if (required)
|
|
backgroundWindow.visible = true;
|
|
holdCommits();
|
|
}
|
|
|
|
function holdCommits() {
|
|
commitWindow = true;
|
|
commitSettleTimer.restart();
|
|
}
|
|
|
|
function surfaceMoved() {
|
|
if (!backgroundWindow.visible)
|
|
return;
|
|
holdCommits();
|
|
if (typeof backgroundWindow.update === "function")
|
|
backgroundWindow.update();
|
|
}
|
|
|
|
function pulseBlurCommit() {
|
|
if (!backgroundWindow.visible)
|
|
return;
|
|
blurCommitSuppress = true;
|
|
blurCommitPulseTimer.restart();
|
|
// Land the off->on toggle on a commit even after overlayActive has dropped.
|
|
holdCommits();
|
|
}
|
|
|
|
onRequiredChanged: {
|
|
if (root.shouldBeVisible)
|
|
backgroundWindow.visible = required;
|
|
}
|
|
|
|
Timer {
|
|
id: commitSettleTimer
|
|
interval: 250
|
|
onTriggered: background.commitWindow = false
|
|
}
|
|
|
|
Timer {
|
|
id: blurCommitPulseTimer
|
|
interval: 16
|
|
onTriggered: background.blurCommitSuppress = false
|
|
}
|
|
|
|
Connections {
|
|
target: overlayLoader.item
|
|
ignoreUnknownSignals: true
|
|
function onOverlayBlurActiveChanged() {
|
|
background.pulseBlurCommit();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root
|
|
function onOpened() {
|
|
background.frozenMaskX = root.maskX;
|
|
background.frozenMaskY = root.maskY;
|
|
background.frozenMaskWidth = root.maskWidth;
|
|
background.frozenMaskHeight = root.maskHeight;
|
|
if (contentWindow.visible && background.required)
|
|
backgroundWindow.visible = true;
|
|
background.holdCommits();
|
|
}
|
|
}
|
|
|
|
readonly property bool contentWindowVisible: contentWindow.visible
|
|
|
|
onContentWindowVisibleChanged: {
|
|
if (!contentWindowVisible)
|
|
backgroundWindow.visible = false;
|
|
}
|
|
|
|
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.
|
|
updatesEnabled: !background.blurCommitSuppress && (background.overlayActive || background.commitWindow)
|
|
|
|
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: background.dismissRequired ? background.frozenMaskX : 0
|
|
y: background.dismissRequired ? background.frozenMaskY : 0
|
|
width: (background.dismissRequired && root.shouldBeVisible && root.backgroundInteractive) ? background.frozenMaskWidth : 0
|
|
height: (background.dismissRequired && root.shouldBeVisible && root.backgroundInteractive) ? background.frozenMaskHeight : 0
|
|
}
|
|
|
|
Rectangle {
|
|
id: contentHoleRect
|
|
visible: false
|
|
color: "transparent"
|
|
x: background.dismissRequired ? root.renderedAlignedX - root.inputMargin : 0
|
|
y: background.dismissRequired ? root._surfaceBodyY - root.inputMargin : 0
|
|
width: (background.dismissRequired && root.shouldBeVisible) ? root.renderedAlignedWidth + root.inputMargin * 2 : 0
|
|
height: (background.dismissRequired && root.shouldBeVisible) ? root._surfaceBodyH + root.inputMargin * 2 : 0
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: false
|
|
enabled: background.dismissRequired && root.shouldBeVisible && root.backgroundInteractive
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: root.backgroundClicked()
|
|
}
|
|
|
|
HoverHandler {
|
|
enabled: root.hoverDismissEnabled && root.shouldBeVisible
|
|
|
|
function notePointer() {
|
|
if (!hovered)
|
|
return;
|
|
PopoutManager.updateHoverCursor(point.position.x, point.position.y);
|
|
hoverDismissController.notePointerMoved();
|
|
}
|
|
|
|
onHoveredChanged: notePointer()
|
|
onPointChanged: notePointer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PanelWindow {
|
|
id: contentWindow
|
|
screen: root.surfaceScreen
|
|
visible: false
|
|
color: "transparent"
|
|
onVisibleChanged: {
|
|
if (!visible) {
|
|
root._surfaceFrameReady = false;
|
|
root._pendingSurfaceX = null;
|
|
root._pendingSurfaceY = null;
|
|
surfaceStepTimerX.stop();
|
|
surfaceStepTimerY.stop();
|
|
if (!root.shouldBeVisible)
|
|
root._contentRenderActive = false;
|
|
if (Qt.inputMethod) {
|
|
Qt.inputMethod.hide();
|
|
Qt.inputMethod.reset();
|
|
}
|
|
}
|
|
if (!root.connected)
|
|
return;
|
|
if (visible)
|
|
root._publishConnectedChromeState(true);
|
|
else
|
|
root._releaseConnectedChromeState();
|
|
}
|
|
|
|
Connections {
|
|
target: contentContainer.Window.window
|
|
enabled: contentWindow.visible && !root._surfaceFrameReady
|
|
|
|
function onFrameSwapped() {
|
|
root._surfaceFrameReady = true;
|
|
}
|
|
}
|
|
|
|
PopoutHoverDismiss {
|
|
id: hoverDismissController
|
|
anchors.fill: parent
|
|
dismissEnabled: root.hoverDismissEnabled
|
|
dismissSuspended: root.hoverDismissSuspended
|
|
surfaceVisible: root.shouldBeVisible
|
|
globalOffsetX: root._surfaceOriginX
|
|
globalOffsetY: root._surfaceOriginY
|
|
onDismissRequested: root.closeFromHoverDismiss()
|
|
}
|
|
|
|
WlrLayershell.namespace: root.layerNamespace
|
|
WlrLayershell.layer: root.effectivePopoutLayer
|
|
WlrLayershell.exclusiveZone: -1
|
|
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(root.shouldBeVisible && (root._keyboardReady || !root.connected), root.customKeyboardFocus)
|
|
|
|
anchors {
|
|
left: root.connected || !root._anchorEndX
|
|
top: root.connected || root.fullHeightSurface || !root._anchorEndY
|
|
right: root.connected || root._anchorEndX
|
|
bottom: root.connected || root.fullHeightSurface || root._anchorEndY
|
|
}
|
|
|
|
WlrLayershell.margins {
|
|
left: root.connected || root._anchorEndX ? 0 : root._surfaceX
|
|
top: root.connected || root.fullHeightSurface || root._anchorEndY ? 0 : root._surfaceY
|
|
right: !root.connected && root._anchorEndX ? root.screenWidth - root._surfaceX - root._surfaceW : 0
|
|
bottom: !root.connected && !root.fullHeightSurface && root._anchorEndY ? root.screenHeight - root._surfaceY - root._surfaceH : 0
|
|
}
|
|
|
|
implicitWidth: root.connected ? 0 : root._surfaceW
|
|
implicitHeight: root.connected || root.fullHeightSurface ? 0 : root._surfaceH
|
|
|
|
mask: contentInputMask
|
|
|
|
Region {
|
|
id: contentInputMask
|
|
item: (root.connected && root.shouldBeVisible && root.backgroundInteractive) ? backgroundDismissalMask : contentMaskRect
|
|
}
|
|
|
|
Item {
|
|
id: backgroundDismissalMask
|
|
visible: false
|
|
x: root.maskX
|
|
y: root.maskY
|
|
width: root.maskWidth
|
|
height: root.maskHeight
|
|
}
|
|
|
|
// Empty input region to avoid the animating surface taking pointer events
|
|
Item {
|
|
id: contentMaskRect
|
|
visible: false
|
|
x: contentContainer.x - contentContainer.horizontalConnectorExtent - root.inputMargin
|
|
y: contentContainer.y - contentContainer.verticalConnectorExtent - root.inputMargin
|
|
width: root.shouldBeVisible ? root.renderedAlignedWidth + contentContainer.horizontalConnectorExtent * 2 + root.inputMargin * 2 : 0
|
|
height: root.shouldBeVisible ? root.renderedAlignedHeight + contentContainer.verticalConnectorExtent * 2 + root.inputMargin * 2 : 0
|
|
}
|
|
|
|
Loader {
|
|
z: -1
|
|
anchors.fill: parent
|
|
active: root.connected
|
|
sourceComponent: Item {
|
|
id: backgroundClickCatcher
|
|
enabled: root.shouldBeVisible && root.backgroundInteractive
|
|
|
|
// Four edge strips that exclude the popup body, so cursor shapes
|
|
// inside the content propagate correctly (full-screen MouseAreas
|
|
// at z:-1 can suppress child cursorShape on Wayland).
|
|
MouseArea {
|
|
x: 0
|
|
y: 0
|
|
width: parent.width
|
|
height: Math.max(0, root.renderedAlignedY - root.topOverflow)
|
|
enabled: parent.enabled
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: root.backgroundClicked()
|
|
}
|
|
MouseArea {
|
|
x: 0
|
|
y: root.renderedAlignedY + root.renderedAlignedHeight + root.bottomOverflow
|
|
width: parent.width
|
|
height: Math.max(0, parent.height - y)
|
|
enabled: parent.enabled
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: root.backgroundClicked()
|
|
}
|
|
MouseArea {
|
|
x: 0
|
|
y: root.renderedAlignedY - root.topOverflow
|
|
width: root.renderedAlignedX
|
|
height: root.renderedAlignedHeight + root.topOverflow + root.bottomOverflow
|
|
enabled: parent.enabled
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: root.backgroundClicked()
|
|
}
|
|
MouseArea {
|
|
x: root.renderedAlignedX + root.renderedAlignedWidth
|
|
y: root.renderedAlignedY - root.topOverflow
|
|
width: Math.max(0, parent.width - root.renderedAlignedX - root.renderedAlignedWidth)
|
|
height: root.renderedAlignedHeight + root.topOverflow + root.bottomOverflow
|
|
enabled: parent.enabled
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: root.backgroundClicked()
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: contentContainer
|
|
clip: root.morphTravelEnabled
|
|
x: (root.morphTravelEnabled ? Theme.snap(root.pubBodyX, root.dpr) : root.renderedAlignedX) - root._surfaceOriginX
|
|
y: (root.morphTravelEnabled ? Theme.snap(root.pubBodyY, root.dpr) : root.renderedAlignedY) - root._surfaceOriginY
|
|
width: root.morphTravelEnabled ? Theme.snap(root.pubBodyW, root.dpr) : root.renderedAlignedWidth
|
|
height: root.morphTravelEnabled ? Theme.snap(root.pubBodyH, root.dpr) : 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.renderedAlignedWidth + Theme.spacingL)
|
|
readonly property real directionalTravelY: Math.max(root.animationOffset, root.renderedAlignedHeight + 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 horizontalConnectorExtent: root.usesConnectedSurfaceChrome && (barTop || barBottom) ? Theme.connectedCornerRadius : 0
|
|
readonly property real verticalConnectorExtent: root.usesConnectedSurfaceChrome && (barLeft || barRight) ? Theme.connectedCornerRadius : 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
|
|
readonly property real morphTravelPx: Math.max(1, Math.abs(offsetX), Math.abs(offsetY), (1 - computedScaleCollapsed) * Math.max(root.alignedWidth, root.alignedHeight), root._fluidMotionActive ? Math.max(root.alignedWidth, root.alignedHeight) : 0)
|
|
|
|
PopoutHoverBodyTracker {
|
|
controller: hoverDismissController
|
|
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
|
|
}
|
|
|
|
SpringMotion {
|
|
id: morph
|
|
onRunningChanged: {
|
|
if (!running && (Theme.isDirectionalEffect || root._fluidMotionActive) && root.isClosing && !root.shouldBeVisible)
|
|
root.closeAnimationFinished();
|
|
}
|
|
enabled: root.animationsEnabled && !(root.connected && root._fluidMotionActive)
|
|
reducedMotion: root.animationDuration <= 0
|
|
positionEpsilon: Math.max(0.001, 0.25 / root.dpr / contentContainer.morphTravelPx)
|
|
velocityEpsilon: positionEpsilon * damping / Math.max(0.001, 2 * mass)
|
|
stiffness: root._geometrySpringParams.stiffness
|
|
damping: root._geometrySpringParams.damping
|
|
|
|
Component.onCompleted: snapTo(root.shouldBeVisible ? 1 : 0)
|
|
}
|
|
readonly property bool morphSettled: !morph.running || Math.abs(morph.value - morph.target) < Theme.morphLayerEpsilon
|
|
readonly property real morphProgress: root.connected || !morphSettled ? morph.value : morph.target
|
|
readonly property real animX: root._fluidMotionActive ? 0 : contentContainer.offsetX * (1 - morphProgress)
|
|
readonly property real animY: root._fluidMotionActive ? 0 : contentContainer.offsetY * (1 - morphProgress)
|
|
onAnimXChanged: {
|
|
if (!root.connected)
|
|
return;
|
|
root._syncPopoutAnim("x");
|
|
}
|
|
onAnimYChanged: {
|
|
if (!root.connected)
|
|
return;
|
|
root._syncPopoutAnim("y");
|
|
}
|
|
readonly property real scaleValue: root._fluidMotionActive ? 1 : contentContainer.computedScaleCollapsed + (1.0 - contentContainer.computedScaleCollapsed) * morphProgress
|
|
|
|
Component.onCompleted: root._captureChromeAnimTravel()
|
|
|
|
readonly property bool rootShouldBeVisible: root.shouldBeVisible
|
|
|
|
onRootShouldBeVisibleChanged: {
|
|
root._captureChromeAnimTravel();
|
|
// Skip reverse emerge animation during a superseded close.
|
|
morph.retarget((rootShouldBeVisible || root._supersededClose) ? 1 : 0);
|
|
}
|
|
|
|
Loader {
|
|
id: chromeLoader
|
|
anchors.fill: parent
|
|
sourceComponent: root.connected ? connectedChrome : standaloneChrome
|
|
}
|
|
|
|
Loader {
|
|
id: contentLoader
|
|
parent: chromeLoader.item ? chromeLoader.item.contentWrapper : contentContainer
|
|
anchors.fill: parent
|
|
sourceComponent: root.popoutHandle.content
|
|
// _contentWarm keeps the tree loaded across close for fast re-open; reclaimed by PopoutService on lock/idle.
|
|
active: root._primeContent || root.shouldBeVisible || contentWindow.visible || root._contentWarm
|
|
asynchronous: root._backgroundWarm && !root._primeContent && !root.shouldBeVisible && !contentWindow.visible
|
|
}
|
|
}
|
|
|
|
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) {
|
|
root.close();
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: overlayLoader
|
|
parent: root.backgroundWindow ? root.backgroundWindow.contentItem : root
|
|
z: 1
|
|
anchors.fill: parent
|
|
active: root.overlayContent !== null && (root.backgroundWindow?.visible ?? false)
|
|
sourceComponent: root.overlayContent
|
|
}
|
|
|
|
Component {
|
|
id: connectedChrome
|
|
|
|
Item {
|
|
id: chrome
|
|
|
|
readonly property alias contentWrapper: contentWrapper
|
|
readonly property alias blur: popoutBlur
|
|
readonly property real surfaceRadius: root.usesConnectedSurfaceChrome ? Theme.connectedSurfaceRadius : Theme.windowRadius
|
|
readonly property color surfaceColor: root.usesConnectedSurfaceChrome ? Theme.connectedSurfaceColor : Theme.readableSurface
|
|
readonly property color surfaceBorderColor: root.usesConnectedSurfaceChrome ? Theme.withAlpha(BlurService.borderColor, 0) : BlurService.borderColor
|
|
readonly property real surfaceBorderWidth: root.usesConnectedSurfaceChrome ? 0 : BlurService.borderWidth
|
|
readonly property real surfaceTopLeftRadius: root.usesConnectedSurfaceChrome && (contentContainer.barTop || contentContainer.barLeft) ? 0 : surfaceRadius
|
|
readonly property real surfaceTopRightRadius: root.usesConnectedSurfaceChrome && (contentContainer.barTop || contentContainer.barRight) ? 0 : surfaceRadius
|
|
readonly property real surfaceBottomLeftRadius: root.usesConnectedSurfaceChrome && (contentContainer.barBottom || contentContainer.barLeft) ? 0 : surfaceRadius
|
|
readonly property real surfaceBottomRightRadius: root.usesConnectedSurfaceChrome && (contentContainer.barBottom || contentContainer.barRight) ? 0 : surfaceRadius
|
|
readonly property vector4d surfaceCornerRadii: Qt.vector4d(surfaceTopLeftRadius, surfaceTopRightRadius, surfaceBottomRightRadius, surfaceBottomLeftRadius)
|
|
|
|
WindowBlur {
|
|
id: popoutBlur
|
|
targetWindow: contentWindow
|
|
blurEnabled: !root.frameOwnsConnectedChrome && (!root.usesConnectedSurfaceChrome || Theme.connectedSurfaceBlurEnabled)
|
|
surfaceColor: chrome.surfaceColor
|
|
|
|
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
|
readonly property bool trackBlurFromBarEdge: root.usesConnectedSurfaceChrome
|
|
|
|
readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) : 0
|
|
readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight) ? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width)) : 0
|
|
|
|
blurX: trackBlurFromBarEdge ? contentContainer.x + (contentContainer.barRight ? _dxClamp : 0) : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) - contentContainer.horizontalConnectorExtent * s
|
|
blurY: (trackBlurFromBarEdge ? contentContainer.y + (contentContainer.barBottom ? _dyClamp : 0) : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - contentContainer.verticalConnectorExtent * s) - root.topOverflow
|
|
blurWidth: root.shouldBeVisible ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.width - Math.abs(_dxClamp)) : (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s) : 0
|
|
blurHeight: root.shouldBeVisible ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.height - Math.abs(_dyClamp)) : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s) + root.topOverflow + root.bottomOverflow : 0
|
|
blurRadius: root.usesConnectedSurfaceChrome ? Theme.connectedCornerRadius : Theme.windowRadius
|
|
}
|
|
|
|
Item {
|
|
id: directionalClipMask
|
|
|
|
visible: !root._surfaceSwitching
|
|
readonly property bool shouldClip: Theme.isDirectionalEffect || root.usesConnectedSurfaceChrome
|
|
readonly property real clipOversize: 1000
|
|
// inputMargin is only non-zero while a grid is being edited; let its chrome cross the connected edge instead of clipping it.
|
|
readonly property real connectedClipAllowance: {
|
|
if (!root.usesConnectedSurfaceChrome)
|
|
return 0;
|
|
if (root.frameOwnsConnectedChrome)
|
|
return root.inputMargin;
|
|
return -Theme.connectedCornerRadius + root.inputMargin;
|
|
}
|
|
|
|
clip: shouldClip
|
|
|
|
x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0
|
|
y: shouldClip ? (contentContainer.barTop ? -connectedClipAllowance : -clipOversize) : 0
|
|
|
|
width: {
|
|
if (!shouldClip)
|
|
return parent.width;
|
|
if (contentContainer.barLeft)
|
|
return parent.width + connectedClipAllowance + clipOversize;
|
|
if (contentContainer.barRight)
|
|
return parent.width + clipOversize + connectedClipAllowance;
|
|
return parent.width + clipOversize * 2;
|
|
}
|
|
height: {
|
|
if (!shouldClip)
|
|
return parent.height;
|
|
if (contentContainer.barTop)
|
|
return parent.height + connectedClipAllowance + clipOversize;
|
|
if (contentContainer.barBottom)
|
|
return parent.height + clipOversize + connectedClipAllowance;
|
|
return parent.height + clipOversize * 2;
|
|
}
|
|
|
|
Item {
|
|
id: rollOutAdjuster
|
|
readonly property real baseWidth: root.renderedAlignedWidth
|
|
readonly property real baseHeight: root.renderedAlignedHeight
|
|
|
|
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
|
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
|
width: baseWidth
|
|
height: baseHeight
|
|
|
|
clip: false
|
|
|
|
ElevationShadow {
|
|
visible: !root.usesConnectedSurfaceChrome
|
|
width: rollOutAdjuster.baseWidth
|
|
height: rollOutAdjuster.baseHeight
|
|
opacity: contentWrapper.publishedOpacity
|
|
scale: contentWrapper.scale
|
|
x: contentWrapper.x
|
|
y: contentWrapper.y
|
|
level: root.shadowLevel
|
|
direction: root.effectiveShadowDirection
|
|
fallbackOffset: root.shadowFallbackOffset
|
|
targetRadius: chrome.surfaceRadius
|
|
topLeftRadius: chrome.surfaceTopLeftRadius
|
|
topRightRadius: chrome.surfaceTopRightRadius
|
|
bottomLeftRadius: chrome.surfaceBottomLeftRadius
|
|
bottomRightRadius: chrome.surfaceBottomRightRadius
|
|
targetColor: chrome.surfaceColor
|
|
borderColor: chrome.surfaceBorderColor
|
|
borderWidth: chrome.surfaceBorderWidth
|
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) && !root.frameOwnsConnectedChrome
|
|
}
|
|
|
|
Item {
|
|
id: localChrome
|
|
visible: root.usesLocalConnectedSurfaceChrome
|
|
|
|
readonly property real extraLeft: (contentContainer.barTop || contentContainer.barBottom) ? Theme.connectedCornerRadius : 0
|
|
readonly property real extraTop: (contentContainer.barLeft || contentContainer.barRight) ? Theme.connectedCornerRadius : 0
|
|
|
|
readonly property bool shadowsOn: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
|
readonly property real shadowBlurPx: root.shadowLevel && root.shadowLevel.blurPx !== undefined ? root.shadowLevel.blurPx : 0
|
|
readonly property real shadowSpreadPx: root.shadowLevel && root.shadowLevel.spreadPx !== undefined ? root.shadowLevel.spreadPx : 0
|
|
readonly property real shadowOffsetX: Theme.elevationOffsetXFor(root.shadowLevel, root.effectiveShadowDirection, root.shadowFallbackOffset)
|
|
readonly property real shadowOffsetY: Theme.elevationOffsetYFor(root.shadowLevel, root.effectiveShadowDirection, root.shadowFallbackOffset)
|
|
readonly property color shadowTint: Theme.elevationShadowColor(root.shadowLevel)
|
|
readonly property var ambient: Theme.elevationAmbient(root.shadowLevel)
|
|
readonly property real pad: shadowsOn ? Math.ceil(Math.max(shadowBlurPx + shadowSpreadPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)), ambient.blurPx + ambient.spreadPx) + 2) : 0
|
|
|
|
width: rollOutAdjuster.baseWidth + extraLeft * 2
|
|
height: rollOutAdjuster.baseHeight + extraTop * 2
|
|
opacity: contentWrapper.publishedOpacity
|
|
scale: contentWrapper.scale
|
|
x: contentWrapper.x - extraLeft
|
|
y: contentWrapper.y - extraTop
|
|
|
|
ShaderEffect {
|
|
anchors.fill: parent
|
|
anchors.topMargin: contentContainer.barTop ? 0 : -localChrome.pad
|
|
anchors.bottomMargin: contentContainer.barBottom ? 0 : -localChrome.pad
|
|
anchors.leftMargin: contentContainer.barLeft ? 0 : -localChrome.pad
|
|
anchors.rightMargin: contentContainer.barRight ? 0 : -localChrome.pad
|
|
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/connected_chrome.frag.qsb")
|
|
|
|
property real widthPx: width
|
|
property real heightPx: height
|
|
property vector4d surfaceColor: Qt.vector4d(chrome.surfaceColor.r, chrome.surfaceColor.g, chrome.surfaceColor.b, chrome.surfaceColor.a)
|
|
property vector4d shadowColor: Qt.vector4d(localChrome.shadowTint.r, localChrome.shadowTint.g, localChrome.shadowTint.b, localChrome.shadowsOn ? localChrome.shadowTint.a : 0)
|
|
property vector4d shadowParam: Qt.vector4d(Math.max(0, localChrome.shadowBlurPx), localChrome.shadowSpreadPx, localChrome.shadowOffsetX, localChrome.shadowOffsetY)
|
|
property vector4d ambientParam: Qt.vector4d(localChrome.ambient.blurPx, localChrome.ambient.spreadPx, localChrome.shadowsOn ? localChrome.ambient.alpha : 0, 0)
|
|
property vector4d bodyRect: Qt.vector4d((contentContainer.barLeft ? 0 : localChrome.pad) + localChrome.extraLeft, (contentContainer.barTop ? 0 : localChrome.pad) + localChrome.extraTop, rollOutAdjuster.baseWidth, rollOutAdjuster.baseHeight)
|
|
property vector4d cornerRadius: Qt.vector4d(chrome.surfaceTopLeftRadius, chrome.surfaceTopRightRadius, chrome.surfaceBottomRightRadius, chrome.surfaceBottomLeftRadius)
|
|
property vector4d edgeParam: Qt.vector4d(contentContainer.barTop ? 0 : (contentContainer.barBottom ? 1 : (contentContainer.barLeft ? 2 : 3)), Theme.connectedCornerRadius, 0, 0)
|
|
}
|
|
}
|
|
|
|
DLayer {
|
|
id: contentWrapper
|
|
width: rollOutAdjuster.baseWidth
|
|
height: rollOutAdjuster.baseHeight
|
|
|
|
property bool _animating: false
|
|
readonly property bool _fadeWithOpacity: (!root._fluidMotionActive && !Theme.isDirectionalEffect) || root._supersededClose
|
|
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
|
|
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (root._fluidMotionActive ? root.fluidContentOpacity : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)))
|
|
readonly property real publishedOpacity: opacity
|
|
|
|
opacity: _targetOpacity
|
|
visible: root._contentRenderActive
|
|
|
|
scale: root._supersededClose ? root._supersededContentScale : contentContainer.scaleValue
|
|
x: root._supersededClose ? root._supersededContentX : (root._fluidMotionActive ? root.renderedAlignedX - contentContainer.x : Theme.snap(contentContainer.animX, root.dpr))
|
|
y: root._supersededClose ? root._supersededContentY : (root._fluidMotionActive ? root.renderedAlignedY - contentContainer.y : Theme.snap(contentContainer.animY, root.dpr))
|
|
|
|
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
|
|
fallbackDevicePixelRatio: root.dpr
|
|
|
|
Behavior on opacity {
|
|
enabled: contentWrapper._fadeWithOpacity
|
|
NumberAnimation {
|
|
duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
onRunningChanged: {
|
|
contentWrapper._animating = running;
|
|
if (!running && !root.shouldBeVisible)
|
|
root._contentRenderActive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
clip: false
|
|
visible: !root.usesConnectedSurfaceChrome
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
antialiasing: true
|
|
topLeftRadius: chrome.surfaceTopLeftRadius
|
|
topRightRadius: chrome.surfaceTopRightRadius
|
|
bottomLeftRadius: chrome.surfaceBottomLeftRadius
|
|
bottomRightRadius: chrome.surfaceBottomRightRadius
|
|
color: chrome.surfaceColor
|
|
border.color: chrome.surfaceBorderColor
|
|
border.width: chrome.surfaceBorderWidth
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: standaloneChrome
|
|
|
|
Item {
|
|
id: chrome
|
|
|
|
readonly property alias contentWrapper: contentWrapper
|
|
readonly property alias blur: popoutBlur
|
|
readonly property rect fluidBody: root._fluidMotionActive ? FluidGeometry.limitOvershoot(FluidGeometry.interpolate(root.fluidCollapsedRect, root.fluidTarget, morph.value), root.fluidTarget, Theme.fluidOvershootLimit) : root.fluidTarget
|
|
readonly property real clampedAnimX: Math.max(-width, Math.min(contentContainer.animX, width))
|
|
readonly property real clampedAnimY: Math.max(-height, Math.min(contentContainer.animY, height))
|
|
readonly property real revealWidth: {
|
|
if (!root.directionalRevealActive)
|
|
return width;
|
|
if (contentContainer.barLeft)
|
|
return Theme.snap(Math.max(0, width + clampedAnimX), root.dpr);
|
|
if (contentContainer.barRight)
|
|
return Theme.snap(Math.max(0, width - clampedAnimX), root.dpr);
|
|
return width;
|
|
}
|
|
readonly property real revealHeight: {
|
|
if (!root.directionalRevealActive)
|
|
return height;
|
|
if (contentContainer.barTop)
|
|
return Theme.snap(Math.max(0, height + clampedAnimY), root.dpr);
|
|
if (contentContainer.barBottom)
|
|
return Theme.snap(Math.max(0, height - clampedAnimY), root.dpr);
|
|
return height;
|
|
}
|
|
readonly property real revealX: root.directionalRevealActive && contentContainer.barRight ? Theme.snap(width - revealWidth, root.dpr) : 0
|
|
readonly property real revealY: root.directionalRevealActive && contentContainer.barBottom ? Theme.snap(height - revealHeight, root.dpr) : 0
|
|
|
|
// WindowBlur property updates don't dirty the scene graph, so render a frame to ship the new region.
|
|
function kickBlurCommit() {
|
|
if (typeof contentWindow.update === "function")
|
|
contentWindow.update();
|
|
}
|
|
|
|
readonly property real morphValue: morph.value
|
|
readonly property real rootAlignedX: root.alignedX
|
|
readonly property real rootAlignedY: root.alignedY
|
|
readonly property real rootAlignedWidth: root.alignedWidth
|
|
readonly property real rootAlignedHeight: root.alignedHeight
|
|
readonly property bool rootShouldBeVisible: root.shouldBeVisible
|
|
|
|
onMorphValueChanged: kickBlurCommit()
|
|
onRootAlignedXChanged: kickBlurCommit()
|
|
onRootAlignedYChanged: kickBlurCommit()
|
|
onRootAlignedWidthChanged: kickBlurCommit()
|
|
onRootAlignedHeightChanged: kickBlurCommit()
|
|
onRootShouldBeVisibleChanged: kickBlurCommit()
|
|
|
|
WindowBlur {
|
|
id: popoutBlur
|
|
targetWindow: contentWindow
|
|
surfaceColor: Theme.readableSurface
|
|
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
|
readonly property real op: Math.max(0, Math.min(1, (morph.value - 0.08) * 1.6))
|
|
readonly property real visibleScale: s * op
|
|
readonly property bool revealClipActive: root.directionalRevealActive || root._fluidMotionActive
|
|
|
|
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)) - root.topOverflow
|
|
blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * visibleScale) : 0
|
|
blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * visibleScale) + root.topOverflow + root.bottomOverflow : 0
|
|
blurRadius: Theme.windowRadius
|
|
clipEnabled: revealClipActive
|
|
clipX: contentContainer.x + (root._fluidMotionActive ? chrome.fluidBody.x : chrome.revealX)
|
|
clipY: contentContainer.y + (root._fluidMotionActive ? chrome.fluidBody.y : chrome.revealY) - root.topOverflow
|
|
clipWidth: root.shouldBeVisible ? (root._fluidMotionActive ? chrome.fluidBody.width : chrome.revealWidth) : 0
|
|
clipHeight: root.shouldBeVisible ? (root._fluidMotionActive ? chrome.fluidBody.height : chrome.revealHeight) + root.topOverflow + root.bottomOverflow : 0
|
|
}
|
|
|
|
SurfaceContentClip {
|
|
id: contentClip
|
|
|
|
active: root._fluidMotionActive
|
|
revealActive: root.directionalRevealActive
|
|
body: chrome.fluidBody
|
|
targetRect: root.fluidTarget
|
|
revealX: chrome.revealX
|
|
revealY: chrome.revealY
|
|
revealWidth: chrome.revealWidth
|
|
revealHeight: chrome.revealHeight
|
|
topOverflow: root.topOverflow
|
|
bottomOverflow: root.bottomOverflow
|
|
|
|
// Covers overflow chrome so the shadow and border don't cross the seam.
|
|
ElevationShadow {
|
|
parent: root._fluidMotionActive ? chrome : contentClip.contentItem
|
|
z: root._fluidMotionActive ? -1 : 0
|
|
width: root._fluidMotionActive ? chrome.fluidBody.width : chrome.width
|
|
height: root._fluidMotionActive ? chrome.fluidBody.height : chrome.height + root.topOverflow + root.bottomOverflow
|
|
opacity: root._fluidMotionActive ? FluidGeometry.chromeOpacity(morph.value) : contentWrapper.publishedOpacity
|
|
scale: root.directionalRevealActive ? 1 : contentWrapper.scale
|
|
x: root._fluidMotionActive ? chrome.fluidBody.x : (root.directionalRevealActive ? 0 : contentWrapper.x)
|
|
y: root._fluidMotionActive ? chrome.fluidBody.y : (root.directionalRevealActive ? 0 : contentWrapper.y) - root.topOverflow
|
|
level: root.shadowLevel
|
|
direction: root.effectiveShadowDirection
|
|
fallbackOffset: root.shadowFallbackOffset
|
|
targetRadius: Theme.windowRadius
|
|
targetColor: root._fluidMotionActive ? Theme.readableSurface : "transparent"
|
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
|
}
|
|
|
|
DLayer {
|
|
id: contentWrapper
|
|
width: chrome.width
|
|
height: chrome.height
|
|
|
|
property real publishedOpacity: Theme.isDirectionalEffect || root._fluidMotionActive ? 1 : (root.shouldBeVisible ? 1 : 0)
|
|
readonly property real settledOpacity: publishedOpacity >= 1 - Theme.morphSettleEpsilon ? 1 : Math.max(0, publishedOpacity)
|
|
|
|
opacity: root._fluidMotionActive || Theme.isDirectionalEffect ? 1 : settledOpacity
|
|
visible: root._contentRenderActive
|
|
scale: contentContainer.scaleValue
|
|
transformOrigin: Item.Center
|
|
x: root._fluidMotionActive ? 0 : contentContainer.animX + (chrome.width - width) * (1 - contentContainer.scaleValue) * 0.5
|
|
y: root._fluidMotionActive ? 0 : contentContainer.animY + (chrome.height - height) * (1 - contentContainer.scaleValue) * 0.5
|
|
|
|
layer.enabled: !Theme.isDirectionalEffect && !root._fluidMotionActive && root._contentRenderActive && (!contentContainer.morphSettled || opacity < 1)
|
|
layer.smooth: true
|
|
fallbackDevicePixelRatio: root.dpr
|
|
|
|
Behavior on publishedOpacity {
|
|
enabled: !Theme.isDirectionalEffect && !root._fluidMotionActive
|
|
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)
|
|
root._contentRenderActive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
visible: !root._fluidMotionActive
|
|
radius: Theme.windowRadius
|
|
color: Theme.readableSurface
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
parent: root._fluidMotionActive ? chrome : contentClip.contentItem
|
|
width: root._fluidMotionActive ? chrome.fluidBody.width : chrome.width
|
|
height: root._fluidMotionActive ? chrome.fluidBody.height : chrome.height + root.topOverflow + root.bottomOverflow
|
|
x: root._fluidMotionActive ? chrome.fluidBody.x : (root.directionalRevealActive ? 0 : contentWrapper.x)
|
|
y: root._fluidMotionActive ? chrome.fluidBody.y : (root.directionalRevealActive ? 0 : contentWrapper.y) - root.topOverflow
|
|
opacity: root._fluidMotionActive ? FluidGeometry.chromeOpacity(morph.value) : contentWrapper.publishedOpacity
|
|
scale: root.directionalRevealActive ? 1 : contentWrapper.scale
|
|
visible: contentWrapper.visible
|
|
radius: Theme.windowRadius
|
|
color: "transparent"
|
|
border.color: BlurService.borderColor
|
|
border.width: BlurService.borderWidth
|
|
z: 100
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|