feat: settings button in quick toggles now animate from util panel

This commit is contained in:
2026-11-04 11:39:02 +01:00
parent 79d74d1966
commit 1f8f1ee556
9 changed files with 366 additions and 25 deletions
-2
View File
@@ -10,7 +10,6 @@ CustomFlickable {
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property real fadeAmount: 0.1 property real fadeAmount: 0.1
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1 property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
readonly property real dpr: Math.ceil((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
function fadeShouldBeActive(isStart: bool): bool { function fadeShouldBeActive(isStart: bool): bool {
// When content is smaller than flickable size, hide fade when rebound starts // When content is smaller than flickable size, hide fade when rebound starts
@@ -30,7 +29,6 @@ CustomFlickable {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
} }
layer.textureSize: Qt.size(width * dpr, height * dpr)
layer.effect: Mask { layer.effect: Mask {
maskSource: mask maskSource: mask
+2 -1
View File
@@ -6,11 +6,12 @@ import qs.Effects
CustomListView { CustomListView {
id: root id: root
readonly property bool horizontal: orientation === ListView.Horizontal
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property bool fadeEnabled: true property bool fadeEnabled: true
property real fadeAmount: 0.1 property real fadeAmount: 0.1
property real fadeThreshold: 0.0 property real fadeThreshold: 0.0
readonly property bool horizontal: orientation === ListView.Horizontal
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1 property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
function contentSize(): real { function contentSize(): real {
+1 -2
View File
@@ -145,9 +145,8 @@ Item {
Utils.Wrapper { Utils.Wrapper {
id: utilities id: utilities
anchors.bottom: parent.bottom
anchors.right: parent.right
screenState: root.screenState screenState: root.screenState
screen: root.screen
sidebar: sidebar sidebar: sidebar
} }
-1
View File
@@ -50,7 +50,6 @@ Region {
R { R {
height: panel.height * (1 - root.panels.utilities.offsetScale) + root.geometry.insetBottom(root.borderThickness) height: panel.height * (1 - root.panels.utilities.offsetScale) + root.geometry.insetBottom(root.borderThickness)
panel: root.panels.utilities panel: root.panels.utilities
y: root.win.height - height
} }
R { R {
+1 -1
View File
@@ -266,7 +266,7 @@ CustomWindow {
deformAmount: 0.1 deformAmount: 0.1
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
panel: panels.utilities panel: panels.utilities
topLeftRadius: 0 radius: panels.utilities.isDetached ? Tokens.rounding.largeIncreased + Tokens.padding.small : 0
} }
PanelBg { PanelBg {
+6 -6
View File
@@ -2,16 +2,15 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import Quickshell.Networking as QSNetwork import Quickshell.Networking as QSNetwork
import ZShell.Components import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Modules.Sidebar.Utils
import qs.Components import qs.Components
import qs.Services import qs.Services
import qs.Helpers import qs.Helpers
import qs.Daemons import qs.Daemons
import qs.Modules.Settings
CustomRect { CustomRect {
id: root id: root
@@ -38,16 +37,18 @@ CustomRect {
return true; return true;
}); });
} }
required property ScreenState screenState
readonly property int splitIndex: Math.ceil(quickToggles.length / 2) readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
required property PopoutState popouts
required property ScreenState screenState
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
radius: Tokens.rounding.small radius: Tokens.rounding.small
SnapLayout { SnapLayout {
orientation: Qt.Vertical
id: layout id: layout
orientation: Qt.Vertical
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
@@ -167,8 +168,7 @@ orientation: Qt.Vertical
isToggle: false isToggle: false
onClicked: { onClicked: {
root.screenState.sidebar = false; root.popouts.detachRequested("appearance");
root.screenState.settings = true;
} }
} }
} }
+4
View File
@@ -10,7 +10,10 @@ import ZShell.Components
Item { Item {
id: root id: root
readonly property int padding: Tokens.padding.small
required property ScreenState screenState required property ScreenState screenState
required property PopoutState popouts
implicitHeight: ZUtils.snap(layout.implicitHeight, root) implicitHeight: ZUtils.snap(layout.implicitHeight, root)
implicitWidth: ZUtils.snap(layout.implicitWidth, root) implicitWidth: ZUtils.snap(layout.implicitWidth, root)
@@ -33,6 +36,7 @@ Item {
Toggles { Toggles {
Layout.fillWidth: true Layout.fillWidth: true
screenState: root.screenState screenState: root.screenState
popouts: root.popouts
} }
} }
} }
+8
View File
@@ -0,0 +1,8 @@
import QtQuick
QtObject {
property string currentName
property string hasCurrent
signal detachRequested(mode: string)
}
+344 -12
View File
@@ -2,41 +2,373 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import ZShell.Config import ZShell.Config
import ZShell
import qs.Modules.Settings as Settings
import qs.Components import qs.Components
Item { Item {
id: root id: root
property real offsetScale: shouldBeActive ? 0 : 1
required property ScreenState screenState required property ScreenState screenState
readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive required property ShellScreen screen
required property Item sidebar required property Item sidebar
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale property string queuedMode
implicitHeight: content.implicitHeight + Tokens.padding.small * 2 property real offsetScale: shouldBeActive ? 0 : 1
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
readonly property string wantedState: {
if (queuedMode.length > 0)
return "detached";
if (screenState.sidebar && !sidebar.chatActive)
return "docked";
return "hidden";
}
readonly property bool isDetached: wantedState === "detached"
readonly property bool shouldBeActive: wantedState !== "hidden"
readonly property int dur: Tokens.anim.durations.expressiveDefaultSpatial
readonly property int slowDur: Tokens.anim.durations.expressiveSlowSpatial
// Sizes come from the specific Comp, never from "whichever child is current"
readonly property real settingsW: settings.implicitWidth + (settings.item?.padding ?? 0) * 2
readonly property real settingsH: settings.implicitHeight + (settings.item?.padding ?? 0) * 2
readonly property real contentW: content.implicitWidth + (content.item?.padding ?? 0) * 2
readonly property real contentH: content.implicitHeight + (content.item?.padding ?? 0) * 2
// Slot positions
readonly property real hiddenX: parent.width
readonly property real hiddenY: parent.height - Config.bar.border + Tokens.padding.small * 2
readonly property real dockedX: parent.width - sidebar.width
readonly property real dockedY: parent.height - contentH
readonly property real centerX: (parent.screen.width - settingsW) / 2
readonly property real centerY: (parent.screen.height - settingsH) / 2
opacity: 1 - offsetScale opacity: 1 - offsetScale
visible: offsetScale < 1 visible: offsetScale < 1
clip: true
function detach(mode: string): void {
queuedMode = mode;
focus = true;
}
function close(): void {
queuedMode = "";
}
// ---- State selection --------------------------------------------------
// `state` is only ever assigned `wantedState`, never an intermediate value.
// The only thing sync() decides is WHEN, i.e. whether the Comp the target
// state takes its size from is Ready yet.
// Restarted whenever either Comp's status or size changes. While it is
// running, a Comp's implicit size is still settling and must not be captured.
Timer {
id: settle
interval: 12 // about two frames; raise it if the height is still read too early
onTriggered: root.sync()
}
function stable(comp: Comp): bool {
return comp.status === Loader.Ready && comp.implicitHeight > 0 && !settle.running;
}
function ready(): bool {
switch (wantedState) {
case "detached":
return stable(settings);
case "docked":
return stable(content);
default:
// Closing from detached passes through the docked slot, so
// content's final size is needed before the transition starts.
return state !== "detached" || stable(content);
}
}
function sync(): void {
if (state !== wantedState && ready())
state = wantedState;
}
onWantedStateChanged: sync()
Component.onCompleted: sync()
Connections {
target: settings
function onStatusChanged(): void {
settle.restart();
}
function onImplicitHeightChanged(): void {
settle.restart();
}
function onImplicitWidthChanged(): void {
settle.restart();
}
}
Connections {
target: content
function onStatusChanged(): void {
settle.restart();
}
function onImplicitHeightChanged(): void {
settle.restart();
}
function onImplicitWidthChanged(): void {
settle.restart();
}
}
// ---- States: every position and size property lives here ------------
states: [
State {
name: "hidden"
PropertyChanges {
root.x: root.hiddenX
root.y: root.hiddenY
root.implicitWidth: 0
root.implicitHeight: 0
}
},
State {
name: "docked"
PropertyChanges {
root.x: root.dockedX
root.y: root.dockedY
root.implicitWidth: root.contentW
root.implicitHeight: root.contentH
}
},
State {
name: "detached"
PropertyChanges {
root.x: root.centerX
root.y: root.centerY
root.implicitWidth: root.settingsW
root.implicitHeight: root.settingsH
}
}
]
// One explicit transition per state pair. No `reversible`, no wildcards.
transitions: [
Transition {
from: "hidden"
to: "docked"
Anim {
properties: "x,y,implicitWidth,implicitHeight"
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
},
Transition {
from: "docked"
to: "hidden"
Anim {
properties: "x,y,implicitWidth,implicitHeight"
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
},
Transition {
from: "docked"
to: "detached"
Anim {
properties: "x,y,implicitWidth,implicitHeight"
duration: root.slowDur
easing: Tokens.anim.expressiveSlowSpatial
}
},
Transition {
from: "hidden"
to: "detached"
Anim {
properties: "x,y,implicitWidth,implicitHeight"
duration: root.slowDur
easing: Tokens.anim.expressiveSlowSpatial
}
},
Transition {
from: "detached"
to: "docked"
Anim {
properties: "x,y,implicitWidth,implicitHeight"
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
},
// 1) x, y and size to the docked slot, 2) only y drops, 3) snap the rest while invisible
Transition {
from: "detached"
to: "hidden"
ParallelAnimation {
Anim {
target: root
property: "x"
to: root.dockedX
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
Anim {
target: root
property: "y"
to: root.dockedY
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
Anim {
target: root
property: "implicitWidth"
to: root.contentW
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
Anim {
target: root
property: "implicitHeight"
to: root.contentH
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
SequentialAnimation {
PauseAnimation {
duration: root.dur - 300
}
Anim {
target: root
property: "y"
from: root.dockedY
to: root.hiddenY
duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial
}
PropertyAction {
target: root
properties: "x,implicitWidth,implicitHeight"
}
}
}
}
]
// Fade only. Nothing that moves or sizes the item is a Behavior any more.
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial duration: root.dur
easing: Tokens.anim.expressiveDefaultSpatial easing: Tokens.anim.expressiveDefaultSpatial
} }
} }
Loader { PopoutState {
id: popoutState
onDetachRequested: mode => root.detach(mode)
}
HyprlandFocusGrab {
active: root.isDetached
windows: [QsWindow.window]
onCleared: root.close()
}
Binding {
when: root.isDetached
target: QsWindow.window
property: "WlrLayershell.keyboardFocus"
value: WlrKeyboardFocus.OnDemand
}
Comp {
id: settings
readonly property int padding: 0
shouldBeActive: root.isDetached
anchors.centerIn: parent
sourceComponent: Settings.Content {
sState.screen: root.screen
sState.animatingContainer: settings.opacity < 1
sState.currentPageIdx: ["appearance", "network", "bluetooth", "audio"].indexOf(root.queuedMode)
onClose: root.close()
}
}
Comp {
id: content id: content
active: root.shouldBeActive || root.visible shouldBeActive: (root.shouldBeActive || root.visible) && !root.isDetached
anchors.left: parent.left anchors.centerIn: parent
anchors.margins: 8
anchors.top: parent.top
sourceComponent: Content { sourceComponent: Content {
implicitWidth: root.implicitWidth - 8 * 2 popouts: popoutState
implicitWidth: root.sidebar.width - Tokens.padding.small * 2
screenState: root.screenState screenState: root.screenState
} }
} }
component Comp: Loader {
id: comp
property bool shouldBeActive
active: false
opacity: 0
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
states: State {
name: "active"
when: comp.shouldBeActive
PropertyChanges {
comp.opacity: 1
comp.active: true
}
}
transitions: [
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
property: "active"
}
Anim {
type: Anim.DefaultEffects
property: "opacity"
}
}
},
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
type: Anim.DefaultEffects
property: "opacity"
}
PropertyAction {
property: "active"
}
}
}
]
}
} }