1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-08 14:52:08 -04:00

(frame): implement ConnectedModeState to better handle component sync

This commit is contained in:
purian23
2026-04-07 23:28:00 -04:00
parent 4fa09d0cad
commit bd4eb0cea1
8 changed files with 1067 additions and 176 deletions

View File

@@ -0,0 +1,153 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property var emptyDockState: ({
"reveal": false,
"barSide": "bottom",
"bodyX": 0,
"bodyY": 0,
"bodyW": 0,
"bodyH": 0,
"slideX": 0,
"slideY": 0
})
// Popout state (updated by DankPopout when connectedFrameModeActive)
property string popoutOwnerToken: ""
property bool popoutVisible: false
property string popoutBarSide: "top"
property real popoutBodyX: 0
property real popoutBodyY: 0
property real popoutBodyW: 0
property real popoutBodyH: 0
property real popoutAnimX: 0
property real popoutAnimY: 0
property string popoutScreen: ""
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
property var dockStates: ({})
// Dock slide offsets — hot-path updates separated from full geometry state
property var dockSlides: ({})
function hasPopoutOwner(token) {
return !!token && popoutOwnerToken === token;
}
function claimPopout(token, state) {
if (!token)
return false;
popoutOwnerToken = token;
return updatePopout(token, state);
}
function updatePopout(token, state) {
if (!hasPopoutOwner(token) || !state)
return false;
if (state.visible !== undefined)
popoutVisible = !!state.visible;
if (state.barSide !== undefined)
popoutBarSide = state.barSide || "top";
if (state.bodyX !== undefined)
popoutBodyX = Number(state.bodyX);
if (state.bodyY !== undefined)
popoutBodyY = Number(state.bodyY);
if (state.bodyW !== undefined)
popoutBodyW = Number(state.bodyW);
if (state.bodyH !== undefined)
popoutBodyH = Number(state.bodyH);
if (state.animX !== undefined)
popoutAnimX = Number(state.animX);
if (state.animY !== undefined)
popoutAnimY = Number(state.animY);
if (state.screen !== undefined)
popoutScreen = state.screen || "";
return true;
}
function releasePopout(token) {
if (!hasPopoutOwner(token))
return false;
popoutOwnerToken = "";
popoutVisible = false;
popoutBarSide = "top";
popoutBodyX = 0;
popoutBodyY = 0;
popoutBodyW = 0;
popoutBodyH = 0;
popoutAnimX = 0;
popoutAnimY = 0;
popoutScreen = "";
return true;
}
function _cloneDockStates() {
const next = {};
for (const screenName in dockStates)
next[screenName] = dockStates[screenName];
return next;
}
function _normalizeDockState(state) {
return {
"reveal": !!(state && state.reveal),
"barSide": state && state.barSide ? state.barSide : "bottom",
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
"slideX": Number(state && state.slideX !== undefined ? state.slideX : 0),
"slideY": Number(state && state.slideY !== undefined ? state.slideY : 0)
};
}
function setDockState(screenName, state) {
if (!screenName || !state)
return false;
const next = _cloneDockStates();
next[screenName] = _normalizeDockState(state);
dockStates = next;
return true;
}
function clearDockState(screenName) {
if (!screenName || !dockStates[screenName])
return false;
const next = _cloneDockStates();
delete next[screenName];
dockStates = next;
// Also clear corresponding slide
if (dockSlides[screenName]) {
const nextSlides = {};
for (const k in dockSlides)
nextSlides[k] = dockSlides[k];
delete nextSlides[screenName];
dockSlides = nextSlides;
}
return true;
}
function setDockSlide(screenName, x, y) {
if (!screenName)
return false;
const next = {};
for (const k in dockSlides)
next[k] = dockSlides[k];
next[screenName] = { "x": Number(x), "y": Number(y) };
dockSlides = next;
return true;
}
}

View File

@@ -975,7 +975,7 @@ Singleton {
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
readonly property real connectedCornerRadius: {
if (typeof SettingsData === "undefined") return 12;
return SettingsData.frameEnabled ? SettingsData.frameRounding : cornerRadius;
return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
}
readonly property color connectedSurfaceColor: {
if (typeof SettingsData === "undefined")

View File

@@ -19,12 +19,12 @@ Variants {
WindowBlur {
targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x - dock.horizontalConnectorExtent
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y - dock.verticalConnectorExtent
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width + dock.horizontalConnectorExtent * 2 : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height + dock.verticalConnectorExtent * 2 : 0
blurRadius: dock.surfaceRadius
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius
}
WlrLayershell.namespace: "dms:dock"
@@ -43,21 +43,15 @@ Variants {
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top"
: SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom"
: SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect
&& !!(dock.screen || modelData)
&& SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide)
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect && !!(dock.screen || modelData) && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide)
readonly property real connectedJoinInset: {
if (!Theme.isConnectedEffect)
return 0;
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
}
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
readonly property color surfaceColor: Theme.isConnectedEffect
? Theme.connectedSurfaceColor
: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
@@ -188,6 +182,51 @@ Variants {
return placement === "left" ? seamY - height : seamY;
}
// ─── ConnectedModeState sync ────────────────────────────────────────
// Dock window origin in screen-relative coordinates (FrameWindow space).
function _dockWindowOriginX() {
if (!dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Right)
return (dock.screen ? dock.screen.width : 0) - dock.width;
return 0;
}
function _dockWindowOriginY() {
if (dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return (dock.screen ? dock.screen.height : 0) - dock.height;
return 0;
}
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
function _syncDockChromeState() {
if (!dock._dockScreenName)
return;
if (!SettingsData.connectedFrameModeActive) {
ConnectedModeState.clearDockState(dock._dockScreenName);
return;
}
ConnectedModeState.setDockState(dock._dockScreenName, {
"reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps,
"barSide": dock.connectedBarSide,
"bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x,
"bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y,
"bodyW": dock.hasApps ? dockBackground.width : 0,
"bodyH": dock.hasApps ? dockBackground.height : 0,
"slideX": dockSlide.x,
"slideY": dockSlide.y
});
}
function _syncDockSlide() {
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive)
return;
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
}
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool revealSticky: false
@@ -349,6 +388,23 @@ Variants {
}
}
Component.onCompleted: Qt.callLater(() => dock._syncDockChromeState())
Component.onDestruction: ConnectedModeState.clearDockState(dock._dockScreenName)
onRevealChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
onVisibleChanged: dock._syncDockChromeState()
onHasAppsChanged: dock._syncDockChromeState()
onConnectedBarSideChanged: dock._syncDockChromeState()
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
dock._syncDockChromeState();
}
}
Connections {
target: SettingsData
function onDockTransparencyChanged() {
@@ -573,6 +629,10 @@ Variants {
return 0;
if (dock.reveal)
return 0;
if (Theme.isConnectedEffect) {
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
}
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance;
@@ -585,6 +645,10 @@ Variants {
return 0;
if (dock.reveal)
return 0;
if (Theme.isConnectedEffect) {
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
}
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance;
@@ -596,28 +660,29 @@ Variants {
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: Theme.isConnectedEffect
? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal)
: Theme.shortDuration
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect
? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve)
: []
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
}
}
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: Theme.isConnectedEffect
? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal)
: Theme.shortDuration
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect
? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve)
: []
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
}
}
onXChanged: {
dock._syncDockSlide();
}
onYChanged: {
dock._syncDockSlide();
}
}
Item {
@@ -647,6 +712,7 @@ Variants {
Rectangle {
anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive
color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
@@ -656,6 +722,7 @@ Variants {
Rectangle {
anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive
color: "transparent"
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
@@ -665,26 +732,34 @@ Variants {
border.width: dock.surfaceBorderWidth
z: 100
}
// Sync dockBackground geometry to ConnectedModeState
onXChanged: dock._syncDockChromeState()
onYChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
}
ConnectedCorner {
visible: Theme.isConnectedEffect && dock.reveal
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
barSide: dock.connectedBarSide
placement: "left"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: dock.surfaceColor
dpr: dock._dpr
x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr)
y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr)
}
ConnectedCorner {
visible: Theme.isConnectedEffect && dock.reveal
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
barSide: dock.connectedBarSide
placement: "right"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: dock.surfaceColor
dpr: dock._dpr
x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr)
y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr)
}

View File

