feat: m3 expressive menus

Also fix the player selector being click through
Fixes #1405
Closes #1350
This commit is contained in:
2 * r + 2 * t 2026-04-19 19:15:31 +10:00
parent 2bb28197a0
commit ec8ce65857
7 changed files with 167 additions and 89 deletions

View file

@ -1,41 +1,116 @@
pragma ComponentBehavior: Bound
import "../effects"
import QtQuick
import QtQuick.Layouts
import Quickshell
import Caelestia.Config
import qs.components
import qs.components.effects
import qs.services
import qs.modules.drawers
Elevation {
MouseArea {
id: root
enum Side {
Top,
Bottom,
Left,
Right
}
required property Item attachTo
property int attachSideX: Menu.Right
property int attachSideY: Menu.Bottom
property int thisSideX: Menu.Right
property int thisSideY: Menu.Top
property real marginX
property real marginY
property list<MenuItem> items
property MenuItem active: items[0] ?? null
property bool expanded
signal itemSelected(item: MenuItem)
radius: Tokens.rounding.small / 2
parent: {
const win = QsWindow.window;
const contentWin = win as ContentWindow; // If inside the drawer content window, put it inside the interaction wrapper so hover works
return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem;
}
anchors.fill: parent
enabled: expanded
onClicked: expanded = false
opacity: expanded ? 1 : 0
layer.enabled: opacity < 1
Behavior on opacity {
Anim {
duration: Tokens.anim.durations.small
}
}
TransformWatcher {
id: watcher
a: root.parent
b: root.attachTo
}
Elevation {
id: menu
x: {
watcher.transform; // mapToItem is not reactive so this forces updates
const item = root.attachTo;
let off = root.attachSideX === Menu.Left ? 0 : item.width;
if (root.thisSideX === Menu.Right)
off -= width;
return item.mapToItem(root.parent, off, 0).x + root.marginX;
}
y: {
watcher.transform; // mapToItem is not reactive so this forces updates
const item = root.attachTo;
let off = root.attachSideY === Menu.Top ? 0 : item.height;
if (root.thisSideY === Menu.Bottom)
off -= height;
return item.mapToItem(root.parent, 0, off).y + root.marginY;
}
radius: Tokens.rounding.normal
level: 2
implicitWidth: Math.max(200, column.implicitWidth)
implicitHeight: root.expanded ? column.implicitHeight : 0
opacity: root.expanded ? 1 : 0
implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2)
implicitHeight: column.implicitHeight + column.anchors.margins * 2
StyledClippingRect {
transform: Scale {
yScale: root.expanded ? 1 : 0.1
origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0
Behavior on yScale {
Anim {
type: Anim.DefaultSpatial
}
}
}
StyledRect {
anchors.fill: parent
radius: parent.radius
color: Colours.palette.m3surfaceContainer
color: Colours.palette.m3surfaceContainerLow
ColumnLayout {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.fill: parent
anchors.margins: Tokens.padding.small
spacing: 0
Repeater {
id: repeater
model: root.items
StyledRect {
@ -49,10 +124,25 @@ Elevation {
implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2
implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2
color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0)
radius: active ? 12 : Tokens.rounding.extraSmall // This should use a token, but tokens are currently extremely scuffed
topLeftRadius: index === 0 ? Tokens.rounding.small : radius
topRightRadius: index === 0 ? Tokens.rounding.small : radius
bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius
bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius
color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0)
Behavior on radius {
Anim {}
}
StateLayer {
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
topLeftRadius: parent.topLeftRadius
topRightRadius: parent.topRightRadius
bottomLeftRadius: parent.bottomLeftRadius
bottomRightRadius: parent.bottomRightRadius
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface
disabled: !root.expanded
onClicked: {
root.itemSelected(item.modelData);
@ -72,14 +162,14 @@ Elevation {
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
text: item.modelData.icon
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
}
StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: item.modelData.text
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface
}
Loader {
@ -90,7 +180,7 @@ Elevation {
sourceComponent: MaterialIcon {
text: item.modelData.trailingIcon
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
}
}
}
@ -98,16 +188,5 @@ Elevation {
}
}
}
Behavior on opacity {
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
}
}
Behavior on implicitHeight {
Anim {
type: Anim.DefaultSpatial
}
}
}

View file

@ -138,24 +138,14 @@ Row {
Behavior on rad {
Anim {}
}
}
Menu {
id: menu
states: State {
when: root.menuOnTop
AnchorChanges {
target: menu
anchors.top: undefined
anchors.bottom: expandBtn.top
}
}
anchors.top: parent.bottom
anchors.right: parent.right
anchors.topMargin: Tokens.spacing.small
anchors.bottomMargin: Tokens.spacing.small
}
attachTo: expandBtn
attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom
thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top
marginY: Tokens.spacing.small * (root.menuOnTop ? -1 : 1)
}
}

View file

@ -17,6 +17,7 @@ StyledWindow {
id: root
readonly property alias bar: bar
readonly property alias interactionWrapper: interactions
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0
@ -229,6 +230,8 @@ StyledWindow {
}
Interactions {
id: interactions
screen: root.screen
popouts: panels.popouts
visibilities: visibilities

View file

@ -28,6 +28,10 @@ void AppearanceRounding::bindTokens(RoundingTokens* tokens) {
connectTokenSignals(tokens, this);
}
int AppearanceRounding::extraSmall() const {
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
}
int AppearanceRounding::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}

View file

@ -19,6 +19,7 @@ class AppearanceRounding : public ConfigObject {
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
@ -30,6 +31,7 @@ public:
void bindTokens(RoundingTokens* tokens);
[[nodiscard]] int extraSmall() const;
[[nodiscard]] int small() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int large() const;

View file

@ -1,5 +1,4 @@
#include "tokens.hpp"
#include "config.hpp"
#include "monitorconfigmanager.hpp"
#include <qqmlengine.h>

View file

@ -39,6 +39,7 @@ class RoundingTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, small, 12)
CONFIG_PROPERTY(int, normal, 17)
CONFIG_PROPERTY(int, large, 25)