hefty: morphing thingy

This commit is contained in:
end-4
2026-01-04 22:08:53 +01:00
parent 60fd1ea030
commit 7872fba6fe
8 changed files with 389 additions and 2 deletions
@@ -0,0 +1,32 @@
import QtQuick
import Quickshell
/**
* Abstract morphed panel to be used in TopLayerPanel.
* Screen width and height are to be supplied when declared in the top layer panel
* Others are to be declared by panels deriving from this
*
* To make sure morph movements don't look weird:
* - Follow the convention of having points start from bottom-middle and go clockwise
* - Make sure the number of points is "balanced" in all directions
* - Tip: Sometimes symmetry is not enough. Try to have more intermediate points if ones you have are too spaced out and act funny.
*/
Item {
id: root
// To be fed
required property int screenWidth
required property int screenHeight
// Some info
property int reservedTop: 0
property int reservedBottom: 0
property int reservedLeft: 0
property int reservedRight: 0
// Main stuff
property var backgroundPolygon
property Region maskRegion: Region {
item: root
}
}
@@ -0,0 +1,89 @@
import QtQuick
import qs.modules.common
import "../../common/widgets/shapes/material-shapes.js" as MaterialShapes
import "../../common/widgets/shapes/shapes/corner-rounding.js" as CornerRounding
import "../../common/widgets/shapes/geometry/offset.js" as Offset
HAbstractMorphedPanel {
id: root
// Own props
property int barHeight: Appearance.sizes.baseBarHeight
function getRounding(cornerStyle) {
switch(cornerStyle) {
case 0: return Appearance.rounding.screenRounding;
case 1: return Appearance.rounding.windowRounding;
case 2: return 0;
default: return Appearance.rounding.screenRounding;
}
}
function getEdgeGap(cornerStyle) {
switch(cornerStyle) {
case 0: return 0;
case 1: return Appearance.sizes.hyprlandGapsOut;
case 2: return 0;
default: return 0;
}
}
function getEdgeRounding(cornerStyle) {
switch(cornerStyle) {
case 0: return 0;
case 1: return Appearance.rounding.windowRounding;
case 2: return 0;
default: return Appearance.rounding.windowRounding;
}
}
function getHug(cornerStyle) {
return cornerStyle === 0;
}
// Some info
reservedTop: barHeight + getEdgeGap(Config.options.bar.cornerStyle)
// Background
backgroundPolygon: {
// It's certainly cleaner to have the below props declared outside, but we do this
// to make sure config change only makes this re-evaluate exactly once
const cornerStyle = Config.options.bar.cornerStyle
const rounding = root.getRounding(cornerStyle)
const edgeGap = root.getEdgeGap(cornerStyle)
const edgeRounding = root.getEdgeRounding(cornerStyle)
const hug = root.getHug(cornerStyle)
const points = [
// bottom-middle
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth / 2, root.barHeight + edgeGap), new CornerRounding.CornerRounding(0)),
// bottom-left /||
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap + rounding, edgeGap + barHeight), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap + barHeight), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap + barHeight + rounding * (hug ? 1 : -1)), new CornerRounding.CornerRounding(edgeRounding)),
// top-left |/-
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap + rounding), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap), new CornerRounding.CornerRounding(edgeRounding)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap + rounding, edgeGap), new CornerRounding.CornerRounding(0)),
// top-middle
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth / 2, edgeGap), new CornerRounding.CornerRounding(0)),
// top-right -\|
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap - rounding, edgeGap), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, edgeGap), new CornerRounding.CornerRounding(edgeRounding)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, edgeGap + rounding), new CornerRounding.CornerRounding(0)),
// bottom-right ||\
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, root.barHeight + edgeGap + rounding * (hug ? 1 : -1)), new CornerRounding.CornerRounding(edgeRounding)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, root.barHeight + edgeGap), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap - rounding, root.barHeight + edgeGap), new CornerRounding.CornerRounding(0)),
]
return MaterialShapes.customPolygon(points, 1, new Offset.Offset(root.screenWidth / 2, edgeGap + barHeight / 2))
}
// Content
implicitHeight: barHeight + getEdgeGap(Config.options.bar.cornerStyle) * 2
anchors {
top: parent.top
left: parent.left
right: parent.right
}
}
@@ -0,0 +1,75 @@
import QtQuick
import Quickshell.Hyprland
import qs
import qs.modules.common
import "../../common/widgets/shapes/material-shapes.js" as MaterialShapes
import "../../common/widgets/shapes/shapes/corner-rounding.js" as CornerRounding
import "../../common/widgets/shapes/geometry/offset.js" as Offset
HAbstractMorphedPanel {
id: root
// Own props
property int edgeGap: Appearance.sizes.hyprlandGapsOut
property real rounding: Appearance.rounding.windowRounding
property real contentHeight: 300 // For now
// Background
backgroundPolygon: MaterialShapes.customPolygon([
// bottom-middle
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth / 2, edgeGap + root.contentHeight), new CornerRounding.CornerRounding(0)),
// bottom-left
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap + rounding, edgeGap + root.contentHeight), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap + root.contentHeight), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap + root.contentHeight - rounding), new CornerRounding.CornerRounding(rounding)),
// top-left
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap + rounding), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap, edgeGap), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(edgeGap + rounding, edgeGap), new CornerRounding.CornerRounding(rounding)),
// top-middle
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth / 2, edgeGap), new CornerRounding.CornerRounding(0)),
// top-right
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap - rounding, edgeGap), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, edgeGap), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, edgeGap + rounding), new CornerRounding.CornerRounding(rounding)),
// bottom-right
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, edgeGap + root.contentHeight - rounding), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap, edgeGap + root.contentHeight), new CornerRounding.CornerRounding(rounding)),
new MaterialShapes.PointNRound(new Offset.Offset(root.screenWidth - edgeGap - rounding, edgeGap + root.contentHeight), new CornerRounding.CornerRounding(rounding)),
], 1, new Offset.Offset(root.screenWidth / 2, edgeGap + contentHeight / 2))
// Keybinds
GlobalShortcut {
name: "searchToggle"
description: "Toggles search on press"
onPressed: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "searchToggleRelease"
description: "Toggles search on release"
onPressed: {
GlobalStates.superReleaseMightTrigger = true;
}
onReleased: {
if (!GlobalStates.superReleaseMightTrigger) {
GlobalStates.superReleaseMightTrigger = true;
return;
}
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "searchToggleReleaseInterrupt"
description: "Interrupts possibility of search being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything."
onPressed: {
GlobalStates.superReleaseMightTrigger = false;
}
}
}
@@ -0,0 +1,15 @@
import QtQuick
import Quickshell
// The stuff that sits on the "top" layer for layershells. Not to be confused with "toplevels" as in windows.
Scope {
id: root
Variants {
model: Quickshell.screens
delegate: HTopLayerPanel {
required property var modelData
screen: modelData
}
}
}
@@ -0,0 +1,123 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs
import "../../common"
import "../../common/widgets/shapes" as S
import "../../common/widgets/shapes/material-shapes.js" as MaterialShapes
import "../../common/widgets/shapes/shapes/corner-rounding.js" as CornerRounding
import "../../common/widgets/shapes/geometry/offset.js" as Offset
/**
* Fullscreen layer. Uses masking to not block clicks on windows n' stuff.
*/
PanelWindow {
id: root
///////////////// Window //////////////////
color: "transparent"
WlrLayershell.namespace: "quickshell:topLayerPanel"
exclusionMode: ExclusionMode.Ignore
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: root.currentPanel
}
// HyprlandWindow.visibleMask: mask // TODO: use this later to optimize hyprland's rendering
///////////////// Content //////////////////
property alias roundedPolygon: backgroundShape.roundedPolygon
S.ShapeCanvas {
id: backgroundShape
anchors.fill: parent
polygonIsNormalized: false
roundedPolygon: MaterialShapes.customPolygon([new MaterialShapes.PointNRound(new Offset.Offset(root.screen.width, 0), new CornerRounding.CornerRounding(9999)),])
animation: NumberAnimation {
duration: 500
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial
}
// animation: SpringAnimation {
// spring: 3.5
// damping: 0.3
// }
color: Appearance.colors.colLayer0
borderWidth: (root.currentPanel === bar && Config.options.bar.cornerStyle !== 1) ? 0 : 1
borderColor: Appearance.colors.colLayer0Border
visible: false // cuz there's already the shadow
// debug: true
}
DropShadow {
id: shadow
source: backgroundShape
anchors.fill: backgroundShape
radius: 10
samples: radius * 2 + 1 // Ideally radius * 2 + 1, see qt docs
color: "#44000000"
}
property HAbstractMorphedPanel currentPanel: bar
roundedPolygon: currentPanel.backgroundPolygon
// Do we want to have reserved area always follow the bar or maybe differ per panel?
EdgeReservedArea {
anchors.top: true
exclusiveZone: bar.reservedTop
}
EdgeReservedArea {
anchors.bottom: true
exclusiveZone: bar.reservedBottom
}
EdgeReservedArea {
anchors.left: true
exclusiveZone: bar.reservedLeft
}
EdgeReservedArea {
anchors.right: true
exclusiveZone: bar.reservedRight
}
////////////// Content: Panels ///////////////
HBar {
id: bar
screenWidth: root.width
screenHeight: root.height
}
HOverview {
id: overview
screenWidth: root.width
screenHeight: root.height
}
Connections {
target: GlobalStates
function onOverviewOpenChanged() {
if (GlobalStates.overviewOpen) {
currentPanel = overview;
} else {
currentPanel = bar;
}
}
}
//////////////// Components /////////////////
component EdgeReservedArea: PanelWindow {
WlrLayershell.namespace: "quickshell:edgeReservedArea"
implicitWidth: 0
implicitHeight: 0
mask: Region {
item: null
}
}
}