This commit is contained in:
pika 2025-06-10 15:34:15 +02:00
parent 32edcff102
commit 0296117901
110 changed files with 9713 additions and 5 deletions

37
modules/Shortcuts.qml Normal file
View file

@ -0,0 +1,37 @@
import "root:/widgets"
import "root:/services"
import Quickshell
Scope {
id: root
property bool launcherInterrupted
CustomShortcut {
name: "session"
description: "Toggle session menu"
onPressed: {
const visibilities = Visibilities.getForActive();
visibilities.session = !visibilities.session;
}
}
CustomShortcut {
name: "launcher"
description: "Toggle launcher"
onPressed: root.launcherInterrupted = false
onReleased: {
if (!root.launcherInterrupted) {
const visibilities = Visibilities.getForActive();
visibilities.launcher = !visibilities.launcher;
}
root.launcherInterrupted = false;
}
}
CustomShortcut {
name: "launcherInterrupt"
description: "Interrupt launcher keybind"
onPressed: root.launcherInterrupted = true
}
}

View file

@ -0,0 +1,26 @@
import "root:/widgets"
import Quickshell
import Quickshell.Wayland
Variants {
model: Quickshell.screens
StyledWindow {
id: win
required property ShellScreen modelData
screen: modelData
name: "background"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Background
color: "black"
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
Wallpaper {}
}
}

View file

@ -0,0 +1,77 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Item {
id: root
property url source: Wallpapers.current ? `file://${Wallpapers.current}` : ""
property Image current: one
anchors.fill: parent
onSourceChanged: {
if (current === one)
two.update();
else
one.update();
}
Img {
id: one
}
Img {
id: two
}
component Img: CachingImage {
id: img
function update(): void {
const srcPath = `${root.source}`.slice(7);
if (thumbnail.originalPath === srcPath) {
root.current = this;
} else
path = srcPath;
}
anchors.fill: parent
loadOriginal: true
asynchronous: true
cache: false
fillMode: Image.PreserveAspectCrop
opacity: 0
scale: Wallpapers.showPreview ? 1 : 0.8
onStatusChanged: {
if (status === Image.Ready)
root.current = this;
}
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
img.scale: 1
}
}
transitions: Transition {
NumberAnimation {
target: img
properties: "opacity,scale"
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

174
modules/bar/Bar.qml Normal file
View file

@ -0,0 +1,174 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/modules/bar/popouts" as BarPopouts
import "components"
import "components/workspaces"
import Quickshell
import QtQuick
Item {
id: root
required property ShellScreen screen
required property BarPopouts.Wrapper popouts
function checkPopout(y: real): void {
const spacing = Appearance.spacing.small;
const aw = activeWindow.child;
const awy = activeWindow.y + aw.y;
const ty = tray.y;
const th = tray.implicitHeight;
const trayItems = tray.items;
const n = statusIconsInner.network;
const ny = statusIcons.y + statusIconsInner.y + n.y - spacing / 2;
const bls = statusIcons.y + statusIconsInner.y + statusIconsInner.bs - spacing / 2;
const ble = statusIcons.y + statusIconsInner.y + statusIconsInner.be + spacing / 2;
const b = statusIconsInner.battery;
const by = statusIcons.y + statusIconsInner.y + b.y - spacing / 2;
if (y >= awy && y <= awy + aw.implicitHeight) {
popouts.currentName = "activewindow";
popouts.currentCenter = Qt.binding(() => activeWindow.y + aw.y + aw.implicitHeight / 2);
popouts.hasCurrent = true;
} else if (y > ty && y < ty + th) {
const index = Math.floor(((y - ty) / th) * trayItems.count);
const item = trayItems.itemAt(index);
popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() => tray.y + item.y + item.implicitHeight / 2);
popouts.hasCurrent = true;
} else if (y >= ny && y <= ny + n.implicitHeight + spacing) {
popouts.currentName = "network";
popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + n.y + n.implicitHeight / 2);
popouts.hasCurrent = true;
} else if (y >= bls && y <= ble) {
popouts.currentName = "bluetooth";
popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + statusIconsInner.bs + (statusIconsInner.be - statusIconsInner.bs) / 2);
popouts.hasCurrent = true;
} else if (y >= by && y <= by + b.implicitHeight + spacing) {
popouts.currentName = "battery";
popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + b.y + b.implicitHeight / 2);
popouts.hasCurrent = true;
} else {
popouts.hasCurrent = false;
}
}
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
implicitWidth: child.implicitWidth + BorderConfig.thickness * 2
Item {
id: child
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Math.max(osIcon.implicitWidth, workspaces.implicitWidth, activeWindow.implicitWidth, tray.implicitWidth, clock.implicitWidth, statusIcons.implicitWidth, power.implicitWidth)
OsIcon {
id: osIcon
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Appearance.padding.large
}
StyledRect {
id: workspaces
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: osIcon.bottom
anchors.topMargin: Appearance.spacing.normal
radius: Appearance.rounding.full
color: Colours.palette.m3surfaceContainer
implicitWidth: workspacesInner.implicitWidth + Appearance.padding.small * 2
implicitHeight: workspacesInner.implicitHeight + Appearance.padding.small * 2
MouseArea {
anchors.fill: parent
anchors.leftMargin: -BorderConfig.thickness
anchors.rightMargin: -BorderConfig.thickness
onWheel: event => {
const activeWs = Hyprland.activeClient?.workspace?.name;
if (activeWs?.startsWith("special:"))
Hyprland.dispatch(`togglespecialworkspace ${activeWs.slice(8)}`);
else if (event.angleDelta.y < 0 || Hyprland.activeWsId > 1)
Hyprland.dispatch(`workspace r${event.angleDelta.y > 0 ? "-" : "+"}1`);
}
}
Workspaces {
id: workspacesInner
anchors.centerIn: parent
}
}
ActiveWindow {
id: activeWindow
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: workspaces.bottom
anchors.bottom: tray.top
anchors.margins: Appearance.spacing.large
monitor: Brightness.getMonitorForScreen(root.screen)
}
Tray {
id: tray
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: clock.top
anchors.bottomMargin: Appearance.spacing.larger
}
Clock {
id: clock
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: statusIcons.top
anchors.bottomMargin: Appearance.spacing.normal
}
StyledRect {
id: statusIcons
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: power.top
anchors.bottomMargin: Appearance.spacing.normal
radius: Appearance.rounding.full
color: Colours.palette.m3surfaceContainer
implicitHeight: statusIconsInner.implicitHeight + Appearance.padding.normal * 2
StatusIcons {
id: statusIconsInner
anchors.centerIn: parent
}
}
Power {
id: power
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Appearance.padding.large
}
}
}

View file

@ -0,0 +1,140 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import QtQuick
Item {
id: root
required property Brightness.Monitor monitor
property color colour: Colours.palette.m3primary
readonly property Item child: child
implicitWidth: child.implicitWidth
implicitHeight: child.implicitHeight
MouseArea {
anchors.top: parent.top
anchors.bottom: child.top
anchors.left: parent.left
anchors.right: parent.right
onWheel: event => {
if (event.angleDelta.y > 0)
Audio.setVolume(Audio.volume + 0.1);
else if (event.angleDelta.y < 0)
Audio.setVolume(Audio.volume - 0.1);
}
}
MouseArea {
anchors.top: child.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
onWheel: event => {
const monitor = root.monitor;
if (event.angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + 0.1);
else if (event.angleDelta.y < 0)
monitor.setBrightness(monitor.brightness - 0.1);
}
}
Item {
id: child
property Item current: text1
anchors.centerIn: parent
clip: true
implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight)
implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin
MaterialIcon {
id: icon
animate: true
text: Icons.getAppCategoryIcon(Hyprland.activeClient?.wmClass, "desktop_windows")
color: root.colour
anchors.horizontalCenter: parent.horizontalCenter
}
Title {
id: text1
}
Title {
id: text2
}
TextMetrics {
id: metrics
text: Hyprland.activeClient?.title ?? qsTr("Desktop")
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
elide: Qt.ElideRight
elideWidth: root.height - icon.height
onTextChanged: {
const next = child.current === text1 ? text2 : text1;
next.text = elidedText;
child.current = next;
}
onElideWidthChanged: child.current.text = elidedText
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
component Title: StyledText {
id: text
anchors.horizontalCenter: icon.horizontalCenter
anchors.top: icon.bottom
anchors.topMargin: Appearance.spacing.small
font.pointSize: metrics.font.pointSize
font.family: metrics.font.family
color: root.colour
opacity: child.current === this ? 1 : 0
transform: Rotation {
angle: 90
origin.x: text.implicitHeight / 2
origin.y: text.implicitHeight / 2
}
width: implicitHeight
height: implicitWidth
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

View file

@ -0,0 +1,33 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Column {
id: root
property color colour: Colours.palette.m3tertiary
spacing: Appearance.spacing.small
MaterialIcon {
id: icon
text: "calendar_month"
color: root.colour
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
id: text
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: StyledText.AlignHCenter
text: Time.format("hh\nmm")
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
color: root.colour
}
}

View file

@ -0,0 +1,11 @@
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
StyledText {
text: Icons.osIcon
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
color: Colours.palette.m3tertiary
}

View file

@ -0,0 +1,27 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
MaterialIcon {
text: "power_settings_new"
color: Colours.palette.m3error
font.bold: true
font.pointSize: Appearance.font.size.normal
StateLayer {
anchors.fill: undefined
anchors.centerIn: parent
anchors.horizontalCenterOffset: 1
implicitWidth: parent.implicitHeight + Appearance.padding.small * 2
implicitHeight: implicitWidth
radius: Appearance.rounding.full
function onClicked(): void {
const v = Visibilities.screens[QsWindow.window.screen];
v.session = !v.session;
}
}
}

View file

@ -0,0 +1,114 @@
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import Quickshell
import Quickshell.Services.UPower
import QtQuick
Item {
id: root
property color colour: Colours.palette.m3secondary
readonly property Item network: network
readonly property real bs: bluetooth.y
readonly property real be: repeater.count > 0 ? devices.y + devices.implicitHeight : bluetooth.y + bluetooth.implicitHeight
readonly property Item battery: battery
clip: true
implicitWidth: Math.max(network.implicitWidth, bluetooth.implicitWidth, devices.implicitWidth, battery.implicitWidth)
implicitHeight: network.implicitHeight + bluetooth.implicitHeight + bluetooth.anchors.topMargin + (repeater.count > 0 ? devices.implicitHeight + devices.anchors.topMargin : 0) + battery.implicitHeight + battery.anchors.topMargin
MaterialIcon {
id: network
animate: true
text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off"
color: root.colour
anchors.horizontalCenter: parent.horizontalCenter
}
MaterialIcon {
id: bluetooth
anchors.horizontalCenter: network.horizontalCenter
anchors.top: network.bottom
anchors.topMargin: Appearance.spacing.small
animate: true
text: Bluetooth.powered ? "bluetooth" : "bluetooth_disabled"
color: root.colour
}
Column {
id: devices
anchors.horizontalCenter: bluetooth.horizontalCenter
anchors.top: bluetooth.bottom
anchors.topMargin: Appearance.spacing.small
Repeater {
id: repeater
model: ScriptModel {
values: Bluetooth.devices.filter(d => d.connected)
}
MaterialIcon {
required property Bluetooth.Device modelData
animate: true
text: Icons.getBluetoothIcon(modelData.icon)
color: root.colour
}
}
}
MaterialIcon {
id: battery
anchors.horizontalCenter: devices.horizontalCenter
anchors.top: repeater.count > 0 ? devices.bottom : bluetooth.bottom
anchors.topMargin: Appearance.spacing.small
animate: true
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
const perc = UPower.displayDevice.percentage;
const charging = !UPower.onBattery;
if (perc === 1)
return charging ? "battery_charging_full" : "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error
fill: 1
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}

View file

@ -0,0 +1,56 @@
import "root:/config"
import Quickshell.Services.SystemTray
import QtQuick
Item {
id: root
readonly property Repeater items: items
clip: true
visible: width > 0 && height > 0 // To avoid warnings about being visible with no size
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
Column {
id: layout
spacing: Appearance.spacing.small
add: Transition {
NumberAnimation {
properties: "scale"
from: 0
to: 1
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
Repeater {
id: items
model: SystemTray.items
TrayItem {}
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}

View file

@ -0,0 +1,48 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import QtQuick
MouseArea {
id: root
required property SystemTrayItem modelData
acceptedButtons: Qt.LeftButton | Qt.RightButton
implicitWidth: Appearance.font.size.small * 2
implicitHeight: Appearance.font.size.small * 2
onClicked: event => {
if (event.button === Qt.LeftButton)
modelData.activate();
else if (modelData.hasMenu)
menu.open();
}
// TODO custom menu
QsMenuAnchor {
id: menu
menu: root.modelData.menu
anchor.window: this.QsWindow.window
}
IconImage {
id: icon
source: {
let icon = root.modelData.icon;
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
icon = `file://${path}/${name.slice(name.lastIndexOf("/") + 1)}`;
}
return icon;
}
asynchronous: true
anchors.fill: parent
}
}

View file

@ -0,0 +1,111 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Effects
StyledRect {
id: root
required property list<Workspace> workspaces
required property Item mask
required property real maskWidth
required property real maskHeight
required property int groupOffset
readonly property int currentWsIdx: Hyprland.activeWsId - 1 - groupOffset
property real leading: getWsY(currentWsIdx)
property real trailing: getWsY(currentWsIdx)
property real currentSize: workspaces[currentWsIdx]?.size ?? 0
property real offset: Math.min(leading, trailing)
property real size: {
const s = Math.abs(leading - trailing) + currentSize;
if (BarConfig.workspaces.activeTrail && lastWs > currentWsIdx)
return Math.min(getWsY(lastWs) + (workspaces[lastWs]?.size ?? 0) - offset, s);
return s;
}
property int cWs
property int lastWs
function getWsY(idx: int): real {
let y = 0;
for (let i = 0; i < idx; i++)
y += workspaces[i]?.size ?? 0;
return y;
}
onCurrentWsIdxChanged: {
lastWs = cWs;
cWs = currentWsIdx;
}
clip: true
x: 1
y: offset + 1
implicitWidth: BarConfig.sizes.innerHeight - 2
implicitHeight: size - 2
radius: BarConfig.workspaces.rounded ? Appearance.rounding.full : 0
color: Colours.palette.m3primary
StyledRect {
id: base
visible: false
anchors.fill: parent
color: Colours.palette.m3onPrimary
}
MultiEffect {
source: base
maskSource: root.mask
maskEnabled: true
maskSpreadAtMin: 1
maskThresholdMin: 0.5
x: 0
y: -parent.offset
implicitWidth: root.maskWidth
implicitHeight: root.maskHeight
anchors.horizontalCenter: parent.horizontalCenter
}
Behavior on leading {
enabled: BarConfig.workspaces.activeTrail
Anim {}
}
Behavior on trailing {
enabled: BarConfig.workspaces.activeTrail
Anim {
duration: Appearance.anim.durations.normal * 2
}
}
Behavior on currentSize {
enabled: BarConfig.workspaces.activeTrail
Anim {}
}
Behavior on offset {
enabled: !BarConfig.workspaces.activeTrail
Anim {}
}
Behavior on size {
enabled: !BarConfig.workspaces.activeTrail
Anim {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}

View file

@ -0,0 +1,99 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Item {
id: root
required property list<Workspace> workspaces
required property var occupied
required property int groupOffset
property list<var> pills: []
onOccupiedChanged: {
let count = 0;
const start = groupOffset;
const end = start + BarConfig.workspaces.shown;
for (const [ws, occ] of Object.entries(occupied)) {
if (ws > start && ws <= end && occ) {
if (!occupied[ws - 1]) {
if (pills[count])
pills[count].start = ws;
else
pills.push(pillComp.createObject(root, {
start: ws
}));
count++;
}
if (!occupied[ws + 1])
pills[count - 1].end = ws;
}
}
if (pills.length > count)
pills.splice(count, pills.length - count).forEach(p => p.destroy());
}
Repeater {
model: ScriptModel {
values: root.pills.filter(p => p)
}
StyledRect {
id: rect
required property var modelData
readonly property Workspace start: root.workspaces[modelData.start - 1 - root.groupOffset] ?? null
readonly property Workspace end: root.workspaces[modelData.end - 1 - root.groupOffset] ?? null
color: Colours.alpha(Colours.palette.m3surfaceContainerHigh, true)
radius: BarConfig.workspaces.rounded ? Appearance.rounding.full : 0
x: start?.x ?? 0
y: start?.y ?? 0
implicitWidth: BarConfig.sizes.innerHeight
implicitHeight: end?.y + end?.height - start?.y
anchors.horizontalCenter: parent.horizontalCenter
scale: 0
Component.onCompleted: scale = 1
Behavior on scale {
Anim {
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
Behavior on x {
Anim {}
}
Behavior on y {
Anim {}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
component Pill: QtObject {
property int start
property int end
}
Component {
id: pillComp
Pill {}
}
}

View file

@ -0,0 +1,93 @@
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property int index
required property var occupied
required property int groupOffset
readonly property bool isWorkspace: true // Flag for finding workspace children
// Unanimated prop for others to use as reference
readonly property real size: childrenRect.height + (hasWindows ? Appearance.padding.normal : 0)
readonly property int ws: groupOffset + index + 1
readonly property bool isOccupied: occupied[ws] ?? false
readonly property bool hasWindows: isOccupied && BarConfig.workspaces.showWindows
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: size
StyledText {
id: indicator
readonly property string label: BarConfig.workspaces.label || root.ws
readonly property string occupiedLabel: BarConfig.workspaces.occupiedLabel || label
readonly property string activeLabel: BarConfig.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label)
animate: true
text: Hyprland.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label
color: BarConfig.workspaces.occupiedBg || root.isOccupied || Hyprland.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.palette.m3outlineVariant
horizontalAlignment: StyledText.AlignHCenter
verticalAlignment: StyledText.AlignVCenter
width: BarConfig.sizes.innerHeight
height: BarConfig.sizes.innerHeight
}
Loader {
id: windows
active: BarConfig.workspaces.showWindows
asynchronous: true
anchors.horizontalCenter: indicator.horizontalCenter
anchors.top: indicator.bottom
sourceComponent: Column {
spacing: Appearance.spacing.small
add: Transition {
Anim {
properties: "scale"
from: 0
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
Repeater {
model: ScriptModel {
values: Hyprland.clients.filter(c => c.workspace?.id === root.ws)
}
MaterialIcon {
required property Hyprland.Client modelData
text: Icons.getAppCategoryIcon(modelData.wmClass, "terminal")
color: Colours.palette.m3onSurfaceVariant
}
}
}
}
Behavior on Layout.preferredWidth {
Anim {}
}
Behavior on Layout.preferredHeight {
Anim {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -0,0 +1,75 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Layouts
Item {
id: root
readonly property list<Workspace> workspaces: layout.children.filter(c => c.isWorkspace).sort((w1, w2) => w1.ws - w2.ws)
readonly property var occupied: Hyprland.workspaces.values.reduce((acc, curr) => {
acc[curr.id] = curr.lastIpcObject.windows > 0;
return acc;
}, {})
readonly property int groupOffset: Math.floor((Hyprland.activeWsId - 1) / BarConfig.workspaces.shown) * BarConfig.workspaces.shown
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
spacing: 0
layer.enabled: true
layer.smooth: true
Repeater {
model: BarConfig.workspaces.shown
Workspace {
occupied: root.occupied
groupOffset: root.groupOffset
}
}
}
Loader {
active: BarConfig.workspaces.occupiedBg
asynchronous: true
z: -1
anchors.fill: parent
sourceComponent: OccupiedBg {
workspaces: root.workspaces
occupied: root.occupied
groupOffset: root.groupOffset
}
}
Loader {
active: BarConfig.workspaces.activeIndicator
asynchronous: true
sourceComponent: ActiveIndicator {
workspaces: root.workspaces
mask: layout
maskWidth: root.width
maskHeight: root.height
groupOffset: root.groupOffset
}
}
MouseArea {
anchors.fill: parent
onPressed: event => {
const ws = layout.childAt(event.x, event.y).index + root.groupOffset + 1;
if (Hyprland.activeWsId !== ws)
Hyprland.dispatch(`workspace ${ws}`);
}
}
}

View file

@ -0,0 +1,75 @@
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import Quickshell.Widgets
import Quickshell.Wayland
import QtQuick
Item {
id: root
implicitWidth: Hyprland.activeClient ? child.implicitWidth : -Appearance.padding.large * 2
implicitHeight: child.implicitHeight
Column {
id: child
anchors.centerIn: parent
spacing: Appearance.spacing.normal
Row {
id: detailsRow
spacing: Appearance.spacing.normal
IconImage {
id: icon
implicitSize: details.implicitHeight
source: Icons.getAppIcon(Hyprland.activeClient?.wmClass ?? "", "image-missing")
}
Column {
id: details
StyledText {
text: Hyprland.activeClient?.title ?? ""
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
width: preview.implicitWidth - icon.implicitWidth - detailsRow.spacing
}
StyledText {
text: Hyprland.activeClient?.wmClass ?? ""
color: Colours.palette.m3onSurfaceVariant
elide: Text.ElideRight
width: preview.implicitWidth - icon.implicitWidth - detailsRow.spacing
}
}
}
ClippingWrapperRectangle {
color: "transparent"
radius: Appearance.rounding.small
ScreencopyView {
id: preview
captureSource: ToplevelManager.toplevels.values.find(t => t.title === Hyprland.activeClient?.title) ?? null
live: visible
constraintSize.width: BarConfig.sizes.windowPreviewSize
constraintSize.height: BarConfig.sizes.windowPreviewSize
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}

View file

@ -0,0 +1,74 @@
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
required property bool invertBottomRounding
readonly property real rounding: BorderConfig.rounding
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
property real ibr: invertBottomRounding ? -1 : 1
strokeWidth: -1
fillColor: BorderConfig.colour
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 2
relativeY: 0
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
relativeX: -root.roundingX * root.ibr
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise
}
PathLine {
relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr)
relativeY: 0
}
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on ibr {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

View file

@ -0,0 +1,235 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell.Services.UPower
import QtQuick
Column {
id: root
spacing: Appearance.spacing.normal
width: BarConfig.sizes.batteryWidth
StyledText {
text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected")
}
StyledText {
function formatSeconds(s: int, fallback: string): string {
const day = Math.floor(s / 86400);
const hr = Math.floor(s / 3600) % 60;
const min = Math.floor(s / 60) % 60;
let comps = [];
if (day > 0)
comps.push(`${day} days`);
if (hr > 0)
comps.push(`${hr} hours`);
if (min > 0)
comps.push(`${min} mins`);
return comps.join(", ") || fallback;
}
text: UPower.displayDevice.isLaptopBattery ? qsTr("Time %1: %2").arg(UPower.onBattery ? "remaining" : "until charged").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, "Calculating...") : formatSeconds(UPower.displayDevice.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(PowerProfile.toString(PowerProfiles.profile))
}
Loader {
anchors.horizontalCenter: parent.horizontalCenter
active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None
asynchronous: true
height: active ? (item?.implicitHeight ?? 0) : 0
sourceComponent: StyledRect {
implicitWidth: child.implicitWidth + Appearance.padding.normal * 2
implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2
color: Colours.palette.m3error
radius: Appearance.rounding.normal
Column {
id: child
anchors.centerIn: parent
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -font.pointSize / 10
text: "warning"
color: Colours.palette.m3onError
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Performance Degraded")
color: Colours.palette.m3onError
font.family: Appearance.font.family.mono
font.weight: 500
}
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -font.pointSize / 10
text: "warning"
color: Colours.palette.m3onError
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason))
color: Colours.palette.m3onError
}
}
}
}
StyledRect {
id: profiles
property string current: {
const p = PowerProfiles.profile;
if (p === PowerProfile.PowerSaver)
return saver.icon;
if (p === PowerProfile.Performance)
return perf.icon;
return balance.icon;
}
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2
implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.full
StyledRect {
id: indicator
color: Colours.palette.m3primary
radius: Appearance.rounding.full
state: profiles.current
states: [
State {
name: saver.icon
Fill {
item: saver
}
},
State {
name: balance.icon
Fill {
item: balance
}
},
State {
name: perf.icon
Fill {
item: perf
}
}
]
transitions: Transition {
AnchorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
Profile {
id: saver
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.small
profile: PowerProfile.PowerSaver
icon: "energy_savings_leaf"
}
Profile {
id: balance
anchors.centerIn: parent
profile: PowerProfile.Balanced
icon: "balance"
}
Profile {
id: perf
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.small
profile: PowerProfile.Performance
icon: "rocket_launch"
}
}
component Fill: AnchorChanges {
required property Item item
target: indicator
anchors.left: item.left
anchors.right: item.right
anchors.top: item.top
anchors.bottom: item.bottom
}
component Profile: Item {
required property string icon
required property int profile
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
StateLayer {
radius: Appearance.rounding.full
color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
function onClicked(): void {
PowerProfiles.profile = parent.profile;
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
text: parent.icon
font.pointSize: Appearance.font.size.large
color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
fill: profiles.current === text ? 1 : 0
Behavior on fill {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
}

View file

@ -0,0 +1,18 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Column {
id: root
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Bluetooth %1").arg(Bluetooth.powered ? "enabled" : "disabled")
}
StyledText {
text: Bluetooth.devices.some(d => d.connected) ? qsTr("Connected to: %1").arg(Bluetooth.devices.filter(d => d.connected).map(d => d.alias).join(", ")) : qsTr("No devices connected")
}
}

View file

@ -0,0 +1,175 @@
pragma ComponentBehavior: Bound
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
Item {
id: root
required property ShellScreen screen
property string currentName
property real currentCenter
property bool hasCurrent
anchors.centerIn: parent
implicitWidth: hasCurrent ? (content.children.find(c => c.shouldBeActive)?.implicitWidth ?? 0) + Appearance.padding.large * 2 : 0
implicitHeight: (content.children.find(c => c.shouldBeActive)?.implicitHeight ?? 0) + Appearance.padding.large * 2
Item {
id: content
anchors.fill: parent
anchors.margins: Appearance.padding.large
clip: true
Popout {
name: "activewindow"
source: "ActiveWindow.qml"
}
Popout {
name: "network"
source: "Network.qml"
}
Popout {
name: "bluetooth"
source: "Bluetooth.qml"
}
Popout {
name: "battery"
source: "Battery.qml"
}
Repeater {
model: ScriptModel {
values: [...SystemTray.items.values]
}
Popout {
id: trayMenu
required property SystemTrayItem modelData
required property int index
name: `traymenu${index}`
sourceComponent: trayMenuComp
Connections {
target: root
function onHasCurrentChanged(): void {
if (root.hasCurrent && trayMenu.shouldBeActive) {
trayMenu.sourceComponent = null;
trayMenu.sourceComponent = trayMenuComp;
}
}
}
Component {
id: trayMenuComp
TrayMenu {
popouts: root
trayItem: trayMenu.modelData.menu
}
}
}
}
}
Behavior on implicitWidth {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
enabled: root.implicitWidth > 0
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on currentCenter {
enabled: root.implicitWidth > 0
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
component Popout: Loader {
id: popout
required property string name
property bool shouldBeActive: root.currentName === name
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
opacity: 0
scale: 0.8
active: false
asynchronous: true
states: State {
name: "active"
when: popout.shouldBeActive
PropertyChanges {
popout.active: true
popout.opacity: 1
popout.scale: 1
}
}
transitions: [
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
properties: "opacity,scale"
duration: Appearance.anim.durations.small
}
PropertyAction {
target: popout
property: "active"
}
}
},
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
target: popout
property: "active"
}
Anim {
properties: "opacity,scale"
}
}
}
]
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -0,0 +1,22 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Column {
id: root
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Connected to: %1").arg(Network.active?.ssid ?? "None")
}
StyledText {
text: qsTr("Strength: %1/100").arg(Network.active?.strength ?? 0)
}
StyledText {
text: qsTr("Frequency: %1 MHz").arg(Network.active?.frequency ?? 0)
}
}

View file

@ -0,0 +1,237 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
StackView {
id: root
required property Item popouts
required property QsMenuHandle trayItem
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
initialItem: SubMenu {
handle: root.trayItem
}
pushEnter: Anim {}
pushExit: Anim {}
popEnter: Anim {}
popExit: Anim {}
component Anim: Transition {
NumberAnimation {
duration: 0
}
}
component SubMenu: Column {
id: menu
required property QsMenuHandle handle
property bool isSubMenu
property bool shown
padding: Appearance.padding.smaller
spacing: Appearance.spacing.small
opacity: shown ? 1 : 0
scale: shown ? 1 : 0.8
Component.onCompleted: shown = true
StackView.onActivating: shown = true
StackView.onDeactivating: shown = false
StackView.onRemoved: destroy()
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on scale {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
QsMenuOpener {
id: menuOpener
menu: menu.handle
}
Repeater {
model: menuOpener.children
StyledRect {
id: item
required property QsMenuEntry modelData
implicitWidth: BarConfig.sizes.trayMenuWidth
implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight
radius: Appearance.rounding.full
color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent"
Loader {
id: children
anchors.left: parent.left
anchors.right: parent.right
active: !item.modelData.isSeparator
asynchronous: true
sourceComponent: Item {
implicitHeight: label.implicitHeight
StateLayer {
anchors.margins: -Appearance.padding.small / 2
anchors.leftMargin: -Appearance.padding.smaller
anchors.rightMargin: -Appearance.padding.smaller
radius: item.radius
disabled: !item.modelData.enabled
function onClicked(): void {
const entry = item.modelData;
if (entry.hasChildren)
root.push(subMenuComp.createObject(null, {
handle: entry,
isSubMenu: true
}));
else {
item.modelData.triggered();
root.popouts.hasCurrent = false;
}
}
}
Loader {
id: icon
anchors.left: parent.left
active: item.modelData.icon !== ""
asynchronous: true
sourceComponent: IconImage {
implicitSize: label.implicitHeight
source: item.modelData.icon
}
}
StyledText {
id: label
anchors.left: icon.right
anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0
text: labelMetrics.elidedText
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
}
TextMetrics {
id: labelMetrics
text: item.modelData.text
font.pointSize: label.font.pointSize
font.family: label.font.family
elide: Text.ElideRight
elideWidth: BarConfig.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0)
}
Loader {
id: expand
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
active: item.modelData.hasChildren
asynchronous: true
sourceComponent: MaterialIcon {
text: "chevron_right"
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
}
}
}
}
}
}
Loader {
active: menu.isSubMenu
asynchronous: true
sourceComponent: Item {
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight + Appearance.spacing.small / 2
Item {
anchors.bottom: parent.bottom
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight
StyledRect {
anchors.fill: parent
anchors.margins: -Appearance.padding.small / 2
anchors.leftMargin: -Appearance.padding.smaller
anchors.rightMargin: -Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Colours.palette.m3secondaryContainer
StateLayer {
radius: parent.radius
color: Colours.palette.m3onSecondaryContainer
function onClicked(): void {
root.pop();
}
}
}
Row {
id: back
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
text: "chevron_left"
color: Colours.palette.m3onSecondaryContainer
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Back")
color: Colours.palette.m3onSecondaryContainer
}
}
}
}
}
}
Component {
id: subMenuComp
SubMenu {}
}
}

View file

@ -0,0 +1,25 @@
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Item {
id: root
required property ShellScreen screen
property alias currentName: content.currentName
property alias currentCenter: content.currentCenter
property alias hasCurrent: content.hasCurrent
visible: width > 0 && height > 0
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
Content {
id: content
screen: root.screen
}
}

View file

@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/services"
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: BorderConfig.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: BorderConfig.colour
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

View file

@ -0,0 +1,123 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
implicitWidth: nonAnimWidth
implicitHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
Tabs {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Appearance.padding.normal
anchors.margins: Appearance.padding.large
nonAnimWidth: root.nonAnimWidth
currentIndex: view.currentIndex
}
ClippingRectangle {
id: viewWrapper
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
radius: Appearance.rounding.normal
color: "transparent"
Flickable {
id: view
readonly property int currentIndex: tabs.currentIndex
readonly property Item currentItem: row.children[currentIndex]
anchors.fill: parent
flickableDirection: Flickable.HorizontalFlick
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
contentX: currentItem.x
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
onContentXChanged: {
if (!moving)
return;
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
tabs.bar.incrementCurrentIndex();
else if (x < -currentItem.implicitWidth / 2)
tabs.bar.decrementCurrentIndex();
}
onDragEnded: {
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 10)
tabs.bar.incrementCurrentIndex();
else if (x < -currentItem.implicitWidth / 10)
tabs.bar.decrementCurrentIndex();
else
contentX = Qt.binding(() => currentItem.x);
}
Row {
id: row
Dash {
shouldUpdate: visible && this === view.currentItem
}
Media {
shouldUpdate: visible && this === view.currentItem
visibilities: root.visibilities
}
Performance {}
}
Behavior on contentX {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}

View file

@ -0,0 +1,86 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "dash"
import QtQuick.Layouts
GridLayout {
id: root
required property bool shouldUpdate
rowSpacing: Appearance.spacing.normal
columnSpacing: Appearance.spacing.normal
Rect {
Layout.column: 2
Layout.columnSpan: 3
Layout.preferredWidth: user.implicitWidth
Layout.preferredHeight: user.implicitHeight
User {
id: user
}
}
Rect {
Layout.row: 0
Layout.columnSpan: 2
Layout.preferredWidth: DashboardConfig.sizes.weatherWidth
Layout.fillHeight: true
Weather {}
}
Rect {
Layout.row: 1
Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true
DateTime {
id: dateTime
}
}
Rect {
Layout.row: 1
Layout.column: 1
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight
Calendar {
id: calendar
}
}
Rect {
Layout.row: 1
Layout.column: 4
Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true
Resources {
id: resources
}
}
Rect {
Layout.row: 0
Layout.column: 5
Layout.rowSpan: 2
Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true
Media {
id: media
shouldUpdate: root.shouldUpdate
}
}
component Rect: StyledRect {
radius: Appearance.rounding.small
color: Colours.palette.m3surfaceContainer
}
}

594
modules/dashboard/Media.qml Normal file
View file

@ -0,0 +1,594 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
Item {
id: root
required property bool shouldUpdate
required property PersistentProperties visibilities
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
return `${Math.floor(length / 60)}:${Math.floor(length % 60).toString().padStart(2, "0")}`;
}
implicitWidth: cover.implicitWidth + DashboardConfig.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2
implicitHeight: Math.max(cover.implicitHeight + DashboardConfig.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2
Behavior on playerProgress {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Timer {
running: root.shouldUpdate && (Players.active?.isPlaying ?? false)
interval: DashboardConfig.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
Connections {
target: Cava
function onValuesChanged(): void {
if (root.shouldUpdate)
visualiser.requestPaint();
}
}
Canvas {
id: visualiser
readonly property real centerX: width / 2
readonly property real centerY: height / 2
readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small
readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small
property color colour: Colours.palette.m3primary
anchors.fill: cover
anchors.margins: -DashboardConfig.sizes.mediaVisualiserSize
onColourChanged: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const values = Cava.values;
const len = values.length;
ctx.strokeStyle = colour;
ctx.lineWidth = 360 / len - Appearance.spacing.small / 4;
ctx.lineCap = "round";
const size = DashboardConfig.sizes.mediaVisualiserSize;
const cx = centerX;
const cy = centerY;
const rx = innerX + ctx.lineWidth / 2;
const ry = innerY + ctx.lineWidth / 2;
for (let i = 0; i < len; i++) {
const v = Math.max(1, Math.min(100, values[i]));
const angle = i * 2 * Math.PI / len;
const magnitude = v / 100 * size;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
ctx.moveTo(cx + rx * cos, cy + ry * sin);
ctx.lineTo(cx + (rx + magnitude) * cos, cy + (ry + magnitude) * sin);
}
ctx.stroke();
}
Behavior on colour {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledClippingRect {
id: cover
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large + DashboardConfig.sizes.mediaVisualiserSize
implicitWidth: DashboardConfig.sizes.mediaCoverArtSize
implicitHeight: DashboardConfig.sizes.mediaCoverArtSize
color: Colours.palette.m3surfaceContainerHigh
radius: Appearance.rounding.full
MaterialIcon {
anchors.centerIn: parent
text: "art_track"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.fill: parent
source: Players.active?.trackArtUrl ?? ""
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
}
}
Column {
id: details
anchors.verticalCenter: parent.verticalCenter
anchors.left: visualiser.right
anchors.leftMargin: Appearance.spacing.normal
spacing: Appearance.spacing.small
StyledText {
id: title
anchors.horizontalCenter: parent.horizontalCenter
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.normal
width: parent.implicitWidth
elide: Text.ElideRight
}
StyledText {
id: album
anchors.horizontalCenter: parent.horizontalCenter
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
width: parent.implicitWidth
elide: Text.ElideRight
}
StyledText {
id: artist
anchors.horizontalCenter: parent.horizontalCenter
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
color: Colours.palette.m3secondary
width: parent.implicitWidth
elide: Text.ElideRight
}
Row {
id: controls
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
Control {
icon: "skip_previous"
canUse: Players.active?.canGoPrevious ?? false
function onClicked(): void {
Players.active?.previous();
}
}
Control {
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
canUse: Players.active?.canTogglePlaying ?? false
primary: true
function onClicked(): void {
Players.active?.togglePlaying();
}
}
Control {
icon: "skip_next"
canUse: Players.active?.canGoNext ?? false
function onClicked(): void {
Players.active?.next();
}
}
}
Slider {
id: slider
implicitWidth: controls.implicitWidth * 1.5
implicitHeight: Appearance.padding.normal * 3
value: root.playerProgress
onMoved: {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
background: Item {
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: slider.handle.x - slider.implicitHeight / 6
color: Colours.palette.m3primary
radius: Appearance.rounding.full
topRightRadius: slider.implicitHeight / 15
bottomRightRadius: slider.implicitHeight / 15
}
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: parent.width - slider.handle.x - slider.handle.implicitWidth - slider.implicitHeight / 6
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.full
topLeftRadius: slider.implicitHeight / 15
bottomLeftRadius: slider.implicitHeight / 15
}
}
handle: StyledRect {
id: rect
x: slider.visualPosition * slider.availableWidth
implicitWidth: slider.implicitHeight / 4.5
implicitHeight: slider.implicitHeight
color: Colours.palette.m3primary
radius: Appearance.rounding.full
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: event => event.accepted = false
}
}
}
Item {
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)
StyledText {
id: position
anchors.left: parent.left
text: root.lengthStr(Players.active?.position ?? -1)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
StyledText {
id: length
anchors.right: parent.right
text: root.lengthStr(Players.active?.length ?? -1)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
Control {
icon: "flip_to_front"
canUse: Players.active?.canRaise ?? false
fontSize: Appearance.font.size.larger
padding: Appearance.padding.small
fill: false
color: Colours.palette.m3surfaceContainer
function onClicked(): void {
Players.active?.raise();
root.visibilities.dashboard = false;
}
}
MouseArea {
id: playerSelector
property bool expanded
anchors.verticalCenter: parent.verticalCenter
implicitWidth: slider.implicitWidth / 2
implicitHeight: currentPlayer.implicitHeight + Appearance.padding.small * 2
cursorShape: Qt.PointingHandCursor
onClicked: expanded = !expanded
RectangularShadow {
anchors.fill: playerSelectorBg
opacity: playerSelector.expanded ? 1 : 0
radius: playerSelectorBg.radius
color: Colours.palette.m3shadow
blur: 5
spread: 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledRect {
id: playerSelectorBg
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: playersWrapper.implicitHeight + Appearance.padding.small * 2
color: Colours.palette.m3secondaryContainer
radius: Appearance.rounding.normal
Item {
id: playersWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.small
clip: true
implicitHeight: playerSelector.expanded && Players.list.length > 1 ? players.implicitHeight : currentPlayer.implicitHeight
Column {
id: players
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
spacing: Appearance.spacing.small
Repeater {
model: Players.list.filter(p => p !== Players.active)
Row {
id: player
required property MprisPlayer modelData
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
IconImage {
id: playerIcon
source: Icons.getAppIcon(player.modelData.identity, "image-missing")
implicitSize: Math.round(identity.implicitHeight * 0.9)
}
StyledText {
id: identity
text: identityMetrics.elidedText
color: Colours.palette.m3onSecondaryContainer
TextMetrics {
id: identityMetrics
text: player.modelData.identity
font.family: identity.font.family
font.pointSize: identity.font.pointSize
elide: Text.ElideRight
elideWidth: playerSelector.implicitWidth - playerIcon.implicitWidth - player.spacing - Appearance.padding.smaller * 2
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Players.manualActive = player.modelData;
playerSelector.expanded = false;
}
}
}
}
}
Item {
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 1
StyledRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: -Appearance.padding.normal
color: Colours.palette.m3secondary
implicitHeight: 1
}
}
Row {
id: currentPlayer
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
IconImage {
id: currentIcon
source: Icons.getAppIcon(Players.active?.identity ?? "", "multimedia-player")
implicitSize: Math.round(currentIdentity.implicitHeight * 0.9)
}
StyledText {
id: currentIdentity
animate: true
text: currentIdentityMetrics.elidedText
color: Colours.palette.m3onSecondaryContainer
TextMetrics {
id: currentIdentityMetrics
text: Players.active?.identity ?? "No players"
font.family: currentIdentity.font.family
font.pointSize: currentIdentity.font.pointSize
elide: Text.ElideRight
elideWidth: playerSelector.implicitWidth - currentIcon.implicitWidth - currentPlayer.spacing - Appearance.padding.smaller * 2
}
}
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
}
}
Control {
icon: "delete"
canUse: Players.active?.canQuit ?? false
fontSize: Appearance.font.size.larger
padding: Appearance.padding.small
fill: false
color: Colours.palette.m3surfaceContainer
function onClicked(): void {
Players.active?.quit();
}
}
}
}
Item {
id: bongocat
anchors.verticalCenter: parent.verticalCenter
anchors.left: details.right
anchors.leftMargin: Appearance.spacing.normal
implicitWidth: visualiser.width
implicitHeight: visualiser.height
AnimatedImage {
anchors.centerIn: parent
width: visualiser.width * 0.75
height: visualiser.height * 0.75
playing: root.shouldUpdate && (Players.active?.isPlaying ?? false)
speed: BeatDetector.bpm / 300
source: "root:/assets/bongocat.gif"
asynchronous: true
fillMode: AnimatedImage.PreserveAspectFit
}
}
component Control: StyledRect {
id: control
required property string icon
required property bool canUse
property int fontSize: Appearance.font.size.extraLarge
property int padding
property bool fill: true
property bool primary
function onClicked(): void {
}
implicitWidth: Math.max(icon.implicitWidth, icon.implicitHeight) + padding * 2
implicitHeight: implicitWidth
radius: Appearance.rounding.full
color: primary && canUse ? Colours.palette.m3primary : "transparent"
StateLayer {
disabled: !control.canUse
radius: parent.radius
color: control.primary ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
function onClicked(): void {
control.onClicked();
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true
fill: control.fill ? 1 : 0
text: control.icon
color: control.canUse ? control.primary ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface : Colours.palette.m3outline
font.pointSize: control.fontSize
}
}
}

View file

@ -0,0 +1,230 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Row {
id: root
spacing: Appearance.spacing.large * 3
padding: Appearance.padding.large
leftPadding: padding * 2
rightPadding: padding * 3
Resource {
value1: Math.min(1, SystemUsage.gpuTemp / 90)
value2: SystemUsage.gpuPerc
label1: `${Math.ceil(SystemUsage.gpuTemp)}°C`
label2: `${Math.round(SystemUsage.gpuPerc * 100)}%`
sublabel1: qsTr("GPU temp")
sublabel2: qsTr("Usage")
}
Resource {
primary: true
value1: Math.min(1, SystemUsage.cpuTemp / 90)
value2: SystemUsage.cpuPerc
label1: `${Math.ceil(SystemUsage.cpuTemp)}°C`
label2: `${Math.round(SystemUsage.cpuPerc * 100)}%`
sublabel1: qsTr("CPU temp")
sublabel2: qsTr("Usage")
}
Resource {
value1: SystemUsage.memPerc
value2: SystemUsage.storagePerc
label1: {
const fmt = SystemUsage.formatKib(SystemUsage.memUsed);
return `${+fmt.value.toFixed(1)}${fmt.unit}`;
}
label2: {
const fmt = SystemUsage.formatKib(SystemUsage.storageUsed);
return `${Math.floor(fmt.value)}${fmt.unit}`;
}
sublabel1: qsTr("Memory")
sublabel2: qsTr("Storage")
}
component Resource: Item {
id: res
required property real value1
required property real value2
required property string sublabel1
required property string sublabel2
required property string label1
required property string label2
property bool primary
readonly property real primaryMult: primary ? 1.2 : 1
readonly property real thickness: DashboardConfig.sizes.resourceProgessThickness * primaryMult
property color fg1: Colours.palette.m3primary
property color fg2: Colours.palette.m3secondary
property color bg1: Colours.palette.m3primaryContainer
property color bg2: Colours.palette.m3secondaryContainer
anchors.verticalCenter: parent.verticalCenter
implicitWidth: DashboardConfig.sizes.resourceSize * primaryMult
implicitHeight: DashboardConfig.sizes.resourceSize * primaryMult
onValue1Changed: canvas.requestPaint()
onValue2Changed: canvas.requestPaint()
onFg1Changed: canvas.requestPaint()
onFg2Changed: canvas.requestPaint()
onBg1Changed: canvas.requestPaint()
onBg2Changed: canvas.requestPaint()
Column {
anchors.centerIn: parent
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.label1
font.pointSize: Appearance.font.size.extraLarge * res.primaryMult
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.sublabel1
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller * res.primaryMult
}
}
Column {
anchors.horizontalCenter: parent.right
anchors.top: parent.verticalCenter
anchors.horizontalCenterOffset: -res.thickness / 2
anchors.topMargin: res.thickness / 2 + Appearance.spacing.small
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.label2
font.pointSize: Appearance.font.size.smaller * res.primaryMult
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.sublabel2
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small * res.primaryMult
}
}
Canvas {
id: canvas
readonly property real centerX: width / 2
readonly property real centerY: height / 2
readonly property real arc1Start: degToRad(45)
readonly property real arc1End: degToRad(220)
readonly property real arc2Start: degToRad(230)
readonly property real arc2End: degToRad(360)
function degToRad(deg: int): real {
return deg * Math.PI / 180;
}
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.lineWidth = res.thickness;
ctx.lineCap = "round";
const radius = (Math.min(width, height) - ctx.lineWidth) / 2;
const cx = centerX;
const cy = centerY;
const a1s = arc1Start;
const a1e = arc1End;
const a2s = arc2Start;
const a2e = arc2End;
ctx.beginPath();
ctx.arc(cx, cy, radius, a1s, a1e, false);
ctx.strokeStyle = res.bg1;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false);
ctx.strokeStyle = res.fg1;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, radius, a2s, a2e, false);
ctx.strokeStyle = res.bg2;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false);
ctx.strokeStyle = res.fg2;
ctx.stroke();
}
}
Behavior on value1 {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on value2 {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on fg1 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on fg2 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on bg1 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on bg2 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

249
modules/dashboard/Tabs.qml Normal file
View file

@ -0,0 +1,249 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
Item {
id: root
required property real nonAnimWidth
property alias currentIndex: bar.currentIndex
readonly property TabBar bar: bar
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
TabBar {
id: bar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
background: null
Tab {
iconName: "dashboard"
text: qsTr("Dashboard")
}
Tab {
iconName: "queue_music"
text: qsTr("Media")
}
Tab {
iconName: "speed"
text: qsTr("Performance")
}
Tab {
iconName: "workspaces"
text: qsTr("Workspaces")
}
}
Item {
id: indicator
anchors.top: bar.bottom
anchors.topMargin: DashboardConfig.sizes.tabIndicatorSpacing
implicitWidth: bar.currentItem.implicitWidth
implicitHeight: DashboardConfig.sizes.tabIndicatorHeight
x: {
const tab = bar.currentItem;
const width = (root.nonAnimWidth - DashboardConfig.sizes.tabIndicatorSpacing * (bar.count - 1) * 2) / bar.count
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
}
clip: true
StyledRect {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight * 2
color: Colours.palette.m3primary
radius: Appearance.rounding.full
}
Behavior on x {
Anim {}
}
Behavior on implicitWidth {
Anim {}
}
}
StyledRect {
id: separator
anchors.top: indicator.bottom
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 1
color: Colours.palette.m3outlineVariant
}
component Tab: TabButton {
id: tab
required property string iconName
readonly property bool current: TabBar.tabBar.currentItem === this
background: null
contentItem: MouseArea {
id: mouse
implicitWidth: Math.max(icon.width, label.width)
implicitHeight: icon.height + label.height
cursorShape: Qt.PointingHandCursor
onPressed: ({x,y}) => {
tab.TabBar.tabBar.setCurrentIndex(tab.TabBar.index);
const stateY = stateWrapper.y;
rippleAnim.x = x;
rippleAnim.y = y - stateY;
const dist = (ox,oy) => ox * ox + oy * oy;
const stateEndY = stateY + stateWrapper.height;
rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)));
rippleAnim.restart();
}
onWheel: event => {
if (event.angleDelta.y < 0)
tab.TabBar.tabBar.incrementCurrentIndex();
else if (event.angleDelta.y > 0)
tab.TabBar.tabBar.decrementCurrentIndex();
}
SequentialAnimation {
id: rippleAnim
property real x
property real y
property real radius
PropertyAction {
target: ripple
property: "x"
value: rippleAnim.x
}
PropertyAction {
target: ripple
property: "y"
value: rippleAnim.y
}
PropertyAction {
target: ripple
property: "opacity"
value: 0.1
}
ParallelAnimation {
Anim {
target: ripple
properties: "implicitWidth,implicitHeight"
from: 0
to: rippleAnim.radius * 2
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
target: ripple
property: "opacity"
to: 0
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
ClippingRectangle {
id: stateWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + DashboardConfig.sizes.tabIndicatorSpacing * 2
color: "transparent"
radius: Appearance.rounding.small
StyledRect {
id: stateLayer
anchors.fill: parent
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0
Behavior on opacity {
Anim {}
}
}
StyledRect {
id: ripple
radius: Appearance.rounding.full
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
opacity: 0
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
}
}
}
MaterialIcon {
id: icon
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
font.pointSize: Appearance.font.size.large
Behavior on fill {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
text: tab.text
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -0,0 +1,55 @@
import QtQuick
import Quickshell
import "root:/config"
Item {
id: root
required property PersistentProperties visibilities
visible: height > 0
implicitHeight: 0
implicitWidth: content.implicitWidth
states: State {
name: "visible"
when: root.visibilities.dashboard
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
NumberAnimation {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
NumberAnimation {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Content {
id: content
visibilities: root.visibilities
}
}

View file

@ -0,0 +1,70 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Controls
Column {
id: root
anchors.left: parent.left
anchors.right: parent.right
padding: Appearance.padding.large
spacing: Appearance.spacing.small
DayOfWeekRow {
id: days
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: parent.padding
delegate: StyledText {
required property var model
horizontalAlignment: Text.AlignHCenter
text: model.shortName
font.family: Appearance.font.family.sans
font.weight: 500
}
}
MonthGrid {
id: grid
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: parent.padding
spacing: 3
delegate: Item {
id: day
required property var model
implicitWidth: implicitHeight
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
StyledRect {
anchors.centerIn: parent
implicitWidth: parent.implicitHeight
implicitHeight: parent.implicitHeight
radius: Appearance.rounding.full
color: model.today ? Colours.palette.m3primary : "transparent"
StyledText {
id: text
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: grid.locale.toString(day.model.date, "d")
color: day.model.today ? Colours.palette.m3onPrimary : day.model.month === grid.month ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3outline
}
}
}
}
}

View file

@ -0,0 +1,71 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Item {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: DashboardConfig.sizes.dateTimeWidth
StyledText {
id: hours
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: (root.height - (hours.implicitHeight + sep.implicitHeight + sep.anchors.topMargin + mins.implicitHeight + mins.anchors.topMargin + date.implicitHeight + date.anchors.topMargin)) / 2
horizontalAlignment: Text.AlignHCenter
text: Time.format("HH")
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
StyledText {
id: sep
anchors.left: parent.left
anchors.right: parent.right
anchors.top: hours.bottom
anchors.topMargin: -font.pointSize * 0.5
horizontalAlignment: Text.AlignHCenter
text: "•••"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge * 0.9
}
StyledText {
id: mins
anchors.left: parent.left
anchors.right: parent.right
anchors.top: sep.bottom
anchors.topMargin: -sep.font.pointSize * 0.45
horizontalAlignment: Text.AlignHCenter
text: Time.format("MM")
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
StyledText {
id: date
anchors.left: parent.left
anchors.right: parent.right
anchors.top: mins.bottom
anchors.topMargin: Appearance.spacing.normal
horizontalAlignment: Text.AlignHCenter
text: Time.format("ddd, d")
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
}

View file

@ -0,0 +1,262 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import QtQuick
import QtQuick.Shapes
Item {
id: root
required property bool shouldUpdate
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: DashboardConfig.sizes.mediaWidth
Behavior on playerProgress {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Timer {
running: root.shouldUpdate && (Players.active?.isPlaying ?? false)
interval: DashboardConfig.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Colours.palette.m3surfaceContainerHigh
strokeWidth: DashboardConfig.sizes.mediaProgressThickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2
sweepAngle: DashboardConfig.sizes.mediaProgressSweep
}
Behavior on strokeColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
ShapePath {
fillColor: "transparent"
strokeColor: Colours.palette.m3primary
strokeWidth: DashboardConfig.sizes.mediaProgressThickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2
sweepAngle: DashboardConfig.sizes.mediaProgressSweep * root.playerProgress
}
Behavior on strokeColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
StyledClippingRect {
id: cover
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Appearance.padding.large + DashboardConfig.sizes.mediaProgressThickness + Appearance.spacing.small
implicitHeight: width
color: Colours.palette.m3surfaceContainerHigh
radius: Appearance.rounding.full
MaterialIcon {
anchors.centerIn: parent
text: "art_track"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.fill: parent
source: Players.active?.trackArtUrl ?? ""
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
}
}
StyledText {
id: title
anchors.top: cover.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.normal
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.normal
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
StyledText {
id: album
anchors.top: title.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
StyledText {
id: artist
anchors.top: album.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
color: Colours.palette.m3secondary
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
Row {
id: controls
anchors.top: artist.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.smaller
spacing: Appearance.spacing.small
Control {
icon: "skip_previous"
canUse: Players.active?.canGoPrevious ?? false
function onClicked(): void {
Players.active?.previous();
}
}
Control {
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
canUse: Players.active?.canTogglePlaying ?? false
function onClicked(): void {
Players.active?.togglePlaying();
}
}
Control {
icon: "skip_next"
canUse: Players.active?.canGoNext ?? false
function onClicked(): void {
Players.active?.next();
}
}
}
AnimatedImage {
id: bongocat
anchors.top: controls.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Appearance.spacing.small
anchors.bottomMargin: Appearance.padding.large
anchors.margins: Appearance.padding.large * 2
playing: root.shouldUpdate && (Players.active?.isPlaying ?? false)
speed: BeatDetector.bpm / 300
source: "root:/assets/bongocat.gif"
asynchronous: true
fillMode: AnimatedImage.PreserveAspectFit
}
component Control: StyledRect {
id: control
required property string icon
required property bool canUse
function onClicked(): void {
}
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small
implicitHeight: implicitWidth
StateLayer {
disabled: !control.canUse
radius: Appearance.rounding.full
function onClicked(): void {
control.onClicked();
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true
text: control.icon
color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline
font.pointSize: Appearance.font.size.large
}
}
}

View file

@ -0,0 +1,85 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Controls
Row {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
padding: Appearance.padding.large
spacing: Appearance.spacing.normal
Resource {
icon: "memory"
value: SystemUsage.cpuPerc
colour: Colours.palette.m3primary
}
Resource {
icon: "memory_alt"
value: SystemUsage.memPerc
colour: Colours.palette.m3secondary
}
Resource {
icon: "hard_disk"
value: SystemUsage.storagePerc
colour: Colours.palette.m3tertiary
}
component Resource: Item {
id: res
required property string icon
required property real value
required property color colour
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
implicitWidth: icon.implicitWidth
StyledRect {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: icon.top
anchors.bottomMargin: Appearance.spacing.small
implicitWidth: DashboardConfig.sizes.resourceProgessThickness
color: Colours.palette.m3surfaceContainerHigh
radius: Appearance.rounding.full
StyledRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: res.value * parent.height
color: res.colour
radius: Appearance.rounding.full
}
}
MaterialIcon {
id: icon
anchors.bottom: parent.bottom
text: res.icon
color: res.colour
}
Behavior on value {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

View file

@ -0,0 +1,116 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/utils"
import Quickshell
import Quickshell.Io
import QtQuick
Row {
id: root
padding: Appearance.padding.large
spacing: Appearance.spacing.large
StyledClippingRect {
implicitWidth: info.implicitHeight
implicitHeight: info.implicitHeight
radius: Appearance.rounding.full
color: Colours.palette.m3surfaceContainerHigh
MaterialIcon {
anchors.centerIn: parent
text: "person"
fill: 1
font.pointSize: (info.implicitHeight / 2) || 1
}
CachingImage {
anchors.fill: parent
path: `${Paths.home}/.face`
}
}
Column {
id: info
spacing: Appearance.spacing.normal
InfoLine {
icon: Icons.osIcon
text: Icons.osName
colour: Colours.palette.m3primary
}
InfoLine {
icon: "select_window_2"
text: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")
colour: Colours.palette.m3secondary
}
InfoLine {
icon: "timer"
text: uptimeProc.uptime
colour: Colours.palette.m3tertiary
Timer {
running: true
repeat: true
interval: 15000
onTriggered: uptimeProc.running = true
}
Process {
id: uptimeProc
property string uptime
running: true
command: ["uptime", "-p"]
stdout: SplitParser {
onRead: data => uptimeProc.uptime = data
}
}
}
}
component InfoLine: Item {
id: line
required property string icon
required property string text
required property color colour
implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin
implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)
MaterialIcon {
id: icon
anchors.left: parent.left
anchors.leftMargin: (DashboardConfig.sizes.infoIconSize - implicitWidth) / 2
text: line.icon
color: line.colour
font.pointSize: Appearance.font.size.normal
font.variableAxes: ({
FILL: 1
})
}
StyledText {
id: text
anchors.verticalCenter: icon.verticalCenter
anchors.left: icon.right
anchors.leftMargin: icon.anchors.leftMargin
text: `: ${line.text}`
font.pointSize: Appearance.font.size.normal
width: DashboardConfig.sizes.infoWidth
elide: Text.ElideRight
}
}
}

View file

@ -0,0 +1,63 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/utils"
import QtQuick
Item {
id: root
anchors.centerIn: parent
implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin
onVisibleChanged: {
if (visible)
Weather.reload();
}
MaterialIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
animate: true
text: Weather.icon || "cloud_alert"
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge * 2
font.variableAxes: ({
opsz: Appearance.font.size.extraLarge * 1.2
})
}
Column {
id: info
anchors.verticalCenter: parent.verticalCenter
anchors.left: icon.right
anchors.leftMargin: Appearance.spacing.large
spacing: Appearance.spacing.small
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: `${Weather.temperature}°C`
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: Weather.description || qsTr("No weather")
elide: Text.ElideRight
width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2)
}
}
}

View file

@ -0,0 +1,66 @@
import "root:/services"
import "root:/config"
import "root:/modules/osd" as Osd
import "root:/modules/notifications" as Notifications
import "root:/modules/session" as Session
import "root:/modules/launcher" as Launcher
import "root:/modules/dashboard" as Dashboard
import "root:/modules/bar/popouts" as BarPopouts
import QtQuick
import QtQuick.Shapes
Shape {
id: root
required property Panels panels
required property Item bar
anchors.fill: parent
anchors.margins: BorderConfig.thickness
anchors.leftMargin: bar.implicitWidth
preferredRendererType: Shape.CurveRenderer
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
Osd.Background {
wrapper: panels.osd
startX: root.width - panels.session.width
startY: (root.height - wrapper.height) / 2 - rounding
}
Notifications.Background {
wrapper: panels.notifications
startX: root.width
startY: 0
}
Session.Background {
wrapper: panels.session
startX: root.width
startY: (root.height - wrapper.height) / 2 - rounding
}
Launcher.Background {
wrapper: panels.launcher
startX: (root.width - wrapper.width) / 2 - rounding
startY: root.height
}
Dashboard.Background {
wrapper: panels.dashboard
startX: (root.width - wrapper.width) / 2 - rounding
startY: 0
}
BarPopouts.Background {
wrapper: panels.popouts
invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height
startX: 0
startY: wrapper.y - rounding
}
}

View file

@ -0,0 +1,47 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Effects
Item {
id: root
required property Item bar
anchors.fill: parent
StyledRect {
id: rect
anchors.fill: parent
color: Colours.alpha(BorderConfig.colour, false)
visible: false
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
anchors.margins: BorderConfig.thickness
anchors.leftMargin: root.bar.implicitWidth
radius: BorderConfig.rounding
}
}
MultiEffect {
anchors.fill: parent
maskEnabled: true
maskInverted: true
maskSource: mask
source: rect
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}

147
modules/drawers/Drawers.qml Normal file
View file

@ -0,0 +1,147 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/modules/bar"
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick
import QtQuick.Effects
Variants {
model: Quickshell.screens
Scope {
id: scope
required property ShellScreen modelData
Exclusions {
screen: scope.modelData
bar: bar
}
StyledWindow {
id: win
screen: scope.modelData
name: "drawers"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
mask: Region {
x: bar.implicitWidth
y: BorderConfig.thickness
width: win.width - bar.implicitWidth - BorderConfig.thickness
height: win.height - BorderConfig.thickness * 2
intersection: Intersection.Xor
regions: regions.instances
}
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
Variants {
id: regions
model: panels.children
Region {
required property Item modelData
x: modelData.x + bar.implicitWidth
y: modelData.y + BorderConfig.thickness
width: modelData.width
height: modelData.height
intersection: Intersection.Subtract
}
}
HyprlandFocusGrab {
active: visibilities.launcher || visibilities.session
windows: [win]
onCleared: {
visibilities.launcher = false;
visibilities.session = false;
}
}
StyledRect {
anchors.fill: parent
opacity: visibilities.session ? 0.5 : 0
color: Colours.palette.m3scrim
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
Item {
id: background
anchors.fill: parent
visible: false
Border {
bar: bar
}
Backgrounds {
panels: panels
bar: bar
}
}
MultiEffect {
anchors.fill: source
source: background
shadowEnabled: true
blurMax: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
}
PersistentProperties {
id: visibilities
property bool osd
property bool session
property bool launcher
property bool dashboard
Component.onCompleted: Visibilities.screens[scope.modelData] = this
}
Interactions {
screen: scope.modelData
popouts: panels.popouts
visibilities: visibilities
panels: panels
bar: bar
Panels {
id: panels
screen: scope.modelData
visibilities: visibilities
bar: bar
}
}
Bar {
id: bar
screen: scope.modelData
popouts: panels.popouts
}
}
}
}

View file

@ -0,0 +1,36 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/config"
import Quickshell
import QtQuick
Scope {
id: root
required property ShellScreen screen
required property Item bar
ExclusionZone {
anchors.left: true
exclusiveZone: root.bar.implicitWidth
}
ExclusionZone {
anchors.top: true
}
ExclusionZone {
anchors.right: true
}
ExclusionZone {
anchors.bottom: true
}
component ExclusionZone: StyledWindow {
screen: root.screen
name: "border-exclusion"
exclusiveZone: BorderConfig.thickness
}
}

View file

@ -0,0 +1,83 @@
import "root:/services"
import "root:/config"
import "root:/modules/bar/popouts" as BarPopouts
import "root:/modules/osd" as Osd
import Quickshell
import QtQuick
MouseArea {
id: root
required property ShellScreen screen
required property BarPopouts.Wrapper popouts
required property PersistentProperties visibilities
required property Panels panels
required property Item bar
property bool osdHovered
property point dragStart
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = BorderConfig.thickness + panel.y;
return y >= panelY - BorderConfig.rounding && y <= panelY + panel.height + BorderConfig.rounding;
}
function inRightPanel(panel: Item, x: real, y: real): bool {
return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y);
}
function inTopPanel(panel: Item, x: real, y: real): bool {
const panelX = bar.implicitWidth + panel.x;
return y < BorderConfig.thickness + panel.y + panel.height && x >= panelX - BorderConfig.rounding && x <= panelX + panel.width + BorderConfig.rounding;
}
anchors.fill: parent
hoverEnabled: true
onPressed: event => dragStart = Qt.point(event.x, event.y)
onContainsMouseChanged: {
if (!containsMouse) {
visibilities.osd = false;
osdHovered = false;
visibilities.dashboard = false;
popouts.hasCurrent = false;
}
}
onPositionChanged: ({x, y}) => {
// Show osd on hover
const showOsd = inRightPanel(panels.osd, x, y);
visibilities.osd = showOsd;
osdHovered = showOsd;
// Show/hide session on drag
if (pressed && withinPanelHeight(panels.session, x, y)) {
const dragX = x - dragStart.x;
if (dragX < -SessionConfig.dragThreshold)
visibilities.session = true;
else if (dragX > SessionConfig.dragThreshold)
visibilities.session = false;
}
// Show dashboard on hover
visibilities.dashboard = inTopPanel(panels.dashboard, x, y);
// Show popouts on hover
const popout = panels.popouts;
if (x < bar.implicitWidth + popout.width) {
if (x < bar.implicitWidth)
// Handle like part of bar
bar.checkPopout(y);
else
// Keep on hover
popouts.hasCurrent = withinPanelHeight(popout, x, y);
} else
popouts.hasCurrent = false;
}
Osd.Interactions {
screen: root.screen
visibilities: root.visibilities
hovered: root.osdHovered
}
}

View file

@ -0,0 +1,93 @@
import "root:/services"
import "root:/config"
import "root:/modules/osd" as Osd
import "root:/modules/notifications" as Notifications
import "root:/modules/session" as Session
import "root:/modules/launcher" as Launcher
import "root:/modules/dashboard" as Dashboard
import "root:/modules/bar/popouts" as BarPopouts
import Quickshell
import QtQuick
Item {
id: root
required property ShellScreen screen
required property PersistentProperties visibilities
required property Item bar
readonly property Osd.Wrapper osd: osd
readonly property Notifications.Wrapper notifications: notifications
readonly property Session.Wrapper session: session
readonly property Launcher.Wrapper launcher: launcher
readonly property Dashboard.Wrapper dashboard: dashboard
readonly property BarPopouts.Wrapper popouts: popouts
anchors.fill: parent
anchors.margins: BorderConfig.thickness
anchors.leftMargin: bar.implicitWidth
Component.onCompleted: Visibilities.panels[screen] = this
Osd.Wrapper {
id: osd
clip: root.visibilities.session
screen: root.screen
visibility: root.visibilities.osd
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: session.width
}
Notifications.Wrapper {
id: notifications
anchors.top: parent.top
anchors.right: parent.right
}
Session.Wrapper {
id: session
visibilities: root.visibilities
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
}
Launcher.Wrapper {
id: launcher
visibilities: root.visibilities
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
}
Dashboard.Wrapper {
id: dashboard
visibilities: root.visibilities
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
BarPopouts.Wrapper {
id: popouts
screen: root.screen
anchors.left: parent.left
anchors.verticalCenter: parent.top
anchors.verticalCenterOffset: {
const off = root.popouts.currentCenter - BorderConfig.thickness;
const diff = root.height - Math.floor(off + implicitHeight / 2);
if (diff < 0)
return off + diff;
return off;
}
}
}

View file

@ -0,0 +1,69 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Item {
id: root
required property Actions.Action modelData
required property var list
implicitHeight: LauncherConfig.sizes.itemHeight
anchors.left: parent?.left
anchors.right: parent?.right
StateLayer {
radius: Appearance.rounding.full
function onClicked(): void {
root.modelData?.onClicked(root.list);
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.larger
anchors.rightMargin: Appearance.padding.larger
anchors.margins: Appearance.padding.smaller
MaterialIcon {
id: icon
text: root.modelData?.icon ?? ""
font.pointSize: Appearance.font.size.extraLarge
anchors.verticalCenter: parent.verticalCenter
}
Item {
anchors.left: icon.right
anchors.leftMargin: Appearance.spacing.larger
anchors.verticalCenter: icon.verticalCenter
implicitWidth: parent.width - icon.width
implicitHeight: name.implicitHeight + desc.implicitHeight
StyledText {
id: name
text: root.modelData?.name ?? ""
font.pointSize: Appearance.font.size.normal
}
StyledText {
id: desc
text: root.modelData?.desc ?? ""
font.pointSize: Appearance.font.size.small
color: Colours.alpha(Colours.palette.m3outline, true)
elide: Text.ElideRight
width: root.width - icon.width - Appearance.rounding.normal * 2
anchors.top: name.bottom
}
}
}
}

View file

@ -0,0 +1,130 @@
pragma Singleton
import "root:/utils/scripts/fuzzysort.js" as Fuzzy
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property list<Action> list: [
Action {
name: qsTr("Scheme")
desc: qsTr("Change the current colour scheme")
icon: "palette"
function onClicked(list: AppList): void {
root.autocomplete(list, "scheme");
}
},
Action {
name: qsTr("Wallpaper")
desc: qsTr("Change the current wallpaper")
icon: "image"
function onClicked(list: AppList): void {
root.autocomplete(list, "wallpaper");
}
},
Action {
name: qsTr("Variant")
desc: qsTr("Change the current scheme variant")
icon: "colors"
function onClicked(list: AppList): void {
root.autocomplete(list, "variant");
}
},
Action {
name: qsTr("Transparency")
desc: qsTr("Change shell transparency")
icon: "opacity"
function onClicked(list: AppList): void {
root.autocomplete(list, "transparency");
}
},
Action {
name: qsTr("Light")
desc: qsTr("Change the scheme to light mode")
icon: "light_mode"
function onClicked(list: AppList): void {
list.visibilities.launcher = false;
Colours.setMode("light");
}
},
Action {
name: qsTr("Dark")
desc: qsTr("Change the scheme to dark mode")
icon: "dark_mode"
function onClicked(list: AppList): void {
list.visibilities.launcher = false;
Colours.setMode("dark");
}
},
Action {
name: qsTr("Lock")
desc: qsTr("Lock the current session")
icon: "lock"
function onClicked(list: AppList): void {
list.visibilities.launcher = false;
lock.running = true;
}
},
Action {
name: qsTr("Sleep")
desc: qsTr("Suspend then hibernate")
icon: "bedtime"
function onClicked(list: AppList): void {
list.visibilities.launcher = false;
sleep.running = true;
}
}
]
readonly property list<var> preppedActions: list.map(a => ({
name: Fuzzy.prepare(a.name),
desc: Fuzzy.prepare(a.desc),
action: a
}))
function fuzzyQuery(search: string): var {
return Fuzzy.go(search.slice(LauncherConfig.actionPrefix.length), preppedActions, {
all: true,
keys: ["name", "desc"],
scoreFn: r => r[0].score > 0 ? r[0].score * 0.9 + r[1].score * 0.1 : 0
}).map(r => r.obj.action);
}
function autocomplete(list: AppList, text: string): void {
list.search.text = `${LauncherConfig.actionPrefix}${text} `;
}
Process {
id: lock
command: ["loginctl", "lock-session"]
}
Process {
id: sleep
command: ["systemctl", "suspend-then-hibernate"]
}
component Action: QtObject {
required property string name
required property string desc
required property string icon
function onClicked(list: AppList): void {
}
}
}

View file

@ -0,0 +1,72 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import QtQuick
Item {
id: root
required property DesktopEntry modelData
required property PersistentProperties visibilities
implicitHeight: LauncherConfig.sizes.itemHeight
anchors.left: parent?.left
anchors.right: parent?.right
StateLayer {
radius: Appearance.rounding.full
function onClicked(): void {
Apps.launch(root.modelData);
root.visibilities.launcher = false;
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.larger
anchors.rightMargin: Appearance.padding.larger
anchors.margins: Appearance.padding.smaller
IconImage {
id: icon
source: Quickshell.iconPath(root.modelData?.icon, "image-missing")
implicitSize: parent.height * 0.8
anchors.verticalCenter: parent.verticalCenter
}
Item {
anchors.left: icon.right
anchors.leftMargin: Appearance.spacing.normal
anchors.verticalCenter: icon.verticalCenter
implicitWidth: parent.width - icon.width
implicitHeight: name.implicitHeight + comment.implicitHeight
StyledText {
id: name
text: root.modelData?.name ?? ""
font.pointSize: Appearance.font.size.normal
}
StyledText {
id: comment
text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? ""
font.pointSize: Appearance.font.size.small
color: Colours.alpha(Colours.palette.m3outline, true)
elide: Text.ElideRight
width: root.width - icon.width - Appearance.rounding.normal * 2
anchors.top: name.bottom
}
}
}
}

View file

@ -0,0 +1,160 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Controls
ListView {
id: root
required property int padding
required property TextField search
required property PersistentProperties visibilities
property bool isAction: search.text.startsWith(LauncherConfig.actionPrefix)
function getModelValues() {
let text = search.text;
if (isAction)
return Actions.fuzzyQuery(text);
if (text.startsWith(LauncherConfig.actionPrefix))
text = search.text.slice(LauncherConfig.actionPrefix.length);
return Apps.fuzzyQuery(text);
}
model: ScriptModel {
values: root.getModelValues()
onValuesChanged: root.currentIndex = 0
}
spacing: Appearance.spacing.small
orientation: Qt.Vertical
implicitHeight: (LauncherConfig.sizes.itemHeight + spacing) * Math.min(LauncherConfig.maxShown, count) - spacing
highlightMoveDuration: Appearance.anim.durations.normal
highlightResizeDuration: 0
highlight: StyledRect {
radius: Appearance.rounding.full
color: Colours.palette.m3onSurface
opacity: 0.08
}
delegate: isAction ? actionItem : appItem
ScrollBar.vertical: StyledScrollBar {}
add: Transition {
Anim {
properties: "opacity,scale"
from: 0
to: 1
}
}
remove: Transition {
Anim {
properties: "opacity,scale"
from: 1
to: 0
}
}
move: Transition {
Anim {
property: "y"
}
Anim {
properties: "opacity,scale"
to: 1
}
}
addDisplaced: Transition {
Anim {
property: "y"
duration: Appearance.anim.durations.small
}
Anim {
properties: "opacity,scale"
to: 1
}
}
displaced: Transition {
Anim {
property: "y"
}
Anim {
properties: "opacity,scale"
to: 1
}
}
Component {
id: appItem
AppItem {
visibilities: root.visibilities
}
}
Component {
id: actionItem
ActionItem {
list: root
}
}
Behavior on isAction {
SequentialAnimation {
ParallelAnimation {
Anim {
target: root
property: "opacity"
from: 1
to: 0
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
target: root
property: "scale"
from: 1
to: 0.9
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
}
PropertyAction {}
ParallelAnimation {
Anim {
target: root
property: "opacity"
from: 0
to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
target: root
property: "scale"
from: 0.9
to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -0,0 +1,63 @@
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: BorderConfig.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: BorderConfig.colour
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

View file

@ -0,0 +1,168 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
readonly property int padding: Appearance.padding.large
readonly property int rounding: Appearance.rounding.large
implicitWidth: listWrapper.width + padding * 2
implicitHeight: searchWrapper.height + listWrapper.height + padding * 2
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
Item {
id: listWrapper
implicitWidth: list.width
implicitHeight: list.height + root.padding
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: searchWrapper.top
anchors.bottomMargin: root.padding
ContentList {
id: list
visibilities: root.visibilities
search: search
padding: root.padding
rounding: root.rounding
}
}
StyledRect {
id: searchWrapper
color: Colours.alpha(Colours.palette.m3surfaceContainer, true)
radius: Appearance.rounding.full
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: root.padding
implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight)
MaterialIcon {
id: searchIcon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.padding
text: "search"
color: Colours.palette.m3onSurfaceVariant
}
StyledTextField {
id: search
anchors.left: searchIcon.right
anchors.right: clearIcon.left
anchors.leftMargin: Appearance.spacing.small
anchors.rightMargin: Appearance.spacing.small
topPadding: Appearance.padding.larger
bottomPadding: Appearance.padding.larger
placeholderText: qsTr("Type \"%1\" for commands").arg(LauncherConfig.actionPrefix)
background: null
onAccepted: {
const currentItem = list.currentList?.currentItem;
if (currentItem) {
if (list.showWallpapers) {
Wallpapers.setWallpaper(currentItem.modelData.path);
root.visibilities.launcher = false;
} else if (text.startsWith(LauncherConfig.actionPrefix)) {
currentItem.modelData.onClicked(list.currentList);
} else {
Apps.launch(currentItem.modelData);
root.visibilities.launcher = false;
}
}
}
Keys.onUpPressed: list.currentList?.decrementCurrentIndex()
Keys.onDownPressed: list.currentList?.incrementCurrentIndex()
Keys.onEscapePressed: root.visibilities.launcher = false
Connections {
target: root.visibilities
function onLauncherChanged(): void {
if (root.visibilities.launcher)
search.forceActiveFocus();
else {
search.text = "";
const current = list.currentList;
if (current)
current.currentIndex = 0;
}
}
}
}
MaterialIcon {
id: clearIcon
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: root.padding
width: search.text ? implicitWidth : implicitWidth / 2
opacity: {
if (!search.text)
return 0;
if (mouse.pressed)
return 0.7;
if (mouse.hovered)
return 0.8;
return 1;
}
text: "close"
color: Colours.palette.m3onSurfaceVariant
MouseArea {
id: mouse
property bool hovered
anchors.fill: parent
hoverEnabled: true
cursorShape: search.text ? Qt.PointingHandCursor : undefined
onEntered: hovered = true
onExited: hovered = false
onClicked: search.text = ""
}
Behavior on width {
NumberAnimation {
duration: Appearance.anim.durations.small
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.small
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
}

View file

@ -0,0 +1,188 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Controls
Item {
id: root
required property PersistentProperties visibilities
required property TextField search
required property int padding
required property int rounding
property bool showWallpapers: search.text.startsWith(`${LauncherConfig.actionPrefix}wallpaper `)
property var currentList: (showWallpapers ? wallpaperList : appList).item
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
clip: true
state: showWallpapers ? "wallpapers" : "apps"
states: [
State {
name: "apps"
PropertyChanges {
root.implicitWidth: LauncherConfig.sizes.itemWidth
root.implicitHeight: Math.max(empty.height, appList.height)
appList.active: true
}
AnchorChanges {
anchors.left: root.parent.left
anchors.right: root.parent.right
}
},
State {
name: "wallpapers"
PropertyChanges {
root.implicitWidth: Math.max(LauncherConfig.sizes.itemWidth, wallpaperList.width)
root.implicitHeight: LauncherConfig.sizes.wallpaperHeight
wallpaperList.active: true
}
}
]
transitions: Transition {
SequentialAnimation {
NumberAnimation {
target: root
property: "opacity"
from: 1
to: 0
duration: Appearance.anim.durations.small
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
PropertyAction {
targets: [appList, wallpaperList]
properties: "active"
}
ParallelAnimation {
NumberAnimation {
target: root
properties: "implicitWidth,implicitHeight"
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
NumberAnimation {
target: root
property: "opacity"
from: 0
to: 1
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
Loader {
id: appList
active: false
asynchronous: true
anchors.left: parent.left
anchors.right: parent.right
sourceComponent: AppList {
padding: root.padding
search: root.search
visibilities: root.visibilities
}
}
Loader {
id: wallpaperList
active: false
asynchronous: true
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: WallpaperList {
search: root.search
visibilities: root.visibilities
}
}
Item {
id: empty
opacity: root.currentList?.count === 0 ? 1 : 0
scale: root.currentList?.count === 0 ? 1 : 0.5
implicitWidth: icon.width + text.width + Appearance.spacing.small
implicitHeight: icon.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
id: icon
text: "manage_search"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraLarge
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: text
anchors.left: icon.right
anchors.leftMargin: Appearance.spacing.small
anchors.verticalCenter: parent.verticalCenter
text: qsTr("No results")
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on scale {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
}

View file

@ -0,0 +1,109 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Effects
StyledRect {
id: root
required property Wallpapers.Wallpaper modelData
required property PersistentProperties visibilities
scale: 0.5
opacity: 0
z: PathView.z ?? 0
Component.onCompleted: {
scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0);
opacity = Qt.binding(() => PathView.onPath ? 1 : 0);
}
implicitWidth: image.width + Appearance.padding.larger * 2
implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal
StateLayer {
radius: Appearance.rounding.normal
function onClicked(): void {
Wallpapers.setWallpaper(root.modelData.path);
root.visibilities.launcher = false;
}
}
CachingImage {
id: image
anchors.horizontalCenter: parent.horizontalCenter
y: Appearance.padding.large
visible: false
path: root.modelData.path
smooth: !root.PathView.view.moving
width: LauncherConfig.sizes.wallpaperWidth
height: width / 16 * 9
}
Rectangle {
id: mask
layer.enabled: true
layer.smooth: true
visible: false
anchors.fill: image
radius: Appearance.rounding.normal
}
RectangularShadow {
opacity: root.PathView.isCurrentItem ? 0.7 : 0
anchors.fill: mask
radius: mask.radius
color: Colours.palette.m3shadow
blur: 10
spread: 3
Behavior on opacity {
Anim {}
}
}
MultiEffect {
anchors.fill: image
source: image
maskEnabled: true
maskSource: mask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
StyledText {
id: label
anchors.top: image.bottom
anchors.topMargin: Appearance.spacing.small / 2
anchors.horizontalCenter: parent.horizontalCenter
width: image.width - Appearance.padding.normal * 2
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
renderType: Text.QtRendering
text: root.modelData.name
font.pointSize: Appearance.font.size.normal
}
Behavior on scale {
Anim {}
}
Behavior on opacity {
Anim {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -0,0 +1,79 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Controls
PathView {
id: root
required property TextField search
required property PersistentProperties visibilities
readonly property int numItems: {
const screenWidth = QsWindow.window?.screen.width * 0.8;
if (!screenWidth)
return 0;
const itemWidth = LauncherConfig.sizes.wallpaperWidth * 0.8;
const max = LauncherConfig.maxWallpapers;
if (max * itemWidth > screenWidth) {
const items = Math.floor(screenWidth / itemWidth);
return items > 1 && items % 2 === 0 ? items - 1 : items;
}
return max;
}
model: ScriptModel {
readonly property string search: root.search.text.split(" ").slice(1).join(" ")
values: {
const list = Wallpapers.fuzzyQuery(search);
if (list.length > 1 && list.length % 2 === 0)
list.length -= 1; // Always show odd number
return list;
}
onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent)
}
Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent)
Component.onDestruction: Wallpapers.stopPreview()
onCurrentItemChanged: {
if (currentItem)
Wallpapers.preview(currentItem.modelData.path);
}
implicitWidth: Math.min(numItems, count) * (LauncherConfig.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2)
pathItemCount: numItems
cacheItemCount: 4
snapMode: PathView.SnapToItem
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
highlightRangeMode: PathView.StrictlyEnforceRange
delegate: WallpaperItem {
visibilities: root.visibilities
}
path: Path {
startY: root.height / 2
PathAttribute {
name: "z"
value: 0
}
PathLine {
x: root.width / 2
relativeY: 0
}
PathAttribute {
name: "z"
value: 1
}
PathLine {
x: root.width
relativeY: 0
}
}
}

View file

@ -0,0 +1,55 @@
import "root:/config"
import Quickshell
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
visible: height > 0
implicitHeight: 0
implicitWidth: content.implicitWidth
states: State {
name: "visible"
when: root.visibilities.launcher
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
NumberAnimation {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
NumberAnimation {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Content {
id: content
visibilities: root.visibilities
}
}

View file

@ -0,0 +1,66 @@
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: BorderConfig.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
property real fullHeightRounding: wrapper.height >= QsWindow.window?.height - BorderConfig.thickness * 2 ? -rounding : rounding
strokeWidth: -1
fillColor: BorderConfig.colour
PathLine {
relativeX: -(root.wrapper.width + root.rounding)
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
relativeX: root.fullHeightRounding
relativeY: root.roundingY
radiusX: Math.abs(root.fullHeightRounding)
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: root.fullHeightRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.fullHeightRounding : root.wrapper.width
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: root.rounding
radiusX: root.rounding
radiusY: root.rounding
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on fullHeightRounding {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

View file

@ -0,0 +1,162 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import QtQuick
Item {
id: root
readonly property int padding: Appearance.padding.large
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: NotifsConfig.sizes.width + padding * 2
implicitHeight: {
const count = list.count;
if (count === 0)
return 0;
let height = (count - 1) * Appearance.spacing.smaller;
for (let i = 0; i < count; i++)
height += list.itemAtIndex(i)?.nonAnimHeight ?? 0;
const screen = QsWindow.window?.screen;
const visibilities = Visibilities.screens[screen];
const panel = Visibilities.panels[screen];
if (visibilities && panel) {
if (visibilities.osd) {
const h = panel.osd.y - BorderConfig.rounding * 2;
if (height > h)
height = h;
}
if (visibilities.session) {
const h = panel.session.y - BorderConfig.rounding * 2;
if (height > h)
height = h;
}
}
return Math.min((screen?.height ?? 0) - BorderConfig.thickness * 2, height + padding * 2);
}
ClippingWrapperRectangle {
anchors.fill: parent
anchors.margins: root.padding
color: "transparent"
radius: Appearance.rounding.normal
ListView {
id: list
model: ScriptModel {
values: [...Notifs.popups].reverse()
}
anchors.fill: parent
orientation: Qt.Vertical
spacing: 0
cacheBuffer: QsWindow.window?.screen.height ?? 0
delegate: Item {
id: wrapper
required property Notifs.Notif modelData
required property int index
readonly property alias nonAnimHeight: notif.nonAnimHeight
property int idx
onIndexChanged: {
if (index !== -1)
idx = index;
}
implicitWidth: notif.implicitWidth
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller)
ListView.onRemove: removeAnim.start()
SequentialAnimation {
id: removeAnim
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: true
}
PropertyAction {
target: wrapper
property: "enabled"
value: false
}
PropertyAction {
target: wrapper
property: "implicitHeight"
value: 0
}
PropertyAction {
target: wrapper
property: "z"
value: 1
}
Anim {
target: notif
property: "x"
to: (notif.x >= 0 ? NotifsConfig.sizes.width : -NotifsConfig.sizes.width) * 2
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.emphasized
}
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: false
}
}
ClippingRectangle {
anchors.top: parent.top
anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller
color: "transparent"
radius: notif.radius
implicitWidth: notif.implicitWidth
implicitHeight: notif.implicitHeight
Notification {
id: notif
modelData: wrapper.modelData
}
}
}
move: Transition {
Anim {
property: "y"
}
}
displaced: Transition {
Anim {
property: "y"
}
}
}
}
Behavior on implicitHeight {
Anim {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}

View file

@ -0,0 +1,489 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
required property Notifs.Notif modelData
readonly property bool hasImage: modelData.image.length > 0
readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
property bool expanded
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer
radius: Appearance.rounding.normal
implicitWidth: NotifsConfig.sizes.width
implicitHeight: inner.implicitHeight
x: NotifsConfig.sizes.width
Component.onCompleted: x = 0
RetainableLock {
object: root.modelData.notification
locked: true
}
MouseArea {
property int startY
anchors.fill: parent
hoverEnabled: true
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
preventStealing: true
onEntered: root.modelData.timer.stop()
onExited: root.modelData.timer.start()
drag.target: parent
drag.axis: Drag.XAxis
onPressed: event => {
startY = event.y;
if (event.button === Qt.MiddleButton)
root.modelData.notification.dismiss();
}
onReleased: event => {
if (Math.abs(root.x) < NotifsConfig.sizes.width * NotifsConfig.clearThreshold)
root.x = 0;
else
root.modelData.popup = false;
}
onPositionChanged: event => {
if (pressed) {
const diffY = event.y - startY;
if (Math.abs(diffY) > NotifsConfig.expandThreshold)
root.expanded = diffY > 0;
}
}
onClicked: event => {
if (!NotifsConfig.actionOnClick || event.button !== Qt.LeftButton)
return;
const actions = root.modelData.actions;
if (actions?.length === 1)
actions[0].invoke();
}
}
Behavior on x {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
Item {
id: inner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Appearance.padding.normal
implicitHeight: root.nonAnimHeight
Behavior on implicitHeight {
Anim {}
}
Loader {
id: image
active: root.hasImage
asynchronous: true
anchors.left: parent.left
anchors.top: parent.top
width: NotifsConfig.sizes.image
height: NotifsConfig.sizes.image
visible: root.hasImage || root.hasAppIcon
sourceComponent: ClippingRectangle {
radius: Appearance.rounding.full
implicitWidth: NotifsConfig.sizes.image
implicitHeight: NotifsConfig.sizes.image
Image {
anchors.fill: parent
source: Qt.resolvedUrl(root.modelData.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
}
}
}
Loader {
id: appIcon
active: root.hasAppIcon || !root.hasImage
asynchronous: true
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
anchors.right: root.hasImage ? image.right : undefined
anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: StyledRect {
radius: Appearance.rounding.full
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer
implicitWidth: root.hasImage ? NotifsConfig.sizes.badge : NotifsConfig.sizes.image
implicitHeight: root.hasImage ? NotifsConfig.sizes.badge : NotifsConfig.sizes.image
Loader {
id: icon
active: root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
visible: !root.modelData.appIcon.endsWith("symbolic")
width: Math.round(parent.width * 0.6)
height: Math.round(parent.width * 0.6)
sourceComponent: IconImage {
implicitSize: Math.round(parent.width * 0.6)
source: Quickshell.iconPath(root.modelData.appIcon)
asynchronous: true
}
}
Loader {
active: root.modelData.appIcon.endsWith("symbolic")
asynchronous: true
anchors.fill: icon
sourceComponent: Colouriser {
source: icon
colorizationColor: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer
}
}
Loader {
active: !root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
sourceComponent: MaterialIcon {
text: {
const summary = root.modelData.summary.toLowerCase();
if (summary.includes("reboot"))
return "restart_alt";
if (summary.includes("recording"))
return "screen_record";
if (summary.includes("battery"))
return "power";
if (summary.includes("screenshot"))
return "screenshot_monitor";
if (summary.includes("welcome"))
return "waving_hand";
if (summary.includes("time") || summary.includes("a break"))
return "schedule";
if (summary.includes("installed"))
return "download";
if (summary.includes("update"))
return "update";
if (summary.startsWith("file"))
return "folder_copy";
if (root.modelData.urgency === NotificationUrgency.Critical)
return "release_alert";
return "chat";
}
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer
font.pointSize: Appearance.font.size.large
}
}
}
}
StyledText {
id: appName
anchors.top: parent.top
anchors.left: image.right
anchors.leftMargin: Appearance.spacing.smaller
animate: true
text: appNameMetrics.elidedText
maximumLineCount: 1
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
TextMetrics {
id: appNameMetrics
text: root.modelData.appName
font.family: appName.font.family
font.pointSize: appName.font.pointSize
elide: Text.ElideRight
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
}
StyledText {
id: summary
anchors.top: parent.top
anchors.left: image.right
anchors.leftMargin: Appearance.spacing.smaller
animate: true
text: summaryMetrics.elidedText
maximumLineCount: 1
height: implicitHeight
states: State {
name: "expanded"
when: root.expanded
PropertyChanges {
summary.maximumLineCount: undefined
}
AnchorChanges {
target: summary
anchors.top: appName.bottom
}
}
transitions: Transition {
PropertyAction {
target: summary
property: "maximumLineCount"
}
AnchorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on height {
Anim {}
}
}
TextMetrics {
id: summaryMetrics
text: root.modelData.summary
font.family: summary.font.family
font.pointSize: summary.font.pointSize
elide: Text.ElideRight
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
}
StyledText {
id: timeSep
anchors.top: parent.top
anchors.left: summary.right
anchors.leftMargin: Appearance.spacing.small
text: "•"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
target: timeSep
anchors.left: appName.right
}
}
transitions: Transition {
AnchorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledText {
id: time
anchors.top: parent.top
anchors.left: timeSep.right
anchors.leftMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignLeft
text: root.modelData.timeStr
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
Item {
id: expandBtn
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: expandIcon.height
implicitHeight: expandIcon.height
StateLayer {
radius: Appearance.rounding.full
function onClicked() {
root.expanded = !root.expanded;
}
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
animate: true
text: root.expanded ? "expand_less" : "expand_more"
font.pointSize: Appearance.font.size.normal
}
}
StyledText {
id: bodyPreview
anchors.left: summary.left
anchors.right: expandBtn.left
anchors.top: summary.bottom
anchors.rightMargin: Appearance.spacing.small
animate: true
textFormat: Text.MarkdownText
text: bodyPreviewMetrics.elidedText
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
opacity: root.expanded ? 0 : 1
Behavior on opacity {
Anim {}
}
}
TextMetrics {
id: bodyPreviewMetrics
text: root.modelData.body
font.family: bodyPreview.font.family
font.pointSize: bodyPreview.font.pointSize
elide: Text.ElideRight
elideWidth: bodyPreview.width
}
StyledText {
id: body
anchors.left: summary.left
anchors.right: expandBtn.left
anchors.top: summary.bottom
anchors.rightMargin: Appearance.spacing.small
animate: true
textFormat: Text.MarkdownText
text: root.modelData.body
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
RowLayout {
id: actions
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: body.bottom
anchors.topMargin: Appearance.spacing.small
spacing: Appearance.spacing.smaller
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
Repeater {
model: root.modelData.actions
delegate: StyledRect {
id: action
required property NotificationAction modelData
radius: Appearance.rounding.full
color: Colours.palette.m3surfaceContainerHigh
Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2
Layout.preferredHeight: actionText.height + Appearance.padding.small * 2
implicitWidth: actionText.width + Appearance.padding.normal * 2
implicitHeight: actionText.height + Appearance.padding.small * 2
StateLayer {
radius: Appearance.rounding.full
function onClicked(): void {
action.modelData.invoke();
}
}
StyledText {
id: actionText
anchors.centerIn: parent
text: actionTextMetrics.elidedText
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
TextMetrics {
id: actionTextMetrics
text: modelData.text
font.family: actionText.font.family
font.pointSize: actionText.font.pointSize
elide: Text.ElideRight
elideWidth: {
const numActions = root.modelData.actions.length;
return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2;
}
}
}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -0,0 +1,14 @@
import "root:/config"
import QtQuick
Item {
id: root
visible: height > 0
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth
Content {
id: content
}
}

View file

@ -0,0 +1,63 @@
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: BorderConfig.rounding
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
strokeWidth: -1
fillColor: BorderConfig.colour
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
PathLine {
relativeX: -(root.wrapper.width - root.roundingX * 2)
relativeY: 0
}
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 2
relativeY: 0
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

43
modules/osd/Content.qml Normal file
View file

@ -0,0 +1,43 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Column {
id: root
required property Brightness.Monitor monitor
padding: Appearance.padding.large
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Appearance.spacing.normal
VerticalSlider {
icon: {
if (Audio.muted)
return "no_sound";
if (value >= 0.5)
return "volume_up";
if (value > 0)
return "volume_down";
return "volume_mute";
}
value: Audio.volume
onMoved: Audio.setVolume(value)
implicitWidth: OsdConfig.sizes.sliderWidth
implicitHeight: OsdConfig.sizes.sliderHeight
}
VerticalSlider {
icon: `brightness_${(Math.round(value * 6) + 1)}`
value: root.monitor?.brightness ?? 0
onMoved: root.monitor?.setBrightness(value)
implicitWidth: OsdConfig.sizes.sliderWidth
implicitHeight: OsdConfig.sizes.sliderHeight
}
}

View file

@ -0,0 +1,48 @@
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Scope {
id: root
required property ShellScreen screen
required property PersistentProperties visibilities
required property bool hovered
readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(screen)
function show(): void {
root.visibilities.osd = true;
timer.restart();
}
Connections {
target: Audio
function onMutedChanged(): void {
root.show();
}
function onVolumeChanged(): void {
root.show();
}
}
Connections {
target: root.monitor
function onBrightnessChanged(): void {
root.show();
}
}
Timer {
id: timer
interval: OsdConfig.hideDelay
onTriggered: {
if (!root.hovered)
root.visibilities.osd = false;
}
}
}

57
modules/osd/Wrapper.qml Normal file
View file

@ -0,0 +1,57 @@
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Item {
id: root
required property ShellScreen screen
required property bool visibility
visible: width > 0
implicitWidth: 0
implicitHeight: content.implicitHeight
states: State {
name: "visible"
when: root.visibility
PropertyChanges {
root.implicitWidth: content.implicitWidth
}
}
transitions: [
Transition {
from: ""
to: "visible"
NumberAnimation {
target: root
property: "implicitWidth"
duration: Appearance.anim.durations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
},
Transition {
from: "visible"
to: ""
NumberAnimation {
target: root
property: "implicitWidth"
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Content {
id: content
monitor: Brightness.getMonitorForScreen(root.screen)
}
}

View file

@ -0,0 +1,63 @@
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: BorderConfig.rounding
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
strokeWidth: -1
fillColor: BorderConfig.colour
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
PathLine {
relativeX: -(root.wrapper.width - root.roundingX * 2)
relativeY: 0
}
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 2
relativeY: 0
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

119
modules/session/Content.qml Normal file
View file

@ -0,0 +1,119 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Io
import QtQuick
Column {
id: root
required property PersistentProperties visibilities
padding: Appearance.padding.large
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Appearance.spacing.large
SessionButton {
id: logout
icon: "logout"
command: ["uwsm", "stop"]
KeyNavigation.down: shutdown
Connections {
target: root.visibilities
function onSessionChanged(): void {
if (root.visibilities.session)
logout.focus = true;
}
}
}
SessionButton {
id: shutdown
icon: "power_settings_new"
command: ["systemctl", "poweroff"]
KeyNavigation.up: logout
KeyNavigation.down: hibernate
}
AnimatedImage {
width: SessionConfig.sizes.button
height: SessionConfig.sizes.button
sourceSize.width: width
sourceSize.height: height
playing: visible
asynchronous: true
speed: 0.7
source: "root:/assets/kurukuru.gif"
}
SessionButton {
id: hibernate
icon: "downloading"
command: ["systemctl", "hibernate"]
KeyNavigation.up: shutdown
KeyNavigation.down: reboot
}
SessionButton {
id: reboot
icon: "cached"
command: ["systemctl", "reboot"]
KeyNavigation.up: hibernate
}
component SessionButton: StyledRect {
id: button
required property string icon
required property list<string> command
implicitWidth: SessionConfig.sizes.button
implicitHeight: SessionConfig.sizes.button
radius: Appearance.rounding.large
color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer
Keys.onEnterPressed: proc.startDetached()
Keys.onReturnPressed: proc.startDetached()
Keys.onEscapePressed: root.visibilities.session = false
Process {
id: proc
command: button.command
}
StateLayer {
radius: parent.radius
function onClicked(): void {
proc.startDetached();
}
}
MaterialIcon {
anchors.centerIn: parent
text: button.icon
color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
font.pointSize: Appearance.font.size.extraLarge
}
}
}

View file

@ -0,0 +1,56 @@
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
visible: width > 0
implicitWidth: 0
implicitHeight: content.implicitHeight
states: State {
name: "visible"
when: root.visibilities.session
PropertyChanges {
root.implicitWidth: content.implicitWidth
}
}
transitions: [
Transition {
from: ""
to: "visible"
NumberAnimation {
target: root
property: "implicitWidth"
duration: Appearance.anim.durations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
},
Transition {
from: "visible"
to: ""
NumberAnimation {
target: root
property: "implicitWidth"
duration: root.visibilities.osd ? Appearance.anim.durations.expressiveFastSpatial : Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: root.visibilities.osd ? Appearance.anim.curves.expressiveFastSpatial : Appearance.anim.curves.emphasized
}
}
]
Content {
id: content
visibilities: root.visibilities
}
}