Merge branch 'main' into cxx-listview
This commit is contained in:
commit
1d6919b375
48 changed files with 2415 additions and 1301 deletions
5
.envrc
5
.envrc
|
|
@ -3,10 +3,11 @@ if has nix; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
shopt -s globstar
|
shopt -s globstar
|
||||||
watch_file assets/cpp/**/*.cpp
|
|
||||||
watch_file assets/cpp/**/*.hpp
|
|
||||||
watch_file plugin/**/*.cpp
|
watch_file plugin/**/*.cpp
|
||||||
watch_file plugin/**/*.hpp
|
watch_file plugin/**/*.hpp
|
||||||
|
watch_file plugin/**/*.qml
|
||||||
|
watch_file plugin/**/*.vert
|
||||||
|
watch_file plugin/**/*.frag
|
||||||
watch_file **/CMakeLists.txt
|
watch_file **/CMakeLists.txt
|
||||||
|
|
||||||
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv
|
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv
|
||||||
|
|
|
||||||
5
.github/workflows/check-format.yml
vendored
5
.github/workflows/check-format.yml
vendored
|
|
@ -34,6 +34,5 @@ jobs:
|
||||||
- name: Check C++ format
|
- name: Check C++ format
|
||||||
shell: fish {0}
|
shell: fish {0}
|
||||||
run: |
|
run: |
|
||||||
for file in (string match -v 'build/*' **.cpp **.hpp)
|
find plugin extras -name '*.cpp' -o -name '*.hpp' \
|
||||||
clang-format $file | diff -u $file - || exit 1
|
| xargs clang-format --dry-run --Werror
|
||||||
end
|
|
||||||
|
|
|
||||||
2
.github/workflows/update-image.yml
vendored
2
.github/workflows/update-image.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
cat > /tmp/Dockerfile <<EOF
|
cat > /tmp/Dockerfile <<EOF
|
||||||
FROM archlinux:latest
|
FROM archlinux:latest
|
||||||
RUN pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clazy qt6-declarative python libpipewire aubio libqalculate && \
|
RUN pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clazy qt6-declarative qt6-shadertools python libpipewire aubio libqalculate && \
|
||||||
useradd -m builder && \
|
useradd -m builder && \
|
||||||
echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
|
echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
|
||||||
sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \
|
sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ JsonObject {
|
||||||
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||||
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
||||||
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||||
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
component AnimDurations: JsonObject {
|
component AnimDurations: JsonObject {
|
||||||
|
|
@ -76,7 +76,7 @@ JsonObject {
|
||||||
property int extraLarge: 1000 * scale
|
property int extraLarge: 1000 * scale
|
||||||
property int expressiveFastSpatial: 350 * scale
|
property int expressiveFastSpatial: 350 * scale
|
||||||
property int expressiveDefaultSpatial: 500 * scale
|
property int expressiveDefaultSpatial: 500 * scale
|
||||||
property int expressiveEffects: 200 * scale
|
property int expressiveSlowSpatial: 650 * scale
|
||||||
}
|
}
|
||||||
|
|
||||||
component Anim: JsonObject {
|
component Anim: JsonObject {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,8 @@ import qs.components.containers
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.config
|
import qs.config
|
||||||
|
|
||||||
Loader {
|
Variants {
|
||||||
asynchronous: true
|
model: Config.background.enabled ? Screens.screens : []
|
||||||
active: Config.background.enabled
|
|
||||||
|
|
||||||
sourceComponent: Variants {
|
|
||||||
model: Screens.screens
|
|
||||||
|
|
||||||
StyledWindow {
|
StyledWindow {
|
||||||
id: win
|
id: win
|
||||||
|
|
@ -163,4 +159,3 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import qs.utils
|
||||||
import qs.modules.bar.popouts as BarPopouts
|
import qs.modules.bar.popouts as BarPopouts
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -12,9 +13,10 @@ Item {
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
required property DrawerVisibilities visibilities
|
required property DrawerVisibilities visibilities
|
||||||
required property BarPopouts.Wrapper popouts
|
required property BarPopouts.Wrapper popouts
|
||||||
required property bool disabled
|
|
||||||
required property bool fullscreen
|
required property bool fullscreen
|
||||||
|
|
||||||
|
readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name)
|
||||||
|
|
||||||
readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth)
|
readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth)
|
||||||
readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
|
readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
|
||||||
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2
|
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
import qs.config
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Wrapper wrapper
|
|
||||||
required property bool invertBottomRounding
|
|
||||||
readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding
|
|
||||||
readonly property bool flatten: wrapper.width < rounding * 2
|
|
||||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
|
||||||
property real ibr: invertBottomRounding ? -1 : 1
|
|
||||||
|
|
||||||
property real sideRounding: startX > 0 ? -1 : 1
|
|
||||||
|
|
||||||
strokeWidth: -1
|
|
||||||
fillColor: Colours.palette.m3surface
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.roundingX
|
|
||||||
relativeY: root.rounding * root.sideRounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: root.wrapper.width - root.roundingX * 2
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: root.wrapper.height - root.rounding * 2
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: -root.roundingX * root.ibr
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr)
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: -root.roundingX
|
|
||||||
relativeY: root.rounding * root.sideRounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on ibr {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on sideRounding {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
modules/bar/popouts/ClipWrapper.qml
Normal file
69
modules/bar/popouts/ClipWrapper.qml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.components
|
||||||
|
import qs.config
|
||||||
|
import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property ShellScreen screen
|
||||||
|
required property real borderThickness
|
||||||
|
|
||||||
|
readonly property alias content: content
|
||||||
|
property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1
|
||||||
|
|
||||||
|
visible: width > 0 && height > 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
implicitWidth: content.implicitWidth * (1 - offsetScale)
|
||||||
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
|
x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0
|
||||||
|
y: {
|
||||||
|
if (content.isDetached)
|
||||||
|
return (parent.height - content.nonAnimHeight) / 2;
|
||||||
|
|
||||||
|
const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2;
|
||||||
|
const diff = parent.height - Math.floor(off + content.nonAnimHeight);
|
||||||
|
if (diff < 0)
|
||||||
|
return off + diff;
|
||||||
|
return Math.max(off, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on offsetScale {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
Anim {
|
||||||
|
duration: content.animLength
|
||||||
|
easing.bezierCurve: content.animCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on y {
|
||||||
|
enabled: root.offsetScale < 1
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: content.animLength
|
||||||
|
easing.bezierCurve: content.animCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Wrapper {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
screen: root.screen
|
||||||
|
offsetScale: root.offsetScale
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,8 +14,6 @@ Item {
|
||||||
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
|
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
|
||||||
readonly property Item current: currentPopout?.item ?? null
|
readonly property Item current: currentPopout?.item ?? null
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2
|
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2
|
||||||
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2
|
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2
|
||||||
|
|
||||||
|
|
@ -171,8 +169,7 @@ Item {
|
||||||
required property string name
|
required property string name
|
||||||
readonly property bool shouldBeActive: root.popouts.currentName === name
|
readonly property bool shouldBeActive: root.popouts.currentName === name
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.centerIn: parent
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
opacity: 0
|
opacity: 0
|
||||||
scale: 0.8
|
scale: 0.8
|
||||||
|
|
|
||||||
|
|
@ -14,45 +14,50 @@ Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
|
required property real offsetScale
|
||||||
|
|
||||||
readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0
|
readonly property alias content: content
|
||||||
|
readonly property alias winfo: winfo
|
||||||
|
readonly property alias controlCenter: controlCenter
|
||||||
|
|
||||||
|
readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth
|
||||||
readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
|
readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
|
||||||
readonly property Item current: (content.item as Content)?.current ?? null
|
readonly property Item current: (content.item as Content)?.current ?? null
|
||||||
|
readonly property bool isDetached: detachedMode.length > 0
|
||||||
|
|
||||||
property alias currentName: popoutState.currentName
|
property alias currentName: popoutState.currentName
|
||||||
property real currentCenter
|
|
||||||
property alias hasCurrent: popoutState.hasCurrent
|
property alias hasCurrent: popoutState.hasCurrent
|
||||||
readonly property PopoutState popState: popoutState
|
property real currentCenter
|
||||||
|
|
||||||
property string detachedMode
|
property string detachedMode
|
||||||
property string queuedMode
|
property string queuedMode
|
||||||
readonly property bool isDetached: detachedMode.length > 0
|
|
||||||
|
|
||||||
property int animLength: Appearance.anim.durations.normal
|
property int animLength: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
property list<real> animCurve: Appearance.anim.curves.emphasized
|
property list<real> animCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
|
|
||||||
|
function setAnims(detach: bool): void {
|
||||||
|
const type = `expressive${detach ? "Slow" : "Default"}Spatial`;
|
||||||
|
animLength = Appearance.anim.durations[type];
|
||||||
|
animCurve = Appearance.anim.curves[type];
|
||||||
|
}
|
||||||
|
|
||||||
function detach(mode: string): void {
|
function detach(mode: string): void {
|
||||||
animLength = Appearance.anim.durations.large;
|
setAnims(true);
|
||||||
if (mode === "winfo") {
|
if (mode === "winfo") {
|
||||||
detachedMode = mode;
|
detachedMode = mode;
|
||||||
} else {
|
} else {
|
||||||
queuedMode = mode;
|
queuedMode = mode;
|
||||||
detachedMode = "any";
|
detachedMode = "any";
|
||||||
}
|
}
|
||||||
|
setAnims(false);
|
||||||
focus = true;
|
focus = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): void {
|
function close(): void {
|
||||||
hasCurrent = false;
|
hasCurrent = false;
|
||||||
animCurve = Appearance.anim.curves.emphasizedAccel;
|
|
||||||
animLength = Appearance.anim.durations.normal;
|
|
||||||
detachedMode = "";
|
detachedMode = "";
|
||||||
animCurve = Appearance.anim.curves.emphasized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: width > 0 && height > 0
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
implicitWidth: nonAnimWidth
|
implicitWidth: nonAnimWidth
|
||||||
implicitHeight: nonAnimHeight
|
implicitHeight: nonAnimHeight
|
||||||
|
|
||||||
|
|
@ -89,15 +94,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
when: root.isDetached
|
when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword")
|
||||||
|
|
||||||
target: QsWindow.window
|
|
||||||
property: "WlrLayershell.keyboardFocus"
|
|
||||||
value: WlrKeyboardFocus.OnDemand
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
|
||||||
when: root.hasCurrent && root.currentName === "wirelesspassword"
|
|
||||||
|
|
||||||
target: QsWindow.window
|
target: QsWindow.window
|
||||||
property: "WlrLayershell.keyboardFocus"
|
property: "WlrLayershell.keyboardFocus"
|
||||||
|
|
@ -108,8 +105,7 @@ Item {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
shouldBeActive: root.hasCurrent && !root.detachedMode
|
shouldBeActive: root.hasCurrent && !root.detachedMode
|
||||||
anchors.right: parent.right
|
anchors.fill: parent
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
popouts: popoutState
|
popouts: popoutState
|
||||||
|
|
@ -117,6 +113,8 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Comp {
|
Comp {
|
||||||
|
id: winfo
|
||||||
|
|
||||||
shouldBeActive: root.detachedMode === "winfo"
|
shouldBeActive: root.detachedMode === "winfo"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
|
@ -127,32 +125,15 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Comp {
|
Comp {
|
||||||
|
id: controlCenter
|
||||||
|
|
||||||
shouldBeActive: root.detachedMode === "any"
|
shouldBeActive: root.detachedMode === "any"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
sourceComponent: ControlCenter {
|
sourceComponent: ControlCenter {
|
||||||
function close(): void {
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
active: root.queuedMode
|
active: root.queuedMode
|
||||||
}
|
onClose: root.close()
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
Anim {
|
|
||||||
duration: root.animLength
|
|
||||||
easing.bezierCurve: root.animCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
enabled: root.implicitWidth > 0
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
duration: root.animLength
|
|
||||||
easing.bezierCurve: root.animCurve
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,7 +145,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
enabled: root.implicitWidth > 0
|
enabled: root.offsetScale < 1
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
duration: root.animLength
|
duration: root.animLength
|
||||||
|
|
@ -180,6 +161,7 @@ Item {
|
||||||
active: false
|
active: false
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
|
||||||
|
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||||
states: State {
|
states: State {
|
||||||
name: "active"
|
name: "active"
|
||||||
when: comp.shouldBeActive
|
when: comp.shouldBeActive
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@ Item {
|
||||||
root: root
|
root: root
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): void {
|
signal close
|
||||||
}
|
|
||||||
|
|
||||||
implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio
|
implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio
|
||||||
implicitHeight: screen.height * Config.controlCenter.sizes.heightMult
|
implicitHeight: screen.height * Config.controlCenter.sizes.heightMult
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,9 @@ Singleton {
|
||||||
ControlCenter {
|
ControlCenter {
|
||||||
id: cc
|
id: cc
|
||||||
|
|
||||||
function close(): void {
|
|
||||||
win.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
screen: win.screen
|
screen: win.screen
|
||||||
|
onClose: win.destroy()
|
||||||
floating: true
|
floating: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
import qs.config
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Wrapper wrapper
|
|
||||||
readonly property real rounding: Config.border.rounding
|
|
||||||
readonly property bool flatten: wrapper.height < rounding * 2
|
|
||||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
|
||||||
|
|
||||||
strokeWidth: -1
|
|
||||||
fillColor: Colours.palette.m3surface
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: root.wrapper.height - root.roundingY * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
relativeX: root.wrapper.width - root.rounding * 2
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: -root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: -(root.wrapper.height - root.roundingY * 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: -root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -29,60 +29,20 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0
|
readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0
|
||||||
|
readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled
|
||||||
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
|
|
||||||
visible: height > 0
|
visible: offsetScale < 1
|
||||||
implicitHeight: 0
|
anchors.topMargin: (-implicitHeight - 5) * offsetScale
|
||||||
implicitWidth: content.implicitWidth
|
implicitHeight: content.implicitHeight
|
||||||
|
implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open
|
||||||
onStateChanged: {
|
opacity: 1 - offsetScale
|
||||||
if (state === "visible" && timer.running) {
|
|
||||||
timer.triggered();
|
|
||||||
timer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
states: State {
|
|
||||||
name: "visible"
|
|
||||||
when: root.visibilities.dashboard && Config.dashboard.enabled
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
root.implicitHeight: content.implicitHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transitions: [
|
|
||||||
Transition {
|
|
||||||
from: ""
|
|
||||||
to: "visible"
|
|
||||||
|
|
||||||
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {
|
||||||
target: root
|
|
||||||
property: "implicitHeight"
|
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Transition {
|
|
||||||
from: "visible"
|
|
||||||
to: ""
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
target: root
|
|
||||||
property: "implicitHeight"
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: timer
|
|
||||||
|
|
||||||
running: true
|
|
||||||
interval: Appearance.anim.durations.extraLarge
|
|
||||||
onTriggered: {
|
|
||||||
content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible);
|
|
||||||
content.visible = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|
@ -91,8 +51,7 @@ Item {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
|
|
||||||
visible: false
|
active: root.shouldBeActive || root.visible
|
||||||
active: true
|
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.config
|
|
||||||
import qs.modules.dashboard as Dashboard
|
|
||||||
import qs.modules.launcher as Launcher
|
|
||||||
import qs.modules.notifications as Notifications
|
|
||||||
import qs.modules.osd as Osd
|
|
||||||
import qs.modules.session as Session
|
|
||||||
import qs.modules.sidebar as Sidebar
|
|
||||||
import qs.modules.utilities as Utilities
|
|
||||||
import qs.modules.bar.popouts as BarPopouts
|
|
||||||
|
|
||||||
Shape {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Panels panels
|
|
||||||
required property Item bar
|
|
||||||
required property real borderThickness
|
|
||||||
required property real borderRounding
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: root.borderThickness
|
|
||||||
anchors.leftMargin: bar.implicitWidth
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
|
|
||||||
Osd.Background {
|
|
||||||
wrapper: root.panels.osd // qmllint disable incompatible-type
|
|
||||||
rounding: Config.border.rounding
|
|
||||||
|
|
||||||
startX: root.width - root.panels.session.width - root.panels.sidebar.width
|
|
||||||
startY: (root.height - wrapper.height) / 2 - rounding
|
|
||||||
}
|
|
||||||
|
|
||||||
Notifications.Background {
|
|
||||||
wrapper: root.panels.notifications // qmllint disable incompatible-type
|
|
||||||
sidebar: sidebar
|
|
||||||
rounding: Config.border.rounding
|
|
||||||
|
|
||||||
startX: root.width
|
|
||||||
startY: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Session.Background {
|
|
||||||
wrapper: root.panels.session // qmllint disable incompatible-type
|
|
||||||
|
|
||||||
startX: root.width - root.panels.sidebar.width
|
|
||||||
startY: (root.height - wrapper.height) / 2 - rounding
|
|
||||||
}
|
|
||||||
|
|
||||||
Launcher.Background {
|
|
||||||
wrapper: root.panels.launcher // qmllint disable incompatible-type
|
|
||||||
|
|
||||||
startX: (root.width - wrapper.width) / 2 - rounding
|
|
||||||
startY: root.height
|
|
||||||
}
|
|
||||||
|
|
||||||
Dashboard.Background {
|
|
||||||
wrapper: root.panels.dashboard // qmllint disable incompatible-type
|
|
||||||
|
|
||||||
startX: (root.width - wrapper.width) / 2 - rounding
|
|
||||||
startY: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
BarPopouts.Background {
|
|
||||||
wrapper: root.panels.popouts // qmllint disable incompatible-type
|
|
||||||
invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height
|
|
||||||
|
|
||||||
startX: wrapper.x
|
|
||||||
startY: wrapper.y - rounding * sideRounding
|
|
||||||
}
|
|
||||||
|
|
||||||
Utilities.Background {
|
|
||||||
wrapper: root.panels.utilities // qmllint disable incompatible-type
|
|
||||||
sidebar: sidebar
|
|
||||||
rounding: root.borderRounding
|
|
||||||
|
|
||||||
startX: root.width
|
|
||||||
startY: root.height
|
|
||||||
}
|
|
||||||
|
|
||||||
Sidebar.Background {
|
|
||||||
id: sidebar
|
|
||||||
|
|
||||||
wrapper: root.panels.sidebar // qmllint disable incompatible-type
|
|
||||||
panels: root.panels
|
|
||||||
rounding: root.borderRounding
|
|
||||||
|
|
||||||
startX: root.width
|
|
||||||
startY: root.panels.notifications.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Item bar
|
|
||||||
required property real borderThickness
|
|
||||||
required property real borderRounding
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Colours.palette.m3surface
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskSource: mask
|
|
||||||
maskEnabled: true
|
|
||||||
maskInverted: true
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: mask
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
layer.enabled: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: root.borderThickness
|
|
||||||
anchors.leftMargin: root.bar.implicitWidth
|
|
||||||
radius: root.borderRounding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
310
modules/drawers/ContentWindow.qml
Normal file
310
modules/drawers/ContentWindow.qml
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Caelestia.Blobs
|
||||||
|
import qs.components
|
||||||
|
import qs.components.containers
|
||||||
|
import qs.services
|
||||||
|
import qs.config
|
||||||
|
import qs.modules.bar
|
||||||
|
|
||||||
|
StyledWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property alias bar: bar
|
||||||
|
|
||||||
|
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
|
||||||
|
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0
|
||||||
|
readonly property bool hasFullscreen: {
|
||||||
|
if (hasSpecialWorkspace) {
|
||||||
|
const specialName = monitor?.lastIpcObject.specialWorkspace?.name;
|
||||||
|
if (!specialName)
|
||||||
|
return false;
|
||||||
|
const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName);
|
||||||
|
return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
|
||||||
|
}
|
||||||
|
return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
|
||||||
|
}
|
||||||
|
property real borderThickness: hasFullscreen ? 0 : Config.border.thickness
|
||||||
|
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness
|
||||||
|
property real borderRounding: hasFullscreen ? 0 : Config.border.rounding
|
||||||
|
property real shadowOpacity: hasFullscreen ? 0 : 0.7
|
||||||
|
|
||||||
|
readonly property int dragMaskPadding: {
|
||||||
|
if (focusGrab.active || panels.popouts.isDetached)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const thresholds = [];
|
||||||
|
for (const panel of ["dashboard", "launcher", "session", "sidebar"])
|
||||||
|
if (Config[panel].enabled)
|
||||||
|
thresholds.push(Config[panel].dragThreshold);
|
||||||
|
return Math.max(...thresholds);
|
||||||
|
}
|
||||||
|
|
||||||
|
onHasFullscreenChanged: {
|
||||||
|
visibilities.launcher = false;
|
||||||
|
visibilities.session = false;
|
||||||
|
visibilities.dashboard = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
name: "drawers"
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
mask: Regions {
|
||||||
|
bar: bar
|
||||||
|
panels: panels
|
||||||
|
win: root
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.top: true
|
||||||
|
anchors.bottom: true
|
||||||
|
anchors.left: true
|
||||||
|
anchors.right: true
|
||||||
|
|
||||||
|
Behavior on borderThickness {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on borderRounding {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on shadowOpacity {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: focusGrab
|
||||||
|
|
||||||
|
active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1)
|
||||||
|
windows: [root]
|
||||||
|
onCleared: {
|
||||||
|
visibilities.launcher = false;
|
||||||
|
visibilities.session = false;
|
||||||
|
visibilities.sidebar = false;
|
||||||
|
visibilities.dashboard = false;
|
||||||
|
panels.popouts.hasCurrent = false;
|
||||||
|
bar.closeTray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
opacity: visibilities.session && Config.session.enabled ? 0.5 : 0
|
||||||
|
color: Colours.palette.m3scrim
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
shadowEnabled: true
|
||||||
|
blurMax: 15
|
||||||
|
shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobGroup {
|
||||||
|
id: blobGroup
|
||||||
|
|
||||||
|
color: Colours.palette.m3surface
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobInvertedRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers
|
||||||
|
group: blobGroup
|
||||||
|
radius: root.borderRounding
|
||||||
|
borderLeft: bar.implicitWidth - anchors.margins
|
||||||
|
borderRight: root.borderThickness - anchors.margins
|
||||||
|
borderTop: root.borderThickness - anchors.margins
|
||||||
|
borderBottom: root.borderThickness - anchors.margins
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: dashBg
|
||||||
|
|
||||||
|
panel: panels.dashboard
|
||||||
|
deformAmount: 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: launcherBg
|
||||||
|
|
||||||
|
panel: panels.launcher
|
||||||
|
deformAmount: 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: sessionBg
|
||||||
|
|
||||||
|
panel: panels.sessionWrapper
|
||||||
|
deformAmount: 0.2
|
||||||
|
x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth
|
||||||
|
implicitWidth: panels.session.width
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: sidebarBg
|
||||||
|
|
||||||
|
panel: panels.sidebar
|
||||||
|
deformAmount: 0.03
|
||||||
|
implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2
|
||||||
|
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg]
|
||||||
|
bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: osdBg
|
||||||
|
|
||||||
|
panel: panels.osdWrapper
|
||||||
|
deformAmount: 0.25
|
||||||
|
x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth
|
||||||
|
implicitWidth: panels.osd.width
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: notifsBg
|
||||||
|
|
||||||
|
panel: panels.notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: utilsBg
|
||||||
|
|
||||||
|
panel: panels.utilities
|
||||||
|
deformAmount: panels.sidebar.visible ? 0.1 : 0.15
|
||||||
|
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
|
||||||
|
topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelBg {
|
||||||
|
id: popoutBg
|
||||||
|
|
||||||
|
// Extra width to prevent vertical movement deformation partially detaching panel from bar
|
||||||
|
property real extraWidth: panels.popouts.isDetached ? 0 : 0.2
|
||||||
|
|
||||||
|
panel: panels.popoutsWrapper
|
||||||
|
deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1
|
||||||
|
x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth
|
||||||
|
implicitWidth: panels.popouts.width * (1 + extraWidth)
|
||||||
|
|
||||||
|
Behavior on extraWidth {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawerVisibilities {
|
||||||
|
id: visibilities
|
||||||
|
|
||||||
|
Component.onCompleted: Visibilities.load(root.screen, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Interactions {
|
||||||
|
screen: root.screen
|
||||||
|
popouts: panels.popouts
|
||||||
|
visibilities: visibilities
|
||||||
|
panels: panels
|
||||||
|
bar: bar
|
||||||
|
borderThickness: root.borderLayoutThickness
|
||||||
|
fullscreen: root.hasFullscreen
|
||||||
|
|
||||||
|
Panels {
|
||||||
|
id: panels
|
||||||
|
|
||||||
|
screen: root.screen
|
||||||
|
visibilities: visibilities
|
||||||
|
bar: bar
|
||||||
|
borderThickness: root.borderThickness
|
||||||
|
|
||||||
|
utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1
|
||||||
|
|
||||||
|
dashboard.transform: Matrix4x4 {
|
||||||
|
matrix: dashBg.deformMatrix
|
||||||
|
}
|
||||||
|
launcher.transform: Matrix4x4 {
|
||||||
|
matrix: launcherBg.deformMatrix
|
||||||
|
}
|
||||||
|
session.transform: Matrix4x4 {
|
||||||
|
matrix: sessionBg.deformMatrix
|
||||||
|
}
|
||||||
|
sidebar.transform: Matrix4x4 {
|
||||||
|
matrix: sidebarBg.deformMatrix
|
||||||
|
}
|
||||||
|
osd.transform: Matrix4x4 {
|
||||||
|
matrix: osdBg.deformMatrix
|
||||||
|
}
|
||||||
|
notifications.transform: Matrix4x4 {
|
||||||
|
matrix: notifsBg.deformMatrix
|
||||||
|
}
|
||||||
|
utilities.transform: Matrix4x4 {
|
||||||
|
matrix: utilsBg.deformMatrix
|
||||||
|
}
|
||||||
|
popouts.transform: Matrix4x4 {
|
||||||
|
matrix: popoutBg.deformMatrix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BarWrapper {
|
||||||
|
id: bar
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
|
||||||
|
screen: root.screen
|
||||||
|
visibilities: visibilities
|
||||||
|
popouts: panels.popouts
|
||||||
|
|
||||||
|
fullscreen: root.hasFullscreen
|
||||||
|
|
||||||
|
Component.onCompleted: Visibilities.bars.set(root.screen, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component PanelBg: BlobRect {
|
||||||
|
required property Item panel
|
||||||
|
property real deformAmount: 0.15
|
||||||
|
|
||||||
|
group: blobGroup
|
||||||
|
x: panel.x + bar.implicitWidth
|
||||||
|
y: panel.y + root.borderThickness
|
||||||
|
implicitWidth: panel.width
|
||||||
|
implicitHeight: panel.height
|
||||||
|
radius: Config.border.rounding
|
||||||
|
deformScale: deformAmount / 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,9 @@
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.components
|
|
||||||
import qs.components.containers
|
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.config
|
import qs.config
|
||||||
import qs.utils
|
|
||||||
import qs.modules.bar
|
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: Screens.screens
|
model: Screens.screens
|
||||||
|
|
@ -20,204 +12,17 @@ Variants {
|
||||||
id: scope
|
id: scope
|
||||||
|
|
||||||
required property ShellScreen modelData
|
required property ShellScreen modelData
|
||||||
readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name)
|
|
||||||
|
|
||||||
Exclusions {
|
Exclusions {
|
||||||
screen: scope.modelData
|
screen: scope.modelData
|
||||||
bar: bar
|
bar: content.bar
|
||||||
borderThickness: Config.border.thickness
|
borderThickness: Config.border.thickness
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledWindow {
|
ContentWindow {
|
||||||
id: win
|
id: content
|
||||||
|
|
||||||
readonly property var monitor: Hypr.monitorFor(screen)
|
|
||||||
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject?.specialWorkspace?.name.length ?? 0) > 0
|
|
||||||
readonly property bool hasFullscreen: {
|
|
||||||
if (hasSpecialWorkspace) {
|
|
||||||
const specialName = monitor?.lastIpcObject?.specialWorkspace?.name;
|
|
||||||
if (!specialName)
|
|
||||||
return false;
|
|
||||||
const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName);
|
|
||||||
return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
|
|
||||||
}
|
|
||||||
return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
|
|
||||||
}
|
|
||||||
property real borderThickness: hasFullscreen ? 0 : Config.border.thickness
|
|
||||||
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness
|
|
||||||
property real borderRounding: hasFullscreen ? 0 : Config.border.rounding
|
|
||||||
property real shadowOpacity: hasFullscreen ? 0 : 0.7
|
|
||||||
readonly property int dragMaskPadding: {
|
|
||||||
if (focusGrab.active || panels.popouts.isDetached)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const mon = Hypr.monitorFor(screen);
|
|
||||||
if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace.lastIpcObject.windows > 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const thresholds = [];
|
|
||||||
for (const panel of ["dashboard", "launcher", "session", "sidebar"])
|
|
||||||
if (Config[panel].enabled)
|
|
||||||
thresholds.push(Config[panel].dragThreshold);
|
|
||||||
return Math.max(...thresholds);
|
|
||||||
}
|
|
||||||
|
|
||||||
onHasFullscreenChanged: {
|
|
||||||
visibilities.launcher = false;
|
|
||||||
visibilities.session = false;
|
|
||||||
visibilities.dashboard = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: scope.modelData
|
screen: scope.modelData
|
||||||
name: "drawers"
|
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
mask: Region {
|
|
||||||
x: bar.clampedWidth + win.dragMaskPadding
|
|
||||||
y: Config.border.clampedThickness + win.dragMaskPadding
|
|
||||||
width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2
|
|
||||||
height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2
|
|
||||||
intersection: Intersection.Xor
|
|
||||||
|
|
||||||
regions: regions.instances // qmllint disable stale-property-read
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.top: true
|
|
||||||
anchors.bottom: true
|
|
||||||
anchors.left: true
|
|
||||||
anchors.right: true
|
|
||||||
|
|
||||||
Behavior on borderThickness {
|
|
||||||
Anim {
|
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on borderRounding {
|
|
||||||
Anim {
|
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on shadowOpacity {
|
|
||||||
Anim {
|
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
id: regions
|
|
||||||
|
|
||||||
model: panels.children
|
|
||||||
|
|
||||||
Region {
|
|
||||||
required property Item modelData
|
|
||||||
|
|
||||||
x: modelData.x + bar.implicitWidth
|
|
||||||
y: modelData.y + win.borderLayoutThickness
|
|
||||||
width: modelData.width
|
|
||||||
height: modelData.height
|
|
||||||
intersection: Intersection.Subtract
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
|
||||||
id: focusGrab
|
|
||||||
|
|
||||||
active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1)
|
|
||||||
windows: [win]
|
|
||||||
onCleared: {
|
|
||||||
visibilities.launcher = false;
|
|
||||||
visibilities.session = false;
|
|
||||||
visibilities.sidebar = false;
|
|
||||||
visibilities.dashboard = false;
|
|
||||||
panels.popouts.hasCurrent = false;
|
|
||||||
bar.closeTray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
opacity: visibilities.session && Config.session.enabled ? 0.5 : 0
|
|
||||||
color: Colours.palette.m3scrim
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: true
|
|
||||||
blurMax: 15
|
|
||||||
shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity))
|
|
||||||
}
|
|
||||||
|
|
||||||
Border {
|
|
||||||
bar: bar
|
|
||||||
borderThickness: win.borderThickness
|
|
||||||
borderRounding: win.borderRounding
|
|
||||||
}
|
|
||||||
|
|
||||||
Backgrounds {
|
|
||||||
panels: panels
|
|
||||||
bar: bar
|
|
||||||
borderThickness: win.borderThickness
|
|
||||||
borderRounding: win.borderRounding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawerVisibilities {
|
|
||||||
id: visibilities
|
|
||||||
|
|
||||||
Component.onCompleted: Visibilities.load(scope.modelData, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
Interactions {
|
|
||||||
screen: scope.modelData
|
|
||||||
popouts: panels.popouts
|
|
||||||
visibilities: visibilities
|
|
||||||
panels: panels
|
|
||||||
bar: bar
|
|
||||||
borderThickness: win.borderLayoutThickness
|
|
||||||
fullscreen: win.hasFullscreen
|
|
||||||
|
|
||||||
Panels {
|
|
||||||
id: panels
|
|
||||||
|
|
||||||
screen: scope.modelData
|
|
||||||
visibilities: visibilities
|
|
||||||
bar: bar
|
|
||||||
borderThickness: win.borderLayoutThickness
|
|
||||||
}
|
|
||||||
|
|
||||||
BarWrapper {
|
|
||||||
id: bar
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
|
|
||||||
screen: scope.modelData
|
|
||||||
visibilities: visibilities
|
|
||||||
popouts: panels.popouts
|
|
||||||
|
|
||||||
disabled: scope.barDisabled
|
|
||||||
fullscreen: win.hasFullscreen
|
|
||||||
|
|
||||||
Component.onCompleted: Visibilities.bars.set(scope.modelData, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,13 @@ CustomMouseArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
function inTopPanel(panel: Item, x: real, y: real): bool {
|
function inTopPanel(panel: Item, x: real, y: real): bool {
|
||||||
return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y);
|
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property
|
||||||
|
return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool {
|
function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool {
|
||||||
return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y);
|
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property
|
||||||
|
return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWheel(event: WheelEvent): void {
|
function onWheel(event: WheelEvent): void {
|
||||||
|
|
@ -107,9 +109,9 @@ CustomMouseArea {
|
||||||
visibilities.bar = false;
|
visibilities.bar = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panels.sidebar.width === 0) {
|
if (panels.sidebar.offsetScale === 1) {
|
||||||
// Show osd on hover
|
// Show osd on hover
|
||||||
const showOsd = inRightPanel(panels.osd, x, y);
|
const showOsd = inRightPanel(panels.osdWrapper, x, y);
|
||||||
|
|
||||||
// Always update visibility based on hover if not in shortcut mode
|
// Always update visibility based on hover if not in shortcut mode
|
||||||
if (!osdShortcutActive) {
|
if (!osdShortcutActive) {
|
||||||
|
|
@ -124,23 +126,23 @@ CustomMouseArea {
|
||||||
const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x);
|
const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x);
|
||||||
|
|
||||||
// Show/hide session on drag
|
// Show/hide session on drag
|
||||||
if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) {
|
if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) {
|
||||||
if (dragX < -Config.session.dragThreshold)
|
if (dragX < -Config.session.dragThreshold)
|
||||||
visibilities.session = true;
|
visibilities.session = true;
|
||||||
else if (dragX > Config.session.dragThreshold)
|
else if (dragX > Config.session.dragThreshold)
|
||||||
visibilities.session = false;
|
visibilities.session = false;
|
||||||
|
|
||||||
// Show sidebar on drag if in session area and session is nearly fully visible
|
// Show sidebar on drag if in session area and session is nearly fully visible
|
||||||
if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold)
|
if (showSidebar && panels.session.offsetScale <= 0 && dragX < -Config.sidebar.dragThreshold)
|
||||||
visibilities.sidebar = true;
|
visibilities.sidebar = true;
|
||||||
} else if (showSidebar && dragX < -Config.sidebar.dragThreshold) {
|
} else if (showSidebar && dragX < -Config.sidebar.dragThreshold) {
|
||||||
// Show sidebar on drag if not in session area
|
// Show sidebar on drag if not in session area
|
||||||
visibilities.sidebar = true;
|
visibilities.sidebar = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const outOfSidebar = x < width - panels.sidebar.width;
|
const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
|
||||||
// Show osd on hover
|
// Show osd on hover
|
||||||
const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y);
|
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
|
||||||
|
|
||||||
// Always update visibility based on hover if not in shortcut mode
|
// Always update visibility based on hover if not in shortcut mode
|
||||||
if (!osdShortcutActive) {
|
if (!osdShortcutActive) {
|
||||||
|
|
@ -153,7 +155,7 @@ CustomMouseArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide session on drag
|
// Show/hide session on drag
|
||||||
if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) {
|
if (pressed && outOfSidebar && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) {
|
||||||
if (dragX < -Config.session.dragThreshold)
|
if (dragX < -Config.session.dragThreshold)
|
||||||
visibilities.session = true;
|
visibilities.session = true;
|
||||||
else if (dragX > Config.session.dragThreshold)
|
else if (dragX > Config.session.dragThreshold)
|
||||||
|
|
@ -209,7 +211,7 @@ CustomMouseArea {
|
||||||
// Show popouts on hover
|
// Show popouts on hover
|
||||||
if (x < bar.implicitWidth) {
|
if (x < bar.implicitWidth) {
|
||||||
bar.checkPopout(y);
|
bar.checkPopout(y);
|
||||||
} else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) {
|
} else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) {
|
||||||
popouts.hasCurrent = false;
|
popouts.hasCurrent = false;
|
||||||
bar.closeTray();
|
bar.closeTray();
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +228,7 @@ CustomMouseArea {
|
||||||
|
|
||||||
// Also hide dashboard and OSD if they're not being hovered
|
// Also hide dashboard and OSD if they're not being hovered
|
||||||
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
|
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
|
||||||
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
|
const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY);
|
||||||
|
|
||||||
if (!inDashboardArea) {
|
if (!inDashboardArea) {
|
||||||
root.visibilities.dashboard = false;
|
root.visibilities.dashboard = false;
|
||||||
|
|
@ -254,7 +256,7 @@ CustomMouseArea {
|
||||||
function onOsdChanged() {
|
function onOsdChanged() {
|
||||||
if (root.visibilities.osd) {
|
if (root.visibilities.osd) {
|
||||||
// OSD became visible, immediately check if this should be shortcut mode
|
// OSD became visible, immediately check if this should be shortcut mode
|
||||||
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
|
const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY);
|
||||||
if (!inOsdArea) {
|
if (!inOsdArea) {
|
||||||
root.osdShortcutActive = true;
|
root.osdShortcutActive = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,37 +22,43 @@ Item {
|
||||||
required property real borderThickness
|
required property real borderThickness
|
||||||
|
|
||||||
readonly property alias osd: osd
|
readonly property alias osd: osd
|
||||||
|
readonly property alias osdWrapper: osdWrapper
|
||||||
readonly property alias notifications: notifications
|
readonly property alias notifications: notifications
|
||||||
readonly property alias session: session
|
readonly property alias session: session
|
||||||
|
readonly property alias sessionWrapper: sessionWrapper
|
||||||
readonly property alias launcher: launcher
|
readonly property alias launcher: launcher
|
||||||
readonly property alias dashboard: dashboard
|
readonly property alias dashboard: dashboard
|
||||||
readonly property alias popouts: popouts
|
readonly property alias popouts: popoutsWrapper.content
|
||||||
|
readonly property alias popoutsWrapper: popoutsWrapper
|
||||||
readonly property alias utilities: utilities
|
readonly property alias utilities: utilities
|
||||||
readonly property alias toasts: toasts
|
readonly property alias toasts: toasts
|
||||||
readonly property alias sidebar: sidebar
|
readonly property alias sidebar: sidebar
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: root.borderThickness
|
anchors.margins: borderThickness
|
||||||
anchors.leftMargin: bar.implicitWidth
|
anchors.leftMargin: bar.implicitWidth
|
||||||
|
|
||||||
Behavior on anchors.margins {
|
Item {
|
||||||
Anim {}
|
id: osdWrapper
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on anchors.leftMargin {
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
Anim {}
|
anchors.right: parent.right
|
||||||
}
|
anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale)
|
||||||
|
clip: sidebar.visible || session.visible
|
||||||
|
|
||||||
|
implicitWidth: osd.implicitWidth * (1 - osd.offsetScale)
|
||||||
|
implicitHeight: osd.implicitHeight
|
||||||
|
|
||||||
Osd.Wrapper {
|
Osd.Wrapper {
|
||||||
id: osd
|
id: osd
|
||||||
|
|
||||||
clip: session.width > 0 || sidebar.width > 0
|
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
sidebarOrSessionVisible: sidebar.visible || session.visible
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: session.width + sidebar.width
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications.Wrapper {
|
Notifications.Wrapper {
|
||||||
|
|
@ -60,23 +66,33 @@ Item {
|
||||||
|
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
sidebarPanel: sidebar
|
sidebarPanel: sidebar
|
||||||
osdPanel: osd
|
osdPanel: osdWrapper
|
||||||
sessionPanel: session
|
sessionPanel: sessionWrapper
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
}
|
}
|
||||||
|
|
||||||
Session.Wrapper {
|
Item {
|
||||||
id: session
|
id: sessionWrapper
|
||||||
|
|
||||||
clip: sidebar.width > 0
|
|
||||||
visibilities: root.visibilities
|
|
||||||
panels: root
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: sidebar.width
|
anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale)
|
||||||
|
clip: sidebar.visible
|
||||||
|
|
||||||
|
implicitWidth: session.implicitWidth * (1 - session.offsetScale)
|
||||||
|
implicitHeight: session.implicitHeight
|
||||||
|
|
||||||
|
Session.Wrapper {
|
||||||
|
id: session
|
||||||
|
|
||||||
|
visibilities: root.visibilities
|
||||||
|
sidebarVisible: sidebar.visible
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Launcher.Wrapper {
|
Launcher.Wrapper {
|
||||||
|
|
@ -99,22 +115,11 @@ Item {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
}
|
}
|
||||||
|
|
||||||
BarPopouts.Wrapper {
|
BarPopouts.ClipWrapper {
|
||||||
id: popouts
|
id: popoutsWrapper
|
||||||
|
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
|
borderThickness: root.borderThickness
|
||||||
x: isDetached ? (root.width - nonAnimWidth) / 2 : 0
|
|
||||||
y: {
|
|
||||||
if (isDetached)
|
|
||||||
return (root.height - nonAnimHeight) / 2;
|
|
||||||
|
|
||||||
const off = currentCenter - root.borderThickness - nonAnimHeight / 2;
|
|
||||||
const diff = root.height - Math.floor(off + nonAnimHeight);
|
|
||||||
if (diff < 0)
|
|
||||||
return off + diff;
|
|
||||||
return Math.max(off, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Utilities.Wrapper {
|
Utilities.Wrapper {
|
||||||
|
|
@ -122,7 +127,7 @@ Item {
|
||||||
|
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
sidebar: sidebar
|
sidebar: sidebar
|
||||||
popouts: popouts
|
popouts: popoutsWrapper.content
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
@ -140,7 +145,6 @@ Item {
|
||||||
id: sidebar
|
id: sidebar
|
||||||
|
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
panels: root
|
|
||||||
|
|
||||||
anchors.top: notifications.bottom
|
anchors.top: notifications.bottom
|
||||||
anchors.bottom: utilities.top
|
anchors.bottom: utilities.top
|
||||||
|
|
|
||||||
81
modules/drawers/Regions.qml
Normal file
81
modules/drawers/Regions.qml
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.config
|
||||||
|
import qs.modules.bar as Bar
|
||||||
|
|
||||||
|
Region {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property Bar.BarWrapper bar
|
||||||
|
required property Panels panels
|
||||||
|
required property var win
|
||||||
|
|
||||||
|
x: bar.clampedWidth + win.dragMaskPadding
|
||||||
|
y: Config.border.clampedThickness + win.dragMaskPadding
|
||||||
|
width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2
|
||||||
|
height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2
|
||||||
|
intersection: Intersection.Xor
|
||||||
|
|
||||||
|
R {
|
||||||
|
panel: root.panels.dashboard
|
||||||
|
y: 0
|
||||||
|
height: panel.height * (1 - root.panels.dashboard.offsetScale) + Config.border.thickness
|
||||||
|
}
|
||||||
|
|
||||||
|
R {
|
||||||
|
panel: root.panels.launcher
|
||||||
|
y: root.win.height - height
|
||||||
|
height: panel.height * (1 - root.panels.launcher.offsetScale) + Config.border.thickness
|
||||||
|
}
|
||||||
|
|
||||||
|
R {
|
||||||
|
id: sessionRegion
|
||||||
|
|
||||||
|
panel: root.panels.sessionWrapper
|
||||||
|
x: root.win.width - width
|
||||||
|
width: panel.width * (1 - root.panels.session.offsetScale) + Config.border.thickness + sidebarRegion.width
|
||||||
|
}
|
||||||
|
|
||||||
|
R {
|
||||||
|
id: sidebarRegion
|
||||||
|
|
||||||
|
panel: root.panels.sidebar
|
||||||
|
x: root.win.width - width
|
||||||
|
width: panel.width * (1 - root.panels.sidebar.offsetScale) + Config.border.thickness
|
||||||
|
}
|
||||||
|
|
||||||
|
R {
|
||||||
|
panel: root.panels.osdWrapper
|
||||||
|
x: root.win.width - width
|
||||||
|
width: panel.width * (1 - root.panels.osd.offsetScale) + Config.border.thickness + sessionRegion.width
|
||||||
|
}
|
||||||
|
|
||||||
|
R {
|
||||||
|
panel: root.panels.notifications
|
||||||
|
y: 0
|
||||||
|
height: panel.height + Config.border.thickness
|
||||||
|
}
|
||||||
|
|
||||||
|
R {
|
||||||
|
panel: root.panels.utilities
|
||||||
|
y: root.win.height - height
|
||||||
|
height: panel.height * (1 - root.panels.utilities.offsetScale) + Config.border.thickness
|
||||||
|
}
|
||||||
|
|
||||||
|
R {
|
||||||
|
panel: root.panels.popoutsWrapper
|
||||||
|
width: panel.width * (1 - root.panels.popoutsWrapper.offsetScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
component R: Region {
|
||||||
|
required property Item panel
|
||||||
|
|
||||||
|
x: panel.x + root.bar.implicitWidth
|
||||||
|
y: panel.y + Config.border.thickness
|
||||||
|
width: panel.width
|
||||||
|
height: panel.height
|
||||||
|
intersection: Intersection.Subtract
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
import qs.config
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Wrapper wrapper
|
|
||||||
readonly property real rounding: Config.border.rounding
|
|
||||||
readonly property bool flatten: wrapper.height < rounding * 2
|
|
||||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
|
||||||
|
|
||||||
strokeWidth: -1
|
|
||||||
fillColor: Colours.palette.m3surface
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: -root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: -(root.wrapper.height - root.roundingY * 2)
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: -root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: root.wrapper.width - root.rounding * 2
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: root.wrapper.height - root.roundingY * 2
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,7 @@ import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import qs.modules.launcher.services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -13,7 +14,6 @@ Item {
|
||||||
required property var panels
|
required property var panels
|
||||||
|
|
||||||
readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled
|
readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled
|
||||||
property int contentHeight
|
|
||||||
|
|
||||||
readonly property real maxHeight: {
|
readonly property real maxHeight: {
|
||||||
let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large;
|
let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large;
|
||||||
|
|
@ -22,91 +22,28 @@ Item {
|
||||||
return max;
|
return max;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMaxHeightChanged: timer.start()
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
|
|
||||||
visible: height > 0
|
|
||||||
implicitHeight: 0
|
|
||||||
implicitWidth: content.implicitWidth
|
|
||||||
|
|
||||||
onShouldBeActiveChanged: {
|
onShouldBeActiveChanged: {
|
||||||
if (shouldBeActive) {
|
if (shouldBeActive)
|
||||||
timer.stop();
|
implicitHeight = Qt.binding(() => content.implicitHeight);
|
||||||
hideAnim.stop();
|
else
|
||||||
showAnim.start();
|
implicitHeight = implicitHeight; // Break binding during close anim
|
||||||
} else {
|
|
||||||
showAnim.stop();
|
|
||||||
hideAnim.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
visible: offsetScale < 1
|
||||||
id: showAnim
|
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||||
|
implicitHeight: content.implicitHeight
|
||||||
|
implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open
|
||||||
|
opacity: 1 - offsetScale
|
||||||
|
|
||||||
|
Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init
|
||||||
|
|
||||||
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {
|
||||||
target: root
|
|
||||||
property: "implicitHeight"
|
|
||||||
to: root.contentHeight
|
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
ScriptAction {
|
|
||||||
script: root.implicitHeight = Qt.binding(() => content.implicitHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: hideAnim
|
|
||||||
|
|
||||||
ScriptAction {
|
|
||||||
script: root.implicitHeight = root.implicitHeight
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: root
|
|
||||||
property: "implicitHeight"
|
|
||||||
to: 0
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onEnabledChanged(): void {
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMaxShownChanged(): void {
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Config.launcher
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onValuesChanged(): void {
|
|
||||||
if (DesktopEntries.applications.values.length < Config.launcher.maxShown)
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: DesktopEntries.applications
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: timer
|
|
||||||
|
|
||||||
interval: Appearance.anim.durations.extraLarge
|
|
||||||
onRunningChanged: {
|
|
||||||
if (running && !root.shouldBeActive) {
|
|
||||||
content.visible = false;
|
|
||||||
content.active = true;
|
|
||||||
} else {
|
|
||||||
root.contentHeight = Math.min(root.maxHeight, content.implicitHeight);
|
|
||||||
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
|
|
||||||
content.visible = true;
|
|
||||||
if (showAnim.running) {
|
|
||||||
showAnim.stop();
|
|
||||||
showAnim.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|
@ -115,16 +52,12 @@ Item {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
visible: false
|
active: root.shouldBeActive || root.visible
|
||||||
active: false
|
|
||||||
Component.onCompleted: timer.start()
|
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
panels: root.panels
|
panels: root.panels
|
||||||
maxHeight: root.maxHeight
|
maxHeight: root.maxHeight
|
||||||
|
|
||||||
Component.onCompleted: root.contentHeight = implicitHeight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Wrapper wrapper
|
|
||||||
required property var sidebar
|
|
||||||
required property real rounding
|
|
||||||
readonly property bool flatten: wrapper.height < rounding * 2
|
|
||||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
|
||||||
|
|
||||||
strokeWidth: -1
|
|
||||||
fillColor: Colours.palette.m3surface
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
relativeX: -(root.wrapper.width + root.rounding)
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: root.wrapper.height - root.roundingY * 2
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: root.roundingY
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.rounding
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: root.rounding
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.config
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -11,27 +10,10 @@ Item {
|
||||||
property alias sessionPanel: content.sessionPanel
|
property alias sessionPanel: content.sessionPanel
|
||||||
|
|
||||||
visible: height > 0
|
visible: height > 0
|
||||||
|
anchors.topMargin: -5
|
||||||
implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth)
|
implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth)
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
states: State {
|
|
||||||
name: "hidden"
|
|
||||||
when: root.visibilities.sidebar && Config.sidebar.enabled
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
root.implicitHeight: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transitions: Transition {
|
|
||||||
Anim {
|
|
||||||
target: root
|
|
||||||
property: "implicitHeight"
|
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Content {
|
Content {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Wrapper wrapper
|
|
||||||
required property real rounding
|
|
||||||
readonly property bool flatten: wrapper.width < rounding * 2
|
|
||||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
|
||||||
|
|
||||||
strokeWidth: -1
|
|
||||||
fillColor: Colours.palette.m3surface
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: -root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: -(root.wrapper.width - root.roundingX * 2)
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: -root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: root.wrapper.height - root.rounding * 2
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: root.wrapper.width - root.roundingX * 2
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,9 +11,13 @@ Item {
|
||||||
|
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
required property DrawerVisibilities visibilities
|
required property DrawerVisibilities visibilities
|
||||||
|
required property bool sidebarOrSessionVisible
|
||||||
|
|
||||||
property bool hovered
|
property bool hovered
|
||||||
readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen)
|
readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen)
|
||||||
readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled)
|
readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled)
|
||||||
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
|
property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0
|
||||||
|
|
||||||
property real volume
|
property real volume
|
||||||
property bool muted
|
property bool muted
|
||||||
|
|
@ -34,41 +38,18 @@ Item {
|
||||||
brightness = root.monitor?.brightness ?? 0;
|
brightness = root.monitor?.brightness ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: width > 0
|
visible: offsetScale < 1
|
||||||
implicitWidth: 0
|
anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale
|
||||||
|
implicitWidth: content.implicitWidth
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
|
opacity: 1 - offsetScale
|
||||||
|
|
||||||
states: State {
|
Behavior on offsetScale {
|
||||||
name: "visible"
|
|
||||||
when: root.shouldBeActive
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
root.implicitWidth: content.implicitWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transitions: [
|
|
||||||
Transition {
|
|
||||||
from: ""
|
|
||||||
to: "visible"
|
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
target: root
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
property: "implicitWidth"
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Transition {
|
|
||||||
from: "visible"
|
|
||||||
to: ""
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
target: root
|
|
||||||
property: "implicitWidth"
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onMutedChanged(): void {
|
function onMutedChanged(): void {
|
||||||
|
|
@ -119,7 +100,8 @@ Item {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
||||||
Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible)
|
asynchronous: true
|
||||||
|
active: root.shouldBeActive || root.visible
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
monitor: root.monitor
|
monitor: root.monitor
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
import qs.config
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Wrapper wrapper
|
|
||||||
readonly property real rounding: Config.border.rounding
|
|
||||||
readonly property bool flatten: wrapper.width < rounding * 2
|
|
||||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
|
||||||
|
|
||||||
strokeWidth: -1
|
|
||||||
fillColor: Colours.palette.m3surface
|
|
||||||
|
|
||||||
PathArc {
|
|
||||||
relativeX: -root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: -(root.wrapper.width - root.roundingX * 2)
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: -root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: root.wrapper.height - root.rounding * 2
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
direction: PathArc.Counterclockwise
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: root.wrapper.width - root.roundingX * 2
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.roundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,44 +8,25 @@ Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property DrawerVisibilities visibilities
|
required property DrawerVisibilities visibilities
|
||||||
required property var panels
|
required property bool sidebarVisible
|
||||||
readonly property real nonAnimWidth: content.implicitWidth
|
readonly property real nonAnimWidth: content.implicitWidth
|
||||||
|
|
||||||
visible: width > 0
|
readonly property bool shouldBeActive: visibilities.session && Config.session.enabled
|
||||||
implicitWidth: 0
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
implicitHeight: content.implicitHeight
|
property real sidebarOffset: sidebarVisible ? 14 : 0
|
||||||
|
|
||||||
states: State {
|
visible: offsetScale < 1
|
||||||
name: "visible"
|
anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale
|
||||||
when: root.visibilities.session && Config.session.enabled
|
implicitWidth: content.implicitWidth
|
||||||
|
implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open
|
||||||
PropertyChanges {
|
opacity: 1 - offsetScale
|
||||||
root.implicitWidth: root.nonAnimWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transitions: [
|
|
||||||
Transition {
|
|
||||||
from: ""
|
|
||||||
to: "visible"
|
|
||||||
|
|
||||||
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {
|
||||||
target: root
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
property: "implicitWidth"
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Transition {
|
|
||||||
from: "visible"
|
|
||||||
to: ""
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
target: root
|
|
||||||
property: "implicitWidth"
|
|
||||||
easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: content
|
id: content
|
||||||
|
|
@ -53,7 +34,7 @@ Item {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
||||||
Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible)
|
active: root.shouldBeActive || root.visible
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.components
|
|
||||||
import qs.services
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Wrapper wrapper
|
|
||||||
required property var panels
|
|
||||||
required property real rounding
|
|
||||||
|
|
||||||
readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width
|
|
||||||
readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding
|
|
||||||
|
|
||||||
readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width
|
|
||||||
readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding
|
|
||||||
|
|
||||||
strokeWidth: -1
|
|
||||||
fillColor: Colours.palette.m3surface
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
relativeX: -root.wrapper.width - root.notifsRoundingX
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: root.notifsRoundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: root.notifsRoundingX
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: 0
|
|
||||||
relativeY: root.wrapper.height - root.rounding * 2
|
|
||||||
}
|
|
||||||
PathArc {
|
|
||||||
relativeX: -root.utilsRoundingX
|
|
||||||
relativeY: root.rounding
|
|
||||||
radiusX: root.utilsRoundingX
|
|
||||||
radiusY: root.rounding
|
|
||||||
}
|
|
||||||
PathLine {
|
|
||||||
relativeX: root.wrapper.width + root.utilsRoundingX
|
|
||||||
relativeY: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,44 +8,22 @@ Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property DrawerVisibilities visibilities
|
required property DrawerVisibilities visibilities
|
||||||
required property var panels
|
|
||||||
readonly property Props props: Props {}
|
readonly property Props props: Props {}
|
||||||
|
|
||||||
visible: width > 0
|
readonly property bool shouldBeActive: visibilities.sidebar && Config.sidebar.enabled
|
||||||
implicitWidth: 0
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
|
|
||||||
states: State {
|
visible: offsetScale < 1
|
||||||
name: "visible"
|
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
|
||||||
when: root.visibilities.sidebar && Config.sidebar.enabled
|
implicitWidth: Config.sidebar.sizes.width
|
||||||
|
opacity: 1 - offsetScale
|
||||||
PropertyChanges {
|
|
||||||
root.implicitWidth: Config.sidebar.sizes.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transitions: [
|
|
||||||
Transition {
|
|
||||||
from: ""
|
|
||||||
to: "visible"
|
|
||||||
|
|
||||||
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {
|
||||||
target: root
|
|
||||||
property: "implicitWidth"
|
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Transition {
|
|
||||||
from: "visible"
|
|
||||||
to: ""
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
target: root
|
|
||||||
property: "implicitWidth"
|
|
||||||
easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: content
|
id: content
|
||||||
|
|
@ -56,8 +34,7 @@ Item {
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.large
|
||||||
anchors.bottomMargin: 0
|
anchors.bottomMargin: 0
|
||||||
|
|
||||||
active: true
|
active: root.shouldBeActive || root.visible
|
||||||
Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible)
|
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2
|
implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,16 @@ import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import qs.modules.sidebar as Sidebar
|
||||||
import qs.modules.bar.popouts as BarPopouts
|
import qs.modules.bar.popouts as BarPopouts
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property DrawerVisibilities visibilities
|
required property DrawerVisibilities visibilities
|
||||||
required property Item sidebar
|
required property Sidebar.Wrapper sidebar
|
||||||
required property BarPopouts.Wrapper popouts
|
required property BarPopouts.Wrapper popouts
|
||||||
|
property real horizontalStretch
|
||||||
|
|
||||||
readonly property PersistentProperties props: PersistentProperties {
|
readonly property PersistentProperties props: PersistentProperties {
|
||||||
property bool recordingListExpanded: false
|
property bool recordingListExpanded: false
|
||||||
|
|
@ -21,75 +23,64 @@ Item {
|
||||||
reloadableId: "utilities"
|
reloadableId: "utilities"
|
||||||
}
|
}
|
||||||
readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled))
|
readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled))
|
||||||
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
|
property real sidebarLerp
|
||||||
|
|
||||||
visible: height > 0
|
visible: offsetScale < 1
|
||||||
implicitHeight: 0
|
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||||
implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width
|
implicitHeight: content.implicitHeight + content.anchors.margins * 2
|
||||||
|
implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp)
|
||||||
onStateChanged: {
|
opacity: 1 - offsetScale
|
||||||
if (state === "visible" && timer.running) {
|
|
||||||
timer.triggered();
|
|
||||||
timer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
states: State {
|
states: State {
|
||||||
name: "visible"
|
name: "attachedToSidebar"
|
||||||
when: root.shouldBeActive
|
when: root.visibilities.sidebar
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
root.sidebarLerp: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transitions: [
|
transitions: [
|
||||||
Transition {
|
Transition {
|
||||||
from: ""
|
from: ""
|
||||||
to: "visible"
|
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
target: root
|
property: "sidebarLerp"
|
||||||
property: "implicitHeight"
|
duration: Appearance.anim.durations.expressiveDefaultSpatial / 2
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Transition {
|
Transition {
|
||||||
from: "visible"
|
|
||||||
to: ""
|
to: ""
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
target: root
|
property: "sidebarLerp"
|
||||||
property: "implicitHeight"
|
duration: Appearance.anim.durations.expressiveDefaultSpatial / 2
|
||||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
Timer {
|
Behavior on offsetScale {
|
||||||
id: timer
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
running: true
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
interval: Appearance.anim.durations.extraLarge
|
|
||||||
onTriggered: {
|
|
||||||
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
|
|
||||||
content.visible = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
asynchronous: true
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.large
|
||||||
|
|
||||||
visible: false
|
asynchronous: true
|
||||||
active: true
|
active: root.shouldBeActive || root.visible
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
implicitWidth: root.implicitWidth - Appearance.padding.large * 2
|
implicitWidth: root.implicitWidth - content.anchors.margins * 2
|
||||||
props: root.props
|
props: root.props
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
popouts: root.popouts
|
popouts: root.popouts
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ ColumnLayout {
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
active: root.client?.lastIpcObject.floating
|
active: root.client?.lastIpcObject.floating ?? false
|
||||||
Layout.fillWidth: active
|
Layout.fillWidth: active
|
||||||
Layout.leftMargin: active ? 0 : -parent.spacing
|
Layout.leftMargin: active ? 0 : -parent.spacing
|
||||||
Layout.rightMargin: active ? 0 : -parent.spacing
|
Layout.rightMargin: active ? 0 : -parent.spacing
|
||||||
|
|
|
||||||
19
plugin/src/Caelestia/Blobs/CMakeLists.txt
Normal file
19
plugin/src/Caelestia/Blobs/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
qml_module(caelestia-blobs
|
||||||
|
URI Caelestia.Blobs
|
||||||
|
SOURCES
|
||||||
|
blobgroup.cpp
|
||||||
|
blobshape.cpp
|
||||||
|
blobrect.cpp
|
||||||
|
blobinvertedrect.cpp
|
||||||
|
blobmaterial.cpp
|
||||||
|
LIBRARIES
|
||||||
|
Qt::Quick
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_shaders(caelestia-blobs "blob_shaders"
|
||||||
|
BATCHABLE OPTIMIZED NOHLSL NOMSL
|
||||||
|
PREFIX "/"
|
||||||
|
FILES
|
||||||
|
shaders/blob.frag
|
||||||
|
shaders/blob.vert
|
||||||
|
)
|
||||||
104
plugin/src/Caelestia/Blobs/blobgroup.cpp
Normal file
104
plugin/src/Caelestia/Blobs/blobgroup.cpp
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
#include "blobgroup.hpp"
|
||||||
|
#include "blobinvertedrect.hpp"
|
||||||
|
#include "blobshape.hpp"
|
||||||
|
|
||||||
|
BlobGroup::BlobGroup(QObject* parent)
|
||||||
|
: QObject(parent) {}
|
||||||
|
|
||||||
|
BlobGroup::~BlobGroup() {
|
||||||
|
for (auto* shape : std::as_const(m_shapes))
|
||||||
|
shape->m_group = nullptr;
|
||||||
|
if (m_invertedRect)
|
||||||
|
static_cast<BlobShape*>(m_invertedRect)->m_group = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::setSmoothing(qreal s) {
|
||||||
|
if (qFuzzyCompare(m_smoothing, s))
|
||||||
|
return;
|
||||||
|
m_smoothing = s;
|
||||||
|
emit smoothingChanged();
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::setColor(const QColor& c) {
|
||||||
|
if (m_color == c)
|
||||||
|
return;
|
||||||
|
m_color = c;
|
||||||
|
emit colorChanged();
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::addShape(BlobShape* shape) {
|
||||||
|
if (!shape || m_shapes.contains(shape))
|
||||||
|
return;
|
||||||
|
m_shapes.append(shape);
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::removeShape(BlobShape* shape) {
|
||||||
|
m_shapes.removeOne(shape);
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::setInvertedRect(BlobInvertedRect* rect) {
|
||||||
|
if (m_invertedRect == rect)
|
||||||
|
return;
|
||||||
|
m_invertedRect = rect;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) {
|
||||||
|
if (m_invertedRect != rect)
|
||||||
|
return;
|
||||||
|
m_invertedRect = nullptr;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::markDirty() {
|
||||||
|
m_physicsUpdated = false;
|
||||||
|
for (auto* shape : std::as_const(m_shapes)) {
|
||||||
|
shape->polish();
|
||||||
|
shape->update();
|
||||||
|
}
|
||||||
|
if (m_invertedRect) {
|
||||||
|
static_cast<BlobShape*>(m_invertedRect)->polish();
|
||||||
|
static_cast<BlobShape*>(m_invertedRect)->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::markShapeDirty(BlobShape* source) {
|
||||||
|
m_physicsUpdated = false;
|
||||||
|
|
||||||
|
source->polish();
|
||||||
|
source->update();
|
||||||
|
|
||||||
|
// Use cached padded rects to find spatial neighbors
|
||||||
|
const float pad = static_cast<float>(m_smoothing) * 2.0f;
|
||||||
|
const QRectF srcRect(static_cast<double>(source->m_cachedPaddedX - pad),
|
||||||
|
static_cast<double>(source->m_cachedPaddedY - pad), static_cast<double>(source->m_cachedPaddedW + pad * 2.0f),
|
||||||
|
static_cast<double>(source->m_cachedPaddedH + pad * 2.0f));
|
||||||
|
|
||||||
|
for (auto* shape : std::as_const(m_shapes)) {
|
||||||
|
if (shape == source)
|
||||||
|
continue;
|
||||||
|
const QRectF otherRect(static_cast<double>(shape->m_cachedPaddedX), static_cast<double>(shape->m_cachedPaddedY),
|
||||||
|
static_cast<double>(shape->m_cachedPaddedW), static_cast<double>(shape->m_cachedPaddedH));
|
||||||
|
if (srcRect.intersects(otherRect)) {
|
||||||
|
shape->polish();
|
||||||
|
shape->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_invertedRect && static_cast<BlobShape*>(m_invertedRect) != source) {
|
||||||
|
static_cast<BlobShape*>(m_invertedRect)->polish();
|
||||||
|
static_cast<BlobShape*>(m_invertedRect)->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobGroup::ensurePhysicsUpdated() {
|
||||||
|
if (m_physicsUpdated)
|
||||||
|
return;
|
||||||
|
m_physicsUpdated = true;
|
||||||
|
for (auto* shape : std::as_const(m_shapes))
|
||||||
|
shape->updatePhysics();
|
||||||
|
}
|
||||||
53
plugin/src/Caelestia/Blobs/blobgroup.hpp
Normal file
53
plugin/src/Caelestia/Blobs/blobgroup.hpp
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qcolor.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
|
||||||
|
class BlobShape;
|
||||||
|
class BlobInvertedRect;
|
||||||
|
|
||||||
|
class BlobGroup : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged)
|
||||||
|
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BlobGroup(QObject* parent = nullptr);
|
||||||
|
~BlobGroup() override;
|
||||||
|
|
||||||
|
qreal smoothing() const { return m_smoothing; }
|
||||||
|
|
||||||
|
void setSmoothing(qreal s);
|
||||||
|
|
||||||
|
QColor color() const { return m_color; }
|
||||||
|
|
||||||
|
void setColor(const QColor& c);
|
||||||
|
|
||||||
|
void addShape(BlobShape* shape);
|
||||||
|
void removeShape(BlobShape* shape);
|
||||||
|
|
||||||
|
void setInvertedRect(BlobInvertedRect* rect);
|
||||||
|
void clearInvertedRect(BlobInvertedRect* rect);
|
||||||
|
|
||||||
|
const QList<BlobShape*>& shapes() const { return m_shapes; }
|
||||||
|
|
||||||
|
BlobInvertedRect* invertedRect() const { return m_invertedRect; }
|
||||||
|
|
||||||
|
void markDirty();
|
||||||
|
void markShapeDirty(BlobShape* source);
|
||||||
|
void ensurePhysicsUpdated();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void smoothingChanged();
|
||||||
|
void colorChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
qreal m_smoothing = 32.0;
|
||||||
|
QColor m_color{ 0x44, 0x88, 0xff };
|
||||||
|
QList<BlobShape*> m_shapes;
|
||||||
|
BlobInvertedRect* m_invertedRect = nullptr;
|
||||||
|
bool m_physicsUpdated = false;
|
||||||
|
};
|
||||||
184
plugin/src/Caelestia/Blobs/blobinvertedrect.cpp
Normal file
184
plugin/src/Caelestia/Blobs/blobinvertedrect.cpp
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
#include "blobinvertedrect.hpp"
|
||||||
|
#include "blobgroup.hpp"
|
||||||
|
#include "blobmaterial.hpp"
|
||||||
|
|
||||||
|
#include <qsggeometry.h>
|
||||||
|
#include <qsgnode.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
BlobInvertedRect::BlobInvertedRect(QQuickItem* parent)
|
||||||
|
: BlobShape(parent) {}
|
||||||
|
|
||||||
|
static void setFrameIndices(quint16* idx) {
|
||||||
|
// Top strip: 0-1-4, 1-5-4
|
||||||
|
idx[0] = 0;
|
||||||
|
idx[1] = 1;
|
||||||
|
idx[2] = 4;
|
||||||
|
idx[3] = 1;
|
||||||
|
idx[4] = 5;
|
||||||
|
idx[5] = 4;
|
||||||
|
// Right strip: 1-2-5, 2-6-5
|
||||||
|
idx[6] = 1;
|
||||||
|
idx[7] = 2;
|
||||||
|
idx[8] = 5;
|
||||||
|
idx[9] = 2;
|
||||||
|
idx[10] = 6;
|
||||||
|
idx[11] = 5;
|
||||||
|
// Bottom strip: 2-3-6, 3-7-6
|
||||||
|
idx[12] = 2;
|
||||||
|
idx[13] = 3;
|
||||||
|
idx[14] = 6;
|
||||||
|
idx[15] = 3;
|
||||||
|
idx[16] = 7;
|
||||||
|
idx[17] = 6;
|
||||||
|
// Left strip: 3-0-7, 0-4-7
|
||||||
|
idx[18] = 3;
|
||||||
|
idx[19] = 0;
|
||||||
|
idx[20] = 7;
|
||||||
|
idx[21] = 0;
|
||||||
|
idx[22] = 4;
|
||||||
|
idx[23] = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
|
||||||
|
if (!m_group) {
|
||||||
|
delete oldNode;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float pad = static_cast<float>(m_group->smoothing());
|
||||||
|
|
||||||
|
// Compute inner hole boundary in local coords
|
||||||
|
// Inset past the inner border edge by 2x smoothing to cover the blend zone
|
||||||
|
const float inset = pad * 2.0f;
|
||||||
|
const float holeLeft = static_cast<float>(m_borderLeft) + inset;
|
||||||
|
const float holeTop = static_cast<float>(m_borderTop) + inset;
|
||||||
|
const float holeRight = static_cast<float>(width() - m_borderRight) - inset;
|
||||||
|
const float holeBot = static_cast<float>(height() - m_borderBottom) - inset;
|
||||||
|
|
||||||
|
// If the hole is too small or invalid, fall back to full quad
|
||||||
|
if (holeLeft >= holeRight || holeTop >= holeBot)
|
||||||
|
return BlobShape::updatePaintNode(oldNode, nullptr);
|
||||||
|
|
||||||
|
auto* node = static_cast<QSGGeometryNode*>(oldNode);
|
||||||
|
|
||||||
|
const bool needsRebuild = !node || node->geometry()->vertexCount() != 8;
|
||||||
|
|
||||||
|
if (needsRebuild) {
|
||||||
|
delete oldNode;
|
||||||
|
node = new QSGGeometryNode;
|
||||||
|
|
||||||
|
auto* geometry =
|
||||||
|
new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType);
|
||||||
|
geometry->setDrawingMode(QSGGeometry::DrawTriangles);
|
||||||
|
node->setGeometry(geometry);
|
||||||
|
node->setFlag(QSGNode::OwnsGeometry);
|
||||||
|
|
||||||
|
setFrameIndices(geometry->indexDataAsUShort());
|
||||||
|
|
||||||
|
auto* material = new BlobMaterial;
|
||||||
|
material->setFlag(QSGMaterial::Blending);
|
||||||
|
node->setMaterial(material);
|
||||||
|
node->setFlag(QSGNode::OwnsMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer bounds (local coords)
|
||||||
|
const float x0 = static_cast<float>(m_localPaddedRect.x());
|
||||||
|
const float y0 = static_cast<float>(m_localPaddedRect.y());
|
||||||
|
const float x1 = x0 + static_cast<float>(m_localPaddedRect.width());
|
||||||
|
const float y1 = y0 + static_cast<float>(m_localPaddedRect.height());
|
||||||
|
const float w = x1 - x0;
|
||||||
|
const float h = y1 - y0;
|
||||||
|
|
||||||
|
// Update vertex positions and texture coordinates
|
||||||
|
auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
|
||||||
|
|
||||||
|
// Outer corners
|
||||||
|
v[0].set(x0, y0, 0.0f, 0.0f);
|
||||||
|
v[1].set(x1, y0, 1.0f, 0.0f);
|
||||||
|
v[2].set(x1, y1, 1.0f, 1.0f);
|
||||||
|
v[3].set(x0, y1, 0.0f, 1.0f);
|
||||||
|
// Inner corners (hole)
|
||||||
|
v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h);
|
||||||
|
v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h);
|
||||||
|
v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h);
|
||||||
|
v[7].set(holeLeft, holeBot, (holeLeft - x0) / w, (holeBot - y0) / h);
|
||||||
|
|
||||||
|
node->markDirty(QSGNode::DirtyGeometry);
|
||||||
|
|
||||||
|
// Update material uniforms
|
||||||
|
auto* material = static_cast<BlobMaterial*>(node->material());
|
||||||
|
material->m_paddedX = m_cachedPaddedX;
|
||||||
|
material->m_paddedY = m_cachedPaddedY;
|
||||||
|
material->m_paddedW = m_cachedPaddedW;
|
||||||
|
material->m_paddedH = m_cachedPaddedH;
|
||||||
|
material->m_smoothFactor = pad;
|
||||||
|
material->m_myIndex = m_cachedMyIndex;
|
||||||
|
material->m_color = m_group->color();
|
||||||
|
material->m_hasInverted = m_cachedHasInverted ? 1 : 0;
|
||||||
|
material->m_invertedRadius = m_cachedInvertedRadius;
|
||||||
|
memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter));
|
||||||
|
memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner));
|
||||||
|
|
||||||
|
const int count = static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
|
||||||
|
material->m_rectCount = count;
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
material->m_rects[i] = m_cachedRects[i];
|
||||||
|
|
||||||
|
node->markDirty(QSGNode::DirtyMaterial);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobInvertedRect::~BlobInvertedRect() {
|
||||||
|
if (m_group)
|
||||||
|
m_group->clearInvertedRect(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobInvertedRect::setBorderLeft(qreal v) {
|
||||||
|
if (qFuzzyCompare(m_borderLeft, v))
|
||||||
|
return;
|
||||||
|
m_borderLeft = v;
|
||||||
|
emit borderLeftChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobInvertedRect::setBorderRight(qreal v) {
|
||||||
|
if (qFuzzyCompare(m_borderRight, v))
|
||||||
|
return;
|
||||||
|
m_borderRight = v;
|
||||||
|
emit borderRightChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobInvertedRect::setBorderTop(qreal v) {
|
||||||
|
if (qFuzzyCompare(m_borderTop, v))
|
||||||
|
return;
|
||||||
|
m_borderTop = v;
|
||||||
|
emit borderTopChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobInvertedRect::setBorderBottom(qreal v) {
|
||||||
|
if (qFuzzyCompare(m_borderBottom, v))
|
||||||
|
return;
|
||||||
|
m_borderBottom = v;
|
||||||
|
emit borderBottomChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobInvertedRect::registerWithGroup() {
|
||||||
|
if (m_group)
|
||||||
|
m_group->setInvertedRect(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobInvertedRect::unregisterFromGroup() {
|
||||||
|
if (m_group)
|
||||||
|
m_group->clearInvertedRect(this);
|
||||||
|
}
|
||||||
54
plugin/src/Caelestia/Blobs/blobinvertedrect.hpp
Normal file
54
plugin/src/Caelestia/Blobs/blobinvertedrect.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "blobshape.hpp"
|
||||||
|
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
|
||||||
|
class BlobInvertedRect : public BlobShape {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged)
|
||||||
|
Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged)
|
||||||
|
Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged)
|
||||||
|
Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BlobInvertedRect(QQuickItem* parent = nullptr);
|
||||||
|
~BlobInvertedRect() override;
|
||||||
|
|
||||||
|
qreal borderLeft() const { return m_borderLeft; }
|
||||||
|
|
||||||
|
void setBorderLeft(qreal v);
|
||||||
|
|
||||||
|
qreal borderRight() const { return m_borderRight; }
|
||||||
|
|
||||||
|
void setBorderRight(qreal v);
|
||||||
|
|
||||||
|
qreal borderTop() const { return m_borderTop; }
|
||||||
|
|
||||||
|
void setBorderTop(qreal v);
|
||||||
|
|
||||||
|
qreal borderBottom() const { return m_borderBottom; }
|
||||||
|
|
||||||
|
void setBorderBottom(qreal v);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void borderLeftChanged();
|
||||||
|
void borderRightChanged();
|
||||||
|
void borderTopChanged();
|
||||||
|
void borderBottomChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool isInvertedRect() const override { return true; }
|
||||||
|
|
||||||
|
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
|
||||||
|
|
||||||
|
void registerWithGroup() override;
|
||||||
|
void unregisterFromGroup() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
qreal m_borderLeft = 0;
|
||||||
|
qreal m_borderRight = 0;
|
||||||
|
qreal m_borderTop = 0;
|
||||||
|
qreal m_borderBottom = 0;
|
||||||
|
};
|
||||||
96
plugin/src/Caelestia/Blobs/blobmaterial.cpp
Normal file
96
plugin/src/Caelestia/Blobs/blobmaterial.cpp
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
#include "blobmaterial.hpp"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
QSGMaterialType* BlobMaterial::type() const {
|
||||||
|
static QSGMaterialType s_type;
|
||||||
|
return &s_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const {
|
||||||
|
return new BlobMaterialShader;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BlobMaterial::compare(const QSGMaterial* other) const {
|
||||||
|
if (this < other)
|
||||||
|
return -1;
|
||||||
|
if (this > other)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobMaterialShader::BlobMaterialShader() {
|
||||||
|
setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb"));
|
||||||
|
setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) {
|
||||||
|
Q_UNUSED(oldMaterial);
|
||||||
|
auto* mat = static_cast<BlobMaterial*>(newMaterial);
|
||||||
|
QByteArray* buf = state.uniformData();
|
||||||
|
Q_ASSERT(buf->size() >= 1440);
|
||||||
|
|
||||||
|
if (state.isMatrixDirty()) {
|
||||||
|
const QMatrix4x4 m = state.combinedMatrix();
|
||||||
|
memcpy(buf->data(), m.constData(), 64);
|
||||||
|
}
|
||||||
|
if (state.isOpacityDirty()) {
|
||||||
|
const float opacity = state.opacity();
|
||||||
|
memcpy(buf->data() + 64, &opacity, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padded rect (offset 68)
|
||||||
|
memcpy(buf->data() + 68, &mat->m_paddedX, 4);
|
||||||
|
memcpy(buf->data() + 72, &mat->m_paddedY, 4);
|
||||||
|
memcpy(buf->data() + 76, &mat->m_paddedW, 4);
|
||||||
|
memcpy(buf->data() + 80, &mat->m_paddedH, 4);
|
||||||
|
|
||||||
|
// Smooth factor (offset 84)
|
||||||
|
memcpy(buf->data() + 84, &mat->m_smoothFactor, 4);
|
||||||
|
|
||||||
|
// Rect count (offset 88)
|
||||||
|
memcpy(buf->data() + 88, &mat->m_rectCount, 4);
|
||||||
|
|
||||||
|
// My index (offset 92)
|
||||||
|
memcpy(buf->data() + 92, &mat->m_myIndex, 4);
|
||||||
|
|
||||||
|
// Color as vec4 (offset 96, 16 bytes)
|
||||||
|
const float color[4] = {
|
||||||
|
static_cast<float>(mat->m_color.redF()),
|
||||||
|
static_cast<float>(mat->m_color.greenF()),
|
||||||
|
static_cast<float>(mat->m_color.blueF()),
|
||||||
|
static_cast<float>(mat->m_color.alphaF()),
|
||||||
|
};
|
||||||
|
memcpy(buf->data() + 96, color, 16);
|
||||||
|
|
||||||
|
// Has inverted (offset 112)
|
||||||
|
memcpy(buf->data() + 112, &mat->m_hasInverted, 4);
|
||||||
|
|
||||||
|
// Inverted radius (offset 116)
|
||||||
|
memcpy(buf->data() + 116, &mat->m_invertedRadius, 4);
|
||||||
|
|
||||||
|
// Padding at 120-127 (skip)
|
||||||
|
|
||||||
|
// Inverted outer (offset 128, 16 bytes)
|
||||||
|
memcpy(buf->data() + 128, mat->m_invertedOuter, 16);
|
||||||
|
|
||||||
|
// Inverted inner (offset 144, 16 bytes)
|
||||||
|
memcpy(buf->data() + 144, mat->m_invertedInner, 16);
|
||||||
|
|
||||||
|
// Rect data (offset 160, each rect = 5 vec4s = 80 bytes)
|
||||||
|
const int count = qMin(mat->m_rectCount, 16);
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
const auto& r = mat->m_rects[i];
|
||||||
|
const int base = 160 + i * 80;
|
||||||
|
const float d0[4] = { r.cx, r.cy, r.hw, r.hh };
|
||||||
|
const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig };
|
||||||
|
const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f };
|
||||||
|
memcpy(buf->data() + base, d0, 16);
|
||||||
|
memcpy(buf->data() + base + 16, d1, 16);
|
||||||
|
memcpy(buf->data() + base + 32, r.invDeform, 16);
|
||||||
|
memcpy(buf->data() + base + 48, d3, 16);
|
||||||
|
memcpy(buf->data() + base + 64, r.radius, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
44
plugin/src/Caelestia/Blobs/blobmaterial.hpp
Normal file
44
plugin/src/Caelestia/Blobs/blobmaterial.hpp
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qcolor.h>
|
||||||
|
#include <qsgmaterial.h>
|
||||||
|
#include <qsgmaterialshader.h>
|
||||||
|
|
||||||
|
struct BlobRectData {
|
||||||
|
float cx = 0, cy = 0, hw = 0, hh = 0;
|
||||||
|
float offsetX = 0, offsetY = 0;
|
||||||
|
float minEig = 1.0f;
|
||||||
|
// Inverse of 2x2 deformation matrix, column-major for GLSL
|
||||||
|
float invDeform[4] = { 1, 0, 0, 1 };
|
||||||
|
// Screen-space AABB half-extents of the deformed rect
|
||||||
|
float screenHalfX = 0, screenHalfY = 0;
|
||||||
|
// Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU
|
||||||
|
float radius[4] = { 0, 0, 0, 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlobMaterial : public QSGMaterial {
|
||||||
|
public:
|
||||||
|
QSGMaterialType* type() const override;
|
||||||
|
QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override;
|
||||||
|
int compare(const QSGMaterial* other) const override;
|
||||||
|
|
||||||
|
float m_paddedX = 0;
|
||||||
|
float m_paddedY = 0;
|
||||||
|
float m_paddedW = 0;
|
||||||
|
float m_paddedH = 0;
|
||||||
|
float m_smoothFactor = 32.0f;
|
||||||
|
int m_rectCount = 0;
|
||||||
|
int m_myIndex = -2;
|
||||||
|
QColor m_color{ 0x44, 0x88, 0xff };
|
||||||
|
int m_hasInverted = 0;
|
||||||
|
float m_invertedRadius = 0;
|
||||||
|
float m_invertedOuter[4] = {};
|
||||||
|
float m_invertedInner[4] = {};
|
||||||
|
BlobRectData m_rects[16] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlobMaterialShader : public QSGMaterialShader {
|
||||||
|
public:
|
||||||
|
BlobMaterialShader();
|
||||||
|
bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override;
|
||||||
|
};
|
||||||
244
plugin/src/Caelestia/Blobs/blobrect.cpp
Normal file
244
plugin/src/Caelestia/Blobs/blobrect.cpp
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
#include "blobrect.hpp"
|
||||||
|
#include "blobgroup.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
BlobRect::BlobRect(QQuickItem* parent)
|
||||||
|
: BlobShape(parent) {}
|
||||||
|
|
||||||
|
BlobRect::~BlobRect() {
|
||||||
|
if (m_group)
|
||||||
|
m_group->removeShape(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::updatePolish() {
|
||||||
|
BlobShape::updatePolish();
|
||||||
|
|
||||||
|
if (m_physicsActive) {
|
||||||
|
// Check if deformation is visually imperceptible
|
||||||
|
float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
|
||||||
|
float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
|
||||||
|
|
||||||
|
if (totalDelta < 0.004f && totalVel < 0.05f) {
|
||||||
|
// Snap to rest, no visible deformation
|
||||||
|
m_dm00 = 1.0f;
|
||||||
|
m_dm01 = 0.0f;
|
||||||
|
m_dm11 = 1.0f;
|
||||||
|
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
|
||||||
|
m_deformMatrix = QMatrix4x4();
|
||||||
|
emit rawDeformMatrixChanged();
|
||||||
|
updateCenteredDeformMatrix();
|
||||||
|
m_physicsActive = false;
|
||||||
|
} else {
|
||||||
|
QMetaObject::invokeMethod(
|
||||||
|
this,
|
||||||
|
[this]() {
|
||||||
|
if (m_physicsActive && m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
},
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::updatePhysics() {
|
||||||
|
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
|
||||||
|
|
||||||
|
if (!m_hasPrevPos) {
|
||||||
|
m_prevScenePos = scenePos;
|
||||||
|
m_elapsed.start();
|
||||||
|
m_hasPrevPos = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f;
|
||||||
|
if (dt > 0.1f || dt < 0.001f) {
|
||||||
|
m_prevScenePos = scenePos;
|
||||||
|
// Still check atRest on skipped frames to avoid getting stuck
|
||||||
|
if (m_physicsActive)
|
||||||
|
checkAtRest(0.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float velX = static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
|
||||||
|
const float velY = static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
|
||||||
|
m_prevScenePos = scenePos;
|
||||||
|
|
||||||
|
const float speed = std::sqrt(velX * velX + velY * velY);
|
||||||
|
|
||||||
|
if (!m_physicsActive) {
|
||||||
|
if (speed < 5.0f)
|
||||||
|
return;
|
||||||
|
m_physicsActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute target deformation matrix from velocity
|
||||||
|
// R(θ) * diag(stretch, compress) * R(θ)^T
|
||||||
|
const float kStretchFactor = static_cast<float>(m_deformScale);
|
||||||
|
constexpr float kMaxStretch = 0.35f;
|
||||||
|
|
||||||
|
float target00 = 1.0f;
|
||||||
|
float target01 = 0.0f;
|
||||||
|
float target11 = 1.0f;
|
||||||
|
|
||||||
|
if (speed > 5.0f) {
|
||||||
|
const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch);
|
||||||
|
const float targetCompress = 1.0f / targetStretch;
|
||||||
|
|
||||||
|
const float cosA = velX / speed;
|
||||||
|
const float sinA = velY / speed;
|
||||||
|
const float cos2 = cosA * cosA;
|
||||||
|
const float sin2 = sinA * sinA;
|
||||||
|
const float cs = cosA * sinA;
|
||||||
|
|
||||||
|
target00 = targetStretch * cos2 + targetCompress * sin2;
|
||||||
|
target01 = (targetStretch - targetCompress) * cs;
|
||||||
|
target11 = targetStretch * sin2 + targetCompress * cos2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Underdamped spring on each matrix component
|
||||||
|
const float kStiffness = static_cast<float>(m_stiffness);
|
||||||
|
const float kDamping = static_cast<float>(m_damping);
|
||||||
|
|
||||||
|
const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00;
|
||||||
|
m_dmVel00 += accel00 * dt;
|
||||||
|
m_dm00 += m_dmVel00 * dt;
|
||||||
|
|
||||||
|
const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01;
|
||||||
|
m_dmVel01 += accel01 * dt;
|
||||||
|
m_dm01 += m_dmVel01 * dt;
|
||||||
|
|
||||||
|
const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11;
|
||||||
|
m_dmVel11 += accel11 * dt;
|
||||||
|
m_dm11 += m_dmVel11 * dt;
|
||||||
|
|
||||||
|
m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||||
|
emit rawDeformMatrixChanged();
|
||||||
|
updateCenteredDeformMatrix();
|
||||||
|
|
||||||
|
checkAtRest(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::setTopLeftRadius(qreal r) {
|
||||||
|
if (!qFuzzyCompare(m_topLeftRadius, r)) {
|
||||||
|
m_topLeftRadius = r;
|
||||||
|
emit topLeftRadiusChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::setTopRightRadius(qreal r) {
|
||||||
|
if (!qFuzzyCompare(m_topRightRadius, r)) {
|
||||||
|
m_topRightRadius = r;
|
||||||
|
emit topRightRadiusChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::setBottomLeftRadius(qreal r) {
|
||||||
|
if (!qFuzzyCompare(m_bottomLeftRadius, r)) {
|
||||||
|
m_bottomLeftRadius = r;
|
||||||
|
emit bottomLeftRadiusChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::setBottomRightRadius(qreal r) {
|
||||||
|
if (!qFuzzyCompare(m_bottomRightRadius, r)) {
|
||||||
|
m_bottomRightRadius = r;
|
||||||
|
emit bottomRightRadiusChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::cornerRadii(float out[4]) const {
|
||||||
|
const auto base = static_cast<float>(m_radius);
|
||||||
|
out[0] = m_topRightRadius >= 0 ? static_cast<float>(m_topRightRadius) : base;
|
||||||
|
out[1] = m_bottomRightRadius >= 0 ? static_cast<float>(m_bottomRightRadius) : base;
|
||||||
|
out[2] = m_bottomLeftRadius >= 0 ? static_cast<float>(m_bottomLeftRadius) : base;
|
||||||
|
out[3] = m_topLeftRadius >= 0 ? static_cast<float>(m_topLeftRadius) : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BlobRect::isExcluded(const BlobShape* other) const {
|
||||||
|
for (const auto& ptr : m_exclude) {
|
||||||
|
if (ptr == other)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlListProperty<BlobRect> BlobRect::exclude() {
|
||||||
|
return QQmlListProperty<BlobRect>(
|
||||||
|
this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect) {
|
||||||
|
auto* self = static_cast<BlobRect*>(prop->object);
|
||||||
|
self->m_exclude.append(rect);
|
||||||
|
if (self->m_group)
|
||||||
|
self->m_group->markDirty();
|
||||||
|
emit self->excludeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
qsizetype BlobRect::excludeCount(QQmlListProperty<BlobRect>* prop) {
|
||||||
|
auto* self = static_cast<BlobRect*>(prop->object);
|
||||||
|
return self->m_exclude.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobRect* BlobRect::excludeAt(QQmlListProperty<BlobRect>* prop, qsizetype index) {
|
||||||
|
auto* self = static_cast<BlobRect*>(prop->object);
|
||||||
|
return self->m_exclude.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::excludeClear(QQmlListProperty<BlobRect>* prop) {
|
||||||
|
auto* self = static_cast<BlobRect*>(prop->object);
|
||||||
|
if (self->m_exclude.isEmpty())
|
||||||
|
return;
|
||||||
|
self->m_exclude.clear();
|
||||||
|
if (self->m_group)
|
||||||
|
self->m_group->markDirty();
|
||||||
|
emit self->excludeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect) {
|
||||||
|
auto* self = static_cast<BlobRect*>(prop->object);
|
||||||
|
self->m_exclude[index] = rect;
|
||||||
|
if (self->m_group)
|
||||||
|
self->m_group->markDirty();
|
||||||
|
emit self->excludeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::excludeRemoveLast(QQmlListProperty<BlobRect>* prop) {
|
||||||
|
auto* self = static_cast<BlobRect*>(prop->object);
|
||||||
|
if (self->m_exclude.isEmpty())
|
||||||
|
return;
|
||||||
|
self->m_exclude.removeLast();
|
||||||
|
if (self->m_group)
|
||||||
|
self->m_group->markDirty();
|
||||||
|
emit self->excludeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobRect::checkAtRest(float speed) {
|
||||||
|
constexpr float kEpsilon = 0.002f;
|
||||||
|
const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
|
||||||
|
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
|
||||||
|
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f;
|
||||||
|
|
||||||
|
if (atRest) {
|
||||||
|
m_dm00 = 1.0f;
|
||||||
|
m_dm01 = 0.0f;
|
||||||
|
m_dm11 = 1.0f;
|
||||||
|
m_dmVel00 = 0.0f;
|
||||||
|
m_dmVel01 = 0.0f;
|
||||||
|
m_dmVel11 = 0.0f;
|
||||||
|
m_deformMatrix = QMatrix4x4(); // identity
|
||||||
|
emit rawDeformMatrixChanged();
|
||||||
|
updateCenteredDeformMatrix();
|
||||||
|
m_physicsActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
plugin/src/Caelestia/Blobs/blobrect.hpp
Normal file
126
plugin/src/Caelestia/Blobs/blobrect.hpp
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "blobshape.hpp"
|
||||||
|
|
||||||
|
#include <qelapsedtimer.h>
|
||||||
|
#include <qpointer.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
|
||||||
|
class BlobRect : public BlobShape {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged)
|
||||||
|
Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged)
|
||||||
|
Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged)
|
||||||
|
Q_PROPERTY(QQmlListProperty<BlobRect> exclude READ exclude NOTIFY excludeChanged)
|
||||||
|
Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged)
|
||||||
|
Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged)
|
||||||
|
Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BlobRect(QQuickItem* parent = nullptr);
|
||||||
|
~BlobRect() override;
|
||||||
|
|
||||||
|
qreal stiffness() const { return m_stiffness; }
|
||||||
|
|
||||||
|
void setStiffness(qreal s) {
|
||||||
|
if (!qFuzzyCompare(m_stiffness, s)) {
|
||||||
|
m_stiffness = s;
|
||||||
|
emit stiffnessChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal damping() const { return m_damping; }
|
||||||
|
|
||||||
|
void setDamping(qreal d) {
|
||||||
|
if (!qFuzzyCompare(m_damping, d)) {
|
||||||
|
m_damping = d;
|
||||||
|
emit dampingChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal deformScale() const { return m_deformScale; }
|
||||||
|
|
||||||
|
void setDeformScale(qreal s) {
|
||||||
|
if (!qFuzzyCompare(m_deformScale, s)) {
|
||||||
|
m_deformScale = s;
|
||||||
|
emit deformScaleChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlListProperty<BlobRect> exclude();
|
||||||
|
|
||||||
|
bool isExcluded(const BlobShape* other) const override;
|
||||||
|
void cornerRadii(float out[4]) const override;
|
||||||
|
|
||||||
|
qreal topLeftRadius() const { return m_topLeftRadius; }
|
||||||
|
|
||||||
|
void setTopLeftRadius(qreal r);
|
||||||
|
|
||||||
|
qreal topRightRadius() const { return m_topRightRadius; }
|
||||||
|
|
||||||
|
void setTopRightRadius(qreal r);
|
||||||
|
|
||||||
|
qreal bottomLeftRadius() const { return m_bottomLeftRadius; }
|
||||||
|
|
||||||
|
void setBottomLeftRadius(qreal r);
|
||||||
|
|
||||||
|
qreal bottomRightRadius() const { return m_bottomRightRadius; }
|
||||||
|
|
||||||
|
void setBottomRightRadius(qreal r);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void stiffnessChanged();
|
||||||
|
void dampingChanged();
|
||||||
|
void deformScaleChanged();
|
||||||
|
void excludeChanged();
|
||||||
|
void topLeftRadiusChanged();
|
||||||
|
void topRightRadiusChanged();
|
||||||
|
void bottomLeftRadiusChanged();
|
||||||
|
void bottomRightRadiusChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void updatePolish() override;
|
||||||
|
void updatePhysics() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void checkAtRest(float speed);
|
||||||
|
|
||||||
|
// Physics state
|
||||||
|
QPointF m_prevScenePos;
|
||||||
|
QElapsedTimer m_elapsed;
|
||||||
|
bool m_physicsActive = false;
|
||||||
|
bool m_hasPrevPos = false;
|
||||||
|
|
||||||
|
// Symmetric 2x2 deformation matrix components (3 independent: m00, m01,
|
||||||
|
// m11) Rest state is identity: m00=1, m01=0, m11=1
|
||||||
|
float m_dm00 = 1.0f;
|
||||||
|
float m_dm01 = 0.0f;
|
||||||
|
float m_dm11 = 1.0f;
|
||||||
|
|
||||||
|
// Spring velocities for each component
|
||||||
|
float m_dmVel00 = 0.0f;
|
||||||
|
float m_dmVel01 = 0.0f;
|
||||||
|
float m_dmVel11 = 0.0f;
|
||||||
|
|
||||||
|
qreal m_stiffness = 200.0;
|
||||||
|
qreal m_damping = 16.0;
|
||||||
|
qreal m_deformScale = 0.0005;
|
||||||
|
|
||||||
|
qreal m_topLeftRadius = -1;
|
||||||
|
qreal m_topRightRadius = -1;
|
||||||
|
qreal m_bottomLeftRadius = -1;
|
||||||
|
qreal m_bottomRightRadius = -1;
|
||||||
|
|
||||||
|
QList<QPointer<BlobRect>> m_exclude;
|
||||||
|
|
||||||
|
static void excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect);
|
||||||
|
static qsizetype excludeCount(QQmlListProperty<BlobRect>* prop);
|
||||||
|
static BlobRect* excludeAt(QQmlListProperty<BlobRect>* prop, qsizetype index);
|
||||||
|
static void excludeClear(QQmlListProperty<BlobRect>* prop);
|
||||||
|
static void excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect);
|
||||||
|
static void excludeRemoveLast(QQmlListProperty<BlobRect>* prop);
|
||||||
|
};
|
||||||
367
plugin/src/Caelestia/Blobs/blobshape.cpp
Normal file
367
plugin/src/Caelestia/Blobs/blobshape.cpp
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
#include "blobshape.hpp"
|
||||||
|
#include "blobgroup.hpp"
|
||||||
|
#include "blobinvertedrect.hpp"
|
||||||
|
|
||||||
|
#include <qsggeometry.h>
|
||||||
|
#include <qsgnode.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
static float deformPadding(const QMatrix4x4& dm, float hw, float hh) {
|
||||||
|
// Bounding box of the deformed shape: |M * corners|
|
||||||
|
const float dm00 = dm(0, 0), dm01 = dm(0, 1);
|
||||||
|
const float dm10 = dm(1, 0), dm11 = dm(1, 1);
|
||||||
|
const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh;
|
||||||
|
const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh;
|
||||||
|
const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3));
|
||||||
|
const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3));
|
||||||
|
return std::max(extraX, extraY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) {
|
||||||
|
const float dx = std::abs(px - cx) - hw;
|
||||||
|
const float dy = std::abs(py - cy) - hh;
|
||||||
|
const float mdx = std::max(dx, 0.0f);
|
||||||
|
const float mdy = std::max(dy, 0.0f);
|
||||||
|
return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static float cpuSmoothstep(float edge0, float edge1, float x) {
|
||||||
|
const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
|
||||||
|
return t * t * (3.0f - 2.0f * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobShape::BlobShape(QQuickItem* parent)
|
||||||
|
: QQuickItem(parent) {
|
||||||
|
setFlag(ItemHasContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::setGroup(BlobGroup* g) {
|
||||||
|
if (m_group == g)
|
||||||
|
return;
|
||||||
|
if (m_group && isComponentComplete())
|
||||||
|
unregisterFromGroup();
|
||||||
|
m_group = g;
|
||||||
|
if (m_group && isComponentComplete())
|
||||||
|
registerWithGroup();
|
||||||
|
emit groupChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::setRadius(qreal r) {
|
||||||
|
if (qFuzzyCompare(m_radius, r))
|
||||||
|
return;
|
||||||
|
m_radius = r;
|
||||||
|
emit radiusChanged();
|
||||||
|
if (m_group)
|
||||||
|
m_group->markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::componentComplete() {
|
||||||
|
QQuickItem::componentComplete();
|
||||||
|
if (m_group)
|
||||||
|
registerWithGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) {
|
||||||
|
QQuickItem::geometryChange(newGeometry, oldGeometry);
|
||||||
|
updateCenteredDeformMatrix();
|
||||||
|
if (m_group) {
|
||||||
|
// Accumulate sub-pixel drift so slow movements don't desync the shader
|
||||||
|
m_pendingDx += static_cast<float>(newGeometry.x() - oldGeometry.x());
|
||||||
|
m_pendingDy += static_cast<float>(newGeometry.y() - oldGeometry.y());
|
||||||
|
const auto dw = std::abs(newGeometry.width() - oldGeometry.width());
|
||||||
|
const auto dh = std::abs(newGeometry.height() - oldGeometry.height());
|
||||||
|
if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) {
|
||||||
|
m_pendingDx = 0;
|
||||||
|
m_pendingDy = 0;
|
||||||
|
m_group->markShapeDirty(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::updateCenteredDeformMatrix() {
|
||||||
|
const auto cx = static_cast<float>(width()) * 0.5f;
|
||||||
|
const auto cy = static_cast<float>(height()) * 0.5f;
|
||||||
|
QMatrix4x4 result;
|
||||||
|
result.translate(cx, cy);
|
||||||
|
result *= m_deformMatrix;
|
||||||
|
result.translate(-cx, -cy);
|
||||||
|
if (m_centeredDeformMatrix != result) {
|
||||||
|
m_centeredDeformMatrix = result;
|
||||||
|
emit deformMatrixChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::cornerRadii(float out[4]) const {
|
||||||
|
const auto r = static_cast<float>(m_radius);
|
||||||
|
out[0] = r;
|
||||||
|
out[1] = r;
|
||||||
|
out[2] = r;
|
||||||
|
out[3] = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::registerWithGroup() {
|
||||||
|
if (m_group)
|
||||||
|
m_group->addShape(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::unregisterFromGroup() {
|
||||||
|
if (m_group)
|
||||||
|
m_group->removeShape(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobShape::updatePolish() {
|
||||||
|
if (!m_group)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ensure all shapes have up-to-date physics (only once per frame)
|
||||||
|
m_group->ensurePhysicsUpdated();
|
||||||
|
|
||||||
|
const QPointF scenePos = mapToScene(QPointF(0, 0));
|
||||||
|
const float pad = static_cast<float>(m_group->smoothing());
|
||||||
|
|
||||||
|
if (isInvertedRect()) {
|
||||||
|
m_cachedPaddedX = static_cast<float>(scenePos.x());
|
||||||
|
m_cachedPaddedY = static_cast<float>(scenePos.y());
|
||||||
|
m_cachedPaddedW = static_cast<float>(width());
|
||||||
|
m_cachedPaddedH = static_cast<float>(height());
|
||||||
|
m_localPaddedRect = QRectF(0, 0, width(), height());
|
||||||
|
} else {
|
||||||
|
const float hw = static_cast<float>(width()) * 0.5f;
|
||||||
|
const float hh = static_cast<float>(height()) * 0.5f;
|
||||||
|
const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh);
|
||||||
|
|
||||||
|
m_cachedPaddedX = static_cast<float>(scenePos.x()) - totalPad;
|
||||||
|
m_cachedPaddedY = static_cast<float>(scenePos.y()) - totalPad;
|
||||||
|
m_cachedPaddedW = static_cast<float>(width()) + 2.0f * totalPad;
|
||||||
|
m_cachedPaddedH = static_cast<float>(height()) + 2.0f * totalPad;
|
||||||
|
m_localPaddedRect = QRectF(static_cast<double>(-totalPad), static_cast<double>(-totalPad),
|
||||||
|
width() + 2.0 * static_cast<double>(totalPad), height() + 2.0 * static_cast<double>(totalPad));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter nearby normal rects
|
||||||
|
m_cachedRects.clear();
|
||||||
|
m_cachedMyIndex = -2;
|
||||||
|
const QRectF myPadded(static_cast<double>(m_cachedPaddedX), static_cast<double>(m_cachedPaddedY),
|
||||||
|
static_cast<double>(m_cachedPaddedW), static_cast<double>(m_cachedPaddedH));
|
||||||
|
|
||||||
|
for (BlobShape* other : m_group->shapes()) {
|
||||||
|
if (other->isInvertedRect())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip zero-size rects
|
||||||
|
if (other->width() <= 0 || other->height() <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (isExcluded(other))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const QPointF otherScene = other->mapToScene(QPointF(0, 0));
|
||||||
|
|
||||||
|
bool include = false;
|
||||||
|
if (isInvertedRect()) {
|
||||||
|
include = true;
|
||||||
|
} else {
|
||||||
|
const float otherHW = static_cast<float>(other->width()) * 0.5f;
|
||||||
|
const float otherHH = static_cast<float>(other->height()) * 0.5f;
|
||||||
|
const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH);
|
||||||
|
const QRectF otherPadded(otherScene.x() - static_cast<double>(otherPad),
|
||||||
|
otherScene.y() - static_cast<double>(otherPad), other->width() + 2.0 * static_cast<double>(otherPad),
|
||||||
|
other->height() + 2.0 * static_cast<double>(otherPad));
|
||||||
|
include = myPadded.intersects(otherPadded);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include) {
|
||||||
|
if (other == this)
|
||||||
|
m_cachedMyIndex = static_cast<int>(m_cachedRects.size());
|
||||||
|
|
||||||
|
const QMatrix4x4& dm = other->m_deformMatrix;
|
||||||
|
const float a = dm(0, 0), b = dm(1, 0);
|
||||||
|
const float c = dm(0, 1), d = dm(1, 1);
|
||||||
|
|
||||||
|
BlobRectData r;
|
||||||
|
r.cx = static_cast<float>(otherScene.x() + other->width() / 2.0);
|
||||||
|
r.cy = static_cast<float>(otherScene.y() + other->height() / 2.0);
|
||||||
|
r.hw = static_cast<float>(other->width() / 2.0);
|
||||||
|
r.hh = static_cast<float>(other->height() / 2.0);
|
||||||
|
other->cornerRadii(r.radius);
|
||||||
|
r.offsetX = dm(0, 3);
|
||||||
|
r.offsetY = dm(1, 3);
|
||||||
|
|
||||||
|
// Pre-compute inverse deformation matrix
|
||||||
|
const float det = a * d - c * b;
|
||||||
|
const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
|
||||||
|
r.invDeform[0] = d * invDet;
|
||||||
|
r.invDeform[1] = -b * invDet;
|
||||||
|
r.invDeform[2] = -c * invDet;
|
||||||
|
r.invDeform[3] = a * invDet;
|
||||||
|
|
||||||
|
// Pre-compute minimum eigenvalue (avoids per-pixel sqrt)
|
||||||
|
const float halfTr = 0.5f * (a + d);
|
||||||
|
const float halfDiff = 0.5f * (a - d);
|
||||||
|
r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c);
|
||||||
|
|
||||||
|
// Pre-compute screen-space AABB half-extents
|
||||||
|
r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh;
|
||||||
|
r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh;
|
||||||
|
|
||||||
|
m_cachedRects.append(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInvertedRect())
|
||||||
|
m_cachedMyIndex = -1;
|
||||||
|
|
||||||
|
// Cache inverted rect data
|
||||||
|
m_cachedHasInverted = false;
|
||||||
|
m_cachedInvertedRadius = 0;
|
||||||
|
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
|
||||||
|
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
|
||||||
|
|
||||||
|
auto* inv = m_group->invertedRect();
|
||||||
|
if (inv) {
|
||||||
|
const QPointF invScene = inv->mapToScene(QPointF(0, 0));
|
||||||
|
const float outerCX = static_cast<float>(invScene.x() + inv->width() / 2.0);
|
||||||
|
const float outerCY = static_cast<float>(invScene.y() + inv->height() / 2.0);
|
||||||
|
const float outerHW = static_cast<float>(inv->width() / 2.0);
|
||||||
|
const float outerHH = static_cast<float>(inv->height() / 2.0);
|
||||||
|
|
||||||
|
const float innerCX = outerCX + static_cast<float>((inv->borderLeft() - inv->borderRight()) / 2.0);
|
||||||
|
const float innerCY = outerCY + static_cast<float>((inv->borderTop() - inv->borderBottom()) / 2.0);
|
||||||
|
const float innerHW = outerHW - static_cast<float>((inv->borderLeft() + inv->borderRight()) / 2.0);
|
||||||
|
const float innerHH = outerHH - static_cast<float>((inv->borderTop() + inv->borderBottom()) / 2.0);
|
||||||
|
|
||||||
|
// Check if this rect is near the border (within 2x smoothing of inner edge)
|
||||||
|
bool nearBorder = isInvertedRect();
|
||||||
|
if (!nearBorder) {
|
||||||
|
const float margin = pad * 2.0f;
|
||||||
|
const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f;
|
||||||
|
const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f;
|
||||||
|
const float myHW = m_cachedPaddedW * 0.5f;
|
||||||
|
const float myHH = m_cachedPaddedH * 0.5f;
|
||||||
|
// Near border if any edge of padded rect is within margin of inner edge
|
||||||
|
nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) ||
|
||||||
|
(myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearBorder) {
|
||||||
|
m_cachedHasInverted = true;
|
||||||
|
m_cachedInvertedRadius = static_cast<float>(inv->radius());
|
||||||
|
|
||||||
|
m_cachedInvertedOuter[0] = outerCX;
|
||||||
|
m_cachedInvertedOuter[1] = outerCY;
|
||||||
|
m_cachedInvertedOuter[2] = outerHW;
|
||||||
|
m_cachedInvertedOuter[3] = outerHH;
|
||||||
|
|
||||||
|
m_cachedInvertedInner[0] = innerCX;
|
||||||
|
m_cachedInvertedInner[1] = innerCY;
|
||||||
|
m_cachedInvertedInner[2] = innerHW;
|
||||||
|
m_cachedInvertedInner[3] = innerHH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU)
|
||||||
|
const float smoothFactor = pad;
|
||||||
|
constexpr float minR = 2.0f;
|
||||||
|
const auto rectCount = m_cachedRects.size();
|
||||||
|
for (qsizetype i = 0; i < rectCount; ++i) {
|
||||||
|
auto& ri = m_cachedRects[i];
|
||||||
|
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
|
||||||
|
|
||||||
|
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh;
|
||||||
|
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh;
|
||||||
|
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh;
|
||||||
|
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh;
|
||||||
|
|
||||||
|
for (qsizetype j = 0; j < rectCount; ++j) {
|
||||||
|
if (j == i)
|
||||||
|
continue;
|
||||||
|
const auto& rj = m_cachedRects[j];
|
||||||
|
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh)));
|
||||||
|
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh)));
|
||||||
|
fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh)));
|
||||||
|
fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_cachedHasInverted) {
|
||||||
|
const float icx = m_cachedInvertedInner[0];
|
||||||
|
const float icy = m_cachedInvertedInner[1];
|
||||||
|
const float ihw = m_cachedInvertedInner[2];
|
||||||
|
const float ihh = m_cachedInvertedInner[3];
|
||||||
|
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh)));
|
||||||
|
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh)));
|
||||||
|
fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh)));
|
||||||
|
fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine base radii with fill factors into effective per-corner radii
|
||||||
|
ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
|
||||||
|
ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
|
||||||
|
ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
|
||||||
|
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
|
||||||
|
if (!m_group) {
|
||||||
|
delete oldNode;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* node = static_cast<QSGGeometryNode*>(oldNode);
|
||||||
|
if (!node) {
|
||||||
|
node = new QSGGeometryNode;
|
||||||
|
|
||||||
|
auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
|
||||||
|
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
|
||||||
|
node->setGeometry(geometry);
|
||||||
|
node->setFlag(QSGNode::OwnsGeometry);
|
||||||
|
|
||||||
|
auto* material = new BlobMaterial;
|
||||||
|
material->setFlag(QSGMaterial::Blending);
|
||||||
|
node->setMaterial(material);
|
||||||
|
node->setFlag(QSGNode::OwnsMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update geometry
|
||||||
|
auto* geometry = node->geometry();
|
||||||
|
auto* v = geometry->vertexDataAsTexturedPoint2D();
|
||||||
|
|
||||||
|
const float x0 = static_cast<float>(m_localPaddedRect.x());
|
||||||
|
const float y0 = static_cast<float>(m_localPaddedRect.y());
|
||||||
|
const float x1 = x0 + static_cast<float>(m_localPaddedRect.width());
|
||||||
|
const float y1 = y0 + static_cast<float>(m_localPaddedRect.height());
|
||||||
|
|
||||||
|
v[0].set(x0, y0, 0.0f, 0.0f);
|
||||||
|
v[1].set(x1, y0, 1.0f, 0.0f);
|
||||||
|
v[2].set(x0, y1, 0.0f, 1.0f);
|
||||||
|
v[3].set(x1, y1, 1.0f, 1.0f);
|
||||||
|
|
||||||
|
node->markDirty(QSGNode::DirtyGeometry);
|
||||||
|
|
||||||
|
// Update material
|
||||||
|
auto* material = static_cast<BlobMaterial*>(node->material());
|
||||||
|
material->m_paddedX = m_cachedPaddedX;
|
||||||
|
material->m_paddedY = m_cachedPaddedY;
|
||||||
|
material->m_paddedW = m_cachedPaddedW;
|
||||||
|
material->m_paddedH = m_cachedPaddedH;
|
||||||
|
material->m_smoothFactor = static_cast<float>(m_group->smoothing());
|
||||||
|
material->m_myIndex = m_cachedMyIndex;
|
||||||
|
material->m_color = m_group->color();
|
||||||
|
material->m_hasInverted = m_cachedHasInverted ? 1 : 0;
|
||||||
|
material->m_invertedRadius = m_cachedInvertedRadius;
|
||||||
|
memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter));
|
||||||
|
memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner));
|
||||||
|
|
||||||
|
const int count = static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
|
||||||
|
material->m_rectCount = count;
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
material->m_rects[i] = m_cachedRects[i];
|
||||||
|
|
||||||
|
node->markDirty(QSGNode::DirtyMaterial);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
79
plugin/src/Caelestia/Blobs/blobshape.hpp
Normal file
79
plugin/src/Caelestia/Blobs/blobshape.hpp
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "blobmaterial.hpp"
|
||||||
|
|
||||||
|
#include <qmatrix4x4.h>
|
||||||
|
#include <qquickitem.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
class BlobGroup;
|
||||||
|
|
||||||
|
class BlobShape : public QQuickItem {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged)
|
||||||
|
Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
|
||||||
|
Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged)
|
||||||
|
Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged)
|
||||||
|
|
||||||
|
friend class BlobGroup;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BlobShape(QQuickItem* parent = nullptr);
|
||||||
|
~BlobShape() override = default;
|
||||||
|
|
||||||
|
BlobGroup* group() const { return m_group; }
|
||||||
|
|
||||||
|
void setGroup(BlobGroup* g);
|
||||||
|
|
||||||
|
qreal radius() const { return m_radius; }
|
||||||
|
|
||||||
|
void setRadius(qreal r);
|
||||||
|
|
||||||
|
QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; }
|
||||||
|
|
||||||
|
QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void groupChanged();
|
||||||
|
void radiusChanged();
|
||||||
|
void deformMatrixChanged();
|
||||||
|
void rawDeformMatrixChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void componentComplete() override;
|
||||||
|
void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
||||||
|
void updatePolish() override;
|
||||||
|
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
|
||||||
|
|
||||||
|
virtual bool isInvertedRect() const { return false; }
|
||||||
|
|
||||||
|
virtual bool isExcluded(const BlobShape* /*other*/) const { return false; }
|
||||||
|
|
||||||
|
virtual void cornerRadii(float out[4]) const;
|
||||||
|
|
||||||
|
virtual void updatePhysics() {}
|
||||||
|
|
||||||
|
virtual void registerWithGroup();
|
||||||
|
virtual void unregisterFromGroup();
|
||||||
|
void updateCenteredDeformMatrix();
|
||||||
|
|
||||||
|
BlobGroup* m_group = nullptr;
|
||||||
|
qreal m_radius = 0;
|
||||||
|
QMatrix4x4 m_deformMatrix; // identity by default
|
||||||
|
QMatrix4x4 m_centeredDeformMatrix;
|
||||||
|
|
||||||
|
// Cached data from updatePolish
|
||||||
|
float m_cachedPaddedX = 0;
|
||||||
|
float m_cachedPaddedY = 0;
|
||||||
|
float m_cachedPaddedW = 0;
|
||||||
|
float m_cachedPaddedH = 0;
|
||||||
|
QRectF m_localPaddedRect;
|
||||||
|
QVector<BlobRectData> m_cachedRects;
|
||||||
|
int m_cachedMyIndex = -2;
|
||||||
|
float m_pendingDx = 0;
|
||||||
|
float m_pendingDy = 0;
|
||||||
|
bool m_cachedHasInverted = false;
|
||||||
|
float m_cachedInvertedRadius = 0;
|
||||||
|
float m_cachedInvertedOuter[4] = {};
|
||||||
|
float m_cachedInvertedInner[4] = {};
|
||||||
|
};
|
||||||
223
plugin/src/Caelestia/Blobs/shaders/blob.frag
Normal file
223
plugin/src/Caelestia/Blobs/shaders/blob.frag
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
#version 440
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
float paddedX;
|
||||||
|
float paddedY;
|
||||||
|
float paddedW;
|
||||||
|
float paddedH;
|
||||||
|
float smoothFactor;
|
||||||
|
int rectCount;
|
||||||
|
int myIndex;
|
||||||
|
vec4 color;
|
||||||
|
int hasInverted;
|
||||||
|
float invertedRadius;
|
||||||
|
vec4 invertedOuter;
|
||||||
|
vec4 invertedInner;
|
||||||
|
vec4 rectData[80];
|
||||||
|
};
|
||||||
|
|
||||||
|
float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
|
||||||
|
vec2 d = abs(p - center) - halfSize + vec2(radius);
|
||||||
|
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) {
|
||||||
|
// r = (topRight, bottomRight, bottomLeft, topLeft)
|
||||||
|
p -= center;
|
||||||
|
r.xy = (p.x > 0.0) ? r.xy : r.wz;
|
||||||
|
r.x = (p.y > 0.0) ? r.y : r.x;
|
||||||
|
vec2 q = abs(p) - halfSize + r.x;
|
||||||
|
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdBox(vec2 p, vec2 center, vec2 halfSize) {
|
||||||
|
vec2 d = abs(p - center) - halfSize;
|
||||||
|
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float smin(float a, float b, float k) {
|
||||||
|
// Cubic smooth min (C2 continuous — no curvature kinks at blend boundary)
|
||||||
|
float h = max(k - abs(a - b), 0.0) / k;
|
||||||
|
return min(a, b) - h * h * h * k * (1.0/6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float smax(float a, float b, float k) {
|
||||||
|
float h = max(k - abs(a - b), 0.0) / k;
|
||||||
|
return max(a, b) + h * h * h * k * (1.0/6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float smaxSharpA(float a, float b, float k) {
|
||||||
|
// smax variant that keeps a's boundary sharp (no inward rounding at a = 0).
|
||||||
|
// Used for the frame outer edge so it always fills to the edges.
|
||||||
|
float h = max(k - abs(a - b), 0.0) / k;
|
||||||
|
float blend = h * h * h * k * (1.0/6.0);
|
||||||
|
blend *= smoothstep(0.0, k * 0.5, -a);
|
||||||
|
return max(a, b) + blend;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
|
||||||
|
|
||||||
|
float mergedSdf = 1e10;
|
||||||
|
int owner = -2;
|
||||||
|
float minDist = 1e10;
|
||||||
|
|
||||||
|
for (int i = 0; i < rectCount; i++) {
|
||||||
|
vec4 rect = rectData[i * 5]; // cx, cy, hw, hh
|
||||||
|
vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig
|
||||||
|
vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix
|
||||||
|
vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0
|
||||||
|
vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
|
||||||
|
|
||||||
|
// Offset center for asymmetric deformation
|
||||||
|
vec2 center = rect.xy + props.yz;
|
||||||
|
|
||||||
|
// AABB early-out: skip rects far from this pixel
|
||||||
|
vec2 extent = sh.xy + vec2(smoothFactor * 1.5);
|
||||||
|
if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Apply pre-computed inverse deformation to the evaluation point
|
||||||
|
mat2 invDeform = mat2(invDm.xy, invDm.zw);
|
||||||
|
vec2 transformedPixel = center + invDeform * (pixel - center);
|
||||||
|
|
||||||
|
// Use pre-computed effective per-corner radii
|
||||||
|
float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii);
|
||||||
|
|
||||||
|
// Use pre-computed minimum eigenvalue for SDF correction
|
||||||
|
d *= max(props.w, 0.01);
|
||||||
|
|
||||||
|
// Scale SDF on the axis facing a nearby border to narrow the smin blend zone
|
||||||
|
// in that direction only, without reducing k (which would cause sharp edges).
|
||||||
|
if (hasInverted != 0) {
|
||||||
|
vec2 screenHalf = sh.xy;
|
||||||
|
|
||||||
|
float distY0 = (center.y + screenHalf.y) - (invertedInner.y - invertedInner.w);
|
||||||
|
float distY1 = (invertedInner.y + invertedInner.w) - (center.y - screenHalf.y);
|
||||||
|
float distX0 = (center.x + screenHalf.x) - (invertedInner.x - invertedInner.z);
|
||||||
|
float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x);
|
||||||
|
|
||||||
|
// 0 = far from border, 1 = at border (max compression)
|
||||||
|
float yProx = 1.0 - min(
|
||||||
|
smoothstep(0.0, smoothFactor, distY0),
|
||||||
|
smoothstep(0.0, smoothFactor, distY1)
|
||||||
|
);
|
||||||
|
float xProx = 1.0 - min(
|
||||||
|
smoothstep(0.0, smoothFactor, distX0),
|
||||||
|
smoothstep(0.0, smoothFactor, distX1)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Smooth axis weights: gradient-based at corners, face-based inside.
|
||||||
|
vec2 q = abs(pixel - center) - screenHalf;
|
||||||
|
vec2 qp = max(q, vec2(0.0));
|
||||||
|
float cornerLen = length(qp);
|
||||||
|
|
||||||
|
// Gradient direction in corner region (smooth 90-degree rotation)
|
||||||
|
float gradX = qp.x / max(cornerLen, 0.001);
|
||||||
|
float gradY = qp.y / max(cornerLen, 0.001);
|
||||||
|
|
||||||
|
// Smooth face weights for inside/edge (no hard branch)
|
||||||
|
float faceY = smoothstep(-4.0, 4.0, q.y - q.x);
|
||||||
|
float faceX = 1.0 - faceY;
|
||||||
|
|
||||||
|
// Blend: gradient in corner region, face-based inside
|
||||||
|
float t = smoothstep(0.0, 2.0, cornerLen);
|
||||||
|
float xWeight = mix(faceX, gradX, t);
|
||||||
|
float yWeight = mix(faceY, gradY, t);
|
||||||
|
|
||||||
|
float boost = 3.0;
|
||||||
|
float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost;
|
||||||
|
d *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedSdf = smin(mergedSdf, d, smoothFactor);
|
||||||
|
if (d < smoothFactor && d < minDist) {
|
||||||
|
minDist = d;
|
||||||
|
owner = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInverted != 0) {
|
||||||
|
float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0;
|
||||||
|
float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
|
||||||
|
|
||||||
|
// Border sinks: track the opposite rect edge, clamped to border thickness
|
||||||
|
float innerTop = invertedInner.y - invertedInner.w;
|
||||||
|
float innerBot = invertedInner.y + invertedInner.w;
|
||||||
|
float innerLeft = invertedInner.x - invertedInner.z;
|
||||||
|
float innerRight = invertedInner.x + invertedInner.z;
|
||||||
|
float outerTop = invertedOuter.y - invertedOuter.w;
|
||||||
|
float outerBot = invertedOuter.y + invertedOuter.w;
|
||||||
|
float outerLeft = invertedOuter.x - invertedOuter.z;
|
||||||
|
float outerRight = invertedOuter.x + invertedOuter.z;
|
||||||
|
|
||||||
|
float sinkValue = 0.0;
|
||||||
|
for (int i = 0; i < rectCount; i++) {
|
||||||
|
vec4 rect = rectData[i * 5];
|
||||||
|
vec4 sinkProps = rectData[i * 5 + 1];
|
||||||
|
vec2 sinkSh = rectData[i * 5 + 3].xy;
|
||||||
|
|
||||||
|
// Screen-space center (with offset) and pre-computed AABB half-extents
|
||||||
|
vec2 ctr = rect.xy + sinkProps.yz;
|
||||||
|
|
||||||
|
// Delay sink to absorb smin blend depth (cubic smin max = k/6)
|
||||||
|
float preOff = smoothFactor * (1.0/6.0);
|
||||||
|
|
||||||
|
// Top border: track rect's BOTTOM edge, only within border thickness
|
||||||
|
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop);
|
||||||
|
|
||||||
|
// Bottom border: track rect's TOP edge
|
||||||
|
float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot);
|
||||||
|
|
||||||
|
// Left border: track rect's RIGHT edge
|
||||||
|
float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft);
|
||||||
|
|
||||||
|
// Right border: track rect's LEFT edge
|
||||||
|
float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight);
|
||||||
|
|
||||||
|
// Lateral distance from pixel to rect's extent along each edge
|
||||||
|
float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0);
|
||||||
|
float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
|
||||||
|
|
||||||
|
// Perpendicular proximity: full strength in border, fade inside inner area
|
||||||
|
float topZone = 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
|
||||||
|
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
|
||||||
|
float leftZone = 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
|
||||||
|
float rightZone = smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
|
||||||
|
|
||||||
|
float s = smoothFactor * 2.0;
|
||||||
|
float sink = max(
|
||||||
|
max(topPen * smoothstep(s, 0.0, hLat) * topZone,
|
||||||
|
botPen * smoothstep(s, 0.0, hLat) * botZone),
|
||||||
|
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
|
||||||
|
rightPen * smoothstep(s, 0.0, vLat) * rightZone)
|
||||||
|
);
|
||||||
|
sinkValue = max(sinkValue, sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
dInner -= sinkValue;
|
||||||
|
|
||||||
|
float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor);
|
||||||
|
|
||||||
|
mergedSdf = smin(mergedSdf, dFrame, smoothFactor);
|
||||||
|
if (dFrame < minDist) {
|
||||||
|
owner = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each renderer only outputs pixels it owns, but allow rendering
|
||||||
|
// blend zones to prevent gaps (mergedSdf < smoothFactor means in blend)
|
||||||
|
// myIndex == -1: inverted rect renders border-owned pixels
|
||||||
|
// myIndex >= 0: individual rect renders its owned pixels
|
||||||
|
if (owner != myIndex && mergedSdf > smoothFactor)
|
||||||
|
discard;
|
||||||
|
|
||||||
|
float fw = fwidth(mergedSdf);
|
||||||
|
float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
|
||||||
|
fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity;
|
||||||
|
}
|
||||||
29
plugin/src/Caelestia/Blobs/shaders/blob.vert
Normal file
29
plugin/src/Caelestia/Blobs/shaders/blob.vert
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#version 440
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 qt_VertexPosition;
|
||||||
|
layout(location = 1) in vec2 qt_VertexTexCoord;
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 qt_TexCoord0;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
float paddedX;
|
||||||
|
float paddedY;
|
||||||
|
float paddedW;
|
||||||
|
float paddedH;
|
||||||
|
float smoothFactor;
|
||||||
|
int rectCount;
|
||||||
|
int myIndex;
|
||||||
|
vec4 color;
|
||||||
|
int hasInverted;
|
||||||
|
float invertedRadius;
|
||||||
|
vec4 invertedOuter;
|
||||||
|
vec4 invertedInner;
|
||||||
|
vec4 rectData[80];
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = qt_Matrix * qt_VertexPosition;
|
||||||
|
qt_TexCoord0 = qt_VertexTexCoord;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus)
|
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
|
|
@ -61,3 +61,4 @@ add_subdirectory(Components)
|
||||||
add_subdirectory(Internal)
|
add_subdirectory(Internal)
|
||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
add_subdirectory(Services)
|
add_subdirectory(Services)
|
||||||
|
add_subdirectory(Blobs)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
readonly property bool isCustomProvider: typeof providerInput === "object"
|
readonly property bool isCustomProvider: typeof providerInput === "object"
|
||||||
readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
|
readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
|
||||||
readonly property string interfaceName: isCustomProvider ? (providerInput.iface || "") : ""
|
readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : ""
|
||||||
readonly property var currentConfig: {
|
readonly property var currentConfig: {
|
||||||
const name = providerName;
|
const name = providerName;
|
||||||
const iface = interfaceName;
|
const iface = interfaceName;
|
||||||
|
|
@ -36,7 +36,7 @@ Singleton {
|
||||||
return {
|
return {
|
||||||
connectCmd: custom.connectCmd || defaults.connectCmd,
|
connectCmd: custom.connectCmd || defaults.connectCmd,
|
||||||
disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd,
|
disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd,
|
||||||
interface: custom.iface || defaults.interface,
|
interface: custom.interface || defaults.interface,
|
||||||
displayName: custom.displayName || defaults.displayName
|
displayName: custom.displayName || defaults.displayName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue