Watch
1
0
Fork
You've already forked souveraine
0

publish: the public projection begins here

This is a projection, not a development branch. The tree above was constructed
from the internal source named below under a manifest that decides which paths
may leave, then scanned as a whole tree rather than as a series of patches, and
only then published.

Public history starts here because the history before it was not admissible,
and neither was the tree. What used to stand in this repository included a
rescue copy of another machine, a directory of phone handoffs, deployment
wired to one house, and a submodule pointing at a forge no stranger can reach.
None of that was ever the product. It stays in the private forge, which is
allowed to hold the whole working organism, and this is what was deliberately
sent out instead.

Three mechanisms produced this tree, in decreasing order of trust. A top-level
path the manifest does not name never arrives at all, which is the one that
catches directories nobody has thought of yet. Named internal files inside
admitted roots are dropped. A short, reviewed table replaces deployment
defaults that a public build must not carry -- an endpoint aimed at one LAN, a
VPN profile belonging to one phone, packaging built from one checkout path.

Everything after this commit is an ordinary publication with the same three
trailers, so a force push stops being routine and starts meaning that
something deliberate happened. The trailers bind the projection to its source
without pretending the public SHA is the private one: same lineage, different
tree, and the record says so.

Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047
Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27
Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
Fimeg 2026-09-04 15:55:48 -04:00
commit 8f42fc953d
1476 changed files with 238455 additions and 0 deletions

View file

@ -0,0 +1,37 @@
## Waffle
A recreation of Windoes. It's WIP!
- If you install illogical-impulse fully, you can press Super+Alt+W to switch to this style.
- If you're just copying the Quickshell config, run the config as usual (`qs -c ii`) then run `qs -c ii ipc call panelFamily cycle`
## From EWW version to Quickshell
Just a reflection, in case anyone's interested. My blog is probably a better place for this, but it does not exist. Besides, this is going to change as I do more stuff. Currently there's just the bar.
### Improvements
- QtQuick's `Button` has the `{top/bottom/left/right}Inset` properties, so we can have clickable regions expanding beyond the button background for free. With EWW it was annoying to wrap the button content with an `eventbox` that has some padding, then somehow use CSS selectors to make sure hovering effects work. I have to admit, (a large) part of that annoyance was with how bad my copy-pasting coding practice was at the time, but still...
- Fancy effects: Gtk3 CSS does not support transformations. In QtQuick we can smack `rotation` and `scale` almost everywhere, so it's simple to make bouncy icons and rotating chevrons
- Quickshell provides a system tray service (EWW does now but didn't at the time I created the EWW Windoes version), so now there's no Waybar needed for the tray.
- QtQuick has `Loader`s, so we can have this live-switchable from the main style without killing the widget system, moving styles to the correct folder, and relaunching.
- This time my computer is powerful enough to run a VM, so I don't have to occasionally reboot to take quick screenshots for reference. I try to make everything pixel-perfect so this is necessary. Speaking about pixel-perfectness, in the EWW version I hardcoded sizes, but this time I'm still doing that (lol), BUT that's normal and not a problem because Qt has the `QT_SCALE_FACTOR` env var for scaling. (Please feel free to prove me wrong in saying Gtk3 doesn't have that magic)
### Challenges
- Qt is not Gtk and definitely not React
- We don't get directional border on QtQuick `Rectangle`s like in CSS. I was able to get around this with manual drawing, but it was a bit more work
- Fluent Icons is difficult to use, compared to Material Symbols
- No React, so no clean use via a library.
- If we use the font, there's no proper, searchable **codepoint** cheatsheet like Nerd Fonts, and there's no ligatures
- I resorted to downloading individual SVGs. Not that nice, but it's better than scanning the whole table of icons every time I want one. For this we have fluenticon.com and fluenticons.co, but icons are awkwardly named and there's no alias. Why is the reload/refresh icon called "arrow-sync"? Well, the name is not misleading, but arguably reload/refresh are more common actions. From Fluent Design's [page on Iconography](https://fluent2.microsoft.design/iconography):
> Fluent system icons are literal metaphors and are named for the shape or object they represent, not the functionality they provide
"sync" is functionality.

View file

@ -0,0 +1,63 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter.mainPage
WBarAttachedPanelContent {
id: root
readonly property bool barAtBottom: Config.options.waffles.bar.bottom
contentItem: ColumnLayout {
// This somewhat sophisticated anchoring is needed to make opening anim not jump abruptly when stuff appear
anchors {
left: parent.left
right: parent.right
top: root.barAtBottom ? undefined : parent.top
bottom: root.barAtBottom ? parent.bottom : undefined
margins: root.visualMargin
bottomMargin: 0
}
spacing: 12
WPane {
opacity: (MprisController.activePlayer != null && MprisController.isRealPlayer(MprisController.activePlayer)) ? 1 : 0
Layout.fillWidth: true
contentItem: MediaPaneContent {}
}
WPane {
Layout.fillWidth: true
contentItem: WStackView {
id: stackView
implicitWidth: initItem.implicitWidth
implicitHeight: initItem.implicitHeight
initialItem: WPanelPageColumn {
id: initItem
MainPageBody {}
WPanelSeparator {}
MainPageFooter {}
}
Component.onCompleted: {
ActionCenterContext.stackView = this;
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton
onClicked: {
ActionCenterContext.back();
}
}
}
}
}
}

View file

@ -0,0 +1,23 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
Singleton {
id: root
property StackView stackView
function push(component) {
if (stackView) {
stackView.push(component)
}
}
function back() {
if (stackView && stackView.depth > 1) {
stackView.pop()
}
}
}

View file

@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
WChoiceButton {
id: root
property bool expanded: false
checked: expanded
clip: true
horizontalPadding: 12
verticalPadding: 6
animateChoiceHighlight: false
Behavior on implicitHeight {
animation: Looks.transition.resize.createObject(this)
}
onClicked: expanded = !expanded
}

View file

@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
RowLayout {
id: root
required property string title
spacing: 4
WPanelIconButton {
iconName: "arrow-left"
onClicked: ActionCenterContext.back()
}
WText {
id: titleText
Layout.fillWidth: true
elide: Text.ElideRight
text: root.title
font.pixelSize: Looks.font.pixelSize.large
}
}

View file

@ -0,0 +1,156 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
Rectangle {
id: root
implicitHeight: 176
implicitWidth: 358
color: Looks.colors.bgPanelBody
anchors.fill: parent
readonly property var activePlayer: MprisController.activePlayer
Column {
anchors {
fill: parent
leftMargin: 23
rightMargin: 23
topMargin: 16
bottomMargin: 20
}
spacing: 25
AppInfoRow {
anchors {
left: parent.left
right: parent.right
}
}
TrackInfoRow {
anchors {
left: parent.left
right: parent.right
}
}
ControlButtonsRow {
anchors.horizontalCenter: parent.horizontalCenter
}
}
component AppInfoRow: RowLayout {
id: appInfo
spacing: 8
property var desktopEntry: {
const desktopEntryString = root.activePlayer?.desktopEntry ?? "";
return DesktopEntries.byId(desktopEntryString);
}
FluentIcon {
implicitSize: 20
icon: appInfo.desktopEntry?.icon || "music-note-2"
monochrome: !appInfo.desktopEntry?.icon
}
WText {
Layout.fillWidth: true
text: appInfo.desktopEntry?.name ?? Translation.tr("Media")
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
}
}
component TrackInfoRow: RowLayout {
spacing: 16
ColumnLayout {
id: trackInfo
Layout.fillWidth: true
spacing: 0
WText {
Layout.fillWidth: true
font.weight: Looks.font.weight.strong
font.pixelSize: Looks.font.pixelSize.large
elide: Text.ElideRight
text: StringUtils.cleanMusicTitle(root.activePlayer?.trackTitle) || Translation.tr("Unknown Title")
}
WText {
Layout.fillWidth: true
elide: Text.ElideRight
text: root.activePlayer?.trackArtist || Translation.tr("Unknown Artist")
}
}
StyledImage {
id: artImage
Layout.preferredWidth: 58
Layout.preferredHeight: trackInfo.implicitHeight
source: MprisController.activeTrack?.artUrl || ""
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Item {
width: artImage.width
height: artImage.height
Rectangle {
anchors.centerIn: parent
width: artImage.paintedWidth
height: artImage.paintedHeight
radius: Looks.radius.medium
}
}
}
}
}
component ControlButtonsRow: RowLayout {
spacing: 26
MediaControlButton {
iconName: "previous"
enabled: root.activePlayer?.canGoPrevious ?? false
onClicked: root.activePlayer?.previous()
}
MediaControlButton {
readonly property bool playing: root.activePlayer?.isPlaying ?? false
iconName: playing ? "pause" : "play"
enabled: (playing && root.activePlayer?.canPause) || (!playing && root.activePlayer?.canPlay)
onClicked: root.activePlayer?.togglePlaying()
}
MediaControlButton {
iconName: "next"
enabled: root.activePlayer?.canGoNext ?? false
onClicked: root.activePlayer?.next()
}
}
component MediaControlButton: WBorderlessButton {
id: controlButton
required property string iconName
implicitHeight: 40
implicitWidth: 40
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: controlButton.iconName
monochrome: true
filled: true
implicitSize: 18
}
}
}
}

View file

@ -0,0 +1,20 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
WText {
Layout.leftMargin: 12
Layout.rightMargin: 12
Layout.topMargin: 6
Layout.bottomMargin: 6
font {
weight: Looks.font.weight.stronger
pixelSize: Looks.font.pixelSize.large
}
}

View file

@ -0,0 +1,82 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
RowLayout {
id: root
required property string name
property alias description: descriptionText.text
property alias iconName: iconWidget.icon
property alias checked: switchWidget.checked
spacing: 10
FluentIcon {
id: iconWidget
visible: !!root.iconName
Layout.leftMargin: 12
Layout.topMargin: 4
Layout.bottomMargin: 4
Layout.alignment: Qt.AlignTop
icon: root.iconName
implicitSize: 18
}
ColumnLayout {
Layout.topMargin: 4
Layout.bottomMargin: 4
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
spacing: 1
// Name
WText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Looks.font.pixelSize.large
text: root.name
}
// Description
WText {
id: descriptionText
Layout.fillWidth: true
wrapMode: Text.Wrap
color: Looks.colors.subfg
}
}
MouseArea {
Layout.rightMargin: 12
implicitWidth: switchRow.implicitWidth
implicitHeight: switchRow.implicitHeight
onPressed: switchWidget.down = true
onReleased: switchWidget.down = false
onClicked: switchWidget.checked = !switchWidget.checked
Row {
id: switchRow
spacing: 12
WTextWithFixedWidth {
longestText: "Off" // The larger one
text: switchWidget.checked ? Translation.tr("On") : Translation.tr("Off")
font.pixelSize: Looks.font.pixelSize.large
}
WSwitch {
id: switchWidget
Layout.alignment: Qt.AlignVCenter
}
}
}
}

View file

@ -0,0 +1,104 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Scope {
id: root
Connections {
target: GlobalStates
function onSidebarLeftOpenChanged() {
if (GlobalStates.sidebarLeftOpen)
panelLoader.active = true;
}
}
Loader {
id: panelLoader
active: GlobalStates.sidebarLeftOpen
sourceComponent: PanelWindow {
id: panelWindow
exclusiveZone: 0
WlrLayershell.namespace: "quickshell:actionCenter"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
anchors {
bottom: Config.options.waffles.bar.bottom
top: !Config.options.waffles.bar.bottom
right: true
}
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
HyprlandFocusGrab {
id: focusGrab
active: true
windows: [panelWindow]
onCleared: content.close()
}
Connections {
target: GlobalStates
function onSidebarLeftOpenChanged() {
if (!GlobalStates.sidebarLeftOpen)
content.close();
}
}
ActionCenterContent {
id: content
anchors.fill: parent
onClosed: {
GlobalStates.sidebarLeftOpen = false;
panelLoader.active = false;
}
}
}
}
function toggleOpen() {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
IpcHandler {
target: "sidebarLeft"
function toggle() {
root.toggleOpen();
}
}
GlobalShortcut {
name: "sidebarLeftToggle"
description: "Toggles left sidebar on press"
onPressed: root.toggleOpen()
}
IpcHandler {
target: "mediaControls"
function toggle(): void {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
}
GlobalShortcut {
name: "mediaControlsToggle"
description: "Toggles media controls on press"
onPressed: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
}
}

View file

@ -0,0 +1,129 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Bluetooth
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
Item {
id: root
Component.onCompleted: {
if (Bluetooth.defaultAdapter.enabled) Bluetooth.defaultAdapter.discovering = true;
}
Component.onDestruction: {
Bluetooth.defaultAdapter.discovering = false;
}
WPanelPageColumn {
anchors.fill: parent
BodyRectangle {
implicitHeight: 400
implicitWidth: 50
ColumnLayout {
anchors.fill: parent
anchors.margins: 4
spacing: 4
ColumnLayout {
implicitHeight: headerRow.implicitHeight
Layout.fillWidth: true
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: 0
HeaderRow {
id: headerRow
Layout.fillWidth: true
title: Translation.tr("Bluetooth")
}
WSwitch {
id: toggleSwitch
Layout.rightMargin: 12
checked: Bluetooth.defaultAdapter?.enabled ?? false
onCheckedChanged: {
if (Bluetooth.defaultAdapter) {
Bluetooth.defaultAdapter.enabled = checked;
if (checked) {
Bluetooth.defaultAdapter.discovering = true;
} else {
Bluetooth.defaultAdapter.discovering = false;
}
}
}
}
}
FadeLoader {
Layout.leftMargin: -4
Layout.rightMargin: -4
Layout.fillWidth: true
shown: Bluetooth.defaultAdapter?.discovering ?? false
visible: true
sourceComponent: WIndeterminateProgressBar {}
}
}
StyledListView {
id: listView
Layout.fillHeight: true
Layout.fillWidth: true
animateAppearance: false
contentHeight: contentLayout.implicitHeight
contentWidth: width
clip: true
spacing: 4
model: ScriptModel {
values: BluetoothStatus.friendlyDeviceList
}
delegate: BluetoothDeviceItem {
required property BluetoothDevice modelData
device: modelData
width: ListView.view.width
}
}
}
}
WPanelSeparator {}
FooterRectangle {
WTextButton {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
}
text: Translation.tr("More Bluetooth settings")
onClicked: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarLeft", "toggle"]);
Quickshell.execDetached(["bash", "-c", Config.options.apps.bluetooth]);
}
}
WBorderlessButton {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 12
enabled: !Bluetooth.defaultAdapter?.discovering && Bluetooth.defaultAdapter?.enabled
onClicked: {
Bluetooth.defaultAdapter.discovering = true;
}
contentItem: FluentIcon {
icon: "arrow-counterclockwise"
}
}
}
}
}

View file

@ -0,0 +1,89 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
ExpandableChoiceButton {
id: root
required property BluetoothDevice device
contentItem: RowLayout {
id: contentItem
spacing: 20
// Device icon
FluentIcon {
Layout.topMargin: 4
Layout.bottomMargin: 4
Layout.alignment: Qt.AlignTop
icon: WIcons.bluetoothDeviceIcon(root?.device)
implicitSize: 18
}
ColumnLayout {
Layout.topMargin: 4
Layout.bottomMargin: 4
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
spacing: 0
WText {
// Network name
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Looks.font.pixelSize.large
text: root.device?.name || Translation.tr("Unknown device")
textFormat: Text.PlainText
}
WText { // Status
id: statusText
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Looks.font.pixelSize.large
color: Looks.colors.subfg
visible: root.device?.connected || root.expanded
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
text: {
if (!root.device?.paired)
return Translation.tr("Not connected");
let statusText = root.device?.connected ? Translation.tr("Connected") : Translation.tr("Paired");
if (!root.device?.batteryAvailable)
return statusText;
statusText += ` • ${Math.round(root.device?.battery * 100)}%`;
return statusText;
}
}
WButton {
Layout.alignment: Qt.AlignRight
horizontalAlignment: Text.AlignHCenter
visible: root.expanded
checked: !(root.device?.connected ?? false)
colBackground: Looks.colors.bg2
colBackgroundHover: Looks.colors.bg2Hover
colBackgroundActive: Looks.colors.bg2Active
implicitHeight: 30
implicitWidth: 148
text: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect")
onClicked: {
if (root.device?.connected) {
root.device.disconnect();
} else {
root.device.connect();
}
}
}
}
}
}

View file

@ -0,0 +1,37 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
BodyRectangle {
id: root
implicitHeight: contentLayout.implicitHeight
ColumnLayout {
id: contentLayout
anchors.fill: parent
spacing: 0
MainPageBodyToggles {
id: togglesContainer
Layout.fillWidth: true
}
Rectangle {
implicitHeight: 1
Layout.fillWidth: true
color: Looks.colors.bg1Border
}
MainPageBodySliders {
Layout.margins: 12
Layout.topMargin: 18
Layout.bottomMargin: 14
}
}
}

View file

@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
import qs.modules.waffle.actionCenter.volumeControl
ColumnLayout {
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
spacing: 12
RowLayout {
spacing: 4
WPanelIconButton {
color: colBackground
property real animationValue: root.brightnessMonitor?.brightness ?? 0
rotation: animationValue * 180
scale: 0.8 + animationValue * 0.2
iconName: "weather-sunny"
Behavior on animationValue {
animation: Looks.transition.longMovement.createObject(this)
}
}
WSlider {
Layout.fillWidth: true
value: root.brightnessMonitor?.brightness ?? 0
scrollable: true
onMoved: {
root.brightnessMonitor?.setBrightness(value)
}
}
WPanelIconButton {
opacity: 0
}
}
RowLayout {
spacing: 4
WPanelIconButton {
iconName: WIcons.volumeIcon
onClicked: Audio.toggleMute();
}
WSlider {
Layout.fillWidth: true
value: Audio.sink.audio.volume
scrollable: true
onMoved: {
Audio.sink.audio.volume = value;
}
}
WPanelIconButton {
Component {
id: volumeControlComp
VolumeControl {}
}
onClicked: {
ActionCenterContext.push(volumeControlComp)
}
contentItem: Item {
anchors.centerIn: parent
Row {
anchors.centerIn: parent
spacing: -1
FluentIcon {
anchors.verticalCenter: parent.verticalCenter
implicitSize: 18
icon: "options"
}
FluentIcon {
anchors.verticalCenter: parent.verticalCenter
implicitSize: 12
icon: "chevron-right"
}
}
}
}
}
}

View file

@ -0,0 +1,110 @@
pragma ComponentBehavior: Bound
import Qt.labs.synchronizer
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.models.quickToggles
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter.toggles
Item {
id: root
property int columns: 3
property int rows: 2
property int currentPage: 0
readonly property int itemsPerPage: columns * rows
readonly property int pages: Math.ceil(toggles.length / itemsPerPage)
property list<string> toggles: Config.options.waffles.actionCenter.toggles
property real padding: 22
property real reducedBottomPadding: 12
implicitHeight: swipeView.implicitHeight + (padding - swipeView.padding) * 2 - reducedBottomPadding
function togglesInPage(index) {
var start = index * root.itemsPerPage;
var end = start + root.itemsPerPage;
return root.toggles.slice(start, end);
}
function decreasePage() {
if (root.currentPage > 0) {
root.currentPage -= 1;
}
}
function increasePage() {
if (root.currentPage < (root.pages - 1)) {
root.currentPage += 1;
}
}
clip: true
SwipeView {
id: swipeView
anchors {
fill: parent
topMargin: root.padding - swipeView.padding
bottomMargin: root.padding - swipeView.padding - root.reducedBottomPadding
}
padding: 4
leftPadding: root.padding
rightPadding: root.padding
spacing: padding
orientation: Qt.Vertical
clip: true
Synchronizer on currentIndex {
property alias source: root.currentPage
}
Repeater {
model: root.pages
delegate: GridLayout {
id: grid
required property int index
// width: SwipeView.view.width - root.padding * 2
// height: SwipeView.view.height - root.padding * 2
columns: root.columns
rows: root.rows
rowSpacing: 12
columnSpacing: 12
Repeater {
model: ScriptModel {
values: root.togglesInPage(grid.index)
}
delegate: ActionCenterTogglesDelegateChooser {}
}
}
}
}
VerticalPageIndicator {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 6
currentIndex: root.currentPage
count: root.pages
onClicked: (index) => root.currentPage = index
onIncreasePage: root.increasePage();
onDecreasePage: root.decreasePage();
}
FocusedScrollMouseArea {
z: 999
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: false
onScrollUp: root.decreasePage();
onScrollDown: root.increasePage();
}
}

View file

@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
FooterRectangle {
// Battery button
WBorderlessButton {
visible: Battery.available
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 12
contentItem: Row {
spacing: 4
FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: WIcons.batteryLevelIcon
FluentIcon {
anchors.fill: parent
icon: WIcons.batteryIcon
}
}
WText {
anchors.verticalCenter: parent.verticalCenter
text: `${Math.round(Battery.percentage * 100) || 0}%`
}
}
}
// Settings button
WBorderlessButton {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 12
onClicked: {
GlobalStates.sidebarLeftOpen = false;
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("settings.qml")]);
}
contentItem: FluentIcon {
icon: "settings"
}
}
}

View file

@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Bluetooth
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
Item {
id: root
Component.onCompleted: {
if (Bluetooth.defaultAdapter.enabled)
Bluetooth.defaultAdapter.discovering = true;
}
Component.onDestruction: {
Bluetooth.defaultAdapter.discovering = false;
}
WPanelPageColumn {
anchors.fill: parent
BodyRectangle {
implicitHeight: 400
implicitWidth: 50
ColumnLayout {
anchors.fill: parent
anchors.margins: 4
spacing: 4
HeaderRow {
id: headerRow
Layout.fillWidth: true
title: Translation.tr("Eye protection")
}
StyledFlickable {
id: flickable
Layout.fillHeight: true
Layout.fillWidth: true
contentHeight: contentLayout.implicitHeight
contentWidth: width
clip: true
bottomMargin: 12
NightLightOptions {
id: contentLayout
width: flickable.width
}
}
}
}
WPanelSeparator {}
FooterRectangle {}
}
component NightLightOptions: ColumnLayout {
spacing: 10
SectionText {
text: Translation.tr("Night Light")
}
ToggleItem {
name: Translation.tr("Automatic")
description: Translation.tr("Turn on from sunset to sunrise")
iconName: "auto"
checked: Config.options.light.night.automatic
onCheckedChanged: {
Config.options.light.night.automatic = checked;
}
}
ToggleItem {
name: Translation.tr("Enable now")
description: Translation.tr("More comfortable viewing at night")
iconName: WIcons.nightLightIcon
checked: Hyprsunset.temperatureActive
onCheckedChanged: {
Hyprsunset.toggleTemperature(checked);
}
}
IntensityEntry {
Layout.fillWidth: true
}
SectionText {
text: Translation.tr("Anti-flashbang (experimental)")
}
ToggleItem {
name: Translation.tr("Enable")
description: Translation.tr("Balance brightness based on content")
iconName: "flash-off"
checked: Config.options.light.antiFlashbang.enable
onCheckedChanged: {
Config.options.light.antiFlashbang.enable = checked;
}
}
}
component IntensityEntry: RowLayout {
spacing: 10
FluentIcon {
id: iconWidget
Layout.leftMargin: 12
Layout.topMargin: 4
Layout.bottomMargin: 4
Layout.alignment: Qt.AlignTop
icon: "temperature"
implicitSize: 18
}
ColumnLayout {
Layout.fillWidth: true
// Layout.leftMargin: 40
Layout.rightMargin: 12
spacing: 4
ColumnLayout {
Layout.fillWidth: true
spacing: 0
WText {
Layout.fillWidth: true
text: Translation.tr("Intensity")
font.pixelSize: Looks.font.pixelSize.large
}
WText {
Layout.fillWidth: true
text: Translation.tr("Adjust the color temperature")
color: Looks.colors.subfg
}
}
WSlider {
Layout.fillWidth: true
from: 6500
to: 1200
value: Config.options.light.night.colorTemperature
onMoved: Config.options.light.night.colorTemperature = value
tooltipContent: Math.round((value - from) / (to - from) * 100)
}
}
}
}

View file

@ -0,0 +1,118 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
// It should be perfectly fine to use just a Column here, but somehow
// using ColumnLayout prevents weird opening anim stutter
ColumnLayout {
id: root
required property QuickToggleModel toggleModel
property string name: toggleModel?.name ?? ""
property string statusText: (toggleModel?.hasStatusText) ? (toggleModel?.statusText || (toggled ? Translation.tr("Active") : Translation.tr("Inactive"))) : ""
property string tooltipText: toggleModel?.tooltipText ?? ""
required property string icon
property bool available: toggleModel?.available ?? true
property bool toggled: toggleModel?.toggled ?? false
property var mainAction: toggleModel?.mainAction ?? null
property var altAction: toggleModel?.hasMenu ? (() => root.openMenu()) : (toggleModel?.altAction ?? null)
property bool hasMenu: toggleModel?.hasMenu ?? false
property Component menu
property color colBackground: toggled ? Looks.colors.accent : Looks.colors.bg2
property color colBackgroundHovered: toggled ? Looks.colors.accentHover : Looks.colors.bg2Hover
property color colBackgroundActive: toggled ? Looks.colors.accentActive : Looks.colors.bg2Active
property color colBorder: toggled ? Looks.colors.accentHover : Looks.colors.bg2Border
property color colForeground: toggled ? Looks.colors.accentFg : Looks.colors.fg1
spacing: 0
property real wholeToggleWidth: 96
AcrylicRectangle {
Layout.fillWidth: true
implicitWidth: root.wholeToggleWidth
implicitHeight: 48
color: root.colBackground
radius: Looks.radius.medium
RowLayout {
anchors.fill: parent
spacing: 0
ToggleFragment {
topLeftRadius: Looks.radius.medium
bottomLeftRadius: Looks.radius.medium
topRightRadius: root.hasMenu ? 0 : Looks.radius.medium
bottomRightRadius: root.hasMenu ? 0 : Looks.radius.medium
iconName: root.icon
onClicked: root.mainAction && root.mainAction()
}
FadeLoader {
Layout.fillHeight: true
Layout.fillWidth: true
shown: root.hasMenu
sourceComponent: ToggleFragment {
topLeftRadius: 0
bottomLeftRadius: 0
topRightRadius: Looks.radius.medium
bottomRightRadius: Looks.radius.medium
iconName: "chevron-right"
onClicked: {
ActionCenterContext.stackView.push(root.menu)
}
}
}
}
}
Item {
id: toggleNameWidget
implicitWidth: root.wholeToggleWidth
implicitHeight: 36
WText {
id: toggleNameText
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
}
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
text: root.name
}
}
component ToggleFragment: WButton {
id: toggleFragment
required property string iconName
Layout.fillHeight: true
Layout.fillWidth: true
inset: 0
backgroundOpacity: 0.8
checked: root.toggled
border.width: 1
border.color: root.colBorder
contentItem: Item {
anchors.centerIn: parent
FluentIcon {
anchors.centerIn: parent
icon: toggleFragment.iconName
implicitSize: 18
monochrome: true
filled: root.toggled
color: root.colForeground
}
}
}
}

View file

@ -0,0 +1,154 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter.bluetooth
import qs.modules.waffle.actionCenter.nightLight
import qs.modules.waffle.actionCenter.volumeControl
import qs.modules.waffle.actionCenter.wifi
DelegateChooser {
id: root
// role: "type" is implied by usage
DelegateChoice {
roleValue: "antiFlashbang"
ActionCenterToggleButton {
toggleModel: AntiFlashbangToggle {}
icon: "flash-off"
menu: Component {
NightLightControl {}
}
}
}
DelegateChoice {
roleValue: "bluetooth"
ActionCenterToggleButton {
toggleModel: BluetoothToggle {}
name: toggleModel.statusText
icon: WIcons.bluetoothIcon
menu: Component {
BluetoothControl {}
}
}
}
DelegateChoice {
roleValue: "cloudflareWarp"
ActionCenterToggleButton {
toggleModel: CloudflareWarpToggle {}
icon: "cloudflare"
}
}
DelegateChoice {
roleValue: "colorPicker"
ActionCenterToggleButton {
toggleModel: ColorPickerToggle {}
icon: "eyedropper"
}
}
DelegateChoice {
roleValue: "darkMode"
ActionCenterToggleButton {
toggleModel: DarkModeToggle {}
icon: "dark-theme"
}
}
DelegateChoice {
roleValue: "easyEffects"
ActionCenterToggleButton {
toggleModel: EasyEffectsToggle {}
icon: "device-eq"
}
}
DelegateChoice {
roleValue: "gameMode"
ActionCenterToggleButton {
toggleModel: GameModeToggle {}
icon: "games"
}
}
DelegateChoice {
roleValue: "idleInhibitor"
ActionCenterToggleButton {
toggleModel: IdleInhibitorToggle {}
icon: "drink-coffee"
}
}
DelegateChoice {
roleValue: "mic"
ActionCenterToggleButton {
toggleModel: MicToggle {}
icon: WIcons.micIcon
menu: Component {
VolumeControl {
output: false
}
}
}
}
DelegateChoice {
roleValue: "musicRecognition"
ActionCenterToggleButton {
toggleModel: MusicRecognitionToggle {}
icon: "music-note-2"
}
}
DelegateChoice {
roleValue: "network"
ActionCenterToggleButton {
toggleModel: NetworkToggle {}
name: toggleModel.statusText
icon: WIcons.internetIcon
menu: Component {
WifiControl {}
}
}
}
DelegateChoice {
roleValue: "nightLight"
ActionCenterToggleButton {
toggleModel: NightLightToggle {}
icon: WIcons.nightLightIcon
menu: Component {
NightLightControl {}
}
}
}
DelegateChoice {
roleValue: "notifications"
ActionCenterToggleButton {
toggleModel: NotificationToggle {}
icon: WIcons.notificationsIcon
}
}
DelegateChoice {
roleValue: "onScreenKeyboard"
ActionCenterToggleButton {
toggleModel: OnScreenKeyboardToggle {}
icon: GlobalStates.oskOpen ? "keyboard-dock" : "keyboard"
}
}
DelegateChoice {
roleValue: "powerProfile"
ActionCenterToggleButton {
toggleModel: PowerProfilesToggle {}
icon: WIcons.powerProfileIcon
name: toggleModel.statusText
}
}
DelegateChoice {
roleValue: "screenSnip"
ActionCenterToggleButton {
toggleModel: ScreenSnipToggle {}
icon: "cut"
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
ActionCenterToggle {
id: root
name: Network.ethernet ? Translation.tr("Network") : Network.networkName
}

View file

@ -0,0 +1,159 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
Item {
id: root
property bool output: true
WPanelPageColumn {
anchors.fill: parent
BodyRectangle {
implicitHeight: 400
implicitWidth: 50
ColumnLayout {
anchors.fill: parent
anchors.margins: 4
spacing: 4
HeaderRow {
Layout.fillWidth: true
title: root.output ? Translation.tr("Sound output") : Translation.tr("Sound input")
}
StyledFlickable {
id: flickable
Layout.fillHeight: true
Layout.fillWidth: true
contentHeight: contentLayout.implicitHeight
contentWidth: width
clip: true
AudioChoices {
id: contentLayout
width: flickable.width
}
}
}
}
WPanelSeparator {}
FooterRectangle {
WButton {
id: moreSettingsButton
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
}
implicitHeight: 40
implicitWidth: contentItem.implicitWidth + 30
color: "transparent"
onClicked: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarLeft", "toggle"]);
Quickshell.execDetached(["bash", "-c", Config.options.apps.volumeMixer]);
}
contentItem: Item {
anchors.centerIn: parent
implicitWidth: buttonText.implicitWidth
WText {
id: buttonText
anchors.centerIn: parent
text: Translation.tr("More volume settings")
color: moreSettingsButton.pressed ? Looks.colors.fg : Looks.colors.fg1
}
}
}
}
}
component AudioChoices: ColumnLayout {
spacing: 4
SectionText {
text: root.output ? Translation.tr("Output device") : Translation.tr("Input device")
}
Repeater {
model: ScriptModel {
values: root.output ? Audio.outputDevices : Audio.inputDevices
}
delegate: WChoiceButton {
required property var modelData
icon.name: WIcons.audioDeviceIcon(modelData)
text: Audio.friendlyDeviceName(modelData)
checked: (root.output ? Audio.sink : Audio.source) === modelData
onClicked: {
if (root.output) Audio.setDefaultSink(modelData);
else Audio.setDefaultSource(modelData);
}
}
}
WPanelSeparator {
visible: EasyEffects.available && root.output
color: Looks.colors.bg2Hover
}
////////////////////////////////////////////////////////////
SectionText {
visible: EasyEffects.available && root.output
text: Translation.tr("Sound effects")
}
WChoiceButton {
visible: EasyEffects.available && root.output
text: Translation.tr("Off")
checked: !EasyEffects.active
onClicked: EasyEffects.disable()
}
WChoiceButton {
visible: EasyEffects.available && root.output
text: "EasyEffects"
checked: EasyEffects.active
onClicked: EasyEffects.enable()
}
WPanelSeparator {
color: Looks.colors.bg2Hover
}
////////////////////////////////////////////////////////////
SectionText {
visible: EasyEffects.available
text: Translation.tr("Volume mixer")
}
VolumeEntry {
node: root.output ? Audio.sink : Audio.source
icon: root.output ? "speaker" : "mic-on"
monochrome: true
}
Repeater {
model: ScriptModel {
values: root.output ? Audio.outputAppNodes : Audio.inputAppNodes
}
delegate: VolumeEntry {
required property var modelData
node: modelData
}
}
}
}

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
RowLayout {
id: root
required property PwNode node
property alias icon: iconButton.iconName
property alias monochrome: iconButton.monochrome
monochrome: false
PwObjectTracker { // Necessary for useful info to be present in 'node'
objects: [root.node]
}
WPanelIconButton {
id: iconButton
iconName: WIcons.audioAppIcon(root.node)
onClicked: root.node.audio.muted = !root.node?.audio.muted
FluentIcon {
id: muteIcon
visible: root.node?.audio.muted ?? false
anchors {
bottom: parent.bottom
right: parent.right
margins: -1
}
implicitSize: 16
icon: "speaker-mute"
}
WToolTip {
extraVisibleCondition: iconButton.shouldShowTooltip
text: Audio.appNodeDisplayName(root.node)
}
}
WSlider {
Layout.fillWidth: true
Layout.rightMargin: 10
value: root.node?.audio.volume ?? 0
onMoved: root.node.audio.volume = value
}
}

View file

@ -0,0 +1,101 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
ExpandableChoiceButton {
id: root
required property WifiAccessPoint wifiNetwork
contentItem: RowLayout {
id: contentItem
spacing: 12
FluentIcon { // Duotone hack
Layout.bottomMargin: 2
Layout.alignment: Qt.AlignTop
property int strength: root.wifiNetwork?.strength ?? 0
icon: "wifi-1"
implicitSize: 30
color: Looks.colors.inactiveIcon
FluentIcon { // Signal
property int strength: root.wifiNetwork?.strength ?? 0
icon: WIcons.wifiIconForStrength(strength)
implicitSize: 30
FluentIcon { // Security
anchors {
right: parent.right
bottom: parent.bottom
}
visible: root?.wifiNetwork?.isSecure ?? false
icon: "lock-closed"
filled: true
implicitSize: 14
}
}
}
ColumnLayout {
Layout.topMargin: statusText.visible ? 4 : 7
Layout.bottomMargin: 4
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
spacing: 1
Behavior on Layout.topMargin {
animation: Looks.transition.move.createObject(this)
}
WText { // Network name
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Looks.font.pixelSize.large
text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown")
textFormat: Text.PlainText
}
WText { // Status
id: statusText
Layout.fillWidth: true
elide: Text.ElideRight
text: root.wifiNetwork?.active ? Translation.tr("Connected") : root.wifiNetwork?.isSecure ? Translation.tr("Secured") : Translation.tr("Not secured")
font.pixelSize: Looks.font.pixelSize.large
color: Looks.colors.subfg
visible: root.wifiNetwork?.active || root.expanded
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
}
WButton {
Layout.alignment: Qt.AlignRight
horizontalAlignment: Text.AlignHCenter
visible: root.expanded
checked: !(root.wifiNetwork?.active ?? false)
colBackground: Looks.colors.bg2
colBackgroundHover: Looks.colors.bg2Hover
colBackgroundActive: Looks.colors.bg2Active
implicitHeight: 30
implicitWidth: 148
text: root.wifiNetwork?.active ? Translation.tr("Disconnect") : Translation.tr("Connect")
onClicked: {
if (root.wifiNetwork?.active) {
Network.disconnectWifiNetwork();
} else {
Network.connectToWifiNetwork(root.wifiNetwork);
}
}
}
}
}
}

View file

@ -0,0 +1,119 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
Item {
id: root
Component.onCompleted: {
Network.rescanWifi();
}
WPanelPageColumn {
anchors.fill: parent
BodyRectangle {
implicitHeight: 400
implicitWidth: 50
ColumnLayout {
anchors.fill: parent
anchors.margins: 4
spacing: 4
ColumnLayout {
implicitHeight: headerRow.implicitHeight
Layout.fillWidth: true
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: 0
HeaderRow {
id: headerRow
Layout.fillWidth: true
title: Translation.tr("Wi-Fi")
}
WSwitch {
id: toggleSwitch
Layout.rightMargin: 12
checked: Network.wifiStatus !== "disabled"
onCheckedChanged: {
Network.enableWifi(checked);
Network.rescanWifi();
}
}
}
FadeLoader {
Layout.leftMargin: -4
Layout.rightMargin: -4
Layout.fillWidth: true
shown: Network.wifiScanning
visible: true
sourceComponent: WIndeterminateProgressBar {}
}
}
StyledListView {
id: listView
Layout.fillHeight: true
Layout.fillWidth: true
animateAppearance: false
contentHeight: contentLayout.implicitHeight
contentWidth: width
clip: true
spacing: 4
model: ScriptModel {
values: Network.friendlyWifiNetworks
}
delegate: WWifiNetworkItem {
required property WifiAccessPoint modelData
wifiNetwork: modelData
width: ListView.view.width
}
}
}
}
WPanelSeparator {}
FooterRectangle {
WTextButton {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
}
text: Translation.tr("More Internet settings")
onClicked: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarLeft", "toggle"]);
Quickshell.execDetached(["bash", "-c", Config.options.apps.network]);
}
}
WBorderlessButton {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 12
enabled: !Network.wifiScanning
onClicked: {
Network.rescanWifi();
}
contentItem: FluentIcon {
icon: "arrow-counterclockwise"
}
}
}
}
}

View file

@ -0,0 +1,46 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.modules.ii.background.widgets
import qs.modules.ii.background.widgets.clock
import qs.modules.ii.background.widgets.weather
Variants {
id: root
model: Quickshell.screens
PanelWindow {
id: panelRoot
required property var modelData
screen: modelData
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "quickshell:background"
anchors {
top: true
bottom: true
left: true
right: true
}
color: "transparent"
StyledImage {
anchors.fill: parent
source: Config.options.background.wallpaperPath
fillMode: Image.PreserveAspectCrop
}
}
}

View file

@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
BarButton {
id: root
required property string iconName
property bool multiple: false
property bool separateLightDark: false
property alias tryCustomIcon: iconWidget.tryCustomIcon
leftInset: 2
rightInset: 2
implicitWidth: height - topInset - bottomInset + leftInset + rightInset
property real pressedScale: 5/6
onDownChanged: {
scaleAnim.duration = root.down ? 150 : 200
scaleAnim.easing.bezierCurve = root.down ? Looks.transition.easing.bezierCurve.easeIn : Looks.transition.easing.bezierCurve.easeOut
contentItem.scale = root.down ? root.pressedScale : 1 // If/When we do dragging, the scale is 1.25
}
background: Item {
id: background
BackgroundAcrylicRectangle {
id: mainBgRect
anchors.fill: parent
layer.enabled: root.multiple
layer.effect: OpacityMask {
invert: true
maskSource: Item {
width: mainBgRect.width
height: mainBgRect.height
Rectangle {
anchors.fill: parent
anchors.rightMargin: 3
radius: mainBgRect.radius
}
}
}
}
Loader {
anchors.fill: parent
anchors.rightMargin: 5
active: root.multiple
sourceComponent: BackgroundAcrylicRectangle {}
}
}
contentItem: Item {
id: contentItem
anchors.centerIn: root.background
implicitHeight: iconWidget.implicitHeight
implicitWidth: iconWidget.implicitWidth
Behavior on scale {
NumberAnimation {
id: scaleAnim
easing.type: Easing.BezierSpline
}
}
WAppIcon {
id: iconWidget
anchors.centerIn: parent
iconName: root.iconName
separateLightDark: root.separateLightDark
}
}
component BackgroundAcrylicRectangle: AcrylicRectangle {
shiny: ((root.hovered && !root.down) || root.checked)
color: root.color
border.width: 1
border.color: root.colBackgroundBorder
Behavior on border.color {
animation: Looks.transition.color.createObject(this)
}
}
}

View file

@ -0,0 +1,39 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
AcrylicButton {
id: root
property var altAction: () => {}
property var middleClickAction: () => {}
Layout.fillHeight: true
topInset: 4
bottomInset: 4
leftInset: 0
rightInset: 0
horizontalPadding: 8
colBackground: ColorUtils.transparentize(Looks.colors.bg1)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => {
root.down = true;
}
onReleased: (event) => {
root.down = false;
}
onClicked: (event) => {
if (event.button === Qt.LeftButton) root.clicked();
if (event.button === Qt.RightButton) root.altAction();
if (event.button === Qt.MiddleButton) root.middleClickAction();
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
import qs.modules.waffle.bar
BarButton {
id: root
property alias iconName: iconContent.icon
property alias iconSource: iconContent.source
property alias iconSize: iconContent.implicitSize
property alias iconRotation: iconContent.rotation
property alias iconMonochrome: iconContent.monochrome
property alias iconScale: iconContent.scale
property alias tooltipText: tooltip.text
property alias overlayingItems: iconContent.data
implicitWidth: 32
contentItem: Item {
anchors.centerIn: parent
implicitWidth: iconContent.implicitWidth
implicitHeight: iconContent.implicitHeight
FluentIcon {
id: iconContent
anchors.centerIn: parent
implicitSize: 16
icon: root.iconName
monochrome: false
}
}
BarToolTip {
id: tooltip
extraVisibleCondition: root.shouldShowTooltip && text !== ""
}
}

View file

@ -0,0 +1,60 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
BarPopup {
id: root
default property var menuData
property var model: [
{ iconName: "start-here", text: "Start", action: () => {print("hello")} },
{ type : "separator" },
]
readonly property bool hasIcons: model.some(item => item.iconName !== undefined && item.iconName !== "")
padding: 2
contentItem: ColumnLayout {
anchors.centerIn: parent
spacing: 0
Repeater {
model: root.model
delegate: DelegateChooser {
role: "type"
DelegateChoice {
roleValue: "separator"
Rectangle {
Layout.topMargin: 2
Layout.bottomMargin: 2
Layout.fillWidth: true
implicitHeight: 1
color: Looks.colors.bg0Border
}
}
DelegateChoice {
roleValue: undefined
WButton {
id: btn
Layout.fillWidth: true
inset: 2
required property var modelData
forceShowIcon: root.hasIcons
icon.name: modelData.iconName ? modelData.iconName : ""
monochromeIcon: modelData.monochromeIcon ?? true
text: modelData.text ? modelData.text : ""
onClicked: {
if (modelData.action) modelData.action();
root.close();
}
}
}
}
}
}
}

View file

@ -0,0 +1,131 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Loader {
id: root
required property var contentItem
property real padding: Looks.radius.large - Looks.radius.medium
property bool noSmoothClosing: !Config.options.waffles.tweaks.smootherMenuAnimations
property bool closeOnFocusLost: true
signal focusCleared()
property Item anchorItem: parent
property real visualMargin: 12
readonly property bool barAtBottom: Config.options.waffles.bar.bottom
property real ambientShadowWidth: 1
onFocusCleared: {
if (!root.closeOnFocusLost) return;
root.close()
}
function grabFocus() { // Doesn't work
item.grabFocus();
}
function close() {
item.close();
}
function updateAnchor() {
item?.anchor.updateAnchor();
}
active: false
visible: active
sourceComponent: PopupWindow {
id: popupWindow
visible: true
Component.onCompleted: {
openAnim.start();
}
anchor {
adjustment: PopupAdjustment.ResizeY | PopupAdjustment.SlideX
item: root.anchorItem
gravity: root.barAtBottom ? Edges.Top : Edges.Bottom
edges: root.barAtBottom ? Edges.Top : Edges.Bottom
}
HyprlandFocusGrab {
id: focusGrab
active: true
windows: [popupWindow]
onCleared: root.focusCleared();
}
function close() {
if (root.noSmoothClosing) root.active = false;
else closeAnim.start();
}
function grabFocus() {
focusGrab.active = true; // Doesn't work
}
implicitWidth: realContent.implicitWidth + (root.ambientShadowWidth * 2) + (root.visualMargin * 2)
implicitHeight: realContent.implicitHeight + (root.ambientShadowWidth * 2) + (root.visualMargin * 2)
property real sourceEdgeMargin: -implicitHeight
PropertyAnimation {
id: openAnim
target: popupWindow
property: "sourceEdgeMargin"
to: (root.ambientShadowWidth + root.visualMargin)
duration: 200
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn
}
SequentialAnimation {
id: closeAnim
PropertyAnimation {
target: popupWindow
property: "sourceEdgeMargin"
to: -implicitHeight
duration: 150
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut
}
ScriptAction {
script: {
root.active = false;
}
}
}
color: "transparent"
WAmbientShadow {
target: realContent
}
Rectangle {
id: realContent
z: 1
anchors {
left: parent.left
right: parent.right
top: root.barAtBottom ? undefined : parent.top
bottom: root.barAtBottom ? parent.bottom : undefined
margins: root.ambientShadowWidth + root.visualMargin
// Opening anim
bottomMargin: root.barAtBottom ? popupWindow.sourceEdgeMargin : (root.ambientShadowWidth + root.visualMargin)
topMargin: root.barAtBottom ? (root.ambientShadowWidth + root.visualMargin) : popupWindow.sourceEdgeMargin
}
color: Looks.colors.bg1Base
radius: Looks.radius.large
// test
implicitWidth: root.contentItem.implicitWidth + (root.padding * 2)
implicitHeight: root.contentItem.implicitHeight + (root.padding * 2)
children: [root.contentItem]
}
}
}

View file

@ -0,0 +1,8 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.waffle.looks
WPopupToolTip {
anchorEdges: Config.options.waffles.bar.bottom ? Edges.Top : Edges.Bottom
}

View file

@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
AppButton {
id: root
iconName: checked ? "system-search-checked" : "system-search"
separateLightDark: true
checked: GlobalStates.searchOpen && LauncherSearch.query !== ""
onClicked: {
GlobalStates.searchOpen = !GlobalStates.searchOpen; // For now...
}
BarToolTip {
id: tooltip
text: Translation.tr("Search")
extraVisibleCondition: root.shouldShowTooltip
}
}

View file

@ -0,0 +1,68 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
// TODO: Replace the icon with QMLized svg (with /usr/lib/qt6/bin/svgtoqml) for proper micro-animation
AppButton {
id: root
leftInset: Config.options.waffles.bar.leftAlignApps ? 12 : 0
iconName: down ? "start-here-pressed" : "start-here"
checked: GlobalStates.searchOpen && LauncherSearch.query === ""
onClicked: {
GlobalStates.searchOpen = !GlobalStates.searchOpen;
}
BarToolTip {
id: tooltip
text: Translation.tr("Start")
extraVisibleCondition: root.shouldShowTooltip
}
altAction: () => {
contextMenu.active = true;
}
BarMenu {
id: contextMenu
model: [
{
text: Translation.tr("Terminal"),
action: () => {
Quickshell.execDetached(["bash", "-c", Config.options.apps.terminal]);
}
},
{
text: Translation.tr("Task Manager"),
action: () => {
Quickshell.execDetached(["bash", "-c", Config.options.apps.taskManager]);
}
},
{
text: Translation.tr("Settings"),
action: () => {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("settings.qml")]);
}
},
{
text: Translation.tr("File Explorer"),
action: () => {
Qt.openUrlExternally(Directories.home);
}
},
{
text: Translation.tr("Search"),
action: () => {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "overview", "toggle"]);
}
},
]
}
}

View file

@ -0,0 +1,107 @@
import QtQuick
import QtQuick.Layouts
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
BarButton {
id: root
checked: GlobalStates.sidebarLeftOpen
onClicked: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
contentItem: Item {
anchors.fill: parent
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Row {
id: column
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
spacing: 4
IconHoverArea {
id: internetHoverArea
iconItem: FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: "wifi-1"
color: Looks.colors.inactiveIcon
FluentIcon {
anchors.fill: parent
icon: WIcons.internetIcon
}
}
}
IconHoverArea {
id: volumeHoverArea
iconItem: FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: "speaker"
color: Looks.colors.inactiveIcon
FluentIcon {
anchors.fill: parent
icon: WIcons.volumeIcon
}
}
onScrollDown: Audio.decrementVolume();
onScrollUp: Audio.incrementVolume();
}
IconHoverArea {
id: batteryHoverArea
visible: Battery?.available ?? false
iconItem: FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: WIcons.batteryLevelIcon
FluentIcon {
anchors.fill: parent
icon: WIcons.batteryIcon
}
}
}
}
}
component IconHoverArea: FocusedScrollMouseArea {
id: hoverArea
required property var iconItem
anchors {
top: parent.top
bottom: parent.bottom
}
hoverEnabled: true
implicitHeight: hoverArea.iconItem.implicitHeight
implicitWidth: hoverArea.iconItem.implicitWidth
onPressed: (event) => event.accepted = false; // Don't consume clicks
children: [iconItem]
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && internetHoverArea.containsMouse
text: Translation.tr("%1\nInternet access").arg(Network.ethernet ? Translation.tr("Network") : Network.networkName)
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && volumeHoverArea.containsMouse
text: Translation.tr("Speakers (%1): %2") //
.arg(Audio.sink?.nickname || Audio.sink?.description || Translation.tr("Unknown")) //
.arg(Audio.sink?.audio.muted ? Translation.tr("Muted") : `${Math.round(Audio.sink?.audio.volume * 100) || 0}%`) //
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && batteryHoverArea.containsMouse
text: Translation.tr("Battery: %1%2") //
.arg(`${Math.round(Battery.percentage * 100) || 0}%`) //
.arg(Battery.isPluggedIn ? (" " + Translation.tr("(Plugged in)")) : "")
}
}

View file

@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
AppButton {
id: root
iconName: (down && !checked) ? "task-view-pressed" : "task-view"
pressedScale: checked ? 5/6 : 1
separateLightDark: true
checked: GlobalStates.overviewOpen
onClicked: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip
text: Translation.tr("Task View") // Should be a preview of workspaces, but we'll have this for now...
}
}

View file

@ -0,0 +1,55 @@
import QtQuick
import QtQuick.Layouts
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
BarButton {
id: root
rightInset: 12 // For now this is the rightmost button. Desktop peek is useless. (for now)
leftPadding: 12
rightPadding: 22
checked: GlobalStates.sidebarRightOpen
onClicked: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
contentItem: Item {
// anchors.centerIn: parent
implicitHeight: contentLayout.implicitHeight
implicitWidth: contentLayout.implicitWidth
Row {
id: contentLayout
anchors.centerIn: parent
spacing: 7
Column {
anchors.verticalCenter: parent.verticalCenter
WText {
anchors.right: parent.right
text: DateTime.time
}
WText {
anchors.right: parent.right
text: DateTime.date
}
}
FluentIcon {
visible: Notifications.silent
anchors.verticalCenter: parent.verticalCenter
icon: "alert-snooze"
implicitSize: 18
filled: true
}
}
}
BarToolTip {
id: tooltip
extraVisibleCondition: root.shouldShowTooltip
text: `${Qt.locale().toString(DateTime.clock.date, "dddd, MMMM d, yyyy")}\n\n${Qt.locale().toString(DateTime.clock.date, "ddd hh:mm AP")}`
}
}

