feat: m3 expressive menus
Also fix the player selector being click through Fixes #1405 Closes #1350
This commit is contained in:
parent
2bb28197a0
commit
ec8ce65857
7 changed files with 167 additions and 89 deletions
|
|
@ -1,96 +1,187 @@
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import "../effects"
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import qs.components
|
import qs.components
|
||||||
|
import qs.components.effects
|
||||||
import qs.services
|
import qs.services
|
||||||
|
import qs.modules.drawers
|
||||||
|
|
||||||
Elevation {
|
MouseArea {
|
||||||
id: root
|
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 list<MenuItem> items
|
||||||
property MenuItem active: items[0] ?? null
|
property MenuItem active: items[0] ?? null
|
||||||
property bool expanded
|
property bool expanded
|
||||||
|
|
||||||
signal itemSelected(item: MenuItem)
|
signal itemSelected(item: MenuItem)
|
||||||
|
|
||||||
radius: Tokens.rounding.small / 2
|
parent: {
|
||||||
level: 2
|
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
|
||||||
|
|
||||||
implicitWidth: Math.max(200, column.implicitWidth)
|
enabled: expanded
|
||||||
implicitHeight: root.expanded ? column.implicitHeight : 0
|
onClicked: expanded = false
|
||||||
opacity: root.expanded ? 1 : 0
|
|
||||||
|
|
||||||
StyledClippingRect {
|
opacity: expanded ? 1 : 0
|
||||||
anchors.fill: parent
|
layer.enabled: opacity < 1
|
||||||
radius: parent.radius
|
|
||||||
color: Colours.palette.m3surfaceContainer
|
|
||||||
|
|
||||||
ColumnLayout {
|
Behavior on opacity {
|
||||||
id: column
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.small
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors.left: parent.left
|
TransformWatcher {
|
||||||
anchors.right: parent.right
|
id: watcher
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
a: root.parent
|
||||||
model: root.items
|
b: root.attachTo
|
||||||
|
}
|
||||||
|
|
||||||
StyledRect {
|
Elevation {
|
||||||
id: item
|
id: menu
|
||||||
|
|
||||||
required property int index
|
x: {
|
||||||
required property MenuItem modelData
|
watcher.transform; // mapToItem is not reactive so this forces updates
|
||||||
readonly property bool active: modelData === root.active
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
radius: Tokens.rounding.normal
|
||||||
implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2
|
level: 2
|
||||||
implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2
|
|
||||||
|
|
||||||
color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0)
|
implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2)
|
||||||
|
implicitHeight: column.implicitHeight + column.anchors.margins * 2
|
||||||
|
|
||||||
StateLayer {
|
transform: Scale {
|
||||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
yScale: root.expanded ? 1 : 0.1
|
||||||
disabled: !root.expanded
|
origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0
|
||||||
onClicked: {
|
|
||||||
root.itemSelected(item.modelData);
|
|
||||||
root.active = item.modelData;
|
|
||||||
item.modelData.clicked();
|
|
||||||
root.expanded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
Behavior on yScale {
|
||||||
id: menuOptionRow
|
Anim {
|
||||||
|
type: Anim.DefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
StyledRect {
|
||||||
anchors.margins: Tokens.padding.normal
|
anchors.fill: parent
|
||||||
spacing: Tokens.spacing.small
|
radius: parent.radius
|
||||||
|
color: Colours.palette.m3surfaceContainerLow
|
||||||
|
|
||||||
MaterialIcon {
|
ColumnLayout {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
id: column
|
||||||
text: item.modelData.icon
|
|
||||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant
|
anchors.fill: parent
|
||||||
|
anchors.margins: Tokens.padding.small
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: repeater
|
||||||
|
|
||||||
|
model: root.items
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: item
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property MenuItem modelData
|
||||||
|
readonly property bool active: modelData === root.active
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2
|
||||||
|
implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2
|
||||||
|
|
||||||
|
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 {}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StateLayer {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
topLeftRadius: parent.topLeftRadius
|
||||||
Layout.fillWidth: true
|
topRightRadius: parent.topRightRadius
|
||||||
text: item.modelData.text
|
bottomLeftRadius: parent.bottomLeftRadius
|
||||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
bottomRightRadius: parent.bottomRightRadius
|
||||||
|
|
||||||
|
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface
|
||||||
|
disabled: !root.expanded
|
||||||
|
onClicked: {
|
||||||
|
root.itemSelected(item.modelData);
|
||||||
|
root.active = item.modelData;
|
||||||
|
item.modelData.clicked();
|
||||||
|
root.expanded = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
RowLayout {
|
||||||
asynchronous: true
|
id: menuOptionRow
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
active: item.modelData.trailingIcon.length > 0
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: MaterialIcon {
|
anchors.fill: parent
|
||||||
text: item.modelData.trailingIcon
|
anchors.margins: Tokens.padding.normal
|
||||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
text: item.modelData.icon
|
||||||
|
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.m3onTertiaryContainer : Colours.palette.m3onSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
asynchronous: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
active: item.modelData.trailingIcon.length > 0
|
||||||
|
visible: active
|
||||||
|
|
||||||
|
sourceComponent: MaterialIcon {
|
||||||
|
text: item.modelData.trailingIcon
|
||||||
|
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,16 +189,4 @@ Elevation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,24 +138,14 @@ Row {
|
||||||
Behavior on rad {
|
Behavior on rad {
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: menu
|
id: menu
|
||||||
|
|
||||||
states: State {
|
attachTo: expandBtn
|
||||||
when: root.menuOnTop
|
attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom
|
||||||
|
thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top
|
||||||
AnchorChanges {
|
marginY: Tokens.spacing.small * (root.menuOnTop ? -1 : 1)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ StyledWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property alias bar: bar
|
readonly property alias bar: bar
|
||||||
|
readonly property alias interactionWrapper: interactions
|
||||||
|
|
||||||
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
|
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
|
||||||
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0
|
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0
|
||||||
|
|
@ -229,6 +230,8 @@ StyledWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
Interactions {
|
Interactions {
|
||||||
|
id: interactions
|
||||||
|
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
popouts: panels.popouts
|
popouts: panels.popouts
|
||||||
visibilities: visibilities
|
visibilities: visibilities
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ void AppearanceRounding::bindTokens(RoundingTokens* tokens) {
|
||||||
connectTokenSignals(tokens, this);
|
connectTokenSignals(tokens, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AppearanceRounding::extraSmall() const {
|
||||||
|
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
int AppearanceRounding::small() const {
|
int AppearanceRounding::small() const {
|
||||||
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
|
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class AppearanceRounding : public ConfigObject {
|
||||||
|
|
||||||
CONFIG_PROPERTY(qreal, scale, 1)
|
CONFIG_PROPERTY(qreal, scale, 1)
|
||||||
|
|
||||||
|
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
|
||||||
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
|
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
|
||||||
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
|
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
|
||||||
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
|
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
|
||||||
|
|
@ -30,6 +31,7 @@ public:
|
||||||
|
|
||||||
void bindTokens(RoundingTokens* tokens);
|
void bindTokens(RoundingTokens* tokens);
|
||||||
|
|
||||||
|
[[nodiscard]] int extraSmall() const;
|
||||||
[[nodiscard]] int small() const;
|
[[nodiscard]] int small() const;
|
||||||
[[nodiscard]] int normal() const;
|
[[nodiscard]] int normal() const;
|
||||||
[[nodiscard]] int large() const;
|
[[nodiscard]] int large() const;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
#include "tokens.hpp"
|
#include "tokens.hpp"
|
||||||
#include "config.hpp"
|
|
||||||
#include "monitorconfigmanager.hpp"
|
#include "monitorconfigmanager.hpp"
|
||||||
|
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class RoundingTokens : public ConfigObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ANONYMOUS
|
QML_ANONYMOUS
|
||||||
|
|
||||||
|
CONFIG_PROPERTY(int, extraSmall, 4)
|
||||||
CONFIG_PROPERTY(int, small, 12)
|
CONFIG_PROPERTY(int, small, 12)
|
||||||
CONFIG_PROPERTY(int, normal, 17)
|
CONFIG_PROPERTY(int, normal, 17)
|
||||||
CONFIG_PROPERTY(int, large, 25)
|
CONFIG_PROPERTY(int, large, 25)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue