Merge pull request #1331 from caelestia-dots/sdfs

Switch to sdfs for backgrounds
This commit is contained in:
2 * r + 2 * t 2026-04-09 00:55:22 +10:00 committed by GitHub
commit aa2b08dd45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 2280 additions and 1160 deletions

5
.envrc
View file

@ -3,10 +3,11 @@ if has nix; then
fi
shopt -s globstar
watch_file assets/cpp/**/*.cpp
watch_file assets/cpp/**/*.hpp
watch_file plugin/**/*.cpp
watch_file plugin/**/*.hpp
watch_file plugin/**/*.qml
watch_file plugin/**/*.vert
watch_file plugin/**/*.frag
watch_file **/CMakeLists.txt
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv

View file

@ -23,7 +23,7 @@ jobs:
run: |
cat > /tmp/Dockerfile <<EOF
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 && \
echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \

View file

@ -65,7 +65,7 @@ JsonObject {
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> 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 {
@ -76,7 +76,7 @@ JsonObject {
property int extraLarge: 1000 * scale
property int expressiveFastSpatial: 350 * scale
property int expressiveDefaultSpatial: 500 * scale
property int expressiveEffects: 200 * scale
property int expressiveSlowSpatial: 650 * scale
}
component Anim: JsonObject {

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import qs.components
import qs.config
import qs.utils
import qs.modules.bar.popouts as BarPopouts
Item {
@ -12,9 +13,10 @@ Item {
required property ShellScreen screen
required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts
required property bool disabled
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 padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2

View file

@ -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 {}
}
}

View 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
}
}

View file

@ -14,8 +14,6 @@ Item {
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
readonly property Item current: currentPopout?.item ?? null
anchors.centerIn: parent
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2
@ -171,8 +169,7 @@ Item {
required property string name
readonly property bool shouldBeActive: root.popouts.currentName === name
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.centerIn: parent
opacity: 0
scale: 0.8

View file

@ -14,45 +14,50 @@ Item {
id: root
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 Item current: (content.item as Content)?.current ?? null
readonly property bool isDetached: detachedMode.length > 0
property alias currentName: popoutState.currentName
property real currentCenter
property alias hasCurrent: popoutState.hasCurrent
readonly property PopoutState popState: popoutState
property real currentCenter
property string detachedMode
property string queuedMode
readonly property bool isDetached: detachedMode.length > 0
property int animLength: Appearance.anim.durations.normal
property list<real> animCurve: Appearance.anim.curves.emphasized
property int animLength: Appearance.anim.durations.expressiveDefaultSpatial
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 {
animLength = Appearance.anim.durations.large;
setAnims(true);
if (mode === "winfo") {
detachedMode = mode;
} else {
queuedMode = mode;
detachedMode = "any";
}
setAnims(false);
focus = true;
}
function close(): void {
hasCurrent = false;
animCurve = Appearance.anim.curves.emphasizedAccel;
animLength = Appearance.anim.durations.normal;
detachedMode = "";
animCurve = Appearance.anim.curves.emphasized;
}
visible: width > 0 && height > 0
clip: true
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
@ -89,15 +94,7 @@ Item {
}
Binding {
when: root.isDetached
target: QsWindow.window
property: "WlrLayershell.keyboardFocus"
value: WlrKeyboardFocus.OnDemand
}
Binding {
when: root.hasCurrent && root.currentName === "wirelesspassword"
when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword")
target: QsWindow.window
property: "WlrLayershell.keyboardFocus"
@ -108,8 +105,7 @@ Item {
id: content
shouldBeActive: root.hasCurrent && !root.detachedMode
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.fill: parent
sourceComponent: Content {
popouts: popoutState
@ -117,6 +113,8 @@ Item {
}
Comp {
id: winfo
shouldBeActive: root.detachedMode === "winfo"
anchors.centerIn: parent
@ -127,32 +125,15 @@ Item {
}
Comp {
id: controlCenter
shouldBeActive: root.detachedMode === "any"
anchors.centerIn: parent
sourceComponent: ControlCenter {
function close(): void {
root.close();
}
screen: root.screen
active: root.queuedMode
}
}
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
onClose: root.close()
}
}
@ -164,7 +145,7 @@ Item {
}
Behavior on implicitHeight {
enabled: root.implicitWidth > 0
enabled: root.offsetScale < 1
Anim {
duration: root.animLength
@ -180,6 +161,7 @@ Item {
active: false
opacity: 0
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
states: State {
name: "active"
when: comp.shouldBeActive

View file

@ -25,8 +25,7 @@ Item {
root: root
}
function close(): void {
}
signal close
implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio
implicitHeight: screen.height * Config.controlCenter.sizes.heightMult

View file

@ -45,12 +45,9 @@ Singleton {
ControlCenter {
id: cc
function close(): void {
win.destroy();
}
anchors.fill: parent
screen: win.screen
onClose: win.destroy()
floating: true
}

View file

@ -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 {}
}
}

View file

@ -29,59 +29,19 @@ Item {
}
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
implicitHeight: 0
implicitWidth: content.implicitWidth
visible: offsetScale < 1
anchors.topMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open
opacity: 1 - offsetScale
onStateChanged: {
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"
Anim {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.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;
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
@ -91,8 +51,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
visible: false
active: true
active: root.shouldBeActive || root.visible
sourceComponent: Content {
visibilities: root.visibilities

View file

@ -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
}
}

View file

@ -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
}
}
}

View 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
}
}

View file

@ -1,17 +1,9 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.components
import qs.components.containers
import qs.services
import qs.config
import qs.utils
import qs.modules.bar
Variants {
model: Screens.screens
@ -20,204 +12,17 @@ Variants {
id: scope
required property ShellScreen modelData
readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name)
Exclusions {
screen: scope.modelData
bar: bar
bar: content.bar
borderThickness: Config.border.thickness
}
StyledWindow {
id: win
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;
}
ContentWindow {
id: content
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)
}
}
}
}
}

View file

@ -42,11 +42,13 @@ CustomMouseArea {
}
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 {
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 {
@ -107,9 +109,9 @@ CustomMouseArea {
visibilities.bar = false;
}
if (panels.sidebar.width === 0) {
if (panels.sidebar.offsetScale === 1) {
// 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
if (!osdShortcutActive) {
@ -124,23 +126,23 @@ CustomMouseArea {
const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x);
// 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)
visibilities.session = true;
else if (dragX > Config.session.dragThreshold)
visibilities.session = false;
// 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;
} else if (showSidebar && dragX < -Config.sidebar.dragThreshold) {
// Show sidebar on drag if not in session area
visibilities.sidebar = true;
}
} else {
const outOfSidebar = x < width - panels.sidebar.width;
const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
// 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
if (!osdShortcutActive) {
@ -153,7 +155,7 @@ CustomMouseArea {
}
// 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)
visibilities.session = true;
else if (dragX > Config.session.dragThreshold)
@ -209,7 +211,7 @@ CustomMouseArea {
// Show popouts on hover
if (x < bar.implicitWidth) {
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;
bar.closeTray();
}
@ -226,7 +228,7 @@ CustomMouseArea {
// Also hide dashboard and OSD if they're not being hovered
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) {
root.visibilities.dashboard = false;
@ -254,7 +256,7 @@ CustomMouseArea {
function onOsdChanged() {
if (root.visibilities.osd) {
// 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) {
root.osdShortcutActive = true;
}

View file

@ -22,37 +22,43 @@ Item {
required property real borderThickness
readonly property alias osd: osd
readonly property alias osdWrapper: osdWrapper
readonly property alias notifications: notifications
readonly property alias session: session
readonly property alias sessionWrapper: sessionWrapper
readonly property alias launcher: launcher
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 toasts: toasts
readonly property alias sidebar: sidebar
anchors.fill: parent
anchors.margins: root.borderThickness
anchors.margins: borderThickness
anchors.leftMargin: bar.implicitWidth
Behavior on anchors.margins {
Anim {}
}
Behavior on anchors.leftMargin {
Anim {}
}
Osd.Wrapper {
id: osd
clip: session.width > 0 || sidebar.width > 0
screen: root.screen
visibilities: root.visibilities
Item {
id: osdWrapper
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: session.width + sidebar.width
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 {
id: osd
screen: root.screen
visibilities: root.visibilities
sidebarOrSessionVisible: sidebar.visible || session.visible
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
}
}
Notifications.Wrapper {
@ -60,23 +66,33 @@ Item {
visibilities: root.visibilities
sidebarPanel: sidebar
osdPanel: osd
sessionPanel: session
osdPanel: osdWrapper
sessionPanel: sessionWrapper
anchors.top: parent.top
anchors.right: parent.right
}
Session.Wrapper {
id: session
clip: sidebar.width > 0
visibilities: root.visibilities
panels: root
Item {
id: sessionWrapper
anchors.verticalCenter: parent.verticalCenter
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 {
@ -99,22 +115,11 @@ Item {
anchors.top: parent.top
}
BarPopouts.Wrapper {
id: popouts
BarPopouts.ClipWrapper {
id: popoutsWrapper
screen: root.screen
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);
}
borderThickness: root.borderThickness
}
Utilities.Wrapper {
@ -122,7 +127,7 @@ Item {
visibilities: root.visibilities
sidebar: sidebar
popouts: popouts
popouts: popoutsWrapper.content
anchors.bottom: parent.bottom
anchors.right: parent.right
@ -140,7 +145,6 @@ Item {
id: sidebar
visibilities: root.visibilities
panels: root
anchors.top: notifications.bottom
anchors.bottom: utilities.top

View 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
}
}

View file

@ -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 {}
}
}

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import qs.components
import qs.config
import qs.modules.launcher.services
Item {
id: root
@ -13,7 +14,6 @@ Item {
required property var panels
readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled
property int contentHeight
readonly property real maxHeight: {
let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large;
@ -22,91 +22,28 @@ Item {
return max;
}
onMaxHeightChanged: timer.start()
visible: height > 0
implicitHeight: 0
implicitWidth: content.implicitWidth
property real offsetScale: shouldBeActive ? 0 : 1
onShouldBeActiveChanged: {
if (shouldBeActive) {
timer.stop();
hideAnim.stop();
showAnim.start();
} else {
showAnim.stop();
hideAnim.start();
}
if (shouldBeActive)
implicitHeight = Qt.binding(() => content.implicitHeight);
else
implicitHeight = implicitHeight; // Break binding during close anim
}
SequentialAnimation {
id: showAnim
visible: offsetScale < 1
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 {
target: root
property: "implicitHeight"
to: root.contentHeight
duration: Appearance.anim.durations.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 {
@ -115,16 +52,12 @@ Item {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
visible: false
active: false
Component.onCompleted: timer.start()
active: root.shouldBeActive || root.visible
sourceComponent: Content {
visibilities: root.visibilities
panels: root.panels
maxHeight: root.maxHeight
Component.onCompleted: root.contentHeight = implicitHeight
}
}
}

View file

@ -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 {}
}
}

View file

@ -1,6 +1,5 @@
import QtQuick
import qs.components
import qs.config
Item {
id: root
@ -11,27 +10,10 @@ Item {
property alias sessionPanel: content.sessionPanel
visible: height > 0
anchors.topMargin: -5
implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth)
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 {
id: content

View file

@ -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 {}
}
}

View file

@ -11,9 +11,13 @@ Item {
required property ShellScreen screen
required property DrawerVisibilities visibilities
required property bool sidebarOrSessionVisible
property bool hovered
readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen)
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 bool muted
@ -34,42 +38,19 @@ Item {
brightness = root.monitor?.brightness ?? 0;
}
visible: width > 0
implicitWidth: 0
visible: offsetScale < 1
anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
opacity: 1 - offsetScale
states: State {
name: "visible"
when: root.shouldBeActive
PropertyChanges {
root.implicitWidth: content.implicitWidth
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitWidth"
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
Anim {
target: root
property: "implicitWidth"
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Connections {
function onMutedChanged(): void {
root.show();
@ -119,7 +100,8 @@ Item {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible)
asynchronous: true
active: root.shouldBeActive || root.visible
sourceComponent: Content {
monitor: root.monitor

View file

@ -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 {}
}
}

View file

@ -8,52 +8,33 @@ Item {
id: root
required property DrawerVisibilities visibilities
required property var panels
required property bool sidebarVisible
readonly property real nonAnimWidth: content.implicitWidth
visible: width > 0
implicitWidth: 0
implicitHeight: content.implicitHeight
readonly property bool shouldBeActive: visibilities.session && Config.session.enabled
property real offsetScale: shouldBeActive ? 0 : 1
property real sidebarOffset: sidebarVisible ? 14 : 0
states: State {
name: "visible"
when: root.visibilities.session && Config.session.enabled
visible: offsetScale < 1
anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open
opacity: 1 - offsetScale
PropertyChanges {
root.implicitWidth: root.nonAnimWidth
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitWidth"
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 {
id: content
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible)
active: root.shouldBeActive || root.visible
sourceComponent: Content {
visibilities: root.visibilities

View file

@ -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 {}
}
}

View file

@ -8,45 +8,23 @@ Item {
id: root
required property DrawerVisibilities visibilities
required property var panels
readonly property Props props: Props {}
visible: width > 0
implicitWidth: 0
readonly property bool shouldBeActive: visibilities.sidebar && Config.sidebar.enabled
property real offsetScale: shouldBeActive ? 0 : 1
states: State {
name: "visible"
when: root.visibilities.sidebar && Config.sidebar.enabled
visible: offsetScale < 1
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
implicitWidth: Config.sidebar.sizes.width
opacity: 1 - offsetScale
PropertyChanges {
root.implicitWidth: Config.sidebar.sizes.width
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitWidth"
duration: Appearance.anim.durations.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 {
id: content
@ -56,8 +34,7 @@ Item {
anchors.margins: Appearance.padding.large
anchors.bottomMargin: 0
active: true
Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible)
active: root.shouldBeActive || root.visible
sourceComponent: Content {
implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2

View file

@ -4,14 +4,16 @@ import QtQuick
import Quickshell
import qs.components
import qs.config
import qs.modules.sidebar as Sidebar
import qs.modules.bar.popouts as BarPopouts
Item {
id: root
required property DrawerVisibilities visibilities
required property Item sidebar
required property Sidebar.Wrapper sidebar
required property BarPopouts.Wrapper popouts
property real horizontalStretch
readonly property PersistentProperties props: PersistentProperties {
property bool recordingListExpanded: false
@ -21,75 +23,64 @@ Item {
reloadableId: "utilities"
}
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
implicitHeight: 0
implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width
onStateChanged: {
if (state === "visible" && timer.running) {
timer.triggered();
timer.stop();
}
}
visible: offsetScale < 1
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + content.anchors.margins * 2
implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp)
opacity: 1 - offsetScale
states: State {
name: "visible"
when: root.shouldBeActive
name: "attachedToSidebar"
when: root.visibilities.sidebar
PropertyChanges {
root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2
root.sidebarLerp: 1
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
property: "sidebarLerp"
duration: Appearance.anim.durations.expressiveDefaultSpatial / 2
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
},
Transition {
from: "visible"
to: ""
Anim {
target: root
property: "implicitHeight"
easing.bezierCurve: Appearance.anim.curves.emphasized
property: "sidebarLerp"
duration: Appearance.anim.durations.expressiveDefaultSpatial / 2
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
]
Timer {
id: timer
running: true
interval: Appearance.anim.durations.extraLarge
onTriggered: {
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
content.visible = true;
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Loader {
id: content
asynchronous: true
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Appearance.padding.large
visible: false
active: true
asynchronous: true
active: root.shouldBeActive || root.visible
sourceComponent: Content {
implicitWidth: root.implicitWidth - Appearance.padding.large * 2
implicitWidth: root.implicitWidth - content.anchors.margins * 2
props: root.props
visibilities: root.visibilities
popouts: root.popouts

View file

@ -120,7 +120,7 @@ ColumnLayout {
Loader {
asynchronous: true
active: root.client?.lastIpcObject.floating
active: root.client?.lastIpcObject.floating ?? false
Layout.fillWidth: active
Layout.leftMargin: active ? 0 : -parent.spacing
Layout.rightMargin: active ? 0 : -parent.spacing

View 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
)

View 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();
}

View 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;
};

View 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);
}

View 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;
};

View 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;
}

View 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;
};

View 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;
}
}

View 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);
};

View 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;
}

View 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] = {};
};

View 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;
}

View 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;
}

View file

@ -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)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
@ -60,3 +60,4 @@ qml_module(caelestia
add_subdirectory(Internal)
add_subdirectory(Models)
add_subdirectory(Services)
add_subdirectory(Blobs)