added quickshell
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Volume / mic / brightness heads-up, bottom centre. Appears on change and
|
||||
// fades out on its own.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
readonly property bool primaryScreen: Quickshell.screens.length === 0
|
||||
|| Quickshell.screens[0] === modelData
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "takemi-osd"
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors { bottom: true }
|
||||
margins.bottom: 90
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: 78
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
visible: primaryScreen && (shown || fade.running)
|
||||
|
||||
property bool shown: false
|
||||
property string kind: "volume"
|
||||
|
||||
// Suppress the burst of change signals that fires while services warm up.
|
||||
property bool armed: false
|
||||
Timer {
|
||||
interval: 1500
|
||||
running: true
|
||||
onTriggered: root.armed = true
|
||||
}
|
||||
|
||||
readonly property string glyph: {
|
||||
switch (root.kind) {
|
||||
case "mic": return Audio.micIcon;
|
||||
case "brightness": return Brightness.icon;
|
||||
default: return Audio.icon;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string title: {
|
||||
switch (root.kind) {
|
||||
case "mic": return Audio.micMuted ? "Mic muted" : "Microphone";
|
||||
case "brightness": return "Brightness";
|
||||
default: return Audio.muted ? "Muted" : "Volume";
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real level: {
|
||||
switch (root.kind) {
|
||||
case "mic": return Audio.micMuted ? 0 : Audio.micVolume;
|
||||
case "brightness": return Brightness.percent / 100;
|
||||
default: return Audio.muted ? 0 : Audio.volume;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property color tone: {
|
||||
switch (root.kind) {
|
||||
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue;
|
||||
case "brightness": return Theme.warn;
|
||||
default: return Audio.muted ? Theme.accent : Theme.primary;
|
||||
}
|
||||
}
|
||||
|
||||
function flash(k: string) {
|
||||
if (!root.armed) return;
|
||||
root.kind = k;
|
||||
root.shown = true;
|
||||
glyphIcon.bump();
|
||||
hold.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hold
|
||||
interval: 1600
|
||||
onTriggered: root.shown = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
function onVolumeBumped() { root.flash("volume"); }
|
||||
function onMicBumped() { root.flash("mic"); }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Brightness
|
||||
function onBumped() { root.flash("brightness"); }
|
||||
}
|
||||
|
||||
P5Panel {
|
||||
id: panel
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
fill: Theme.mantle
|
||||
fillOpacity: 0.97
|
||||
border: Theme.alpha(root.tone, 0.85)
|
||||
borderWidth: Theme.stroke
|
||||
slash: true
|
||||
slashColor: root.tone
|
||||
slashWidth: 5
|
||||
halftone: true
|
||||
halftoneColor: root.tone
|
||||
halftoneOpacity: 0.08
|
||||
sheen: true
|
||||
lean: 0.09
|
||||
chop: 14
|
||||
padding: Theme.padM
|
||||
|
||||
opacity: root.shown ? 1 : 0
|
||||
scale: root.shown ? 1 : 0.9
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { id: fade; duration: Theme.durBase }
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
Icon {
|
||||
id: glyphIcon
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.glyph
|
||||
color: root.tone
|
||||
font.pixelSize: 30
|
||||
width: 36
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: glyphIcon.right
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: root.title.toUpperCase()
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 90
|
||||
height: 1
|
||||
}
|
||||
|
||||
SplitText {
|
||||
text: Math.round(root.level * 100) + "%"
|
||||
pixelSize: Theme.fsBody
|
||||
split: 1
|
||||
splitOpacity: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
MeterBar {
|
||||
width: parent.width
|
||||
height: 10
|
||||
segments: 20
|
||||
segmentWidth: (width - 19 * 3) / 20
|
||||
spacing: 3
|
||||
value: root.level * 100
|
||||
activeColor: root.tone
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Full-screen power menu. Crimson stripes sweep in behind a stack of leaning
|
||||
// buttons; Escape or a click on the backdrop dismisses it.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
visible: Actions.powerMenuOpen || closeAnim.running
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "takemi-power"
|
||||
WlrLayershell.keyboardFocus: Actions.powerMenuOpen
|
||||
? WlrKeyboardFocus.Exclusive
|
||||
: WlrKeyboardFocus.None
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
readonly property bool open: Actions.powerMenuOpen
|
||||
|
||||
onOpenChanged: {
|
||||
if (open) {
|
||||
openAnim.restart();
|
||||
} else {
|
||||
closeAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- backdrop
|
||||
Rectangle {
|
||||
id: backdrop
|
||||
anchors.fill: parent
|
||||
color: Theme.alpha(Theme.base, 0.82)
|
||||
opacity: 0
|
||||
|
||||
TapHandler {
|
||||
onTapped: Actions.closePowerMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Diagonal stripe field sweeping in from the left.
|
||||
Image {
|
||||
id: stripes
|
||||
anchors.fill: parent
|
||||
source: Theme.texStripes
|
||||
fillMode: Image.Tile
|
||||
opacity: 0
|
||||
smooth: false
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: TintEffect {
|
||||
tintColor: Theme.accent
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
id: stripeShift
|
||||
x: -root.width
|
||||
}
|
||||
}
|
||||
|
||||
// A crimson wedge behind the buttons.
|
||||
Shape {
|
||||
id: wedge
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.alpha(Theme.accentDim, 0.35)
|
||||
strokeColor: "transparent"
|
||||
PathPolyline {
|
||||
path: [
|
||||
Qt.point(root.width * 0.18, 0),
|
||||
Qt.point(root.width * 0.52, 0),
|
||||
Qt.point(root.width * 0.34, root.height),
|
||||
Qt.point(0, root.height),
|
||||
Qt.point(root.width * 0.18, 0)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- title
|
||||
Column {
|
||||
id: title
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.width * 0.12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -60
|
||||
spacing: -14
|
||||
opacity: 0
|
||||
|
||||
SplitText {
|
||||
text: "TAKE"
|
||||
pixelSize: Theme.fsMega
|
||||
split: 4
|
||||
splitOpacity: 0.9
|
||||
}
|
||||
|
||||
SplitText {
|
||||
text: "YOUR"
|
||||
pixelSize: Theme.fsMega
|
||||
color: Theme.accent
|
||||
split: 4
|
||||
splitOpacity: 0.5
|
||||
}
|
||||
|
||||
SplitText {
|
||||
text: "TIME."
|
||||
pixelSize: Theme.fsMega
|
||||
split: 4
|
||||
splitOpacity: 0.9
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "— TAE TAKEMI"
|
||||
color: Theme.primary
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.letterSpacing: 5
|
||||
topPadding: 24
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- actions
|
||||
Column {
|
||||
id: actions
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.width * 0.12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.padM
|
||||
|
||||
Repeater {
|
||||
id: buttonRepeater
|
||||
|
||||
model: [
|
||||
{ label: "Lock", icon: "", danger: false, act: "lock" },
|
||||
{ label: "Suspend", icon: "", danger: false, act: "suspend" },
|
||||
{ label: "Hibernate", icon: "", danger: false, act: "hibernate" },
|
||||
{ label: "Log Out", icon: "", danger: true, act: "logout" },
|
||||
{ label: "Reboot", icon: "", danger: true, act: "reboot" },
|
||||
{ label: "Shut Down", icon: "", danger: true, act: "shutdown" }
|
||||
]
|
||||
|
||||
// A carrier item owns the staggered slide so the button keeps its
|
||||
// own hover transform.
|
||||
Item {
|
||||
id: entry
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: 280
|
||||
height: 52
|
||||
|
||||
opacity: 0
|
||||
transform: Translate { id: slide; x: 90 }
|
||||
|
||||
P5Button {
|
||||
anchors.fill: parent
|
||||
text: entry.modelData.label
|
||||
icon: entry.modelData.icon
|
||||
destructive: entry.modelData.danger
|
||||
accent: entry.modelData.danger ? Theme.accent : Theme.primary
|
||||
lunge: -10
|
||||
onClicked: root.invoke(entry.modelData.act)
|
||||
}
|
||||
|
||||
// Staggered entry, one button after another.
|
||||
Connections {
|
||||
target: root
|
||||
function onOpenChanged() {
|
||||
if (root.open) entryAnim.restart();
|
||||
else entry.opacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: entryAnim
|
||||
PauseAnimation { duration: 40 + entry.index * 55 }
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: entry; property: "opacity"
|
||||
to: 1; duration: Theme.durBase
|
||||
}
|
||||
NumberAnimation {
|
||||
target: slide; property: "x"
|
||||
from: 90; to: 0
|
||||
duration: Theme.durSlow
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hint line.
|
||||
Text {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 40
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "ESC TO CANCEL"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 4
|
||||
opacity: backdrop.opacity
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
function invoke(what: string) {
|
||||
Actions.closePowerMenu();
|
||||
switch (what) {
|
||||
case "lock": Actions.lock(); break;
|
||||
case "suspend": Actions.suspend(); break;
|
||||
case "hibernate": Actions.hibernate(); break;
|
||||
case "logout": Actions.logout(); break;
|
||||
case "reboot": Actions.reboot(); break;
|
||||
case "shutdown": Actions.shutdown(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Escape closes. The layer takes exclusive focus while open.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: root.open
|
||||
Keys.onEscapePressed: Actions.closePowerMenu()
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: openAnim
|
||||
NumberAnimation {
|
||||
target: backdrop; property: "opacity"
|
||||
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: stripes; property: "opacity"
|
||||
to: 0.1; duration: Theme.durSlow
|
||||
}
|
||||
NumberAnimation {
|
||||
target: stripeShift; property: "x"
|
||||
from: -root.width; to: 0
|
||||
duration: Theme.durLazy; easing.type: Easing.OutExpo
|
||||
}
|
||||
NumberAnimation {
|
||||
target: wedge; property: "opacity"
|
||||
to: 1; duration: Theme.durSlow; easing.type: Easing.OutExpo
|
||||
}
|
||||
NumberAnimation {
|
||||
target: title; property: "opacity"
|
||||
to: 1; duration: Theme.durSlow
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: closeAnim
|
||||
NumberAnimation {
|
||||
target: backdrop; property: "opacity"
|
||||
to: 0; duration: Theme.durFast
|
||||
}
|
||||
NumberAnimation {
|
||||
target: stripes; property: "opacity"
|
||||
to: 0; duration: Theme.durFast
|
||||
}
|
||||
NumberAnimation {
|
||||
target: wedge; property: "opacity"
|
||||
to: 0; duration: Theme.durFast
|
||||
}
|
||||
NumberAnimation {
|
||||
target: title; property: "opacity"
|
||||
to: 0; duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Notifications
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Floating notification stack under the bar, top right. Replaces dunst.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
// Only the primary bar screen shows toasts, to avoid duplicates.
|
||||
readonly property bool primaryScreen: Quickshell.screens.length === 0
|
||||
|| Quickshell.screens[0] === modelData
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "takemi-toasts"
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors { top: true; right: true }
|
||||
margins {
|
||||
top: Theme.barHeight + Theme.barMargin * 2
|
||||
right: Theme.barMargin
|
||||
}
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: Math.max(1, stack.implicitHeight)
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
visible: primaryScreen && stack.children.length > 0
|
||||
|
||||
// Toasts currently on screen, newest first.
|
||||
property list<Notification> live: []
|
||||
property int maxLive: 5
|
||||
|
||||
Connections {
|
||||
target: Notifs
|
||||
enabled: root.primaryScreen
|
||||
|
||||
function onPopupRequested(notif) {
|
||||
root.live = [notif, ...root.live].slice(0, root.maxLive);
|
||||
}
|
||||
}
|
||||
|
||||
function retire(notif) {
|
||||
const idx = root.live.indexOf(notif);
|
||||
if (idx === -1) return;
|
||||
const next = root.live.slice();
|
||||
next.splice(idx, 1);
|
||||
root.live = next;
|
||||
}
|
||||
|
||||
Column {
|
||||
id: stack
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
width: parent.width
|
||||
spacing: Theme.padS
|
||||
|
||||
Repeater {
|
||||
model: root.live
|
||||
|
||||
Item {
|
||||
id: toast
|
||||
required property var modelData
|
||||
|
||||
width: stack.width
|
||||
height: card.implicitHeight
|
||||
|
||||
NotifCard {
|
||||
id: card
|
||||
width: parent.width
|
||||
notif: toast.modelData
|
||||
onDismissed: {
|
||||
Notifs.dismiss(toast.modelData);
|
||||
root.retire(toast.modelData);
|
||||
}
|
||||
}
|
||||
|
||||
// Countdown rule that drains along the bottom edge.
|
||||
Rectangle {
|
||||
id: fuse
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
height: 2
|
||||
width: parent.width
|
||||
color: Theme.alpha(card.tone, 0.7)
|
||||
visible: life.running
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: life
|
||||
target: fuse
|
||||
property: "width"
|
||||
from: toast.width
|
||||
to: 0
|
||||
running: false
|
||||
duration: Notifs.timeoutFor(toast.modelData)
|
||||
easing.type: Easing.Linear
|
||||
onFinished: root.retire(toast.modelData)
|
||||
}
|
||||
|
||||
// Slide in from the right, Persona-style.
|
||||
opacity: 0
|
||||
transform: Translate { id: shove; x: 60 }
|
||||
|
||||
Component.onCompleted: {
|
||||
enter.start();
|
||||
const ms = Notifs.timeoutFor(toast.modelData);
|
||||
if (ms > 0) life.start();
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: enter
|
||||
NumberAnimation {
|
||||
target: toast; property: "opacity"
|
||||
to: 1; duration: Theme.durBase
|
||||
}
|
||||
NumberAnimation {
|
||||
target: shove; property: "x"
|
||||
from: 60; to: 0
|
||||
duration: Theme.durSlow
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
// Hovering a toast pauses its fuse.
|
||||
HoverHandler {
|
||||
id: toastHover
|
||||
onHoveredChanged: {
|
||||
if (!life.duration) return;
|
||||
if (hovered) life.pause();
|
||||
else if (life.paused) life.resume();
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.retire(toast.modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user