@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: win
@@ -26,8 +27,6 @@ PanelWindow {
}
color: "transparent"
// No input — pass everything through to apps and bar
mask: Region {}
readonly property var barEdges: {
@@ -36,20 +35,50 @@ PanelWindow {
}
readonly property real _dpr: CompositorService.getScreenScale(win.screen)
readonly property bool _frameActive: SettingsData.frameEnabled
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
readonly property int _windowRegionWidth: win._regionInt(win.width)
readonly property bool _frameActive: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
readonly property int _windowRegionWidth: win._regionInt(win.width)
readonly property int _windowRegionHeight: win._regionInt(win.height)
readonly property string _screenName: win.screen ? win.screen.name : ""
readonly property var _dockState: ConnectedModeState.dockStates[win._screenName] || ConnectedModeState.emptyDockState
readonly property var _dockSlide: ConnectedModeState.dockSlides[win._screenName] || ({
"x": 0,
"y": 0
})
// ─── Connected chrome convenience properties ──────────────────────────────
readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive
readonly property string _barSide: {
const edges = win.barEdges;
if (edges.includes("top"))
return "top";
if (edges.includes("bottom"))
return "bottom";
if (edges.includes("left"))
return "left";
return "right";
}
readonly property real _ccr: Theme.connectedCornerRadius
readonly property real _effectivePopoutCcr: {
const extent = win._popoutArcExtent();
const isHoriz = ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom";
const crossSize = isHoriz ? _popoutBodyBlurAnchor.width : _popoutBodyBlurAnchor.height;
return Math.max(0, Math.min(win._ccr, extent, crossSize / 2));
}
readonly property color _surfaceColor: Theme.connectedSurfaceColor
readonly property real _surfaceOpacity: _surfaceColor.a
readonly property color _opaqueSurfaceColor: Qt.rgba(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b, 1)
readonly property real _surfaceRadius: Theme.connectedSurfaceRadius
readonly property real _seamOverlap: Theme.hairline(win._dpr)
function _regionInt(value) {
return Math.max(0, Math.round(Theme.px(value, win._dpr)));
}
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
readonly property int cutoutRadius: {
const requested = win._regionInt(SettingsData.frameRounding);
@@ -57,7 +86,6 @@ PanelWindow {
return Math.max(0, Math.min(requested, maxRadius));
}
// Slightly expand the subtractive blur cutout at very low opacity levels
readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0
readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation)
readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation)
@@ -69,7 +97,7 @@ PanelWindow {
return Math.max(0, Math.min(requested, maxRadius));
}
// Must stay visible so Region.item can resolve scene coordinates.
// Invisible items providing scene coordinates for blur Region anchors
Item {
id: _blurCutout
x: win._blurCutoutLeft
@@ -78,48 +106,471 @@ PanelWindow {
height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop)
}
property var _frameBlurRegion: null
Item {
id: _popoutBodyBlurAnchor
visible: false
readonly property bool _active: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
readonly property real _dyClamp: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? Math.max(-ConnectedModeState.popoutBodyH, Math.min(ConnectedModeState.popoutAnimY, ConnectedModeState.popoutBodyH)) : 0
readonly property real _dxClamp: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? Math.max(-ConnectedModeState.popoutBodyW, Math.min(ConnectedModeState.popoutAnimX, ConnectedModeState.popoutBodyW)) : 0
x: _active ? ConnectedModeState.popoutBodyX + (ConnectedModeState.popoutBarSide === "right" ? _dxClamp : 0) : 0
y: _active ? ConnectedModeState.popoutBodyY + (ConnectedModeState.popoutBarSide === "bottom" ? _dyClamp : 0) : 0
width: _active ? Math.max(0, ConnectedModeState.popoutBodyW - Math.abs(_dxClamp)) : 0
height: _active ? Math.max(0, ConnectedModeState.popoutBodyH - Math.abs(_dyClamp)) : 0
}
Item {
id: _dockBodyBlurAnchor
visible: false
readonly property bool _active: win._dockState.reveal && win._dockState.bodyW > 0 && win._dockState.bodyH > 0
x: _active ? win._dockState.bodyX + (win._dockSlide.x || 0) : 0
y: _active ? win._dockState.bodyY + (win._dockSlide.y || 0) : 0
width: _active ? win._dockState.bodyW : 0
height: _active ? win._dockState.bodyH : 0
}
Item {
id: _popoutBodyBlurCap
opacity: 0
readonly property string _side: ConnectedModeState.popoutBarSide
readonly property real _capThickness: win._popoutBlurCapThickness()
readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(_capThickness, _popoutBodyBlurAnchor.width) : _popoutBodyBlurAnchor.width
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(_capThickness, _popoutBodyBlurAnchor.height) : _popoutBodyBlurAnchor.height
x: !_active ? 0 : (_side === "right" ? _popoutBodyBlurAnchor.x + _popoutBodyBlurAnchor.width - _capWidth : _popoutBodyBlurAnchor.x)
y: !_active ? 0 : (_side === "bottom" ? _popoutBodyBlurAnchor.y + _popoutBodyBlurAnchor.height - _capHeight : _popoutBodyBlurAnchor.y)
width: _active ? _capWidth : 0
height: _active ? _capHeight : 0
}
Item {
id: _dockBodyBlurCap
opacity: 0
readonly property string _side: win._dockState.barSide
readonly property bool _active: _dockBodyBlurAnchor._active && _dockBodyBlurAnchor.width > 0 && _dockBodyBlurAnchor.height > 0
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.width) : _dockBodyBlurAnchor.width
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.height) : _dockBodyBlurAnchor.height
x: !_active ? 0 : (_side === "right" ? _dockBodyBlurAnchor.x + _dockBodyBlurAnchor.width - _capWidth : _dockBodyBlurAnchor.x)
y: !_active ? 0 : (_side === "bottom" ? _dockBodyBlurAnchor.y + _dockBodyBlurAnchor.height - _capHeight : _dockBodyBlurAnchor.y)
width: _active ? _capWidth : 0
height: _active ? _capHeight : 0
}
Item {
id: _dockLeftConnectorBlurAnchor
opacity: 0
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
readonly property real _w: win._dockConnectorWidth(0)
readonly property real _h: win._dockConnectorHeight(0)
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0), win._dpr) : 0
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _dockRightConnectorBlurAnchor
opacity: 0
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
readonly property real _w: win._dockConnectorWidth(0)
readonly property real _h: win._dockConnectorHeight(0)
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0), win._dpr) : 0
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _dockLeftConnectorCutout
opacity: 0
readonly property bool _active: _dockLeftConnectorBlurAnchor.width > 0 && _dockLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "left")
x: _active ? win._connectorCutoutX(_dockLeftConnectorBlurAnchor.x, _dockLeftConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_dockLeftConnectorBlurAnchor.y, _dockLeftConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
width: _active ? win._dockConnectorRadius() * 2 : 0
height: _active ? win._dockConnectorRadius() * 2 : 0
}
Item {
id: _dockRightConnectorCutout
opacity: 0
readonly property bool _active: _dockRightConnectorBlurAnchor.width > 0 && _dockRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "right")
x: _active ? win._connectorCutoutX(_dockRightConnectorBlurAnchor.x, _dockRightConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_dockRightConnectorBlurAnchor.y, _dockRightConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
width: _active ? win._dockConnectorRadius() * 2 : 0
height: _active ? win._dockConnectorRadius() * 2 : 0
}
Item {
id: _popoutLeftConnectorBlurAnchor
opacity: 0
readonly property bool _active: win._popoutArcVisible()
readonly property real _w: win._popoutConnectorWidth(0)
readonly property real _h: win._popoutConnectorHeight(0)
x: _active ? Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "left", 0), win._dpr) : 0
y: _active ? Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "left", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _popoutRightConnectorBlurAnchor
opacity: 0
readonly property bool _active: win._popoutArcVisible()
readonly property real _w: win._popoutConnectorWidth(0)
readonly property real _h: win._popoutConnectorHeight(0)
x: _active ? Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "right", 0), win._dpr) : 0
y: _active ? Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "right", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _popoutLeftConnectorCutout
opacity: 0
readonly property bool _active: _popoutLeftConnectorBlurAnchor.width > 0 && _popoutLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(ConnectedModeState.popoutBarSide, "left")
x: _active ? win._connectorCutoutX(_popoutLeftConnectorBlurAnchor.x, _popoutLeftConnectorBlurAnchor.width, _arcCorner) : 0
y: _active ? win._connectorCutoutY(_popoutLeftConnectorBlurAnchor.y, _popoutLeftConnectorBlurAnchor.height, _arcCorner) : 0
width: _active ? win._effectivePopoutCcr * 2 : 0
height: _active ? win._effectivePopoutCcr * 2 : 0
}
Item {
id: _popoutRightConnectorCutout
opacity: 0
readonly property bool _active: _popoutRightConnectorBlurAnchor.width > 0 && _popoutRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(ConnectedModeState.popoutBarSide, "right")
x: _active ? win._connectorCutoutX(_popoutRightConnectorBlurAnchor.x, _popoutRightConnectorBlurAnchor.width, _arcCorner) : 0
y: _active ? win._connectorCutoutY(_popoutRightConnectorBlurAnchor.y, _popoutRightConnectorBlurAnchor.height, _arcCorner) : 0
width: _active ? win._effectivePopoutCcr * 2 : 0
height: _active ? win._effectivePopoutCcr * 2 : 0
}
Region {
id: _staticBlurRegion
x: 0
y: 0
width: win._windowRegionWidth
height: win._windowRegionHeight
// Frame cutout (always active when frame is on)
Region {
item: _blurCutout
intersection: Intersection.Subtract
radius: win._blurCutoutRadius
}
// ── Connected popout blur regions ──
Region {
item: _popoutBodyBlurAnchor
readonly property string _bs: ConnectedModeState.popoutBarSide
topLeftRadius: (_bs === "top" || _bs === "left") ? win._effectivePopoutCcr : win._surfaceRadius
topRightRadius: (_bs === "top" || _bs === "right") ? win._effectivePopoutCcr : win._surfaceRadius
bottomLeftRadius: (_bs === "bottom" || _bs === "left") ? win._effectivePopoutCcr : win._surfaceRadius
bottomRightRadius: (_bs === "bottom" || _bs === "right") ? win._effectivePopoutCcr : win._surfaceRadius
}
Region {
item: _popoutBodyBlurCap
}
Region {
item: _popoutLeftConnectorBlurAnchor
Region {
item: _popoutLeftConnectorCutout
intersection: Intersection.Subtract
radius: win._effectivePopoutCcr
}
}
Region {
item: _popoutRightConnectorBlurAnchor
Region {
item: _popoutRightConnectorCutout
intersection: Intersection.Subtract
radius: win._effectivePopoutCcr
}
}
// ── Connected dock blur regions ──
Region {
item: _dockBodyBlurAnchor
radius: win._dockBodyBlurRadius()
}
Region {
item: _dockBodyBlurCap
}
Region {
item: _dockLeftConnectorBlurAnchor
radius: win._dockConnectorRadius()
Region {
item: _dockLeftConnectorCutout
intersection: Intersection.Subtract
radius: win._dockConnectorRadius()
}
}
Region {
item: _dockRightConnectorBlurAnchor
radius: win._dockConnectorRadius()
Region {
item: _dockRightConnectorCutout
intersection: Intersection.Subtract
radius: win._dockConnectorRadius()
}
}
}
// ─── Connector position helpers (mirror DankPopout / Dock logic) ──────────
function _popoutConnectorWidth(spacing) {
const barSide = ConnectedModeState.popoutBarSide;
return (barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr : (spacing + win._effectivePopoutCcr);
}
function _popoutConnectorHeight(spacing) {
const barSide = ConnectedModeState.popoutBarSide;
return (barSide === "top" || barSide === "bottom") ? (spacing + win._effectivePopoutCcr) : win._effectivePopoutCcr;
}
function _popoutConnectorX(baseX, bodyWidth, placement, spacing) {
const barSide = ConnectedModeState.popoutBarSide;
const seamX = (barSide === "top" || barSide === "bottom") ? (placement === "left" ? baseX : baseX + bodyWidth) : (barSide === "left" ? baseX : baseX + bodyWidth);
const w = _popoutConnectorWidth(spacing);
if (barSide === "top" || barSide === "bottom")
return placement === "left" ? seamX - w : seamX;
return barSide === "left" ? seamX : seamX - w;
}
function _popoutConnectorY(baseY, bodyHeight, placement, spacing) {
const barSide = ConnectedModeState.popoutBarSide;
const seamY = barSide === "top" ? baseY : barSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight);
const h = _popoutConnectorHeight(spacing);
if (barSide === "top")
return seamY;
if (barSide === "bottom")
return seamY - h;
return placement === "left" ? seamY - h : seamY;
}
function _dockBodyBlurRadius() {
return _dockBodyBlurAnchor._active ? Math.max(0, Math.min(win._surfaceRadius, _dockBodyBlurAnchor.width / 2, _dockBodyBlurAnchor.height / 2)) : win._surfaceRadius;
}
function _dockConnectorRadius() {
if (!_dockBodyBlurAnchor._active)
return win._ccr;
const dockSide = win._dockState.barSide;
const thickness = (dockSide === "left" || dockSide === "right") ? _dockBodyBlurAnchor.width : _dockBodyBlurAnchor.height;
const bodyRadius = win._dockBodyBlurRadius();
const maxConnectorRadius = Math.max(0, thickness - bodyRadius - win._seamOverlap);
return Math.max(0, Math.min(win._ccr, bodyRadius, maxConnectorRadius));
}
function _dockConnectorWidth(spacing) {
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
const radius = win._dockConnectorRadius();
return isVert ? (spacing + radius) : radius;
}
function _dockConnectorHeight(spacing) {
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
const radius = win._dockConnectorRadius();
return isVert ? radius : (spacing + radius);
}
function _dockConnectorX(baseX, bodyWidth, placement, spacing) {
const dockSide = win._dockState.barSide;
const isVert = dockSide === "left" || dockSide === "right";
const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (dockSide === "left" ? baseX : baseX + bodyWidth);
const w = _dockConnectorWidth(spacing);
if (!isVert)
return placement === "left" ? seamX - w : seamX;
return dockSide === "left" ? seamX : seamX - w;
}
function _dockConnectorY(baseY, bodyHeight, placement, spacing) {
const dockSide = win._dockState.barSide;
const seamY = dockSide === "top" ? baseY : dockSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight);
const h = _dockConnectorHeight(spacing);
if (dockSide === "top")
return seamY;
if (dockSide === "bottom")
return seamY - h;
return placement === "left" ? seamY - h : seamY;
}
function _popoutFillOverlapX() {
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? win._seamOverlap : 0;
}
function _popoutFillOverlapY() {
return (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? win._seamOverlap : 0;
}
function _dockFillOverlapX() {
return (win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._seamOverlap : 0;
}
function _dockFillOverlapY() {
return (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._seamOverlap : 0;
}
function _popoutArcExtent() {
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? _popoutBodyBlurAnchor.height : _popoutBodyBlurAnchor.width;
}
function _popoutArcVisible() {
if (!_popoutBodyBlurAnchor._active || _popoutBodyBlurAnchor.width <= 0 || _popoutBodyBlurAnchor.height <= 0)
return false;
return win._popoutArcExtent() >= win._ccr * (1 + win._ccr * 0.02);
}
function _popoutBlurCapThickness() {
const extent = win._popoutArcExtent();
return Math.max(0, Math.min(win._effectivePopoutCcr, extent - win._surfaceRadius));
}
function _popoutChromeX() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyX - ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr : 0);
}
function _popoutChromeY() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyY - ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr : 0);
}
function _popoutChromeWidth() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyW + ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr * 2 : 0);
}
function _popoutChromeHeight() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyH + ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr * 2 : 0);
}
function _popoutClipX() {
return _popoutBodyBlurAnchor.x - win._popoutChromeX() - win._popoutFillOverlapX();
}
function _popoutClipY() {
return _popoutBodyBlurAnchor.y - win._popoutChromeY() - win._popoutFillOverlapY();
}
function _popoutClipWidth() {
return _popoutBodyBlurAnchor.width + win._popoutFillOverlapX() * 2;
}
function _popoutClipHeight() {
return _popoutBodyBlurAnchor.height + win._popoutFillOverlapY() * 2;
}
function _popoutBodyXInClip() {
return (ConnectedModeState.popoutBarSide === "left" ? _popoutBodyBlurAnchor._dxClamp : 0) - win._popoutFillOverlapX();
}
function _popoutBodyYInClip() {
return (ConnectedModeState.popoutBarSide === "top" ? _popoutBodyBlurAnchor._dyClamp : 0) - win._popoutFillOverlapY();
}
function _popoutBodyFullWidth() {
return ConnectedModeState.popoutBodyW + win._popoutFillOverlapX() * 2;
}
function _popoutBodyFullHeight() {
return ConnectedModeState.popoutBodyH + win._popoutFillOverlapY() * 2;
}
function _dockChromeX() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.x - ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() : 0);
}
function _dockChromeY() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.y - ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() : 0);
}
function _dockChromeWidth() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.width + ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() * 2 : 0);
}
function _dockChromeHeight() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.height + ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() * 2 : 0);
}
function _dockBodyXInChrome() {
return ((win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapX();
}
function _dockBodyYInChrome() {
return ((win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapY();
}
function _connectorArcCorner(barSide, placement) {
if (barSide === "top")
return placement === "left" ? "bottomLeft" : "bottomRight";
if (barSide === "bottom")
return placement === "left" ? "topLeft" : "topRight";
if (barSide === "left")
return placement === "left" ? "topRight" : "bottomRight";
return placement === "left" ? "topLeft" : "bottomLeft";
}
function _connectorCutoutX(connectorX, connectorWidth, arcCorner, radius) {
const r = radius === undefined ? win._effectivePopoutCcr : radius;
return (arcCorner === "topLeft" || arcCorner === "bottomLeft") ? connectorX - r : connectorX + connectorWidth - r;
}
function _connectorCutoutY(connectorY, connectorHeight, arcCorner, radius) {
const r = radius === undefined ? win._effectivePopoutCcr : radius;
return (arcCorner === "topLeft" || arcCorner === "topRight") ? connectorY - r : connectorY + connectorHeight - r;
}
// ─── Blur build / teardown ────────────────────────────────────────────────
function _buildBlur() {
_teardownBlur();
// Follow the global blur toggle
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible)
return;
try {
const region = Qt.createQmlObject(
'import QtQuick; import Quickshell; Region {' +
' property Item cutoutItem;' +
' property int cutoutRadius: 0;' +
' Region {' +
' item: cutoutItem;' +
' intersection: Intersection.Subtract;' +
' radius: cutoutRadius;' +
' }' +
'}',
win, "FrameBlurRegion");
region.x = Qt.binding(() => 0);
region.y = Qt.binding(() => 0);
region.width = Qt.binding(() => win._windowRegionWidth);
region.height = Qt.binding(() => win._windowRegionHeight);
region.cutoutItem = _blurCutout;
region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius);
win.BackgroundEffect.blurRegion = region;
win._frameBlurRegion = region;
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) {
win.BackgroundEffect.blurRegion = null;
return;
}
win.BackgroundEffect.blurRegion = _staticBlurRegion;
} catch (e) {
console.warn("FrameWindow: Failed to create blur region:", e);
console.warn("FrameWindow: Failed to set blur region:", e);
}
}
function _teardownBlur() {
if (!win._frameBlurRegion)
return;
try {
win.BackgroundEffect.blurRegion = null;
} catch (e) {}
win._frameBlurRegion.destroy();
win._frameBlurRegion = null;
}
Timer {
@@ -130,24 +581,44 @@ PanelWindow {
Connections {
target: SettingsData
function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); }
function onFrameEnabledChanged() { _blurRebuildTimer.restart(); }
function onFrameThicknessChanged() { _blurRebuildTimer.restart(); }
function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); }
function onFrameOpacityChanged() { _blurRebuildTimer.restart(); }
function onFrameRoundingChanged() { _blurRebuildTimer.restart(); }
function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); }
function onBarConfigsChanged() { _blurRebuildTimer.restart(); }
function onFrameBlurEnabledChanged() {
_blurRebuildTimer.restart();
}
function onFrameEnabledChanged() {
_blurRebuildTimer.restart();
}
function onFrameThicknessChanged() {
_blurRebuildTimer.restart();
}
function onFrameBarSizeChanged() {
_blurRebuildTimer.restart();
}
function onFrameOpacityChanged() {
_blurRebuildTimer.restart();
}
function onFrameRoundingChanged() {
_blurRebuildTimer.restart();
}
function onFrameScreenPreferencesChanged() {
_blurRebuildTimer.restart();
}
function onBarConfigsChanged() {
_blurRebuildTimer.restart();
}
function onConnectedFrameModeActiveChanged() {
_blurRebuildTimer.restart();
}
}
Connections {
target: BlurService
function onEnabledChanged() { _blurRebuildTimer.restart(); }
function onEnabledChanged() {
_blurRebuildTimer.restart();
}
}
onVisibleChanged: {
if (visible) {
win._frameBlurRegion = null;
_blurRebuildTimer.restart();
} else {
_teardownBlur();
@@ -157,6 +628,8 @@ PanelWindow {
Component.onCompleted: Qt.callLater(() => win._buildBlur())
Component.onDestruction: win._teardownBlur()
// ─── Frame border ─────────────────────────────────────────────────────────
FrameBorder {
anchors.fill: parent
visible: win._frameActive
@@ -166,4 +639,132 @@ PanelWindow {
cutoutRightInset: win.cutoutRightInset
cutoutRadius: win.cutoutRadius
}
// ─── Connected chrome fills ───────────────────────────────────────────────
Item {
id: _connectedChrome
anchors.fill: parent
visible: win._connectedActive
Item {
id: _popoutChrome
visible: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
x: win._popoutChromeX()
y: win._popoutChromeY()
width: win._popoutChromeWidth()
height: win._popoutChromeHeight()
opacity: win._surfaceOpacity
layer.enabled: opacity < 1
layer.smooth: false
Item {
id: _popoutClip
x: win._popoutClipX()
y: win._popoutClipY()
width: win._popoutClipWidth()
height: win._popoutClipHeight()
clip: true
Rectangle {
id: _popoutFill
x: win._popoutBodyXInClip()
y: win._popoutBodyYInClip()
width: win._popoutBodyFullWidth()
height: win._popoutBodyFullHeight()
color: win._opaqueSurfaceColor
z: 1
topLeftRadius: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "left") ? 0 : win._surfaceRadius
topRightRadius: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "right") ? 0 : win._surfaceRadius
bottomLeftRadius: (ConnectedModeState.popoutBarSide === "bottom" || ConnectedModeState.popoutBarSide === "left") ? 0 : win._surfaceRadius
bottomRightRadius: (ConnectedModeState.popoutBarSide === "bottom" || ConnectedModeState.popoutBarSide === "right") ? 0 : win._surfaceRadius
}
}
ConnectedCorner {
id: _connPopoutLeft
visible: win._popoutArcVisible()
barSide: ConnectedModeState.popoutBarSide
placement: "left"
spacing: 0
connectorRadius: win._effectivePopoutCcr
color: win._opaqueSurfaceColor
edgeStrokeWidth: win._seamOverlap
edgeStrokeColor: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "left", 0) - _popoutChrome.x, win._dpr)
y: Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "left", 0) - _popoutChrome.y, win._dpr)
}
ConnectedCorner {
id: _connPopoutRight
visible: win._popoutArcVisible()
barSide: ConnectedModeState.popoutBarSide
placement: "right"
spacing: 0
connectorRadius: win._effectivePopoutCcr
color: win._opaqueSurfaceColor
edgeStrokeWidth: win._seamOverlap
edgeStrokeColor: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._popoutConnectorX(ConnectedModeState.popoutBodyX, ConnectedModeState.popoutBodyW, "right", 0) - _popoutChrome.x, win._dpr)
y: Theme.snap(win._popoutConnectorY(ConnectedModeState.popoutBodyY, ConnectedModeState.popoutBodyH, "right", 0) - _popoutChrome.y, win._dpr)
}
}
Item {
id: _dockChrome
visible: _dockBodyBlurAnchor._active
x: win._dockChromeX()
y: win._dockChromeY()
width: win._dockChromeWidth()
height: win._dockChromeHeight()
opacity: win._surfaceOpacity
layer.enabled: opacity < 1
layer.smooth: false
Rectangle {
id: _dockFill
x: win._dockBodyXInChrome()
y: win._dockBodyYInChrome()
width: _dockBodyBlurAnchor.width + win._dockFillOverlapX() * 2
height: _dockBodyBlurAnchor.height + win._dockFillOverlapY() * 2
color: win._opaqueSurfaceColor
z: 1
readonly property string _dockSide: win._dockState.barSide
readonly property real _dockRadius: win._dockBodyBlurRadius()
topLeftRadius: (_dockSide === "top" || _dockSide === "left") ? 0 : _dockRadius
topRightRadius: (_dockSide === "top" || _dockSide === "right") ? 0 : _dockRadius
bottomLeftRadius: (_dockSide === "bottom" || _dockSide === "left") ? 0 : _dockRadius
bottomRightRadius: (_dockSide === "bottom" || _dockSide === "right") ? 0 : _dockRadius
}
ConnectedCorner {
id: _connDockLeft
visible: _dockBodyBlurAnchor._active
barSide: win._dockState.barSide
placement: "left"
spacing: 0
connectorRadius: win._dockConnectorRadius()
color: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0) - _dockChrome.x, win._dpr)
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0) - _dockChrome.y, win._dpr)
}
ConnectedCorner {
id: _connDockRight
visible: _dockBodyBlurAnchor._active
barSide: win._dockState.barSide
placement: "right"
spacing: 0
connectorRadius: win._dockConnectorRadius()
color: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0) - _dockChrome.x, win._dpr)
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0) - _dockChrome.y, win._dpr)
}
}
}
}