View file

@ -0,0 +1,34 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
import qs.modules.waffle.bar.tray
BarIconButton {
id: root
visible: Updates.updateAdvised || Updates.updateStronglyAdvised
padding: 4
iconName: "arrow-sync"
iconSize: 20 // Needed because the icon appears to have some padding
iconMonochrome: true
tooltipText: Translation.tr("Get the latest features and security improvements with\nthe newest feature update.\n\n%1 packages").arg(Updates.count)
onClicked: {
Quickshell.execDetached(["bash", "-c", Config.options.apps.update]);
}
overlayingItems: Rectangle {
anchors {
right: parent.right
bottom: parent.bottom
margins: 1
}
implicitWidth: 8
implicitHeight: implicitWidth
radius: height / 2
color: Updates.updateStronglyAdvised ? Looks.colors.warning : Looks.colors.accent
}
}

View file

@ -0,0 +1,88 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Scope {
id: root
LazyLoader {
id: barLoader
active: GlobalStates.barOpen
component: Variants {
model: Quickshell.screens
delegate: PanelWindow { // Bar window
id: barRoot
required property var modelData
screen: modelData
exclusionMode: ExclusionMode.Ignore
exclusiveZone: implicitHeight
WlrLayershell.namespace: "quickshell:bar"
anchors {
left: true
right: true
bottom: Config.options.waffles.bar.bottom
top: !Config.options.waffles.bar.bottom
}
color: "transparent"
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth
WaffleBarContent {
id: content
anchors.fill: parent
}
}
}
}
IpcHandler {
target: "bar"
function toggle(): void {
GlobalStates.barOpen = !GlobalStates.barOpen
}
function close(): void {
GlobalStates.barOpen = false
}
function open(): void {
GlobalStates.barOpen = true
}
}
GlobalShortcut {
name: "barToggle"
description: "Toggles bar on press"
onPressed: {
GlobalStates.barOpen = !GlobalStates.barOpen;
}
}
GlobalShortcut {
name: "barOpen"
description: "Opens bar on press"
onPressed: {
GlobalStates.barOpen = true;
}
}
GlobalShortcut {
name: "barClose"
description: "Closes bar on press"
onPressed: {
GlobalStates.barOpen = false;
}
}
}

View file

@ -0,0 +1,83 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.bar.tasks
import qs.modules.waffle.bar.tray
Rectangle {
id: root
color: Looks.colors.bg0
implicitHeight: 48
Rectangle {
id: border
anchors {
left: parent.left
right: parent.right
top: Config.options.waffles.bar.bottom ? parent.top : undefined
bottom: Config.options.waffles.bar.bottom ? undefined : parent.bottom
}
color: Looks.colors.bg0Border
implicitHeight: 1
}
BarGroupRow {
id: bloatRow
anchors.left: parent.left
opacity: Config.options.waffles.bar.leftAlignApps ? 0 : 1
visible: opacity > 0
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
WidgetsButton {}
}
BarGroupRow {
id: appsRow
anchors.left: undefined
anchors.horizontalCenter: parent.horizontalCenter
states: State {
name: "left"
when: Config.options.waffles.bar.leftAlignApps
AnchorChanges {
target: appsRow
anchors.left: parent.left
anchors.horizontalCenter: undefined
}
}
transitions: Transition {
animations: Looks.transition.anchor.createObject(this)
}
StartButton {}
SearchButton {}
TaskViewButton {}
Tasks {}
}
BarGroupRow {
id: systemRow
anchors.right: parent.right
FadeLoader {
Layout.fillHeight: true
shown: Config.options.waffles.bar.leftAlignApps
sourceComponent: WidgetsButton {}
}
Tray {}
UpdatesButton {}
SystemButton {}
TimeButton {}
}
component BarGroupRow: RowLayout {
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: 0
}
}

View file

@ -0,0 +1,72 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
AppButton {
id: root
readonly property bool expandedForm: Config.options.waffles.bar.leftAlignApps
leftInset: Config.options.waffles.bar.leftAlignApps ? 0 : 12
implicitWidth: expandedForm ? 148 : (height - topInset - bottomInset + leftInset + rightInset)
iconName: "widgets"
checked: GlobalStates.sidebarLeftOpen
onClicked: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen
}
onDownChanged: {
scaleAnim.duration = root.down ? 150 : 200
scaleAnim.easing.bezierCurve = root.down ? Looks.transition.easing.bezierCurve.easeIn : Looks.transition.easing.bezierCurve.easeOut
iconWidget.scale = root.down ? 5/6 : 1 // If/When we do dragging, the scale is 1.25
}
contentItem: Item {
anchors {
verticalCenter: parent.verticalCenter
left: root.expandedForm ? parent.left : undefined
horizontalCenter: root.expandedForm ? undefined : background.horizontalCenter
}
implicitHeight: row.implicitHeight
implicitWidth: row.implicitWidth
Row {
id: row
anchors {
verticalCenter: parent.verticalCenter
left: root.expandedForm ? parent.left : undefined
horizontalCenter: root.expandedForm ? undefined : parent.horizontalCenter
margins: 8
}
spacing: 6
WAppIcon {
id: iconWidget
anchors.verticalCenter: parent.verticalCenter
iconName: root.iconName
Behavior on scale {
NumberAnimation {
id: scaleAnim
easing.type: Easing.BezierSpline
}
}
}
Column {
visible: root.expandedForm
anchors.verticalCenter: parent.verticalCenter
WText {
text: Translation.tr("Widgets")
}
}
}
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip
text: Translation.tr("Widgets")
}
}

View file

@ -0,0 +1,139 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.bar
import Quickshell
AppButton {
id: root
required property var appEntry
readonly property bool isSeparator: appEntry.appId === "SEPARATOR"
property var desktopEntry: DesktopEntries.heuristicLookup(appEntry.appId)
Timer {
// Retry looking up the desktop entry if it failed (e.g. database not loaded yet)
property int retryCount: 5
interval: 1000
running: !root.isSeparator && root.desktopEntry === null && retryCount > 0
repeat: true
onTriggered: {
retryCount--;
root.desktopEntry = DesktopEntries.heuristicLookup(root.appEntry.appId);
}
}
property bool active: root.appEntry.toplevels.some(t => t.activated)
property bool hasWindows: appEntry.toplevels.length > 0
signal hoverPreviewRequested()
signal hoverPreviewDismissed()
multiple: appEntry.toplevels.length > 1
checked: active
iconName: AppSearch.guessIcon(appEntry.appId)
tryCustomIcon: false
onHoverTimedOut: {
root.hoverPreviewRequested()
}
onClicked: {
root.hoverTimer.stop() // Prevents preview showing up when clicking to focus
if (root.multiple) {
root.hoverPreviewRequested()
} else if (root.appEntry.toplevels.length === 1) {
root.appEntry.toplevels[0].activate()
} else {
root.desktopEntry.execute()
}
}
middleClickAction: () => {
if (root.desktopEntry) {
desktopEntry.execute()
}
}
altAction: () => {
root.hoverPreviewDismissed()
root.hoverTimer.stop()
contextMenu.active = true;
}
// Active indicator
Rectangle {
id: activeIndicator
opacity: root.hasWindows ? 1 : 0
anchors {
horizontalCenter: root.background.horizontalCenter
bottom: root.background.bottom
bottomMargin: 1
}
implicitWidth: root.active ? 16 : 6
implicitHeight: 3
radius: height / 2
color: root.active ? Looks.colors.accent : Looks.colors.accentUnfocused
Behavior on implicitWidth {
animation: Looks.transition.enter.createObject(this)
}
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && !root.hasWindows
text: desktopEntry ? desktopEntry.name : appEntry.appId
}
BarMenu {
id: contextMenu
noSmoothClosing: false // On the real thing this is always smooth
model: [
...((root.desktopEntry?.actions.length > 0) ? root.desktopEntry.actions.map(action =>({
iconName: action.icon,
text: action.name,
action: () => {
action.execute()
}
})).concat({ type: "separator" }) : []),
{
iconName: root.iconName,
text: root.desktopEntry ? root.desktopEntry.name : StringUtils.toTitleCase(appEntry.appId),
monochromeIcon: false,
action: () => {
if (root.desktopEntry) {
root.desktopEntry.execute()
}
}
},
{
iconName: root.appEntry.pinned ? "pin-off" : "pin",
text: root.appEntry.pinned ? Translation.tr("Unpin from taskbar") : Translation.tr("Pin to taskbar"),
action: () => {
TaskbarApps.togglePin(root.appEntry.appId);
}
},
...(root.appEntry.toplevels.length > 0 ? [{
iconName: "dismiss",
text: root.multiple ? Translation.tr("Close all windows") : Translation.tr("Close window"),
action: () => {
for (let toplevel of root.appEntry.toplevels) {
toplevel.close();
}
}
}] : []),
]
}
}

View file

@ -0,0 +1,120 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import Quickshell
PopupWindow {
id: root
///////////////////// Properties ////////////////////
required property bool tasksHovered
property var appEntry
property Item anchorItem
//////////////////// Functions ////////////////////
function close() { // Closing doesn't animate, not sure if they're just lazy or it's intentional
marginBehavior.enabled = false;
root.visible = false;
}
function open() {
marginBehavior.enabled = true;
root.visible = true;
}
function show(appEntry: var, button: Item) {
root.appEntry = appEntry;
root.anchorItem = button;
root.anchor.updateAnchor();
root.open();
}
///////////////////// Internals /////////////////////
readonly property bool bottom: Config.options.waffles.bar.bottom
property real visualMargin: 12
property real ambientShadowWidth: 1
visible: false
color: "transparent"
implicitWidth: contentItem.implicitWidth + ambientShadowWidth + (visualMargin * 2)
implicitHeight: contentItem.implicitHeight + ambientShadowWidth + (visualMargin * 2)
anchor {
adjustment: PopupAdjustment.Slide
item: root.anchorItem
gravity: bottom ? Edges.Top : Edges.Bottom
edges: bottom ? Edges.Top : Edges.Bottom
}
Timer {
interval: 250
running: root.visible && !hoverChecker.containsMouse && !root.tasksHovered
onTriggered: {
root.close();
}
}
// Content
MouseArea {
id: hoverChecker
anchors.fill: parent
hoverEnabled: true
// Shadow
WAmbientShadow {
target: contentItem
}
Rectangle {
id: contentItem
property real sourceEdgeMargin: root.visible ? (root.ambientShadowWidth + root.visualMargin) : -root.implicitHeight
Behavior on sourceEdgeMargin {
id: marginBehavior
animation: Looks.transition.enter.createObject(this)
}
anchors {
left: parent.left
right: parent.right
top: root.bottom ? undefined : parent.top
bottom: root.bottom ? parent.bottom : undefined
margins: root.ambientShadowWidth + root.visualMargin
// Opening anim
bottomMargin: root.bottom ? sourceEdgeMargin : (root.ambientShadowWidth + root.visualMargin)
topMargin: root.bottom ? (root.ambientShadowWidth + root.visualMargin) : sourceEdgeMargin
}
color: Looks.colors.bg1Base
radius: Looks.radius.large
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: contentItem.width
height: contentItem.height
radius: contentItem.radius
}
}
// Testing
implicitHeight: Math.min(158, windowsRow.implicitHeight)
implicitWidth: windowsRow.implicitWidth
RowLayout {
id: windowsRow
anchors.fill: parent
Repeater {
model: ScriptModel {
values: root.appEntry?.toplevels ?? []
}
delegate: WindowPreview {
required property var modelData
toplevel: modelData
}
}
}
}
}
}

View file

@ -0,0 +1,59 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
MouseArea {
id: root
Layout.fillHeight: true
implicitHeight: appRow.implicitHeight
implicitWidth: appRow.implicitWidth
hoverEnabled: true
function showPreviewPopup(appEntry, button) {
previewPopup.show(appEntry, button);
}
Behavior on implicitWidth {
animation: Looks.transition.move.createObject(this)
}
WListView {
id: appRow
anchors {
top: parent.top
bottom: parent.bottom
}
orientation: Qt.Horizontal
spacing: 0
implicitWidth: contentWidth
clip: true
interactive: false
// TODO: Include only apps (and windows) in current workspace only | wait, does that even make sense in a Hyprland workflow?
model: ScriptModel {
objectProp: "appId"
values: TaskbarApps.apps.filter(app => app.appId !== "SEPARATOR")
}
delegate: TaskAppButton {
required property var modelData
appEntry: modelData
onHoverPreviewRequested: {
root.showPreviewPopup(appEntry, this);
}
onHoverPreviewDismissed: {
previewPopup.close();
}
}
}
// Previews popup
TaskPreview {
id: previewPopup
tasksHovered: root.containsMouse
anchor.window: root.QsWindow.window
}
}

View file

@ -0,0 +1,104 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.bar
import Quickshell
import Quickshell.Wayland
Button {
id: root
required property var toplevel
property real previewWidthConstraint: 200
property real previewHeightConstraint: 110
padding: 5
Layout.fillHeight: true
onClicked: {
root.toplevel.activate(); // TODO: make this work with those who disable focus on activate because telegram is abusive
}
background: Rectangle {
id: background
radius: Looks.radius.medium
color: root.down ? Looks.colors.bg2Active : (root.hovered ? Looks.colors.bg2Hover : ColorUtils.transparentize(Looks.colors.bg2))
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
}
contentItem: ColumnLayout {
id: contentItem
anchors.fill: parent
anchors.margins: root.padding
spacing: 5
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: false
spacing: 8
WAppIcon {
id: appIcon
Layout.leftMargin: Looks.radius.large - root.padding + 2
Layout.alignment: Qt.AlignVCenter
iconName: AppSearch.guessIcon(root.toplevel.appId)
implicitSize: 16
}
Item {
id: appTitleContainer
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: closeButton.implicitHeight // Enforce height, because closeButton doesn't contribute when it's invisible
WText {
id: appTitleText
anchors.fill: parent
text: root.toplevel.title
elide: Text.ElideRight
font.pixelSize: Looks.font.pixelSize.large
font.weight: Looks.font.weight.thin
color: Looks.colors.fg1
}
}
WindowCloseButton {
id: closeButton
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: Looks.radius.large - root.padding
Layout.topMargin: 0
implicitWidth: Math.max(screencopyView.implicitWidth, 80)
implicitHeight: screencopyView.implicitHeight
ScreencopyView {
id: screencopyView
anchors.centerIn: parent
captureSource: root.toplevel
live: true
paintCursor: true
constraintSize: Qt.size(root.previewWidthConstraint, root.previewHeightConstraint)
}
}
}
component WindowCloseButton: CloseButton {
visible: root.hovered
Layout.leftMargin: 4
implicitHeight: 30
implicitWidth: 30
radius: Looks.radius.large - root.padding
onClicked: {
root.toplevel.close();
}
}
}

View file

@ -0,0 +1,118 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt.labs.synchronizer
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.bar
RowLayout {
id: root
property bool overflowOpen: false
property bool dragging: false
Layout.fillHeight: true
spacing: 0
BarIconButton {
id: overflowButton
visible: (TrayService.unpinnedItems.length > 0 || root.dragging)
checked: root.overflowOpen
iconName: "chevron-down"
iconMonochrome: true
iconRotation: (Config.options.waffles.bar.bottom ? 180 : 0) + (root.overflowOpen ? 180 : 0)
Behavior on iconRotation {
animation: Looks.transition.rotate.createObject(this)
}
onClicked: {
root.overflowOpen = !root.overflowOpen;
}
TrayOverflowMenu {
id: trayOverflowLayout
Synchronizer on active {
property alias source: root.overflowOpen
}
}
BarToolTip {
extraVisibleCondition: overflowButton.shouldShowTooltip
text: Translation.tr("Show hidden icons")
}
DropArea {
id: pinDropArea
anchors.fill: parent
property bool willPin: false
onEntered: willPin = true
onExited: willPin = false
}
}
Repeater {
model: ScriptModel {
values: TrayService.pinnedItems
}
delegate: TrayButton {
id: trayButton
required property var modelData
item: modelData
property real initialX
property real initialY
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
drag.axis: Drag.XAxis
drag.threshold: 2
onPressed: event => {
trayButton.Drag.hotSpot.x = event.x;
trayButton.initialX = trayButton.x;
root.dragging = true;
trayButton.Drag.active = true;
}
onPositionChanged: {
pinTooltip.updateAnchor();
}
onReleased: {
if (!dragArea.drag.active) {
trayButton.click();
} else {
if (pinDropArea.containsDrag && pinDropArea.willPin) {
// Quickshell would crash if we don't hide this item first. Took me fucking 3 hours to figure out...
trayButton.visible = false;
TrayService.togglePin(trayButton.item.id);
pinDropArea.willPin = false;
} else {
trayButton.x = trayButton.initialX;
}
}
trayButton.Drag.active = false;
root.dragging = false;
}
}
BarToolTip {
id: pinTooltip
extraVisibleCondition: trayButton.Drag.active && pinDropArea.containsDrag && pinDropArea.willPin
horizontalPadding: 6
verticalPadding: 6
realContentItem: FluentIcon {
anchors.centerIn: parent
icon: "pin-off"
implicitSize: 18
}
}
}
}
}

View file

