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
}
}
}
@@ -0,0 +1,48 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.ii.background
import qs.modules.ii.cheatsheet
import qs.modules.ii.dock
import qs.modules.ii.lock
import qs.modules.ii.mediaControls
import qs.modules.ii.notificationPopup
import qs.modules.ii.onScreenDisplay
import qs.modules.ii.onScreenKeyboard
import qs.modules.ii.overview
import qs.modules.ii.polkit
import qs.modules.ii.regionSelector
import qs.modules.ii.screenCorners
import qs.modules.ii.sessionScreen
import qs.modules.ii.sidebarLeft
import qs.modules.ii.sidebarRight
import qs.modules.ii.overlay
import qs.modules.ii.verticalBar
import qs.modules.ii.wallpaperSelector
import qs.modules.hefty.topLayer
Scope {
// New
PanelLoader { component: HTopLayer {} }
// Fallbacks
PanelLoader { component: Background {} }
PanelLoader { component: Cheatsheet {} }
PanelLoader { extraCondition: Config.options.dock.enable; component: Dock {} }
PanelLoader { component: Lock {} }
PanelLoader { component: MediaControls {} }
PanelLoader { component: NotificationPopup {} }
PanelLoader { component: OnScreenDisplay {} }
PanelLoader { component: OnScreenKeyboard {} }
PanelLoader { component: Overlay {} }
// PanelLoader { component: Overview {} }
PanelLoader { component: Polkit {} }
PanelLoader { component: RegionSelector {} }
PanelLoader { component: ScreenCorners {} }
PanelLoader { component: SessionScreen {} }
PanelLoader { component: SidebarLeft {} }
PanelLoader { component: SidebarRight {} }
PanelLoader { component: WallpaperSelector {} }
}
+6 -1
View File
@@ -34,7 +34,7 @@ ShellRoot {
// Panel families
property list<string> families: ["ii", "waffle"]
property list<string> families: ["ii", "waffle", "hefty"]
function cyclePanelFamily() {
const currentIndex = families.indexOf(Config.options.panelFamily)
const nextIndex = (currentIndex + 1) % families.length
@@ -57,6 +57,11 @@ ShellRoot {
component: WaffleFamily {}
}
PanelFamilyLoader {
identifier: "hefty"
component: HeftyHypeFamily {}
}
// Shortcuts
IpcHandler {