feat: settings button in quick toggles now animate from util panel
This commit is contained in:
@@ -2,41 +2,373 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import ZShell.Config
|
||||
import ZShell
|
||||
import qs.Modules.Settings as Settings
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real offsetScale: shouldBeActive ? 0 : 1
|
||||
required property ScreenState screenState
|
||||
readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive
|
||||
required property ShellScreen screen
|
||||
required property Item sidebar
|
||||
|
||||
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
|
||||
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
|
||||
property string queuedMode
|
||||
property real offsetScale: shouldBeActive ? 0 : 1
|
||||
|
||||
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
|
||||
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 {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
duration: root.dur
|
||||
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
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 8
|
||||
anchors.top: parent.top
|
||||
shouldBeActive: (root.shouldBeActive || root.visible) && !root.isDetached
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: Content {
|
||||
implicitWidth: root.implicitWidth - 8 * 2
|
||||
popouts: popoutState
|
||||
implicitWidth: root.sidebar.width - Tokens.padding.small * 2
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user