@ -0,0 +1,52 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.SystemTray
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.bar
BarIconButton {
id: root
required property SystemTrayItem item
property alias menuOpen: menu.visible
readonly property bool barAtBottom: Config.options.waffles.bar.bottom
iconSource: item.icon
iconScale: 0
Component.onCompleted: {
root.iconScale = 1
}
Behavior on iconScale {
animation: Looks.transition.enter.createObject(this)
}
onClicked: {
item.activate();
}
altAction: () => {
if (item.hasMenu) menu.open()
}
// This is lazy, but it's not like tray menus on Windoes are consistent...
// TODO: Figure out how to do cascading menus then use a custom menu
QsMenuAnchor {
id: menu
menu: root.item.menu
anchor {
adjustment: PopupAdjustment.ResizeY | PopupAdjustment.SlideX
item: root
gravity: root.barAtBottom ? Edges.Top : Edges.Bottom
edges: root.barAtBottom ? Edges.Top : Edges.Bottom
}
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && !root.Drag.active
text: TrayService.getTooltipForItem(root.item)
}
}

View file

@ -0,0 +1,124 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.bar
BarPopup {
id: root
closeOnFocusLost: false
onFocusCleared: {
const hasMenuOpen = contentItem.children.some(c => (c.menuOpen));
if (!hasMenuOpen)
root.close();
else
root.grabFocus();
}
contentItem: Item {
id: contentItem
anchors.centerIn: parent
implicitWidth: contentGrid.implicitWidth
implicitHeight: contentGrid.implicitHeight
GridLayout {
id: contentGrid
anchors.centerIn: parent
rows: Math.floor(Math.sqrt(TrayService.unpinnedItems.length))
columns: Math.ceil(TrayService.unpinnedItems.length / rows)
columnSpacing: 0
rowSpacing: 0
Repeater {
model: ScriptModel {
values: TrayService.unpinnedItems
onValuesChanged: {
root.updateAnchor();
if (values.length === 0) {
root.close();
}
}
}
delegate: TrayButton {
id: trayButton
required property var modelData
item: modelData
topInset: 0
bottomInset: 0
implicitWidth: 40
implicitHeight: 40
colBackground: ColorUtils.transparentize(Looks.colors.bg2)
colBackgroundHover: Looks.colors.bg2Hover
colBackgroundActive: Looks.colors.bg2Active
onMenuOpenChanged: {
// The overflow menu should only be closed when the user clicks outside
// However the focus grab refuses to reactivate, so we can't have that
// But most of the time the user dismisses the menu by clicking outside anyway,
// so this is acceptable.
if (!menuOpen) {
root.close();
}
}
property real initialX
property real initialY
Behavior on x {
animation: Looks.transition.move.createObject(this)
}
Behavior on y {
animation: Looks.transition.move.createObject(this)
}
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
drag.threshold: 2
onPressed: event => {
trayButton.Drag.hotSpot.x = event.x;
trayButton.Drag.hotSpot.y = event.y;
trayButton.initialX = trayButton.x;
trayButton.initialY = trayButton.y;
trayButton.Drag.active = true;
}
onReleased: {
if (!dragArea.drag.active) {
trayButton.click();
} else {
if (!unpinDropArea.containsDrag && unpinDropArea.willUnpin) {
// Quickshell would crash if we don't hide this item first. Took me fucking 3 hours to figure out...
trayButton.visible = false;
TrayService.togglePin(trayButton.item.id);
unpinDropArea.willUnpin = false;
} else {
trayButton.x = trayButton.initialX;
trayButton.y = trayButton.initialY;
}
}
trayButton.Drag.active = false;
}
}
}
}
}
DropArea {
id: unpinDropArea
anchors.fill: parent
property bool willUnpin: false
onEntered: willUnpin = false
onExited: willUnpin = true
}
}
}

View file

@ -0,0 +1,381 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.panels.lock
import qs.modules.waffle.looks
import qs.modules.waffle.sessionScreen as SessionScreen
LockScreen {
id: root
property bool passwordView: false
lockSurface: Item {
id: lockSurfaceItem
Component.onCompleted: {
root.passwordView = false;
lockSurfaceItem.forceActiveFocus();
}
Keys.onPressed: {
interactables.switchToFocusedView();
}
StyledImage {
id: bg
z: 0
width: parent.width
height: parent.height
onStatusChanged: {
if (status === Image.Ready) {
print("Lock wallpaper loaded");
print(lockSurfaceItem.height);
y = -lockSurfaceItem.height;
openAnim.restart();
}
}
source: Config.options.background.wallpaperPath
fillMode: Image.PreserveAspectCrop
PropertyAnimation {
id: openAnim
target: bg
property: "y"
to: 0
duration: 350
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn
}
}
GaussianBlur {
z: 1
anchors.fill: bg
source: bg
radius: 100
samples: radius * 2 + 1
scale: root.passwordView ? 1.1 : 1
opacity: root.passwordView ? 1 : 0
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
Behavior on scale {
NumberAnimation {
duration: 400
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn
}
}
}
Interactables {
id: interactables
z: 2
anchors.fill: bg
}
}
component Interactables: Rectangle {
id: interactablesComponent
color: ColorUtils.transparentize("#000000", 0.8)
// Button {
// onClicked: {
// root.context.unlocked(LockContext.ActionEnum.Unlock);
// GlobalStates.screenLocked = false;
// }
// text: "woah it doesnt work let me out pls uwu colon three"
// }
function switchToFocusedView() {
switchToPasswordViewAnim.restart();
}
SequentialAnimation {
id: switchToPasswordViewAnim
PropertyAnimation {
target: unfocusedContent
property: "y"
from: 0
to: -height * 1.1
duration: 250
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn
}
ScriptAction {
script: {
root.passwordView = true;
}
}
}
Item {
id: unfocusedContent
width: parent.width
height: parent.height
visible: !root.passwordView
ClockTextGroup {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: interactablesComponent.height * 0.1
}
}
RowLayout {
anchors {
bottom: parent.bottom
right: parent.right
bottomMargin: 21
rightMargin: 31
}
IconIndicator {
baseIcon: "wifi-1"
icon: WIcons.internetIcon
}
IconIndicator {
baseIcon: WIcons.batteryIcon
icon: WIcons.batteryLevelIcon
}
}
}
Item {
id: focusedContent
anchors.fill: parent
visible: root.passwordView
PasswordGroup {
visible: root.passwordView
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
}
RowLayout {
visible: root.passwordView
anchors {
bottom: parent.bottom
right: parent.right
bottomMargin: 21
rightMargin: 31
}
SessionScreen.PowerButton {
id: powerButton
}
}
}
}
component IconIndicator: Item {
id: iconIndicator
required property string baseIcon
required property string icon
default property alias indicatorData: iconWidget.data
implicitWidth: 40
implicitHeight: 40
FluentIcon {
id: iconWidget
anchors.centerIn: parent
icon: iconIndicator.baseIcon
color: Looks.darkColors.inactiveIcon
implicitSize: 20
FluentIcon {
anchors.fill: parent
icon: iconIndicator.icon
}
}
}
component ClockTextGroup: Column {
id: clockTextGroup
spacing: -3
WText {
anchors.horizontalCenter: parent.horizontalCenter
color: Looks.darkColors.fg
font.pixelSize: 133
font.weight: Looks.font.weight.strong
text: {
// Don't take am/pm
// Match groups of digits separated by non-digit chars (e.g., "12:34", "12.34", "12-34")
let match = DateTime.time.match(/(\d{1,2})\D+(\d{2})/);
return match ? `${match[1]}${DateTime.time.match(/\D+/)[0]}${match[2]}` : DateTime.time;
}
}
WText {
id: dateLabel
color: Looks.darkColors.fg
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: 28
font.weight: Looks.font.weight.strong
text: DateTime.collapsedCalendarFormat
}
}
component PasswordGroup: ColumnLayout {
id: passwordGroup
spacing: 15
WUserAvatar {
Layout.alignment: Qt.AlignHCenter
sourceSize: Qt.size(192, 192)
}
WText {
Layout.alignment: Qt.AlignHCenter
text: SystemInfo.username
color: Looks.darkColors.fg
font.pixelSize: 26
font.weight: Looks.font.weight.strong
}
Rectangle {
id: passwordInputWrapper
Layout.topMargin: 10
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 132
color: "transparent"
implicitWidth: 296
implicitHeight: 36
border.width: 2
border.color: Looks.applyContentTransparency(Looks.darkColors.bg1Border)
radius: Looks.radius.medium
Rectangle {
id: passwordInputBackground
anchors.fill: parent
anchors.margins: 2
radius: Looks.radius.small + 1
color: passwordInput.focus ? Looks.applyBackgroundTransparency(Looks.darkColors.bg1Base) : Looks.applyContentTransparency(Looks.darkColors.bg1)
RowLayout {
anchors.fill: parent
anchors.margins: 6
spacing: 3
WTextInput {
id: passwordInput
Layout.fillHeight: true
Layout.fillWidth: true
verticalAlignment: TextInput.AlignVCenter
inputMethodHints: Qt.ImhSensitiveData
echoMode: passwordVisibilityButton.pressed ? TextInput.Normal : TextInput.Password
color: Looks.darkColors.fg
font.pixelSize: 12
WText {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
visible: passwordInput.text.length === 0
text: Translation.tr("Password")
font.pixelSize: Looks.font.pixelSize.large
color: Looks.darkColors.fg
opacity: 0.8
}
onTextChanged: root.context.currentText = this.text
onAccepted: {
root.context.tryUnlock();
}
Connections {
target: root.context
function onCurrentTextChanged() {
passwordInput.text = root.context.currentText;
}
}
Connections {
target: root
function onPasswordViewChanged() {
passwordInput.forceActiveFocus();
}
}
Keys.onPressed: event => {
root.context.resetClearTimer();
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
}
}
PasswordBoxButton {
id: passwordVisibilityButton
property bool passwordVisible: false
visible: passwordInput.text.length > 0
onPressed: passwordVisible = true
onReleased: passwordVisible = false
icon.name: passwordVisible ? "eye-off" : "eye"
}
PasswordBoxButton {
onClicked: {
root.context.tryUnlock();
}
icon.name: "arrow-right"
}
}
}
Rectangle {
id: activeIndicatorLine
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
implicitHeight: 2
color: passwordInput.focus ? Looks.colors.accent : Looks.applyContentTransparency(Looks.darkColors.bg2Border)
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: passwordInputWrapper.width
height: passwordInputWrapper.height
radius: passwordInputWrapper.radius
}
}
}
Item {}
}
component PasswordBoxButton: WButton {
id: pwBoxBtn
implicitWidth: 28
implicitHeight: 22
property color colBackground: ColorUtils.transparentize(Looks.darkColors.bg1)
property color colBackgroundHover: ColorUtils.transparentize(Looks.darkColors.bg2Hover)
property color colBackgroundActive: ColorUtils.transparentize(Looks.darkColors.bg2Active)
fgColor: checked ? Looks.colors.accentFg : Looks.darkColors.fg
checked: hovered
contentItem: Item {
FluentIcon {
color: pwBoxBtn.fgColor
anchors.centerIn: parent
icon: pwBoxBtn.icon.name
implicitSize: 16
}
}
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
WButton {
id: root
colBackground: Looks.colors.bg1
colBackgroundHover: Looks.colors.bg1Hover
colBackgroundActive: Looks.colors.bg1Active
property color colBackgroundBorder
property color color
property alias border: background.border
property alias shinyColor: background.borderColor
colBackgroundBorder: ColorUtils.transparentize(color, (root.checked || root.hovered) ? Looks.backgroundTransparency : 0)
color: {
if (root.down) {
return root.colBackgroundActive
} else if ((root.hovered && !root.down) || root.checked) {
return root.colBackgroundHover
} else {
return root.colBackground
}
}
background: AcrylicRectangle {
id: background
shiny: ((root.hovered && !root.down) || root.checked)
color: root.color
radius: Looks.radius.medium
border.width: 1
border.color: root.colBackgroundBorder
Behavior on border.color {
animation: Looks.transition.color.createObject(this)
}
}
}

View file

@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Rectangle {
id: root
property bool shiny: true // Top border
property color borderColor: ColorUtils.transparentize(Looks.colors.bg1Hover, 0.7)
property color internalBorderColor: ColorUtils.transparentize(borderColor, shiny ? 0.0 : 1)
color: Looks.colors.bg1Hover
radius: Looks.radius.medium
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
Behavior on internalBorderColor {
animation: Looks.transition.color.createObject(this)
}
onInternalBorderColorChanged: {
borderCanvas.requestPaint();
}
// 1px border at the top or bottom
Canvas {
id: borderCanvas
anchors.fill: parent
// For dark mode we have a shiny top border, and for light mode we have sort of a shadow
rotation: Looks.dark ? 0 : 180
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
var borderColor = root.internalBorderColor;
var r = root.radius;
var fadeLength = Math.max(1, r);
var fadeLengthPercent = fadeLength / width;
// Compute normalized stops
var leftFadeStop = fadeLengthPercent;
var rightFadeStop = 1 - fadeLengthPercent;
var grad = ctx.createLinearGradient(0, 0, width, 0);
grad.addColorStop(0, Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0));
grad.addColorStop(leftFadeStop, borderColor);
grad.addColorStop(rightFadeStop, borderColor);
grad.addColorStop(1, Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0));
ctx.strokeStyle = grad;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(r, 0.5);
ctx.lineTo(width - r, 0.5);
// Top-right curve
ctx.arcTo(width, 0.5, width, r + 0.5, r);
// Top-left curve
ctx.moveTo(width - r, 0.5);
ctx.arcTo(0, 0.5, 0, r + 0.5, r);
ctx.stroke();
}
}
}

View file

@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Rectangle {
Layout.fillHeight: true
Layout.fillWidth: true
color: Looks.colors.bgPanelBody
}

View file

@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.bar
import Quickshell
Button {
id: reusableCloseButton
implicitHeight: 30
implicitWidth: 30
property alias radius: closeButtonBg.radius
Rectangle {
z: 0
color: "transparent"
anchors.fill: closeButtonBg
anchors.margins: -1
opacity: closeButtonBg.opacity
border.width: 1
radius: closeButtonBg.radius + 1
border.color: Looks.colors.bg2Border
}
background: Rectangle {
id: closeButtonBg
z: 1
opacity: reusableCloseButton.hovered ? 1 : 0
color: reusableCloseButton.pressed ? Looks.colors.dangerActive : Looks.colors.danger
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
}
contentItem: FluentIcon {
z: 2
anchors.centerIn: parent
icon: "dismiss"
implicitSize: 10
}
}

View file

@ -0,0 +1,23 @@
import QtQuick
import org.kde.kirigami as Kirigami
import qs.modules.common
import qs.modules.waffle.looks
Kirigami.Icon {
id: root
required property string icon
property bool filled: false
property alias monochrome: root.isMask
// Should be 16, but it appears the icons have some padding,
// Unlike the Windows-only Segoe UI icons, the open source FluentUI ones are hella small
property int implicitSize: 20
implicitWidth: implicitSize
implicitHeight: implicitSize
source: icon === "" ? "" : `${Looks.iconsPath}/${root.icon}${filled ? "-filled" : ""}.svg`
fallback: root.icon
roundToIconSize: false
color: Looks.colors.fg
isMask: true
animated: true
}

View file

@ -0,0 +1,17 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Rectangle {
Layout.fillHeight: false
Layout.fillWidth: true
color: "transparent"
implicitWidth: 358
implicitHeight: 47
}

View file