View File

@@ -56,6 +56,9 @@ Item {
settingKey: "frameRounding"
tags: ["frame", "border", "rounding", "radius", "corner"]
text: I18n.tr("Border Radius")
description: SettingsData.connectedFrameModeActive
? I18n.tr("Controls the radius of the frame and all connected popout, dock, and modal surfaces while Connected Mode is active")
: I18n.tr("Controls the frame border radius. This also becomes the connected surface radius whenever Connected Mode is active")
unit: "px"
minimum: 0
maximum: 100

View File

@@ -11,9 +11,7 @@ import qs.Modules.Settings.Widgets
Item {
id: themeColorsTab
readonly property bool connectedFrameModeActive: SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedCursorThemes: SettingsData.availableCursorThemes
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
@@ -1639,7 +1637,9 @@ Item {
tags: ["corner", "radius", "rounded", "square"]
settingKey: "cornerRadius"
text: I18n.tr("Corner Radius")
description: I18n.tr("0 = square corners")
description: themeColorsTab.connectedFrameModeActive
? I18n.tr("Controls general UI rounding. Connected frame popouts, docks, and modal surfaces follow Border Radius in the Frame tab while Connected Frame mode is active")
: I18n.tr("0 = square corners")
value: SettingsData.cornerRadius
minimum: 0
maximum: 32

View File

@@ -2,17 +2,7 @@ import QtQuick
import QtQuick.Shapes
import qs.Common
// ConnectedCorner — Seam-complement connector that fills the void between
// a bar's rounded corner and a popout's flush edge, creating a seamless junction.
//
// Usage: Place as a sibling to contentWrapper inside unrollCounteract (DankPopout)
// or as a sibling to dockBackground (Dock). Position using contentWrapper.x/y.
//
// barSide: "top" | "bottom" | "left" | "right" — which edge the bar is on
// placement: "left" | "right" — which lateral end of that edge
// spacing: gap between bar surface and popout surface (storedBarSpacing, ~4px)
// connectorRadius: bar corner radius to match (frameRounding or Theme.cornerRadius)
// color: fill color matching the popout surface
// Concave arc connector filling the gap between a bar corner and an adjacent surface.
Item {
id: root
@@ -22,9 +12,13 @@ Item {
property real spacing: 4
property real connectorRadius: 12
property color color: "transparent"
property real edgeStrokeWidth: 0
property color edgeStrokeColor: color
property real dpr: 1
readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom"
readonly property bool isPlacementLeft: placement === "left"
readonly property real _edgeStrokeWidth: Math.max(0, edgeStrokeWidth)
readonly property string arcCorner: {
if (barSide === "top")
return isPlacementLeft ? "bottomLeft" : "bottomRight";
@@ -113,37 +107,46 @@ Item {
}
}
// Horizontal bar: connector is tall (bridges vertical gap), narrow (corner radius wide)
// Vertical bar: connector is wide (bridges horizontal gap), short (corner radius tall)
width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius)
height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius
Shape {
anchors.fill: parent
x: -root._edgeStrokeWidth
y: -root._edgeStrokeWidth
width: root.width + root._edgeStrokeWidth * 2
height: root.height + root._edgeStrokeWidth * 2
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.smooth: true
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
ShapePath {
fillColor: root.color
strokeColor: "transparent"
strokeWidth: 0
startX: root.pathStartX
startY: root.pathStartY
strokeColor: root._edgeStrokeWidth > 0 ? root.edgeStrokeColor : "transparent"
strokeWidth: root._edgeStrokeWidth * 2
joinStyle: ShapePath.RoundJoin
capStyle: ShapePath.RoundCap
fillRule: ShapePath.WindingFill
startX: root.pathStartX + root._edgeStrokeWidth
startY: root.pathStartY + root._edgeStrokeWidth
PathLine {
x: root.firstLineX
y: root.firstLineY
x: root.firstLineX + root._edgeStrokeWidth
y: root.firstLineY + root._edgeStrokeWidth
}
PathLine {
x: root.secondLineX
y: root.secondLineY
x: root.secondLineX + root._edgeStrokeWidth
y: root.secondLineY + root._edgeStrokeWidth
}
PathAngleArc {
centerX: root.arcCenterX
centerY: root.arcCenterY
radiusX: root.width
radiusY: root.height
centerX: root.arcCenterX + root._edgeStrokeWidth
centerY: root.arcCenterY + root._edgeStrokeWidth
radiusX: root.connectorRadius
radiusY: root.connectorRadius
startAngle: root.arcStartAngle
sweepAngle: root.arcSweepAngle
}

View File

@@ -34,6 +34,8 @@ Item {
property bool _resizeActive: false
property real _surfaceMarginLeft: 0
property real _surfaceW: 0
property string _connectedChromeToken: ""
property int _connectedChromeSerial: 0
property real storedBarThickness: Theme.barHeight - 4
property real storedBarSpacing: 4
@@ -151,6 +153,98 @@ Item {
setBarContext(pos, bottomGap);
}
function _nextConnectedChromeToken() {
_connectedChromeSerial += 1;
return layerNamespace + ":" + _connectedChromeSerial + ":" + (new Date()).getTime();
}
function _connectedChromeState(visibleOverride) {
const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible;
return {
"visible": visible,
"barSide": contentContainer.connectedBarSide,
"bodyX": root.alignedX,
"bodyY": root.alignedY,
"bodyW": root.alignedWidth,
"bodyH": root.alignedHeight,
"animX": contentContainer.animX,
"animY": contentContainer.animY,
"screen": root.screen ? root.screen.name : ""
};
}
function _publishConnectedChromeState(forceClaim, visibleOverride) {
if (!SettingsData.connectedFrameModeActive || !root.screen || !_connectedChromeToken)
return;
const state = _connectedChromeState(visibleOverride);
if (forceClaim || !ConnectedModeState.hasPopoutOwner(_connectedChromeToken)) {
ConnectedModeState.claimPopout(_connectedChromeToken, state);
} else {
ConnectedModeState.updatePopout(_connectedChromeToken, state);
}
}
function _releaseConnectedChromeState() {
if (_connectedChromeToken)
ConnectedModeState.releasePopout(_connectedChromeToken);
_connectedChromeToken = "";
}
// ─── Exposed animation state for ConnectedModeState ────────────────────
readonly property real contentAnimX: contentContainer.animX
readonly property real contentAnimY: contentContainer.animY
// ─── ConnectedModeState sync ────────────────────────────────────────────
function _syncPopoutChromeState() {
if (!SettingsData.connectedFrameModeActive) {
_releaseConnectedChromeState();
return;
}
if (!root.screen) {
_releaseConnectedChromeState();
return;
}
if (!contentWindow.visible && !shouldBeVisible)
return;
if (!_connectedChromeToken)
_connectedChromeToken = _nextConnectedChromeToken();
_publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_connectedChromeToken));
}
onAlignedXChanged: _syncPopoutChromeState()
onAlignedYChanged: _syncPopoutChromeState()
onAlignedWidthChanged: _syncPopoutChromeState()
onContentAnimXChanged: _syncPopoutChromeState()
onContentAnimYChanged: _syncPopoutChromeState()
onScreenChanged: _syncPopoutChromeState()
onEffectiveBarPositionChanged: _syncPopoutChromeState()
Connections {
target: contentWindow
function onVisibleChanged() {
if (contentWindow.visible)
root._publishConnectedChromeState(true);
else
root._releaseConnectedChromeState();
}
}
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
if (SettingsData.connectedFrameModeActive) {
if (contentWindow.visible || root.shouldBeVisible) {
if (!root._connectedChromeToken)
root._connectedChromeToken = root._nextConnectedChromeToken();
root._publishConnectedChromeState(true);
}
} else {
root._releaseConnectedChromeState();
}
}
}
readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
function updateSurfacePosition() {
@@ -188,6 +282,13 @@ Item {
contentContainer.scaleValue = root.animationScaleCollapsed;
}
if (SettingsData.connectedFrameModeActive) {
_connectedChromeToken = _nextConnectedChromeToken();
_publishConnectedChromeState(true, true);
} else {
_connectedChromeToken = "";
}
if (useBackgroundWindow) {
_surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2;
@@ -254,11 +355,14 @@ Item {
}
}
Component.onDestruction: _releaseConnectedChromeState()
readonly property real screenWidth: screen ? screen.width : 0
readonly property real screenHeight: screen ? screen.height : 0
readonly property real dpr: screen ? screen.devicePixelRatio : 1
readonly property real frameInset: {
if (!SettingsData.frameEnabled) return 0;
if (!SettingsData.frameEnabled)
return 0;
const ft = SettingsData.frameThickness;
const fr = SettingsData.frameRounding;
const ccr = Theme.connectedCornerRadius;
@@ -324,6 +428,7 @@ Item {
}
onAlignedHeightChanged: {
_syncPopoutChromeState();
if (!suspendShadowWhileResizing || !shouldBeVisible)
return;
_resizeActive = true;
@@ -536,43 +641,20 @@ Item {
WindowBlur {
id: popoutBlur
targetWindow: contentWindow
blurEnabled: root.effectiveSurfaceBlurEnabled
blurEnabled: root.effectiveSurfaceBlurEnabled && !SettingsData.connectedFrameModeActive
readonly property real s: Math.min(1, contentContainer.scaleValue)
readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect
|| (typeof SettingsData !== "undefined"
&& Theme.isDirectionalEffect
&& SettingsData.directionalAnimationMode !== 2)
readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || (typeof SettingsData !== "undefined" && Theme.isDirectionalEffect && SettingsData.directionalAnimationMode !== 2)
// Directional popouts clip to the bar edge, so the blur needs to grow from
// that same edge instead of translating through the bar before settling.
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
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
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0)
? (trackBlurFromBarEdge
? Math.max(0, contentContainer.width - Math.abs(_dxClamp))
: (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s)
: 0
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0)
? (trackBlurFromBarEdge
? Math.max(0, contentContainer.height - Math.abs(_dyClamp))
: (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s)
: 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
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.width - Math.abs(_dxClamp)) : (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s) : 0
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.height - Math.abs(_dyClamp)) : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s) : 0
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : Theme.connectedSurfaceRadius
}
@@ -663,9 +745,7 @@ Item {
readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right"))
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
readonly property color surfaceColor: Theme.popupLayerColor(Theme.surfaceContainer)
readonly property color surfaceBorderColor: Theme.isConnectedEffect
? "transparent"
: (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium)
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (barTop || barLeft) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (barTop || barRight) ? 0 : surfaceRadius
@@ -821,13 +901,9 @@ Item {
Item {
id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect
&& typeof SettingsData !== "undefined"
&& SettingsData.directionalAnimationMode > 0
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
readonly property real clipOversize: 1000
readonly property real connectedClipAllowance: Theme.isConnectedEffect
? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2)
: 0
readonly property real connectedClipAllowance: Theme.isConnectedEffect ? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2) : 0
clip: shouldClip
@@ -908,7 +984,7 @@ Item {
Item {
anchors.fill: parent
visible: Theme.isConnectedEffect
visible: Theme.isConnectedEffect && !SettingsData.connectedFrameModeActive
clip: false
Rectangle {
@@ -930,6 +1006,7 @@ Item {
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
dpr: root.dpr
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
}
@@ -941,6 +1018,7 @@ Item {
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
dpr: root.dpr
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
}
@@ -986,28 +1064,6 @@ Item {
border.color: contentContainer.surfaceBorderColor
border.width: contentContainer.surfaceBorderWidth
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "left"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr)
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "right"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr)
}
}
Loader {