@ -0,0 +1,266 @@
pragma ComponentBehavior: Bound
pragma Singleton
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.common.functions
Singleton {
id: root
property QtObject darkColors
property QtObject lightColors
property QtObject colors
property QtObject radius
property QtObject font
property QtObject transition
property string iconsPath: `${Directories.assetsPath}/icons/fluent`
property bool dark: Appearance.m3colors.darkmode
readonly property bool transparencyEnabled: Config.options.appearance.transparency.enable
property real backgroundTransparency: transparencyEnabled ? 0.16 : 0
property real panelBackgroundTransparency: transparencyEnabled ? 0.14 : 0
property real panelLayerTransparency: root.dark ? 0.9 : 0.7
property real contentTransparency: root.dark ? 0.87 : 0.5
function applyBackgroundTransparency(col) {
return ColorUtils.applyAlpha(col, 1 - root.backgroundTransparency)
}
function applyContentTransparency(col) {
return ColorUtils.applyAlpha(col, 1 - root.contentTransparency)
}
lightColors: QtObject {
id: lightColors
property color bgPanelBody: "#F2F2F2"
property color bgPanelSeparator: "#E0E0E0"
property color bg0: "#EEEEEE"
property color bg0Border: '#BEBEBE'
property color bg1Base: "#F7F7F7"
property color bg1: "#F7F7F7"
property color bg1Hover: "#F7F7F7"
property color bg1Active: '#EFEFEF'
property color bg1Border: '#E9E9E9'
property color bg2: "#FBFBFB"
property color bg2Base: "#FBFBFB"
property color bg2Hover: '#ffffff'
property color bg2Active: '#eeeeee'
property color bg2Border: '#E0E0E0'
property color subfg: "#5C5C5C"
property color fg: "#000000"
property color fg1: "#626262"
property color inactiveIcon: "#C4C4C4"
property color controlBgInactive: '#555458'
property color controlBg: '#807F85'
property color controlBgHover: '#57575B'
property color controlFg: "#FFFFFF"
property color accentUnfocused: "#848484"
property color link: "#235CCF"
property color inputBg: ColorUtils.transparentize(bg0, 0.4)
}
darkColors: QtObject {
id: darkColors
property color bgPanelBody: '#242424'
property color bgPanelSeparator: "#191919"
property color bg0: "#1C1C1C"
property color bg0Border: "#404040"
property color bg1Base: '#2C2C2C'
property color bg1: '#2C2C2C'
property color bg1Hover: "#292929"
property color bg1Active: '#252525'
property color bg1Border: '#bebebe'
property color bg2Base: "#313131"
property color bg2: '#313131'
property color bg2Hover: '#363636'
property color bg2Active: '#2B2B2B'
property color bg2Border: '#404040'
property color subfg: "#CED1D7"
property color fg: "#FFFFFF"
property color fg1: "#D1D1D1"
property color inactiveIcon: "#494949"
property color controlBgInactive: "#CDCECF"
property color controlBg: "#9B9B9B"
property color controlBgHover: "#CFCED1"
property color controlFg: "#454545"
property color accentUnfocused: "#989898"
property color link: "#A7C9FC"
property color inputBg: ColorUtils.transparentize(darkColors.bg0, 0.5)
}
colors: QtObject {
id: colors
// Special
property color shadow: ColorUtils.transparentize('#161616', 0.62)
property color ambientShadow: ColorUtils.transparentize("#000000", 0.75)
property color bgPanelFooterBase: root.dark ? root.darkColors.bg0 : root.lightColors.bg0
property color bgPanelFooterBackground: ColorUtils.transparentize(root.dark ? root.darkColors.bg0 : root.lightColors.bg0, root.panelBackgroundTransparency)
property color bgPanelFooter: ColorUtils.transparentize(bgPanelFooterBackground, root.panelLayerTransparency)
property color bgPanelBodyBase: root.dark ? root.darkColors.bgPanelBody : root.lightColors.bgPanelBody
property color bgPanelBody: ColorUtils.solveOverlayColor(bgPanelFooterBackground,bgPanelBodyBase, 1 - root.panelLayerTransparency)
property color bgPanelSeparator: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bgPanelSeparator : root.lightColors.bgPanelSeparator, 1 - root.panelBackgroundTransparency)
// Layer 0
property color bg0Base: root.dark ? root.darkColors.bg0 : root.lightColors.bg0
property color bg0: ColorUtils.transparentize(bg0Base, root.backgroundTransparency)
property color bg0Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg0Border : root.lightColors.bg0Border, root.backgroundTransparency)
// Layer 1
property color bg1Base: root.dark ? root.darkColors.bg1 : root.lightColors.bg1
property color bg1: ColorUtils.solveOverlayColor(bg0Base, bg1Base, 1 - root.contentTransparency)
property color bg1Hover: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Hover : root.lightColors.bg1Hover, 1 - root.contentTransparency)
property color bg1Active: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Active : root.lightColors.bg1Active, 1 - root.contentTransparency)
property color bg1Border: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Border : root.lightColors.bg1Border, 1 - root.contentTransparency)
// Layer 2
property color bg2Base: root.dark ? root.darkColors.bg2 : root.lightColors.bg2
property color bg2: ColorUtils.solveOverlayColor(bgPanelBodyBase, bg2Base, 1 - root.contentTransparency)
property color bg2Hover: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Hover : root.lightColors.bg2Hover, 1 - root.contentTransparency)
property color bg2Active: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Active : root.lightColors.bg2Active, 1 - root.contentTransparency)
property color bg2Border: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Border : root.lightColors.bg2Border, 1 - root.contentTransparency)
// Foreground / Text
property color subfg: root.dark ? root.darkColors.subfg : root.lightColors.subfg
property color fg: root.dark ? root.darkColors.fg : root.lightColors.fg
property color fg1: root.dark ? root.darkColors.fg1 : root.lightColors.fg1
property color inactiveIcon: root.dark ? root.darkColors.inactiveIcon : root.lightColors.inactiveIcon
property color link: root.dark ? root.darkColors.link : root.lightColors.link
// Controls
property color controlBgInactive: root.dark ? root.darkColors.controlBgInactive : root.lightColors.controlBgInactive
property color controlBg: root.dark ? root.darkColors.controlBg : root.lightColors.controlBg
property color controlBgHover: root.dark ? root.darkColors.controlBgHover : root.lightColors.controlBgHover
property color controlFg: root.dark ? root.darkColors.controlFg : root.lightColors.controlFg
property color inputBg: root.dark ? root.darkColors.inputBg : root.lightColors.inputBg
property color danger: "#C42B1C"
property color dangerActive: "#B62D1F"
property color warning: "#FF9900"
// Accent
property color accent: Appearance.colors.colPrimary
property color accentHover: Appearance.colors.colPrimaryHover
property color accentActive: Appearance.colors.colPrimaryActive
property color accentUnfocused: root.dark ? root.darkColors.accentUnfocused : root.lightColors.accentUnfocused
property color accentFg: ColorUtils.isDark(accent) ? "#FFFFFF" : "#000000"
property color selection: Appearance.colors.colPrimaryContainer
property color selectionFg: Appearance.colors.colOnPrimaryContainer
}
radius: QtObject {
id: radius
property int none: 0
property int small: 2
property int medium: 4
property int large: 8
property int xLarge: 12
}
font: QtObject {
id: font
property QtObject family: QtObject {
property string ui: "Noto Sans"
}
property QtObject weight: QtObject { // Noto is not Segoe, so we might use slightly different weights
property int thin: Font.Normal
property int regular: Font.Medium
property int strong: Font.DemiBold
property int stronger: (Font.DemiBold + 2*Font.Bold) / 3
property int strongest: Font.Bold
}
property QtObject pixelSize: QtObject {
property real normal: 11
property real large: 13
property real larger: 15
property real xlarger: 17
}
property QtObject variableAxes: QtObject {
property var ui: ({
"wdth": 25
})
}
}
transition: QtObject {
id: transition
property int velocity: 850
property QtObject easing: QtObject {
property QtObject bezierCurve: QtObject {
readonly property list<real> easeInOut: [0.42,0.00,0.58,1.00,1,1]
readonly property list<real> easeIn: [0,1,1,1,1,1]
readonly property list<real> easeOut: [1,0,1,1,1,1]
}
}
property Component color: Component {
ColorAnimation {
duration: 80
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
}
property Component opacity: Component {
NumberAnimation {
duration: 120
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
}
property Component resize: Component { // TODO: better curve needed
NumberAnimation {
duration: 200
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
}
property Component enter: Component {
NumberAnimation {
duration: 250
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
}
property Component exit: Component {
NumberAnimation {
duration: 250
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeOut
}
}
property Component move: Component {
NumberAnimation {
duration: 170
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeInOut
}
}
property Component rotate: Component {
NumberAnimation {
duration: 170
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeInOut
}
}
property Component anchor: Component {
AnchorAnimation {
duration: 160
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
}
property Component longMovement: Component {
NumberAnimation {
duration: 1000
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
}
property Component scroll: Component {
NumberAnimation {
duration: 250
easing.type: Easing.BezierSpline
easing.bezierCurve: [0.0, 0.0, 0.25, 1.0, 1, 1]
}
}
}
}

View file

@ -0,0 +1,73 @@
pragma ComponentBehavior: Bound
import Qt.labs.synchronizer
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.waffle.looks
Column {
id: root
property bool showArrows: true
property int currentIndex: 0
property int count: 1
signal clicked(int index)
signal increasePage()
signal decreasePage()
visible: count > 1
spacing: 6
NavigationArrow {
visible: root.showArrows
down: false
}
Repeater {
model: root.count
delegate: MouseArea {
id: pageIndicator
required property int index
hoverEnabled: true
onClicked: root.clicked(index);
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: 6
implicitHeight: 6
Circle {
anchors.centerIn: parent
diameter: (index === root.currentIndex || pageIndicator.containsMouse) && !pageIndicator.pressed ? 6 : 4
color: pageIndicator.containsMouse ? Looks.colors.controlBgHover : Looks.colors.controlBg
}
}
}
NavigationArrow {
visible: root.showArrows
down: true
}
component NavigationArrow: FluentIcon {
id: navArrow
required property bool down
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: 12
implicitWidth: 12 - (2 * upArea.containsPress)
icon: down ? "caret-down" : "caret-up"
color: upArea.containsMouse ? Looks.colors.controlBgHover : Looks.colors.controlBg
filled: true
opacity: ((down && root.currentIndex < root.count - 1) || (!down && root.currentIndex > 0)) ? 1 : 0
MouseArea {
id: upArea
anchors.fill: parent
hoverEnabled: true
onClicked: navArrow.down ? root.increasePage() : root.decreasePage();
}
}
}

View file

@ -0,0 +1,25 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Rectangle {
id: root
required property var target
z: 0
anchors {
fill: target
margins: -border.width
}
border.color: Looks.colors.ambientShadow
border.width: 1
color: "transparent"
radius: target.radius + border.width
}

View file

@ -0,0 +1,22 @@
import QtQuick
import org.kde.kirigami as Kirigami
import qs.services
import qs.modules.common
Kirigami.Icon {
id: root
required property string iconName
property bool separateLightDark: false
property bool tryCustomIcon: true
property real implicitSize: 26
implicitWidth: implicitSize
implicitHeight: implicitSize
animated: true
roundToIconSize: false
fallback: root.iconName
source: tryCustomIcon ? `${Looks.iconsPath}/${root.iconName}${!root.separateLightDark ? "" : Looks.dark ? "-dark" : "-light"}.svg` : fallback
color: Looks.colors.fg
}

View file

@ -0,0 +1,98 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
Item {
id: root
signal closed
required property Item contentItem
property real visualMargin: 12
property int closeAnimDuration: 150
property bool revealFromSides: false
property bool revealFromLeft: true
function close() {
closeAnim.start();
}
readonly property bool barAtBottom: Config.options.waffles.bar.bottom
implicitHeight: contentItem.implicitHeight + visualMargin * 2
implicitWidth: contentItem.implicitWidth + visualMargin * 2
focus: true
Keys.onPressed: event => { // Esc to close
if (event.key === Qt.Key_Escape) {
content.close();
}
}
Item {
id: panelContent
anchors {
left: (root.revealFromSides && !root.revealFromLeft) ? undefined : parent.left
right: (root.revealFromSides && root.revealFromLeft) ? undefined : parent.right
top: (!root.revealFromSides && root.barAtBottom) ? undefined : parent.top
bottom: (!root.revealFromSides && !root.barAtBottom) ? undefined : parent.bottom
// Opening anim
bottomMargin: (!root.revealFromSides && root.barAtBottom) ? sourceEdgeMargin : root.visualMargin
topMargin: (!root.revealFromSides && !root.barAtBottom) ? sourceEdgeMargin : root.visualMargin
leftMargin: (root.revealFromSides && root.revealFromLeft) ? sideEdgeMargin : root.visualMargin
rightMargin: (root.revealFromSides && !root.revealFromLeft) ? sideEdgeMargin : root.visualMargin
}
Component.onCompleted: {
openAnim.start();
}
property real sourceEdgeMargin: -(implicitHeight + root.visualMargin)
property real sideEdgeMargin: -(implicitWidth + root.visualMargin)
OpenAnim {
id: openAnim
properties: "sourceEdgeMargin, sideEdgeMargin"
}
SequentialAnimation {
id: closeAnim
ParallelAnimation {
CloseAnim {
property: "sourceEdgeMargin"
to: -(implicitHeight + root.visualMargin)
}
CloseAnim {
property: "sideEdgeMargin"
to: -(implicitWidth + root.visualMargin)
}
}
ScriptAction {
script: {
root.closed();
}
}
}
implicitWidth: root.contentItem.implicitWidth
implicitHeight: root.contentItem.implicitHeight
children: [root.contentItem]
}
component OpenAnim: PropertyAnimation {
target: panelContent
to: root.visualMargin
duration: 200
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn
}
component CloseAnim: PropertyAnimation {
target: panelContent
duration: root.closeAnimDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut
}
}

View file

@ -0,0 +1,18 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
WButton {
id: root
colBackground: Looks.colors.bg2
colBackgroundHover: Looks.colors.bg2Hover
colBackgroundActive: Looks.colors.bg2Active
property color colBorder: Looks.colors.bg2Border
property color colBorderToggled: Looks.colors.accent
border.color: checked ? colBorderToggled : colBorder
border.width: root.pressed ? 2 : 1
}

View file

@ -0,0 +1,35 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Button {
id: root
implicitHeight: 36
property color colBackground: ColorUtils.transparentize(Looks.colors.bg1)
property color colBackgroundHover: Looks.colors.bg1Hover
property color colBackgroundActive: Looks.colors.bg1Active
property color color
property color colForeground: Looks.colors.fg
color: {
if (!root.enabled) return colBackground;
if (root.down) {
return root.colBackgroundActive
} else if ((root.hovered && !root.down) || root.checked) {
return root.colBackgroundHover
} else {
return root.colBackground
}
}
property alias radius: background.radius
background: Rectangle {
id: background
radius: Looks.radius.medium
color: root.color
}
}

View file

@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
// Generic button with background
Button {
id: root
property color colBackground: ColorUtils.transparentize(Looks.colors.bg1)
property color colBackgroundHover: Looks.colors.bg2Hover
property color colBackgroundActive: Looks.colors.bg2Active
property color colBackgroundToggled: Looks.colors.accent
property color colBackgroundToggledHover: Looks.colors.accentHover
property color colBackgroundToggledActive: Looks.colors.accentActive
property color colForeground: Looks.colors.fg
property color colForegroundToggled: Looks.colors.accentFg
property color colForegroundDisabled: ColorUtils.transparentize(Looks.colors.subfg, 0.4)
property alias backgroundOpacity: backgroundRect.opacity
property color color: {
if (!root.enabled) return colBackground;
if (root.checked) {
if (root.down) {
return root.colBackgroundToggledActive;
} else if (root.hovered) {
return root.colBackgroundToggledHover;
} else {
return root.colBackgroundToggled;
}
}
if (root.down) {
return root.colBackgroundActive;
} else if (root.hovered) {
return root.colBackgroundHover;
} else {
return root.colBackground;
}
}
property color fgColor: {
if (!root.enabled) return root.colForegroundDisabled
if (root.checked) return root.colForegroundToggled
if (root.enabled) return root.colForeground
return root.colForeground
}
property alias horizontalAlignment: buttonText.horizontalAlignment
font {
family: Looks.font.family.ui
pixelSize: Looks.font.pixelSize.large
weight: Looks.font.weight.regular
}
// Hover stuff
signal hoverTimedOut
property bool shouldShowTooltip: false
ToolTip.delay: 400
property Timer hoverTimer: Timer {
id: hoverTimer
running: root.hovered
interval: root.ToolTip.delay
onTriggered: {
root.hoverTimedOut();
}
}
onHoverTimedOut: {
root.shouldShowTooltip = true;
}
onHoveredChanged: {
if (!root.hovered) {
root.shouldShowTooltip = false;
root.hoverTimer.stop();
}
}
property alias monochromeIcon: buttonIcon.monochrome
property alias buttonSpacing: contentLayout.spacing
property bool forceShowIcon: false
property var altAction: () => {}
property var middleClickAction: () => {}
property real inset: 0
topInset: inset
bottomInset: inset
leftInset: inset
rightInset: inset
property alias radius: backgroundRect.radius
property alias topLeftRadius: backgroundRect.topLeftRadius
property alias topRightRadius: backgroundRect.topRightRadius
property alias bottomLeftRadius: backgroundRect.bottomLeftRadius
property alias bottomRightRadius: backgroundRect.bottomRightRadius
property alias border: backgroundRect.border
horizontalPadding: 10
verticalPadding: 6
implicitHeight: contentItem.implicitHeight + verticalPadding * 2 + topInset + bottomInset
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2 + leftInset + rightInset
background: Rectangle {
id: backgroundRect
radius: Looks.radius.medium
color: root.color
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton | Qt.MiddleButton
onClicked: event => {
if (event.button === Qt.LeftButton)
root.clicked();
if (event.button === Qt.RightButton)
root.altAction();
if (event.button === Qt.MiddleButton)
root.middleClickAction();
}
}
contentItem: Item {
anchors {
fill: parent
margins: root.inset
}
implicitWidth: contentLayout.implicitWidth
implicitHeight: contentLayout.implicitHeight
RowLayout {
id: contentLayout
anchors {
fill: parent
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
spacing: 12
FluentIcon {
id: buttonIcon
monochrome: true
implicitSize: 18
Layout.leftMargin: root.iconLeftMargin
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter
icon: root.icon.name
color: root.fgColor
visible: root.icon.name !== ""
}
WText {
id: buttonText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.text
horizontalAlignment: Text.AlignLeft
font: root.font
color: root.fgColor
}
}
}
}

View file

@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
WButton {
id: root
property bool animateChoiceHighlight: true
Layout.fillWidth: true
implicitWidth: contentItem.implicitWidth
horizontalPadding: 10
verticalPadding: 11
buttonSpacing: 8
color: {
if (root.checked) {
if (root.down) {
return root.colBackgroundHover;
} else if (root.hovered && !root.down) {
return root.colBackgroundActive;
} else {
return root.colBackgroundHover;
}
}
if (root.down) {
return root.colBackgroundActive;
} else if (root.hovered && !root.down) {
return root.colBackgroundHover;
} else {
return root.colBackground;
}
}
fgColor: colForeground
background: Rectangle {
id: backgroundRect
radius: Looks.radius.medium
color: root.color
Behavior on color {
enabled: root.animateChoiceHighlight
animation: Looks.transition.color.createObject(this)
}
WFadeLoader {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
shown: root.checked
sourceComponent: Rectangle {
implicitWidth: 3
implicitHeight: 3
radius: width / 2
color: Looks.colors.accent
property bool forceZeroHeight: true
height: forceZeroHeight ? 0 : Math.max(16, root.background.height - 18 * 2)
Component.onCompleted: {
forceZeroHeight = false;
}
Behavior on height {
enabled: root.animateChoiceHighlight
animation: Looks.transition.opacity.createObject(this)
}
}
}
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
import qs.modules.common
// Yes, this is (mostly) a copy of FadeLoader.
// The animation of a Behavior cannot be changed... I'd love to be proven wrong.
Loader {
id: root
property bool shown: true
property alias fade: opacityBehavior.enabled
property alias animation: opacityBehavior.animation
opacity: shown ? 1 : 0
visible: opacity > 0
active: opacity > 0
Behavior on opacity {
id: opacityBehavior
animation: Looks.transition.opacity.createObject(null)
}
}

View file

@ -0,0 +1,187 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import qs.services
Singleton {
id: root
function pathForName(iconName) {
return Quickshell.shellPath(`assets/icons/fluent/${iconName}.svg`);
}
function wifiIconForStrength(strength) {
if (strength > 75)
return "wifi-1";
if (strength > 50)
return "wifi-2";
if (strength > 25)
return "wifi-3";
return "wifi-4";
}
property string internetIcon: {
if (Network.ethernet)
return "ethernet";
if (Network.wifiEnabled) {
const strength = Network.networkStrength;
return wifiIconForStrength(strength);
}
if (Network.wifiStatus === "connecting")
return "wifi-4";
if (Network.wifiStatus === "disconnected")
return "wifi-off";
if (Network.wifiStatus === "disabled")
return "wifi-off";
return "wifi-warning";
}
property string batteryIcon: {
if (Battery.isCharging)
return "battery-charge";
if (Battery.isCriticalAndNotCharging)
return "battery-warning";
if (Battery.percentage >= 0.9)
return "battery-full";
return `battery-0`;
}
property string batteryLevelIcon: {
const discreteLevel = Math.ceil(Battery.percentage * 10);
return `battery-${discreteLevel > 9 ? "full" : discreteLevel}`;
}
property string volumeIcon: {
const muted = Audio.sink?.audio.muted ?? false;
const volume = Audio.sink?.audio.volume ?? 0;
if (muted)
return "speaker-mute";
if (volume == 0)
return "speaker-none";
if (volume < 0.5)
return "speaker-1";
return "speaker";
}
property string micIcon: {
const muted = Audio.source?.audio.muted ?? false;
return muted ? "mic-off" : "mic";
}
property string bluetoothIcon: BluetoothStatus.connected ? "bluetooth-connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth-disabled"
property string nightLightIcon: Hyprsunset.temperatureActive ? "weather-moon" : "weather-moon-off"
property string notificationsIcon: Notifications.silent ? "alert-snooze" : "alert"
property string powerProfileIcon: {
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "leaf-two";
case PowerProfile.Balanced:
return "flash-on";
case PowerProfile.Performance:
return "fire";
}
}
function audioDeviceIcon(node) {
if (!node.isSink)
return "mic-on";
const monitor = /monitor|hdmi/i;
const headphones = /headset|headphone|bluez|wireless/i;
const speakers = /speaker|output/i;
if (monitor.test(node.nickname) || monitor.test(node.description) || monitor.test(node.name)) {
return "desktop-speaker";
}
if (headphones.test(node.nickname) || headphones.test(node.description) || headphones.test(node.name)) {
return "headphones";
}
if (speakers.test(node.nickname) || speakers.test(node.description) || speakers.test(node.name)) {
return "speaker";
}
return "speaker";
}
function audioAppIcon(node) {
let icon;
icon = AppSearch.guessIcon(node?.properties["application.icon-name"] ?? "");
if (AppSearch.iconExists(icon))
return icon;
icon = AppSearch.guessIcon(node?.properties["node.name"] ?? "");
return icon;
}
function bluetoothDeviceIcon(device) {
const systemIconName = device?.icon || "";
if (systemIconName.includes("headset") || systemIconName.includes("headphones"))
return "headphones";
if (systemIconName.includes("audio"))
return "speaker";
if (systemIconName.includes("phone"))
return "phone";
if (systemIconName.includes("mouse"))
return "bluetooth";
if (systemIconName.includes("keyboard"))
return "keyboard";
return "bluetooth";
}
function fluentFromMaterial(icon) {
switch (icon) {
case "calculate":
return "calculator";
case "keyboard_return":
return "arrow-enter-left";
case "open_in_new":
return "open";
case "settings_suggest":
return "wand";
case "terminal":
return "app-generic";
case "travel_explore":
return "globe-search";
case "keep":
return "pin";
case "keep_off":
return "pin-off";
default:
return "apps";
}
}
function guessIconForName(name) {
const lowerName = name.toLowerCase();
if (lowerName.includes("app") || lowerName.includes("desktop"))
return "apps";
if (lowerName.includes("news"))
return "news";
if (lowerName.includes("new") || lowerName.includes("create") || lowerName.includes("add"))
return "add";
if (lowerName.includes("open"))
return "open";
if (lowerName.includes("friends") || lowerName.includes("contact") || lowerName.includes("family"))
return "people";
if (lowerName.includes("community"))
return "people-team";
if (lowerName.includes("library"))
return "library";
if (lowerName.includes("setting"))
return "settings";
if (lowerName.includes("gallery"))
return "image-copy";
if (lowerName.includes("server"))
return "server";
if (lowerName.includes("picture") || lowerName.includes("photo") || lowerName.includes("image"))
return "image";
if (lowerName.includes("store") || lowerName.includes("shop"))
return "store-microsoft";
if (lowerName.includes("record") || lowerName.includes("capture"))
return "record";
if (lowerName.includes("screen") || lowerName.includes("display") || lowerName.includes("monitor") || lowerName.includes("desktop"))
return "desktop";
return "apps";
}
}

View file

@ -0,0 +1,22 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
StyledIndeterminateProgressBar {
id: progressBar
implicitHeight: 3
background: null
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: progressBar.width
height: progressBar.height
radius: progressBar.height / 2
}
}
}

View file

@ -0,0 +1,19 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
ListView {
id: root
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: WScrollBar {}
displaced: Transition {
animations: [Looks.transition.enter.createObject(this, {
property: "y"
})]
}
}

View file

@ -0,0 +1,104 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Menu {
id: root
property bool downDirection: false
property bool hasIcons: false // TODO: implement
property color color: Looks.colors.bg1Base
property alias backgroundPane: bgPane
implicitWidth: background.implicitWidth + margins * 2
implicitHeight: background.implicitHeight + margins * 2
margins: 10
padding: 3
property real sourceEdgeMargin: -implicitHeight
clip: true
enter: Transition {
NumberAnimation {
property: "sourceEdgeMargin"
from: -root.implicitHeight
to: root.margins
duration: 200
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn
}
}
exit: Transition {
NumberAnimation {
property: "sourceEdgeMargin"
from: root.margins
to: -root.implicitHeight
duration: 150
easing.type: Easing.BezierSpline
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut
}
}
background: Item {
id: bgItem
implicitWidth: bgPane.implicitWidth
implicitHeight: bgPane.implicitHeight
WPane {
id: bgPane
anchors {
left: parent.left
right: parent.right
top: root.downDirection ? parent.top : undefined
bottom: root.downDirection ? undefined : parent.bottom
margins: root.margins
topMargin: root.downDirection ? root.sourceEdgeMargin : root.margins
bottomMargin: root.downDirection ? root.margins : root.sourceEdgeMargin
}
contentItem: Rectangle {
color: root.color
implicitWidth: menuListView.implicitWidth + root.padding * 2
implicitHeight: root.contentItem.implicitHeight + root.padding * 2
}
}
}
Component.onCompleted: {
menuListView.itemAtIndex(0)?.forceActiveFocus();
}
contentItem: Item {
implicitWidth: menuListView.implicitWidth
implicitHeight: menuListView.implicitHeight
WListView {
id: menuListView
interactive: contentHeight > height
anchors {
left: parent.left
right: parent.right
top: root.downDirection ? parent.top : undefined
bottom: root.downDirection ? undefined : parent.bottom
margins: root.margins // ????
topMargin: root.downDirection ? root.sourceEdgeMargin : root.margins
bottomMargin: root.downDirection ? root.margins : root.sourceEdgeMargin
}
clip: true
implicitHeight: contentHeight
implicitWidth: Array.from({
length: count
}, (_, i) => itemAtIndex(i)?.implicitWidth ?? 0).reduce((a, b) => a > b ? a : b)
model: root.contentModel
}
}
delegate: WMenuItem {
id: menuItemDelegate
}
}

View file

@ -0,0 +1,127 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
MenuItem {
id: root
property color colBackground: ColorUtils.transparentize(Looks.colors.bg1)
property color colBackgroundHover: Looks.colors.bg2Hover
property color colBackgroundActive: Looks.colors.bg2Active
property color colBackgroundToggled: Looks.colors.bg2Hover
property color colBackgroundToggledHover: Looks.colors.bg2Active
property color colBackgroundToggledActive: Looks.colors.bg2Hover
property color colForeground: Looks.colors.fg
property color colForegroundToggled: Looks.colors.fg
property color colForegroundDisabled: ColorUtils.transparentize(Looks.colors.subfg, 0.4)
property color color: {
if (!root.enabled)
return colBackground;
if (root.checked) {
if (root.down) {
return root.colBackgroundToggledActive;
} else if (root.hovered) {
return root.colBackgroundToggledHover;
} else {
return root.colBackgroundToggled;
}
}
if (root.down) {
return root.colBackgroundActive;
} else if (root.hovered) {
return root.colBackgroundHover;
} else {
return root.colBackground;
}
}
property color fgColor: {
if (root.checked)
return root.colForegroundToggled;
if (root.enabled)
return root.colForeground;
return root.colForegroundDisabled;
}
property real inset: 2
topInset: inset
bottomInset: inset
leftInset: inset
rightInset: inset
horizontalPadding: 11
width: ListView.view?.width
height: visible ? implicitHeight : 0
background: Rectangle {
id: backgroundRect
radius: Looks.radius.medium
color: root.color
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
}
implicitHeight: Math.max(28, contentItem.implicitHeight) + topInset + bottomInset
implicitWidth: contentItem.implicitWidth + leftInset + rightInset + leftPadding + rightPadding
contentItem: Item {
implicitWidth: contentLayout.implicitWidth
implicitHeight: contentLayout.implicitHeight
RowLayout {
id: contentLayout
anchors.fill: parent
spacing: 12
FluentIcon {
id: buttonIcon
monochrome: true
implicitSize: 20
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter
color: root.fgColor
visible: root.icon.name !== ""
icon: root.icon.name
}
WText {
id: buttonText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.text
horizontalAlignment: Text.AlignLeft
font.pixelSize: Looks.font.pixelSize.large
color: root.fgColor
}
}
WFadeLoader {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: -root.leftPadding + width
}
shown: root.checked
sourceComponent: Rectangle {
implicitWidth: 3
implicitHeight: 3
radius: width / 2
color: Looks.colors.accent
property bool forceZeroHeight: true
height: forceZeroHeight ? 0 : Math.max(root.down ? 10 : 16, root.background.height - 18 * 2)
Component.onCompleted: {
forceZeroHeight = false;
}
Behavior on height {
animation: Looks.transition.resize.createObject(this)
}
}
}
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
MouseArea {
id: root
property real radius: Looks.radius.medium
hoverEnabled: true
property color colBackground: ColorUtils.transparentize(Looks.colors.bg2)
property color colBackgroundHover: Looks.colors.bg2Hover
property color colBackgroundActive: Looks.colors.bg2Active
property color colBorder: ColorUtils.transparentize(Looks.colors.bg2Border)
property color colBorderHover: Looks.colors.bg2Border
property color color: {
if (containsMouse) {
return pressed ? colBackgroundActive : colBackgroundHover;
} else {
return colBackground;
}
}
property color borderColor: {
if (containsMouse) {
return colBorderHover;
} else {
return colBorder;
}
}
property Item background: Rectangle {
id: bgRect
parent: root
anchors.fill: parent
color: root.color
radius: root.radius
border.color: root.borderColor
border.width: 1
}
}

View file

@ -0,0 +1,60 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
Item {
id: root
property Item contentItem
property real radius: Looks.radius.large
property alias color: contentRect.color
property alias border: borderRect
property alias borderColor: borderRect.border.color
property alias borderWidth: borderRect.border.width
implicitWidth: borderRect.implicitWidth
implicitHeight: borderRect.implicitHeight
WRectangularShadow {
target: borderRect
}
Rectangle {
id: borderRect
z: 1
color: "transparent"
radius: root.radius
border.color: Looks.colors.bg2Border
border.width: 1
implicitWidth: contentItem.implicitWidth + border.width * 2
implicitHeight: contentItem.implicitHeight + border.width * 2
anchors.fill: contentRect
anchors.margins: -border.width
}
Rectangle {
id: contentRect
anchors.centerIn: parent
z: 0
color: Looks.colors.bgPanelFooterBackground
implicitWidth: contentItem.implicitWidth
implicitHeight: contentItem.implicitHeight
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
id: contentAreaMask
width: contentRect.width
height: contentRect.height
radius: root.radius - borderRect.border.width
}
}
children: [root.contentItem]
}
}

View file

@ -0,0 +1,26 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
import qs.modules.waffle.bar
WButton {
id: root
property alias iconName: iconContent.icon
property alias iconSize: iconContent.implicitSize
property alias monochrome: iconContent.monochrome
implicitWidth: 40
implicitHeight: 40
contentItem: Item {
FluentIcon {
id: iconContent
anchors.centerIn: parent
implicitSize: 18
icon: root.iconName
}
}
}

View file

@ -0,0 +1,6 @@
import QtQuick
import QtQuick.Layouts
ColumnLayout {
spacing: 0
}

View file

@ -0,0 +1,10 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.waffle.looks
Rectangle {
Layout.fillHeight: false
Layout.fillWidth: true
color: Looks.colors.bgPanelSeparator
implicitHeight: 1
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
PopupToolTip {
id: root
required property Item realContentItem
realContentItem: WText {
text: root.text
anchors.centerIn: parent
}
property real visualMargin: 11
verticalPadding: 8
horizontalPadding: 10
verticalMargin: visualMargin
horizontalMargin: visualMargin
contentItem: WToolTipContent {
id: tooltipContent
realContentItem: root.realContentItem
horizontalPadding: root.horizontalPadding
verticalPadding: root.verticalPadding
}
}

View file

@ -0,0 +1,49 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Widgets
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
ProgressBar {
id: root
Behavior on value {
SmoothedAnimation {
velocity: Looks.transition.velocity
}
}
implicitHeight: 4
background: null
contentItem: Item {
id: background
Rectangle {
id: trackTrough
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
radius: root.implicitHeight / 2
color: Looks.colors.controlBg
implicitHeight: root.implicitHeight
}
Rectangle {
id: trackHighlight
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
radius: root.implicitHeight / 2
color: Looks.colors.accent
implicitHeight: root.implicitHeight
width: background.width * root.value
}
}
}

View file

@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Effects
import qs.modules.common
import qs.modules.common.widgets
StyledRectangularShadow {
blur: 10
spread: 2
offset: Qt.vector2d(0.0, 4)
color: Looks.colors.shadow
}

View file

@ -0,0 +1,15 @@
import QtQuick
import QtQuick.Effects
import qs.modules.common
import qs.modules.common.widgets
Item {
default property Item contentItem
property Item shadow: WRectangularShadow {
target: contentItem
}
implicitWidth: contentItem.implicitWidth
implicitHeight: contentItem.implicitHeight
children: [shadow, contentItem]
}

View file

@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Controls
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
ScrollBar {
id: root
policy: ScrollBar.AsNeeded
active: hovered || pressed
property color color: Looks.colors.controlBg
contentItem: Rectangle {
implicitWidth: root.active ? 4 : 2
implicitHeight: root.visualSize
radius: 9999
color: root.color
opacity: root.policy === ScrollBar.AlwaysOn || (root.active && root.size < 1.0) ? 0.5 : 0
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
}
}

View file

@ -0,0 +1,105 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Widgets
import qs.modules.common
import qs.modules.common.widgets
Slider {
id: root
property real trackWidth: 4
property string tooltipContent: `${Math.round(value * 100)}`
property bool scrollable: false
stepSize: 0.02
leftPadding: 0
rightPadding: 0
implicitHeight: handle.implicitHeight
Behavior on value { // This makes the adjusted value (like volume) shift smoothly
SmoothedAnimation {
velocity: Looks.transition.velocity
}
}
background: MouseArea {
id: background
anchors.fill: parent
onWheel: (event) => {
if (!root.scrollable) {
event.accepted = false;
return;
}
if (event.angleDelta.y > 0) {
root.value = Math.min(root.value + root.stepSize, 1)
root.moved()
} else {
root.value = Math.max(root.value - root.stepSize, 0)
root.moved()
}
}
Rectangle {
id: trackHighlight
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
topLeftRadius: root.trackWidth / 2
bottomLeftRadius: root.trackWidth / 2
color: Looks.colors.accent
implicitHeight: root.trackWidth
width: background.width * root.visualPosition
}
Rectangle {
id: trackTrough
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
topRightRadius: root.trackWidth / 2
bottomRightRadius: root.trackWidth / 2
color: Looks.colors.controlBg
implicitHeight: root.trackWidth
width: background.width * (1 - root.visualPosition)
}
}
handle: Circle {
id: handle
anchors.verticalCenter: parent.verticalCenter
x: (diameter / 2) + root.visualPosition * (root.width - diameter) - (diameter / 2)
diameter: 20
color: Looks.colors.controlFg
MouseArea {
id: handleMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
Circle {
anchors.centerIn: parent
diameter: root.pressed ? 10 : handleMouseArea.containsMouse ? 14 : 12
color: Looks.colors.accent
Behavior on diameter {
animation: Looks.transition.enter.createObject(this)
}
}
WToolTip {
id: tooltip
extraVisibleCondition: root.pressed
text: root.tooltipContent
font.pixelSize: Looks.font.pixelSize.larger
verticalPadding: 3
horizontalPadding: 8
}
}
}

View file

@ -0,0 +1,84 @@
import QtQuick
import QtQuick.Controls
import qs.modules.waffle.looks
StackView {
id: root
property real moveDistance: 30
property int pushDuration: 200
property int fadeDuration: 80
property list<real> bezierCurve: Looks.transition.easing.bezierCurve.easeIn
property list<real> fadeBezierCurve: Looks.transition.easing.bezierCurve.easeInOut
clip: true
background: null
pushEnter: Transition {
XAnimator {
from: -root.moveDistance
to: 0
duration: root.pushDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.bezierCurve
}
NumberAnimation {
properties: "opacity"
from: 0
to: 1
duration: root.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.fadeBezierCurve
}
}
pushExit: Transition {
XAnimator {
from: 0
to: root.moveDistance
duration: root.pushDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.bezierCurve
}
NumberAnimation {
properties: "opacity"
from: 1
to: 0
duration: root.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.fadeBezierCurve
}
}
popEnter: Transition {
XAnimator {
from: root.moveDistance
to: 0
duration: root.pushDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.bezierCurve
}
NumberAnimation {
properties: "opacity"
from: 0
to: 1
duration: root.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.fadeBezierCurve
}
}
popExit: Transition {
XAnimator {
from: 0
to: -root.moveDistance
duration: root.pushDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.bezierCurve
}
NumberAnimation {
properties: "opacity"
from: 1
to: 0
duration: root.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.fadeBezierCurve
}
}
}

View file

@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import qs.modules.common
import qs.modules.waffle.looks
Switch {
id: root
implicitWidth: 40
implicitHeight: 20
property real indicatorHeight: 12
property real indicatorPressedHeight: 14
property real indicatorPressedWidth: 17
property color checkedColor: Looks.colors.accent
property color uncheckedColor: Looks.colors.bg1
property color borderColor: Looks.colors.controlBgInactive
readonly property real indicatorPressedWidthDiff: indicatorPressedWidth - indicatorHeight
background: Rectangle {
width: parent.width
height: parent.height
radius: height / 2
color: root.checked ? root.checkedColor : root.uncheckedColor
border.width: 1
border.color: root.checked ? root.checkedColor : root.borderColor
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
Behavior on border.color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
// Custom thumb styling
indicator: Rectangle {
implicitWidth: (root.pressed || root.down) ? root.indicatorPressedWidth : root.indicatorHeight
implicitHeight: (root.pressed || root.down) ? root.indicatorPressedHeight : root.indicatorHeight
radius: height / 2
color: root.checked ? Looks.colors.accentFg : root.borderColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: {
if (root.checked) {
return 24 - (root.pressed || root.down ? root.indicatorPressedWidthDiff : 0);
} else {
return (root.pressed || root.down) ? 3 : (Config.options.waffles.tweaks.switchHandlePositionFix ? 4 : 3);
}
}
Behavior on anchors.leftMargin {
animation: Looks.transition.enter.createObject(this)
}
Behavior on implicitWidth {
animation: Looks.transition.resize.createObject(this)
}
Behavior on implicitHeight {
animation: Looks.transition.resize.createObject(this)
}
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
Text {
id: root
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
color: Looks.colors.fg
font {
hintingPreference: Font.PreferDefaultHinting
family: Looks.font.family.ui
pixelSize: Looks.font.pixelSize.normal
weight: Looks.font.weight.regular
variableAxes: Looks.font.variableAxes.ui
}
linkColor: Looks.colors.link
}

View file

@ -0,0 +1,23 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.modules.waffle.looks
WButton {
id: root
implicitHeight: 40
implicitWidth: contentItem.implicitWidth + 30
color: "transparent"
contentItem: Item {
id: contentItem
anchors.centerIn: parent
implicitWidth: buttonText.implicitWidth
WText {
id: buttonText
anchors.centerIn: parent
color: root.pressed ? Looks.colors.fg : Looks.colors.fg1
text: root.text
}
}
}

View file

@ -0,0 +1,31 @@
import qs.modules.common
import QtQuick
import QtQuick.Controls.FluentWinUI3
import QtQuick.Controls
TextField {
id: root
clip: true
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
color: Looks.colors.fg
palette {
active: Looks.colors.accent
}
font {
hintingPreference: Font.PreferDefaultHinting
family: Looks.font.family.ui
pixelSize: Looks.font.pixelSize.normal
weight: Looks.font.weight.regular
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
cursorShape: Qt.IBeamCursor
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
import QtQuick.Controls
TextInput {
id: root
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
color: Looks.colors.fg
font {
hintingPreference: Font.PreferFullHinting
family: Looks.font.family.ui
pixelSize: Looks.font.pixelSize.large
weight: Looks.font.weight.regular
}
selectionColor: Looks.colors.selection
selectedTextColor: Looks.colors.selectionFg
}

View file

@ -0,0 +1,31 @@
import QtQuick
Item {
id: root
property string longestText
property alias text: textItem.text
property alias font: textItem.font
property alias horizontalAlignment: textItem.horizontalAlignment
property alias verticalAlignment: textItem.verticalAlignment
property alias color: textItem.color
implicitWidth: longestTextMetrics.width
implicitHeight: longestTextMetrics.height
TextMetrics {
id: longestTextMetrics
text: root.longestText
font {
family: Looks.font.family.ui
pixelSize: Looks.font.pixelSize.large
weight: Looks.font.weight.regular
}
}
WText {
id: textItem
anchors.fill: parent
font.pixelSize: Looks.font.pixelSize.large
}
}

View file

@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
StyledToolTip {
id: root
required property Item realContentItem
font {
family: Looks.font.family.ui
pixelSize: Looks.font.pixelSize.normal
weight: Looks.font.weight.regular
}
realContentItem: WText {
text: root.text
font: root.font
anchors.centerIn: parent
}
verticalPadding: 8
horizontalPadding: 10
delay: 400
contentItem: WToolTipContent {
id: tooltipContent
realContentItem: root.realContentItem
horizontalPadding: root.horizontalPadding
verticalPadding: root.verticalPadding
}
}

View file

@ -0,0 +1,30 @@
import QtQuick
import Quickshell
import qs.modules.waffle.looks
Item {
id: root
anchors.centerIn: parent
required property Item realContentItem
property alias radius: realContent.radius
property real verticalPadding: 8
property real horizontalPadding: 10
implicitWidth: realContent.implicitWidth + 2 * 2
implicitHeight: realContent.implicitHeight + 2 * 2
WAmbientShadow {
target: realContent
}
Rectangle {
id: realContent
z: 1
anchors.centerIn: parent
implicitWidth: root.realContentItem.implicitWidth + root.horizontalPadding * 2
implicitHeight: root.realContentItem.implicitHeight + root.verticalPadding * 2
color: Looks.colors.bg1Base
radius: Looks.radius.medium
children: [root.realContentItem]
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Item {
id: root
property real padding: 9
property alias colBackground: background.color
property alias spacing: toolbarLayout.spacing
property alias radius: background.radius
default property alias toolbarData: toolbarLayout.data
implicitWidth: background.implicitWidth
implicitHeight: background.implicitHeight
Rectangle {
id: background
anchors.fill: parent
implicitHeight: 50
implicitWidth: toolbarLayout.implicitWidth + root.padding * 2
radius: Looks.radius.large
color: Looks.colors.bg0Base
border.width: 1
border.color: Looks.colors.bg1Border
RowLayout {
id: toolbarLayout
spacing: 4
anchors {
fill: parent
margins: root.padding
}
}
}
}

View file

@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
WButton {
implicitHeight: 32
radius: Looks.radius.medium
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
WToolbarButton {
id: root
implicitWidth: height
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: root.icon.name
implicitSize: 18
color: root.fgColor
}
}
}

View file

@ -0,0 +1,21 @@
import QtQuick
import QtQuick.Controls
import qs.modules.common
TabButton {
id: root
implicitWidth: 38
implicitHeight: 32
padding: 0
background: null
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: root.icon.name
color: root.icon.color
implicitSize: 18
}
}
}

View file

@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
Rectangle {
Layout.leftMargin: 4
Layout.rightMargin: 4
implicitHeight: 24
implicitWidth: 1
color: Looks.colors.bg0Border
}

View file

@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import qs.modules.common
import qs.modules.common.functions
TabBar {
id: root
implicitHeight: 32
background: Rectangle {
radius: Looks.radius.medium
color: Looks.colors.bgPanelFooter
border.color: ColorUtils.transparentize(Looks.colors.bg0Border, 0.7)
border.width: 1
// Indicator
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: root.currentIndex * (root.width / root.count)
Behavior on leftMargin {
animation: Looks.transition.resize.createObject(this)
}
}
radius: Looks.radius.medium
color: Looks.colors.bg2Base
border.color: Looks.colors.bg0Border
border.width: 1
width: root.width / root.count
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 1
}
implicitWidth: pressDetector.containsPress ? 16 : 12
implicitHeight: 3
radius: height / 2
color: Looks.colors.accent
}
}
}
MouseArea {
id: pressDetector
z: 9999
anchors.fill: parent
acceptedButtons: Qt.LeftButton
}
}

View file

@ -0,0 +1,27 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
StyledImage {
id: avatar
Layout.alignment: Qt.AlignTop
sourceSize: Qt.size(32, 32)
source: Directories.userAvatarPathAccountsService
fallbacks: [Directories.userAvatarPathRicersAndWeirdSystems, Directories.userAvatarPathRicersAndWeirdSystems2]
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Circle {
diameter: avatar.height
}
}
}

View file

@ -0,0 +1,142 @@
pragma ComponentBehavior: Bound
import QtQml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.waffle.looks
BodyRectangle {
id: root
// State
property bool collapsed
// Locale
property var locale: Qt.locale(Config.options.calendar.locale)
implicitHeight: collapsed ? 0 : contentColumn.implicitHeight
implicitWidth: contentColumn.implicitWidth
Behavior on implicitHeight {
animation: Looks.transition.enter.createObject(this)
}
clip: true
ColumnLayout {
id: contentColumn
spacing: 12
CalendarHeader {
Layout.topMargin: 10
Layout.fillWidth: true
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: 5
Layout.rightMargin: 5
spacing: 1
DayOfWeekRow {
Layout.fillWidth: true
locale: root.locale
spacing: calendarView.buttonSpacing
implicitHeight: calendarView.buttonSize
delegate: Item {
id: dayOfWeekItem
required property var model
implicitHeight: calendarView.buttonSize
implicitWidth: calendarView.buttonSize
WText {
anchors.centerIn: parent
text: {
var result = dayOfWeekItem.model.shortName;
if (Config.options.waffles.calendar.force2CharDayOfWeek) result = result.substring(0,2);
return result;
}
color: Looks.colors.fg
font.pixelSize: Looks.font.pixelSize.large
}
}
}
CalendarView {
id: calendarView
locale: root.locale
verticalPadding: 2
buttonSize: 41 // ???
buttonSpacing: 6
buttonVerticalSpacing: 1
Layout.fillWidth: true
delegate: DayButton {}
}
}
}
component DayButton: WButton {
id: dayButton
required property var model
checked: model.today
enabled: hovered || checked || model.month === calendarView.focusedMonth
implicitWidth: calendarView.buttonSize
implicitHeight: calendarView.buttonSize
radius: height / 2
required property int index
contentItem: Item {
WText {
anchors.centerIn: parent
text: dayButton.model.day
color: dayButton.fgColor
font.pixelSize: Looks.font.pixelSize.larger
}
}
}
component CalendarHeader: RowLayout {
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 8
WBorderlessButton {
Layout.fillWidth: true
implicitHeight: 34
contentItem: Item {
WText {
anchors.fill: parent
horizontalAlignment: Text.AlignLeft
text: Qt.locale().toString(calendarView.focusedDate, "MMMM yyyy")
font.pixelSize: Looks.font.pixelSize.large
font.weight: Looks.font.weight.strong
}
}
}
ScrollMonthButton {
scrollDown: false
}
ScrollMonthButton {
scrollDown: true
}
}
component ScrollMonthButton: WBorderlessButton {
id: scrollMonthButton
required property bool scrollDown
Layout.alignment: Qt.AlignVCenter
onClicked: {
calendarView.scrollMonthsAndSnap(scrollDown ? 1 : -1);
}
implicitWidth: 32
implicitHeight: 34
contentItem: FluentIcon {
filled: true
implicitSize: 12
icon: scrollMonthButton.scrollDown ? "caret-down" : "caret-up"
}
}
}

View file

@ -0,0 +1,55 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
FooterRectangle {
id: root
implicitWidth: 0
property bool collapsed
color: ColorUtils.transparentize(Looks.colors.bgPanelBody, collapsed ? 0 : 1)
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
RowLayout {
anchors {
fill: parent
leftMargin: 16
rightMargin: 16
topMargin: 12
bottomMargin: 12
}
WText {
Layout.fillWidth: true
font.pixelSize: Looks.font.pixelSize.large
text: DateTime.collapsedCalendarFormat
}
WBorderedButton {
implicitWidth: 24
implicitHeight: 24
padding: 0
onClicked: root.collapsed = !root.collapsed
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
implicitSize: 12
icon: "chevron-down"
rotation: root.collapsed ? 180 : 0
Behavior on rotation {
animation: Looks.transition.rotate.createObject(this)
}
}
}
}
}
}

View file

@ -0,0 +1,72 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
FooterRectangle {
Layout.fillWidth: true
implicitWidth: 0
color: Looks.colors.bgPanelBody
RowLayout {
anchors {
fill: parent
leftMargin: 16
rightMargin: 16
topMargin: 12
bottomMargin: 12
}
spacing: 0
SmallBorderedIconButton {
visible: !TimerService.pomodoroRunning
icon.name: "subtract"
onClicked: Config.options.time.pomodoro.focus -= 300 // 5 mins
}
WTextWithFixedWidth {
visible: !TimerService.pomodoroRunning
implicitWidth: 81
horizontalAlignment: Text.AlignHCenter
color: Looks.colors.subfg
text: Translation.tr("%1 mins").arg(`<font color="${Looks.colors.fg.toString()}">${TimerService.focusTime / 60}</font>`)
}
SmallBorderedIconButton {
visible: !TimerService.pomodoroRunning
icon.name: "add"
onClicked: Config.options.time.pomodoro.focus += 300 // 5 mins
}
WText {
visible: TimerService.pomodoroRunning
font.pixelSize: Looks.font.pixelSize.large
text: Translation.tr("Focusing")
}
Item {
Layout.fillWidth: true
}
SmallBorderedIconAndTextButton {
iconName: TimerService.pomodoroRunning ? "stop" : "play"
text: TimerService.pomodoroRunning ? Translation.tr("End session") : Translation.tr("Focus")
onClicked: {
if (TimerService.pomodoroRunning) {
TimerService.togglePomodoro();
TimerService.resetPomodoro();
} else {
TimerService.togglePomodoro();
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarRight", "toggle"]);
}
}
}
}
}

View file

@ -0,0 +1,99 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Qt.labs.synchronizer
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
WBarAttachedPanelContent {
id: root
readonly property bool barAtBottom: Config.options.waffles.bar.bottom
revealFromSides: true
revealFromLeft: false
property bool collapsed: false
contentItem: ColumnLayout {
id: contentLayout
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: parent.bottom
}
spacing: 12
Item {
id: notificationArea
Layout.fillHeight: true
implicitWidth: notificationPane.implicitWidth
WPane {
id: notificationPane
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
contentItem: NotificationPaneContent {
implicitWidth: calendarColumnLayout.implicitWidth
implicitHeight: {
if (Notifications.list.length > 0) {
return ((contentLayout.height - calendarPane.height - contentLayout.spacing) - notificationPane.borderWidth * 2)
}
return 230;
}
Timer {
id: enableTimer
interval: Config.options.hacks.arbitraryRaceConditionDelay
onTriggered: heightBehavior.enabled = true;
}
Behavior on implicitHeight {
id: heightBehavior
enabled: false
Component.onCompleted: {
enableTimer.restart();
}
animation: Looks.transition.enter.createObject(this)
}
}
}
}
WPane {
id: calendarPane
contentItem: WPanelPageColumn {
id: calendarColumnLayout
DateHeader {
Layout.fillWidth: true
Synchronizer on collapsed {
property alias source: root.collapsed
}
}
WPanelSeparator {
visible: !root.collapsed
}
CalendarWidget {
Layout.fillWidth: true
Synchronizer on collapsed {
property alias source: root.collapsed
}
}
WPanelSeparator {}
FocusFooter {
Layout.fillWidth: true
}
}
}
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.waffle.looks
WBorderlessButton {
id: root
Layout.fillWidth: false
property real implicitSize: 16
implicitWidth: implicitSize
implicitHeight: implicitSize
color: "transparent"
colForeground: root.hovered && !root.pressed ? Looks.colors.fg : Looks.colors.fg1
Behavior on colForeground {
animation: Looks.transition.color.createObject(this)
}
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
implicitSize: root.implicitSize
icon: root.icon.name
color: root.colForeground
}
}
}

Some files were not shown because too many files have changed in this diff Show more