commit 9e44812e939c9a436c2f949274f995f688d6ff99 Author: kossLAN Date: Sat Jun 7 04:01:14 2025 -0400 Initial commit remove syncthing folder bar/popops: fix menu window anims and positioning bar/popops: change anims a little and add dropshadow Update README.md widgets/coloredicon: move to colorization, looks worse but..., yea bar/popops: make popup window dissapear on menu close README: add todo list, and brief desc Update README.md Update README.md Update README.md bar/systray: issue recreate on interact bar/systray: hide popup on interact bar/systray: add arrow for entries with children bar/battery: start of battery widget wallpaper/matugen: add foot template extra sizing conditions for sys tray bar/systray: add some more margin to text update settings schema bar/workspaces: filter by monitor, switch to scriptmodel settings: fix settings lol bar/systray: fix right item feat: screenshot tool clipboard one day... feat: init lockscreen mpris: add ipc handler for multimedia keys mpris stuff save progress put shell in subdir, and add nix package move readme back woops bar/volume: make tool bar smaller greeter: init greeter greeter: fixed resource links readme: update checklist progress maybe, maybe not fix: fixed screenshot tool not working fix: bar layout issues progress save progress update track styled popup still broken but getting there still broken but getting there fix: gitignore qmlls.ini fix: remove qmlls.ini progress save new popup system new popup system new popup system more work on popups fix: mask issues on popups update readme diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bb1a90 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +shell/.qmlls.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..187b6e4 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# kossLAN's personal quickshell dots, for personal use. + +The idea is to eventually be minimal but also use material 3 design language, low padding, low margin, and not distracting. + +## TODO List +- [x] Custom Popup Window Surface for smooth anims on top bar +- [x] Lockscreen (WIP) +- [x] Make volume mixer +- [x] Screenshot tool (WIP - kinda scuffed, but is functional) +- [ ] Recording/Clip widget with gpuscreenrecorder +- [x] Session Manager +- [ ] Battery Profile Popup +- [ ] REDO Volume OSD (WIP) +- [ ] REDO Launcher (wallpaper picker, calculator, commands, etc...) +- [ ] Music Player Popup V4 lol +- [ ] Add bluetooth module support diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..85e2e9c --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1750178436, + "narHash": "sha256-t1lcWocjeNT3kYqxYUj3R/O/9PbNsvYFzW50NRkx6X4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "479251543fd2a1256569108a5dee5c79e6caf8bd", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d5d5d62 --- /dev/null +++ b/flake.nix @@ -0,0 +1,31 @@ +{ + description = "kossLAN's quickshell dots"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs"; + }; + + outputs = { + self, + nixpkgs, + }: let + forEachSystem = fn: + nixpkgs.lib.genAttrs + ["x86_64-linux" "aarch64-linux"] + (system: fn system nixpkgs.legacyPackages.${system}); + in { + packages = forEachSystem (system: pkgs: rec { + default = minmat; + minmat = pkgs.stdenv.mkDerivation { + pname = "minmat"; + version = "0.1.0"; + src = ./shell; + + installPhase = '' + mkdir -p $out/etc/quickshell + cp -r * $out/etc/quickshell + ''; + }; + }); + }; +} diff --git a/shell/ShellSettings.qml b/shell/ShellSettings.qml new file mode 100644 index 0000000..133e16d --- /dev/null +++ b/shell/ShellSettings.qml @@ -0,0 +1,46 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + property alias settings: jsonAdapter.settings + property alias sizing: jsonAdapter.sizing + + property QtObject colors: QtObject { + property color surface: Qt.rgba(1.0, 1.0, 1.0, 1.0) + property color surface_translucent: Qt.rgba(0.0, 0.0, 0.0, 0.15) + property color surface_container: Qt.rgba(0.25, 0.25, 0.25, 1.0) + property color surface_container_translucent: Qt.rgba(0.25, 0.25, 0.25, 0.25) + property color highlight: Qt.rgba(1.0, 1.0, 1.0, 0.85) + // property color primary: "#2EADC6" + property color active: Qt.rgba(1.0, 1.0, 1.0, 1.0) + property color active_translucent: Qt.rgba(1.0, 1.0, 1.0, 0.15) + property color border_translucent: Qt.rgba(1.0, 1.0, 1.0, 0.05) + property color inactive: Qt.rgba(0.25, 0.25, 0.25, 1.0) + property color inactive_translucent: Qt.rgba(0.25, 0.25, 0.25, 0.15) + } + + FileView { + path: `${Quickshell.dataPath("settings")}/quickshell/settings.json` + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + blockLoading: true + + JsonAdapter { + id: jsonAdapter + + property JsonObject settings: JsonObject { + property string wallpaperUrl: Qt.resolvedUrl("root:resources/wallpapers/pixelart0.jpg") + property string screenshotPath: "/home/koss/Pictures" + property real opacity: 0.55 + } + + property JsonObject sizing: JsonObject { + property int barHeight: 25 + } + } + } +} diff --git a/shell/bar/ActiveWindow.qml b/shell/bar/ActiveWindow.qml new file mode 100644 index 0000000..4e5e126 --- /dev/null +++ b/shell/bar/ActiveWindow.qml @@ -0,0 +1,12 @@ +import QtQuick +import Quickshell.Wayland +import ".." + +Text { + id: windowText + text: ToplevelManager.activeToplevel?.title ?? "" + color: ShellSettings.colors.active + font.pointSize: 11 + visible: text !== "" + elide: Text.ElideRight +} diff --git a/shell/bar/Bar.qml b/shell/bar/Bar.qml new file mode 100644 index 0000000..85c220d --- /dev/null +++ b/shell/bar/Bar.qml @@ -0,0 +1,101 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import "power" +// import "volume" +import "systray" +// import qs.widgets +import qs + +Variants { + model: Quickshell.screens + + delegate: PanelWindow { + id: root + color: ShellSettings.colors.surface_translucent + implicitHeight: ShellSettings.sizing.barHeight + screen: modelData + + required property var modelData + + anchors { + top: true + left: true + right: true + } + + readonly property Popup popup: Popup { + bar: root + } + + RowLayout { + spacing: 0 + + anchors { + fill: parent + leftMargin: 5 + rightMargin: 5 + } + + // Left side of bar + RowLayout { + spacing: 15 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignLeft + + Workspaces { + screen: root.screen + Layout.fillHeight: true + } + + ActiveWindow { + id: activeWindow + Layout.preferredWidth: 400 + } + } + + // PowerMenu { + // bar: root + // Layout.fillHeight: true + // } + + // Right side of bar + RowLayout { + spacing: 10 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignRight + + SysTray { + bar: root + Layout.fillHeight: true + } + + // VolumeIndicator { + // id: volumeIndicator + // popup: root.popup + // Layout.preferredWidth: this.height + // Layout.fillHeight: true + // Layout.topMargin: 2 + // Layout.bottomMargin: 2 + // } + + // PowerMenu { + // bar: root + // Layout.fillHeight: true + // } + + // Widgets.Separator { + // Layout.leftMargin: 5 + // Layout.rightMargin: 5 + // } + + Clock { + id: clock + color: ShellSettings.colors.active + } + } + } + } +} diff --git a/shell/bar/Clock.qml b/shell/bar/Clock.qml new file mode 100644 index 0000000..eee72e8 --- /dev/null +++ b/shell/bar/Clock.qml @@ -0,0 +1,21 @@ +import QtQuick +import Quickshell + +Text { + property string ap: sysClock.hours >= 12 ? "PM" : "AM" + property string minutes: sysClock.minutes.toString().padStart(2, '0') + property string hours: { + var value = sysClock.hours % 12; + if (value === 0) + return 12; + return value; + } + + SystemClock { + id: sysClock + enabled: true + } + + text: `${hours}:${minutes} ${ap}` + font.pointSize: 11 +} diff --git a/shell/bar/Popup.qml b/shell/bar/Popup.qml new file mode 100644 index 0000000..f8dada8 --- /dev/null +++ b/shell/bar/Popup.qml @@ -0,0 +1,197 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Hyprland +import qs.widgets + +Scope { + id: root + + required property var bar + + property real gaps: 5 + + property Item parentItem + property PopupItem activeItem + property PopupItem lastActiveItem + + property PopupItem shownItem: activeItem ?? lastActiveItem + + onActiveItemChanged: { + if (activeItem != null) { + activeItem.targetVisible = true; + + if (parentItem) { + activeItem.parent = parentItem; + } + } + + if (lastActiveItem != null && lastActiveItem != activeItem) { + lastActiveItem.targetVisible = false; + } + + if (activeItem != null) { + lastActiveItem = activeItem; + } + } + + function setItem(item: PopupItem) { + activeItem = item; + } + + function removeItem(item: PopupItem) { + if (activeItem == item) { + activeItem = null; + } + } + + function onHidden(item: PopupItem) { + if (item == lastActiveItem) { + console.log("triggered"); + lastActiveItem = null; + } + } + + property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0 + + Behavior on scaleMul { + SmoothedAnimation { + velocity: 5 + } + } + + LazyLoader { + id: popupLoader + activeAsync: root.shownItem != null + + PopupWindow { + id: popup + visible: true + color: "transparent" + implicitWidth: root.bar.width + implicitHeight: Math.max(800, parentItem.targetHeight) + + anchor { + window: root.bar + rect: Qt.rect(0, 0, root.bar.width, root.bar.height) + edges: Edges.Bottom | Edges.Left + gravity: Edges.Bottom | Edges.Right + adjustment: PopupAdjustment.None + } + + mask: Region { + item: parentItem + } + + HyprlandFocusGrab { + id: grab + active: true + windows: [popup, root.bar] + onCleared: { + if (!active) { + root.shownItem.closed(); + } + } + } + + // HyprlandWindow.opacity: root.scaleMul + + HyprlandWindow.visibleMask: popup.mask + + Connections { + target: root + + function onScaleMulChanged() { + popup.mask.changed(); + } + } + + StyledRectangle { + id: parentItem + width: targetWidth + height: targetHeight + x: targetX + y: root.gaps + + transform: Scale { + origin.x: parentItem.targetX + origin.y: 0 + xScale: 1 + yScale: root.scaleMul + } + + readonly property var targetWidth: root.shownItem?.implicitWidth ?? 0 + readonly property var targetHeight: root.shownItem?.implicitHeight ?? 0 + + readonly property var targetX: { + if (root.shownItem == null) { + return 0; + } + + let owner = root.shownItem.owner; + let bar = root.bar; + let isCentered = root.shownItem.centered; + let xPos = owner.mapToItem(bar.contentItem, 0, bar.height, owner.width, 0).x; + + let rightEdge = xPos + targetWidth; + let maxRightEdge = popup.width; + + if (isCentered) { + return xPos - (targetWidth / 2) + (owner.width / 2); + } + + if (rightEdge > maxRightEdge) { + // touching right edge, reposition + // console.log("touching right edge"); + return maxRightEdge - targetWidth - root.gaps; + } + + return xPos; + } + + Component.onCompleted: { + root.parentItem = this; + + if (root.activeItem) { + root.activeItem.parent = this; + } + } + + // TODO: Make a close animation, a little complicated, will need to track if an animation is running + // and stop unload from occuring until its done, in the LazyLoader. + + Behavior on x { + enabled: root.lastActiveItem != null + SmoothedAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } + } + + Behavior on width { + enabled: root.lastActiveItem != null + SmoothedAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } + } + + Behavior on height { + enabled: root.lastActiveItem != null + SmoothedAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } + } + + // SmoothedAnimation on height { + // duration: 200 + // easing.type: Easing.InOutQuad + // to: parentItem.targetHeight + // onToChanged: restart() + // } + } + } + } +} diff --git a/shell/bar/PopupItem.qml b/shell/bar/PopupItem.qml new file mode 100644 index 0000000..525ae7c --- /dev/null +++ b/shell/bar/PopupItem.qml @@ -0,0 +1,66 @@ +import QtQuick +import qs.widgets + +Item { + id: root + visible: false + opacity: root.targetOpacity + + onShowChanged: { + if (show) { + popup.setItem(this); + } else { + popup.removeItem(this); + } + } + + onTargetVisibleChanged: { + if (targetVisible) { + visible = true; + targetOpacity = 1; + } else { + console.log("closed"); + closed(); + targetOpacity = 0; + } + } + + onTargetOpacityChanged: { + if (!targetVisible && targetOpacity == 0) { + visible = false; + this.parent = null; + if (popup) + popup.onHidden(this); + } + } + + readonly property alias contentItem: contentItem + default property alias data: contentItem.data + readonly property Item item: contentItem + + Item { + id: contentItem + anchors.fill: parent + // anchors.margins: 5 + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + } + + required property var popup + required property var owner + property bool centered: false + property bool show: false + + signal closed + + property bool targetVisible: false + property real targetOpacity: 0 + + Behavior on targetOpacity { + id: opacityAnimation + SmoothedAnimation { + velocity: 5 + } + } +} diff --git a/shell/bar/Workspaces.qml b/shell/bar/Workspaces.qml new file mode 100644 index 0000000..ab14fa9 --- /dev/null +++ b/shell/bar/Workspaces.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import qs + +RowLayout { + spacing: 6 + visible: Hyprland.monitors.values.length != 0 + + required property var screen + + Repeater { + id: workspaceButtons + + model: ScriptModel { + values: Hyprland.workspaces.values.slice().filter(workspace => workspace.monitor === Hyprland.monitorFor(screen)) + } + + Rectangle { + radius: height / 2 + + color: { + let value = ShellSettings.colors.active_translucent; + + if (!modelData?.id || !Hyprland.focusedMonitor?.activeWorkspace?.id) + return value; + + if (workspaceButton.containsMouse) { + value = ShellSettings.colors.highlight; + } else if (Hyprland.focusedMonitor.activeWorkspace.id === modelData.id) { + value = ShellSettings.colors.highlight; + } + + return value; + } + + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: 12 + Layout.preferredWidth: { + if (Hyprland.focusedMonitor?.activeWorkspace?.id === modelData?.id) + return 25; + + return 12; + } + + required property var modelData + + Behavior on Layout.preferredWidth { + SmoothedAnimation { + duration: 150 + velocity: 200 + easing.type: Easing.OutCubic + } + } + + Behavior on color { + ColorAnimation { + duration: 100 + easing.type: Easing.OutQuad + } + } + + MouseArea { + id: workspaceButton + anchors.fill: parent + hoverEnabled: true + onPressed: Hyprland.dispatch(`workspace ${parent.modelData.id}`) + } + } + } +} diff --git a/shell/bar/mpris/Button.qml b/shell/bar/mpris/Button.qml new file mode 100644 index 0000000..715aed8 --- /dev/null +++ b/shell/bar/mpris/Button.qml @@ -0,0 +1,42 @@ +import QtQuick +import "../../mpris" as Mpris +import "../../widgets" as Widgets +import "../.." + +Widgets.MaterialButton { + id: root + radius: 6 + implicitWidth: mediaInfo.contentWidth + 8 + implicitHeight: parent.height + // onClicked: { + // popup.visible = !popup.visible; + // } + + required property var bar + property var player: Mpris.Controller.trackedPlayer + + Text { + id: mediaInfo + text: root.player?.trackTitle ?? "" + color: root.containsMouse ? ShellSettings.colors["inverse_primary"] : ShellSettings.colors["inverse_surface"] + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: 11 + anchors.centerIn: parent + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } +} + +// WidgetWindow { +// id: popup +// visible: false +// parentWindow: root.bar +// +// // anchor.window: root.bar +// } diff --git a/shell/bar/mpris/WidgetWindow.qml b/shell/bar/mpris/WidgetWindow.qml new file mode 100644 index 0000000..78126bd --- /dev/null +++ b/shell/bar/mpris/WidgetWindow.qml @@ -0,0 +1,9 @@ +import Quickshell + +PopupWindow { + id: root + color: "red" + implicitWidth: 500 + implicitHeight: 500 + +} diff --git a/shell/bar/notifications/NotificationButton.qml b/shell/bar/notifications/NotificationButton.qml new file mode 100644 index 0000000..028244f --- /dev/null +++ b/shell/bar/notifications/NotificationButton.qml @@ -0,0 +1,70 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Qt5Compat.GraphicalEffects +import Quickshell.Widgets +import "../.." + +Item { + id: root + required property var bar + property var implicitSize: 0 + readonly property real actualSize: Math.min(root.width, root.height) + + implicitWidth: parent.height + implicitHeight: parent.height + + NotificationCenter { + id: notificationCenter + } + + Rectangle { + color: mouseArea.containsMouse ? ShellSettings.colors["primary"] : "transparent" + radius: 5 + + anchors { + fill: parent + margins: 1 + } + } + + MouseArea { + id: mouseArea + hoverEnabled: true + anchors.fill: parent + onPressed: { + if (notificationCenter.visible) { + notificationCenter.hide(); + } else { + notificationCenter.show(); + } + } + } + + Item { + implicitWidth: root.implicitSize + implicitHeight: root.implicitSize + anchors.centerIn: parent + + layer.enabled: true + layer.effect: OpacityMask { + source: Rectangle { + width: root.actualSize + height: root.actualSize + color: "white" + } + + maskSource: IconImage { + implicitSize: root.actualSize + source: "root:resources/general/notification.svg" + } + } + + Rectangle { + color: mouseArea.containsMouse ? ShellSettings.colors["inverse_primary"] : ShellSettings.colors["inverse_surface"] + anchors.fill: parent + } + } + + // TODO: notification number overlay +} diff --git a/shell/bar/power/PowerMenu.qml b/shell/bar/power/PowerMenu.qml new file mode 100644 index 0000000..a693e03 --- /dev/null +++ b/shell/bar/power/PowerMenu.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Qt5Compat.GraphicalEffects +import Quickshell.Widgets +import Quickshell.Services.UPower +import qs.widgets +import qs.bar +import qs + +// todo: redo the tray icon handling +StyledMouseArea { + id: root + implicitWidth: height + 8 // for margin + visible: UPower.displayDevice.isLaptopBattery + onClicked: showMenu = !showMenu + + required property var bar + property bool showMenu: false + + Item { + implicitWidth: parent.height + implicitHeight: parent.height + anchors.centerIn: parent + layer.enabled: true + layer.effect: OpacityMask { + source: Rectangle { + width: root.width + height: root.height + color: "white" + } + + maskSource: IconImage { + implicitSize: root.width + source: "root:resources/battery/battery.svg" + } + } + + Rectangle { + id: batteryBackground + color: Qt.color(ShellSettings.colors["surface"]).lighter(4) + opacity: 0.75 + anchors { + fill: parent + margins: 2 + } + } + + Rectangle { + id: batteryPercentage + width: (parent.width - 4) * UPower.displayDevice.percentage + color: ShellSettings.colors["inverse_surface"] + + anchors { + left: batteryBackground.left + top: batteryBackground.top + bottom: batteryBackground.bottom + } + } + } + + property PopupItem menu: PopupItem { + owner: root + popup: root.bar.popup + show: root.showMenu + onClosed: root.showMenu = false + centered: true + + implicitWidth: 250 + implicitHeight: 250 + } +} diff --git a/shell/bar/systray/SysTray.qml b/shell/bar/systray/SysTray.qml new file mode 100644 index 0000000..7c69646 --- /dev/null +++ b/shell/bar/systray/SysTray.qml @@ -0,0 +1,96 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.SystemTray +import "../../widgets" +import ".." + +// TODO: +// 1. Get rid of leftItem/rightItem properties on menu +// 2. Load menu properly, right now its pretty buggy +// 3. Fix bug that causes close on update (nm-applet wifi networks updating) + +RowLayout { + id: root + spacing: 5 + visible: SystemTray.items.values.length > 0 + + required property var bar + + Repeater { + id: repeater + model: SystemTray.items + + delegate: StyledMouseArea { + id: button + Layout.preferredWidth: parent.height + Layout.fillHeight: true + + required property SystemTrayItem modelData + property bool showMenu: false + + onClicked: { + menuOpener.menu = modelData.menu; + showMenu = !showMenu; + } + + IconImage { + id: trayIcon + anchors.fill: parent + source: { + // console.log(trayField.modelData.id); + switch (button.modelData.id) { + case "obs": + return "image://icon/obs-tray"; + default: + return button.modelData.icon; + } + } + } + + QsMenuOpener { + id: menuOpener + } + + property PopupItem menu: PopupItem { + id: menu + owner: button + popup: root.bar.popup + show: button.showMenu + onClosed: button.showMenu = false + + implicitWidth: content.implicitWidth + (2 * 8) + implicitHeight: content.implicitHeight + (2 * 8) + + property var leftItem: false + property var rightItem: false + + ColumnLayout { + id: content + spacing: 2 + anchors.centerIn: parent + + Repeater { + model: menuOpener.children + + delegate: TrayMenuItem { + id: sysTrayContent + Layout.fillWidth: true + Layout.fillHeight: true + + rootMenu: menu + + onInteracted: { + button.showMenu = false; + menuOpener.menu = null; + } + } + } + } + } + } + } +} diff --git a/shell/bar/systray/TrayMenuEntry.qml b/shell/bar/systray/TrayMenuEntry.qml new file mode 100644 index 0000000..6f178ab --- /dev/null +++ b/shell/bar/systray/TrayMenuEntry.qml @@ -0,0 +1,177 @@ +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import "../../widgets" as Widgets +import "../.." + +ColumnLayout { + id: root + required property QsMenuEntry menuData + required property var rootMenu + signal interacted + + Component.onCompleted: { + if (menuData?.buttonType !== QsMenuButtonType.None || menuData?.icon != "") { + rootMenu.leftItem = true; + } + + if (menuData?.hasChildren) { + rootMenu.rightItem = true; + } + } + + WrapperRectangle { + Layout.fillWidth: true + Layout.preferredHeight: 25 + radius: 4 + color: { + if (!root.menuData?.enabled) + return "transparent"; + + if (entryArea.containsMouse) { + return ShellSettings.colors.active_translucent; + } + + return "transparent"; + } + + WrapperMouseArea { + id: entryArea + hoverEnabled: true + anchors.fill: parent + onClicked: { + if (!root.menuData?.enabled) + return; + + if (root.menuData?.hasChildren) { + subTrayMenu.visible = !subTrayMenu.visible; + return; + } + + root.menuData?.triggered(); + root.interacted(); + } + + RowLayout { + id: menuEntry + spacing: 5 + Layout.fillWidth: true + + Item { + visible: root.rootMenu.leftItem + Layout.preferredWidth: 20 + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: 5 + + RadioButton { + id: radioButton + visible: (root.menuData?.buttonType === QsMenuButtonType.RadioButton) ?? false + checked: (root.menuData?.checkState) ?? false + anchors.centerIn: parent + } + + CheckBox { + id: checkBox + visible: (root.menuData?.buttonType === QsMenuButtonType.CheckBox) ?? false + checked: (root.menuData?.checkState) ?? false + anchors.centerIn: parent + } + + IconImage { + id: entryImage + visible: (root.menuData?.buttonType === QsMenuButtonType.None && root.menuData?.icon !== "") ?? false + source: (root.menuData?.icon) ?? "" + anchors.fill: parent + } + } + + Text { + id: text + text: root.menuData?.text ?? "" + verticalAlignment: Text.AlignVCenter + color: { + let color = Qt.color(ShellSettings.colors.active); + + if (!root.menuData?.enabled) + return color.darker(2); + + // if (entryArea.containsMouse) + // return Qt.color(ShellSettings.colors["inverse_primary"]); + + return color; + } + + Layout.fillWidth: true + Layout.fillHeight: true + } + + Item { + visible: root.rootMenu.rightItem + Layout.preferredHeight: 20 + Layout.preferredWidth: 20 + Layout.rightMargin: 5 + + Widgets.IconButton { + id: arrowButton + visible: root.menuData?.hasChildren ?? false + activeRectangle: false + source: "root:resources/general/right-arrow.svg" + rotation: subTrayMenu.visible ? 90 : 0 + anchors.fill: parent + + Behavior on rotation { + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + + onClicked: { + root.expanded = !root.expanded; + } + } + } + } + } + } + + WrapperRectangle { + id: subTrayMenu + color: ShellSettings.colors.surface_container_translucent + radius: 8 + visible: false + + border { + width: 1 + color: ShellSettings.colors.active_translucent + } + + Layout.fillWidth: true + + QsMenuOpener { + id: menuOpener + menu: root.menuData + } + + ColumnLayout { + id: subTrayContainer + spacing: 2 + Layout.fillWidth: true + + Repeater { + model: menuOpener.children + + delegate: BoundComponent { + id: subMenuEntry + source: "TrayMenuItem.qml" + Layout.fillWidth: true + required property var modelData + property var rootMenu: root.rootMenu + } + } + } + } +} diff --git a/shell/bar/systray/TrayMenuItem.qml b/shell/bar/systray/TrayMenuItem.qml new file mode 100644 index 0000000..685d4d5 --- /dev/null +++ b/shell/bar/systray/TrayMenuItem.qml @@ -0,0 +1,30 @@ +import Quickshell +import QtQuick +import QtQuick.Layouts +import qs + +ColumnLayout { + id: root + required property QsMenuEntry modelData + required property var rootMenu + property var leftItem + signal interacted + + Rectangle { + visible: (root.modelData?.isSeparator ?? false) + color: ShellSettings.colors.inactive_translucent + // opacity: 0.1 + Layout.fillWidth: true + Layout.preferredHeight: 2 + Layout.leftMargin: 8 + Layout.rightMargin: 8 + } + + TrayMenuEntry { + visible: !root.modelData?.isSeparator + rootMenu: root.rootMenu + menuData: root.modelData + Layout.fillWidth: true + onInteracted: root.interacted() + } +} diff --git a/shell/bar/volume/ApplicationMixer.qml b/shell/bar/volume/ApplicationMixer.qml new file mode 100644 index 0000000..584be77 --- /dev/null +++ b/shell/bar/volume/ApplicationMixer.qml @@ -0,0 +1,70 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +ColumnLayout { + id: root + + Loader { + id: sinkLoader + active: sink + + property PwNode sink: Pipewire.defaultAudioSink + + sourceComponent: WrapperItem { + PwNodeLinkTracker { + id: linkTracker + node: sinkLoader.sink + } + + ColumnLayout { + Repeater { + model: linkTracker.linkGroups + + delegate: Loader { + id: nodeLoader + active: modelData.source !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + required property PwLinkGroup modelData + + sourceComponent: VolumeCard { + id: nodeCard + node: nodeLoader.modelData.source + text: node.properties["media.name"] ?? "" + + // if icon-name is undefined, just gonna fallback on the application name + icon: IconImage { + source: { + if (nodeCard.node.properties["application.icon-name"] !== undefined) + return `image://icon/${nodeCard.node.properties["application.icon-name"]}`; + + let applicationName = nodeCard.node.properties["application.name"]; + return `image://icon/${applicationName?.toLowerCase() ?? "image-missing"}`; + } + } + + button: Widgets.FontIconButton { + hoverEnabled: false + iconName: nodeCard.node.audio.muted ? "volume_off" : "volume_up" + checked: !nodeCard.node.audio.muted + inactiveColor: ShellSettings.colors["surface_container_highest"] + onClicked: { + nodeCard.node.audio.muted = !nodeCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } + } + } + } + } +} diff --git a/shell/bar/volume/DeviceMixer.qml b/shell/bar/volume/DeviceMixer.qml new file mode 100644 index 0000000..ed37a83 --- /dev/null +++ b/shell/bar/volume/DeviceMixer.qml @@ -0,0 +1,64 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +ColumnLayout { + id: root + + // headphones + // don't load until the node is not null + Loader { + id: sinkLoader + active: sink !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + property PwNode sink: Pipewire.defaultAudioSink + + sourceComponent: VolumeCard { + id: sinkCard + node: sinkLoader.sink + button: Widgets.FontIconButton { + hoverEnabled: false + iconName: sinkCard.node.audio.muted ? "volume_off" : "volume_up" + checked: !sinkCard.node.audio.muted + inactiveColor: ShellSettings.colors["surface_container_highest"] + onClicked: { + sinkCard.node.audio.muted = !sinkCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } + + // microphone, same as above + Loader { + id: sourceLoader + active: source !== null + Layout.preferredWidth: 350 + Layout.preferredHeight: 45 + + property PwNode source: Pipewire.defaultAudioSource + + sourceComponent: VolumeCard { + id: sourceCard + node: sourceLoader.source + button: Widgets.FontIconButton { + hoverEnabled: false + iconName: sourceCard.node.audio.muted ? "mic_off" : "mic" + checked: !sourceCard.node.audio.muted + inactiveColor: ShellSettings.colors["surface_container_highest"] + onClicked: { + sourceCard.node.audio.muted = !sourceCard.node.audio.muted; + } + } + + anchors.fill: parent + } + } +} diff --git a/shell/bar/volume/VolumeCard.qml b/shell/bar/volume/VolumeCard.qml new file mode 100644 index 0000000..32ff535 --- /dev/null +++ b/shell/bar/volume/VolumeCard.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import "../../widgets/" as Widgets +import "../.." + +WrapperRectangle { + id: root + color: ShellSettings.colors["surface_container"] + radius: width / 2 + margin: 6 + + required property PwNode node + property string text + property Component button + property Component icon + + PwObjectTracker { + id: tracker + objects: [root.node] + } + + RowLayout { + Widgets.MaterialSlider { + value: root.node.audio.volume ?? 0 + text: root.text + icon: root.icon + + onValueChanged: { + // only allow changes when the node is ready other wise you will combust + if (!root.node.ready) + return; + + root.node.audio.volume = value; + } + + Layout.fillWidth: true + Layout.fillHeight: true + } + + Loader { + id: buttonLoader + sourceComponent: root.button + + Layout.preferredWidth: this.height + Layout.fillHeight: true + } + } +} diff --git a/shell/bar/volume/VolumeControl.qml b/shell/bar/volume/VolumeControl.qml new file mode 100644 index 0000000..4423de1 --- /dev/null +++ b/shell/bar/volume/VolumeControl.qml @@ -0,0 +1,34 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import "../../widgets/" as Widgets + +WrapperItem { + id: root + visible: false + + ColumnLayout { + spacing: 10 + + Widgets.TabBar { + id: tabBar + model: ["headphones", "tune"] + Layout.fillWidth: true + Layout.preferredHeight: 35 + } + + StackLayout { + id: page + currentIndex: tabBar.currentIndex + Layout.fillWidth: true + Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0 + + readonly property Item currentItem: children[currentIndex] + + DeviceMixer {} + ApplicationMixer {} + } + } +} diff --git a/shell/bar/volume/VolumeIndicator.qml b/shell/bar/volume/VolumeIndicator.qml new file mode 100644 index 0000000..66f7a6e --- /dev/null +++ b/shell/bar/volume/VolumeIndicator.qml @@ -0,0 +1,27 @@ +import QtQuick +import "../../widgets/" as Widgets + +Item { + id: root + + required property var popup + + Widgets.FontIconButton { + id: button + iconName: "volume_up" + anchors.fill: parent + onClicked: { + if (root.popup.content == volumeMenu) { + root.popup.hide(); + return; + } + + root.popup.set(this, volumeMenu); + root.popup.show(); + } + } + + VolumeControl { + id: volumeMenu + } +} diff --git a/shell/greeter.qml b/shell/greeter.qml new file mode 100644 index 0000000..4ca3505 --- /dev/null +++ b/shell/greeter.qml @@ -0,0 +1,34 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Greetd +import "lockscreen" + +ShellRoot { + id: root + + GreeterContext { + id: context + + onLaunch: { + lock.locked = false; + Greetd.launch(["hyprland"]); + } + } + + WlSessionLock { + id: lock + locked: true + + WlSessionLockSurface { + LockSurface { + state: context.state + + // TODO: env var for wallpaper + wallpaper: "root:resources/wallpapers/wallhaven-96y9qk.jpg" + anchors.fill: parent + } + } + } +} diff --git a/shell/launcher/Controller.qml b/shell/launcher/Controller.qml new file mode 100644 index 0000000..b7cbab7 --- /dev/null +++ b/shell/launcher/Controller.qml @@ -0,0 +1,313 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import ".." + +Singleton { + PersistentProperties { + id: persist + property bool launcherOpen: false + } + + IpcHandler { + target: "launcher" + + function open(): void { + persist.launcherOpen = true; + } + + function close(): void { + persist.launcherOpen = false; + } + + function toggle(): void { + persist.launcherOpen = !persist.launcherOpen; + } + } + + LazyLoader { + id: loader + activeAsync: persist.launcherOpen + + PanelWindow { + implicitWidth: 500 + implicitHeight: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10 + color: "transparent" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.namespace: "shell:launcher" + + Rectangle { + id: container + color: ShellSettings.colors["surface"] + radius: 18 + + anchors { + fill: parent + margins: 10 + } + + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 7 + anchors.topMargin: 10 + anchors.bottomMargin: 0 + spacing: 0 + + Rectangle { + id: searchContainer + Layout.fillWidth: true + implicitHeight: searchbox.implicitHeight + 15 + radius: 10 + color: ShellSettings.colors["surface_container"] + border.color: ShellSettings.colors["secondary"] + + RowLayout { + id: searchbox + anchors.fill: parent + anchors.margins: 5 + + TextInput { + id: search + Layout.fillWidth: true + color: ShellSettings.colors["inverse_surface"] + + focus: true + Keys.forwardTo: [list] + Keys.onEscapePressed: persist.launcherOpen = false + + Keys.onPressed: event => { + if (event.modifiers & Qt.ControlModifier) { + if (event.key == Qt.Key_J) { + list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1; + event.accepted = true; + } else if (event.key == Qt.Key_K) { + list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1; + event.accepted = true; + } + } + } + + onAccepted: { + if (list.currentItem) { + list.currentItem.clicked(null); + } + } + + onTextChanged: { + list.currentIndex = 0; + } + } + } + } + + ListView { + id: list + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + cacheBuffer: 0 // works around QTBUG-131106 + //reuseItems: true + model: ScriptModel { + values: DesktopEntries.applications.values.map(object => { + const stxt = search.text.toLowerCase(); + const ntxt = object.name.toLowerCase(); + let si = 0; + let ni = 0; + + let matches = []; + let startMatch = -1; + + for (let si = 0; si != stxt.length; ++si) { + const sc = stxt[si]; + + while (true) { + // Drop any entries with letters that don't exist in order + if (ni == ntxt.length) + return null; + + const nc = ntxt[ni++]; + + if (nc == sc) { + if (startMatch == -1) + startMatch = ni; + break; + } else { + if (startMatch != -1) { + matches.push({ + index: startMatch, + length: ni - startMatch + }); + + startMatch = -1; + } + } + } + } + + if (startMatch != -1) { + matches.push({ + index: startMatch, + length: ni - startMatch + 1 + }); + } + + return { + object: object, + matches: matches + }; + }).filter(entry => entry !== null).sort((a, b) => { + let ai = 0; + let bi = 0; + let s = 0; + + while (ai != a.matches.length && bi != b.matches.length) { + const am = a.matches[ai]; + const bm = b.matches[bi]; + + s = bm.length - am.length; + if (s != 0) + return s; + + s = am.index - bm.index; + if (s != 0) + return s; + + ++ai; + ++bi; + } + + s = a.matches.length - b.matches.length; + if (s != 0) + return s; + + s = a.object.name.length - b.object.name.length; + if (s != 0) + return s; + + return a.object.name.localeCompare(b.object.name); + }).map(entry => entry.object) + + onValuesChanged: list.currentIndex = 0 + } + + topMargin: 7 + bottomMargin: list.count == 0 ? 0 : 7 + + add: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 100 + } + } + + displaced: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 1 + duration: 100 + } + } + + move: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 1 + duration: 100 + } + } + + remove: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 0 + duration: 100 + } + } + + highlight: Rectangle { + radius: 12 + color: ShellSettings.colors["primary"] + } + + keyNavigationEnabled: true + keyNavigationWraps: true + highlightMoveVelocity: -1 + highlightMoveDuration: 100 + preferredHighlightBegin: list.topMargin + preferredHighlightEnd: list.height - list.bottomMargin + highlightRangeMode: ListView.ApplyRange + snapMode: ListView.SnapToItem + + readonly property real delegateHeight: 44 + + delegate: MouseArea { + required property DesktopEntry modelData + + implicitHeight: list.delegateHeight + implicitWidth: ListView.view.width + + onClicked: { + modelData.execute(); + persist.launcherOpen = false; + } + + RowLayout { + id: delegateLayout + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 5 + } + + IconImage { + Layout.alignment: Qt.AlignVCenter + asynchronous: true + implicitSize: 30 + source: Quickshell.iconPath(modelData.icon) + } + Text { + text: modelData.name + color: ShellSettings.colors["inverse_surface"] + Layout.alignment: Qt.AlignVCenter + } + } + } + } + } + } + } + } + + function init() { + } +} diff --git a/shell/lockscreen/Controller.qml b/shell/lockscreen/Controller.qml new file mode 100644 index 0000000..49bca77 --- /dev/null +++ b/shell/lockscreen/Controller.qml @@ -0,0 +1,53 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import ".." + +Singleton { + id: root + + PersistentProperties { + id: persist + property bool locked: false + } + + IpcHandler { + target: "lockscreen" + + function lock(): void { + persist.locked = true; + } + } + + LockContext { + id: context + + Connections { + target: context.state + + function onUnlocked() { + persist.locked = false; + } + } + } + + WlSessionLock { + id: lock + locked: persist.locked + + WlSessionLockSurface { + LockSurface { + state: context.state + wallpaper: ShellSettings.settings.wallpaperUrl + anchors.fill: parent + } + } + } + + function init() { + } +} diff --git a/shell/lockscreen/GreeterContext.qml b/shell/lockscreen/GreeterContext.qml new file mode 100644 index 0000000..ea35866 --- /dev/null +++ b/shell/lockscreen/GreeterContext.qml @@ -0,0 +1,39 @@ +import QtQuick +import Quickshell +import Quickshell.Services.Greetd +import "../lockscreen" + +Scope { + id: root + signal launch + + property LockState state: LockState { + onTryUnlock: { + this.unlockInProgress = true; + + // TODO: env var for user + Greetd.createSession("koss"); + } + } + + Connections { + target: Greetd + + function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) { + if (responseRequired) { + Greetd.respond(root.state.currentText); + } // else ignore - only supporting passwords + } + + function onAuthFailure() { + root.state.currentText = ""; + root.state.failed(); + root.state.unlockInProgress = false; + } + + function onReadyToLaunch() { + root.state.unlockInProgress = false; + root.launch(); + } + } +} diff --git a/shell/lockscreen/LockContext.qml b/shell/lockscreen/LockContext.qml new file mode 100644 index 0000000..64f4c27 --- /dev/null +++ b/shell/lockscreen/LockContext.qml @@ -0,0 +1,48 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Services.Pam + +Scope { + id: root + + property LockState state: LockState { + onTryUnlock: { + if (this.currentText === "") + return; + + this.unlockInProgress = true; + pam.start(); + } + } + + PamContext { + id: pam + + // Its best to have a custom pam config for quickshell, as the system one + // might not be what your interface expects, and break in some way. + // This particular example only supports passwords. + configDirectory: "pam" + config: "password.conf" + + // pam_unix will ask for a response for the password prompt + onPamMessage: { + if (this.responseRequired) { + this.respond(root.state.currentText); + } + } + + // pam_unix won't send any important messages so all we need is the completion status. + onCompleted: result => { + if (result == PamResult.Success) { + root.state.unlocked(); + root.state.currentText = ""; + } else { + root.state.showFailure = true; + } + + root.state.unlockInProgress = false; + } + } +} diff --git a/shell/lockscreen/LockState.qml b/shell/lockscreen/LockState.qml new file mode 100644 index 0000000..f50c105 --- /dev/null +++ b/shell/lockscreen/LockState.qml @@ -0,0 +1,12 @@ +import Quickshell + +Scope { + property string currentText: "" + property bool unlockInProgress: false + property bool showFailure: false + signal unlocked + signal failed + signal tryUnlock + + onCurrentTextChanged: showFailure = false +} diff --git a/shell/lockscreen/LockSurface.qml b/shell/lockscreen/LockSurface.qml new file mode 100644 index 0000000..cad1742 --- /dev/null +++ b/shell/lockscreen/LockSurface.qml @@ -0,0 +1,197 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Qt5Compat.GraphicalEffects + +Item { + id: root + required property LockState state + required property string wallpaper + + Item { + anchors.fill: parent + + Image { + id: bgImage + source: root.wallpaper + fillMode: Image.PreserveAspectCrop + anchors.fill: parent + visible: false + } + + FastBlur { + anchors.fill: bgImage + source: bgImage + radius: 80 + transparentBorder: false + } + + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.3 + } + + Rectangle { + anchors.fill: parent + color: "transparent" + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.2) + } + GradientStop { + position: 0.5 + color: Qt.rgba(0, 0, 0, 0.1) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0.4) + } + } + } + } + + // Date and time display + ColumnLayout { + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: 120 + } + spacing: 10 + + Text { + id: clock + horizontalAlignment: Text.AlignHCenter + renderType: Text.NativeRendering + font.pointSize: 72 + font.weight: Font.Light + color: "white" + text: { + const now = this.date; + let hours = now.getHours(); + const minutes = now.getMinutes().toString().padStart(2, '0'); + const ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; // 0 should be 12 + return `${hours}:${minutes}`; + } + + property var date: new Date() + Layout.alignment: Qt.AlignHCenter + + Timer { + running: true + repeat: true + interval: 1000 + onTriggered: clock.date = new Date() + } + + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 0 + radius: 20 + samples: 41 + color: Qt.rgba(1, 1, 1, 0.3) + } + } + } + + // login section + ColumnLayout { + visible: Window.active + anchors.centerIn: parent + spacing: 30 + + Rectangle { + id: profileImage + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 120 + Layout.preferredHeight: 120 + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: profileImage.width + height: profileImage.height + radius: width / 2 + color: "black" + } + } + + Image { + source: "root:resources/general/pfp.png" + anchors.fill: parent + } + } + + // password input, should probably split this out into a seperate comp + LoginField { + id: passwordBox + enabled: !root.state.unlockInProgress + + Layout.preferredWidth: 250 + Layout.preferredHeight: 30 + Layout.maximumHeight: 30 + Layout.alignment: Qt.AlignHCenter + + onTextChanged: root.state.currentText = this.text + onAccepted: root.state.tryUnlock() + + Connections { + target: root.state + + function onCurrentTextChanged() { + if (!passwordBox.shaking) { + passwordBox.text = root.state.currentText; + } + } + + function onShowFailureChanged() { + if (root.state.showFailure && !passwordBox.shaking) { + passwordBox.shaking = true; + } + } + } + } + } + + // hint text + Text { + text: "Press Enter to unlock" + color: Qt.rgba(1, 1, 1, 0.5) + font.pointSize: 12 + horizontalAlignment: Text.AlignHCenter + opacity: passwordBox.text.length > 0 ? 1.0 : 0.0 + + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 60 + } + + Behavior on opacity { + NumberAnimation { + duration: 300 + } + } + } + + // testing button + Button { + visible: false + text: "Emergency Unlock" + onClicked: root.state.unlocked() + + anchors { + right: parent.right + bottom: parent.bottom + margins: 20 + } + } +} diff --git a/shell/lockscreen/LoginField.qml b/shell/lockscreen/LoginField.qml new file mode 100644 index 0000000..f387a17 --- /dev/null +++ b/shell/lockscreen/LoginField.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects + +TextField { + id: root + color: "white" + scale: activeFocus ? 1.05 : 1.0 + padding: 8 + focus: true + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + font.pointSize: 11 + horizontalAlignment: Text.AlignHCenter + + background: Rectangle { + color: Qt.rgba(1, 1, 1, 0.1) + border.color: root.activeFocus ? Qt.rgba(1, 1, 1, 0.5) : Qt.rgba(1, 1, 1, 0.2) + border.width: 1 + radius: 8 + + layer.enabled: true + layer.effect: FastBlur { + radius: 10 + transparentBorder: true + } + } + + transform: Translate { + id: shakeTransform + x: 0 + } + + property bool shaking: false + + onShakingChanged: { + if (shaking) + shakeAnimation.start(); + } + + Behavior on scale { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + SequentialAnimation { + id: shakeAnimation + + NumberAnimation { + target: shakeTransform + property: "x" + to: -8 + duration: 50 + easing.type: Easing.OutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: 8 + duration: 100 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: -6 + duration: 80 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: 6 + duration: 80 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: -3 + duration: 60 + easing.type: Easing.InOutQuad + } + + onFinished: { + root.shaking = false; + root.text = ""; + } + } +} diff --git a/shell/lockscreen/pam/fingerprint.conf b/shell/lockscreen/pam/fingerprint.conf new file mode 100644 index 0000000..14e89bd --- /dev/null +++ b/shell/lockscreen/pam/fingerprint.conf @@ -0,0 +1 @@ +auth sufficient pam_fprintd.so diff --git a/shell/lockscreen/pam/password.conf b/shell/lockscreen/pam/password.conf new file mode 100644 index 0000000..7e5d75a --- /dev/null +++ b/shell/lockscreen/pam/password.conf @@ -0,0 +1 @@ +auth required pam_unix.so diff --git a/shell/mpris/Controller.qml b/shell/mpris/Controller.qml new file mode 100644 index 0000000..3d6e89f --- /dev/null +++ b/shell/mpris/Controller.qml @@ -0,0 +1,77 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris + +Singleton { + id: root + property MprisPlayer trackedPlayer + + IpcHandler { + target: "mpris" + + function next(): void { + root.trackedPlayer.next(); + } + + function prev(): void { + root.trackedPlayer.previous(); + } + + function play(): void { + root.trackedPlayer.play(); + } + + function pause(): void { + root.trackedPlayer.pause(); + } + + function play_pause(): void { + if (root.trackedPlayer.isPlaying) { + root.trackedPlayer.pause(); + } else { + root.trackedPlayer.play(); + } + } + } + + Instantiator { + model: Mpris.players + + Connections { + required property MprisPlayer modelData + target: modelData + + Component.onCompleted: { + if (root.trackedPlayer == null || modelData.isPlaying) { + root.trackedPlayer = modelData; + } + } + + Component.onDestruction: { + if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) { + for (const player of Mpris.players.values) { + if (player.playbackState.isPlaying) { + root.trackedPlayer = player; + break; + } + } + + if (root.trackedPlayer == null && Mpris.players.values.length != 0) { + root.trackedPlayer = Mpris.players.values[0]; + } + } + } + + function onPlaybackStateChanged() { + if (root.trackedPlayer !== modelData) + root.trackedPlayer = modelData; + } + } + } + + function init() {} +} diff --git a/shell/notifications/ActiveToast.qml b/shell/notifications/ActiveToast.qml new file mode 100644 index 0000000..0816308 --- /dev/null +++ b/shell/notifications/ActiveToast.qml @@ -0,0 +1,218 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import "../widgets/" as Widgets +import ".." +import "../.." + +Item { + id: root + required property var notification + signal expired(Notification notification) + signal closed(Notification notification) + + width: parent.width + height: Math.min(row.implicitHeight + 30, 400) + + Rectangle { + id: container + radius: 10 + color: ShellSettings.colors["surface_container"] + anchors.fill: parent + + Item { + id: timerController + property int totalDuration: 5000 + property int remainingTime: totalDuration + property bool isRunning: false + property real lastTime: 0 + + Timer { + id: internalTimer + interval: 16 + repeat: true + running: timerController.isRunning + + onTriggered: { + var currentTime = Date.now(); + if (timerController.lastTime > 0) { + var delta = currentTime - timerController.lastTime; + timerController.remainingTime -= delta; + if (timerController.remainingTime <= 0) { + timerController.isRunning = false; + root.expired(root.notification); + } + } + timerController.lastTime = currentTime; + } + } + + function start() { + if (!isRunning) { + lastTime = Date.now(); + isRunning = true; + } + } + + function pause() { + isRunning = false; + lastTime = 0; + } + + Component.onCompleted: { + start(); + } + } + + MouseArea { + id: notificationArea + hoverEnabled: true + anchors.fill: parent + + onContainsMouseChanged: { + progressAnimation.paused = containsMouse; + if (containsMouse) { + timerController.pause(); + } else { + timerController.start(); + } + } + } + + RowLayout { + id: row + spacing: 5 + + anchors { + fill: parent + margins: 15 + } + + ColumnLayout { + Layout.fillWidth: true + + RowLayout { + id: topRow + spacing: 10 + Layout.fillWidth: true + + IconImage { + visible: root.notification.appIcon != "" + source: Quickshell.iconPath(root.notification.appIcon) + implicitSize: 24 + } + + RowLayout { + Layout.fillWidth: true + + Text { + id: appName + text: root.notification.appName + color: ShellSettings.colors["inverse_surface"] + font.pointSize: 11 + font.bold: true + elide: Text.ElideRight + maximumLineCount: 1 + Layout.preferredWidth: implicitWidth + Layout.maximumWidth: topRow.width * 0.3 + } + + Widgets.Separator {} + + Text { + id: summaryText + text: root.notification.summary + color: ShellSettings.colors["inverse_surface"] + font.pointSize: 11 + elide: Text.ElideRight + maximumLineCount: 1 + Layout.fillWidth: true + } + } + + Item { + id: closeButton + width: 24 + height: 24 + Layout.alignment: Qt.AlignTop + + Canvas { + id: progressCircle + anchors.fill: parent + antialiasing: true + + property real progress: 1.0 + onProgressChanged: requestPaint() + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + var centerX = width / 2; + var centerY = height / 2; + var radius = Math.min(width, height) / 2 - 2; + + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + 2 * Math.PI * progress); + ctx.strokeStyle = ShellSettings.colors["primary"]; + ctx.lineWidth = 2; + ctx.stroke(); + } + } + + NumberAnimation { + id: progressAnimation + target: progressCircle + property: "progress" + from: 1.0 + to: 0.0 + duration: 5000 + running: true + easing.type: Easing.Linear + } + + Rectangle { + id: closeButtonBg + anchors.centerIn: parent + width: 16 + height: 16 + color: "#FF474D" + radius: 10 + visible: closeButtonArea.containsMouse + } + + MouseArea { + id: closeButtonArea + hoverEnabled: true + anchors.fill: parent + onPressed: root.closed(root.notification) + } + + IconImage { + source: "image://icon/window-close" + implicitSize: 16 + anchors.centerIn: parent + } + } + } + + ColumnLayout { + Layout.fillWidth: true + + Text { + id: bodyText + text: root.notification.body + color: ShellSettings.colors["inverse_surface"] + font.pointSize: 11 + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 10 + Layout.fillWidth: true + } + } + } + } + } +} diff --git a/shell/notifications/Controller.qml b/shell/notifications/Controller.qml new file mode 100644 index 0000000..f325439 --- /dev/null +++ b/shell/notifications/Controller.qml @@ -0,0 +1,98 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import ".." + +Scope { + id: root + + Connections { + target: Notifications.notificationServer + + function onNotification(notification) { + notificationLoader.item.visible = true; + } + } + + LazyLoader { + id: notificationLoader + loading: true + + PanelWindow { + id: notificationWindow + property var visibleCount: { + let count = 0; + + for (let i = 0; i < toastList.count; i++) { + let item = toastList.itemAt(i); + + if (item && item.visible) { + count++; + } + } + + return count; + } + + onVisibleCountChanged: visible = visibleCount != 0 + + color: "transparent" + implicitWidth: 525 + visible: false + exclusionMode: ExclusionMode.Normal + + mask: Region { + item: notifLayout + } + + anchors { + top: true + bottom: true + right: true + } + + Text { + text: "length: " + notificationWindow.visibleCount + } + + ColumnLayout { + id: notifLayout + spacing: 15 + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 5 + } + + Repeater { + id: toastList + model: ScriptModel { + values: Notifications.notificationServer.trackedNotifications.values.concat() + } + + delegate: ActiveToast { + id: toast + required property var modelData + notification: modelData + + Connections { + target: toast + + function onExpired(notification) { + toast.visible = false; + } + + function onClosed(notification) { + notification.dismiss(); + } + } + } + } + } + } + } +} diff --git a/shell/notifications/NotificationCenter.qml b/shell/notifications/NotificationCenter.qml new file mode 100644 index 0000000..90c3cba --- /dev/null +++ b/shell/notifications/NotificationCenter.qml @@ -0,0 +1,278 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Qt5Compat.GraphicalEffects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import "../widgets" as Widgets +import ".." + +Singleton { + PersistentProperties { + id: persist + property bool notificationsOpen: false + } + + IpcHandler { + id: ipc + target: "notifications" + + function open(): void { + persist.notificationsOpen = true; + } + + function close(): void { + persist.notificationsOpen = false; + } + + function toggle(): void { + persist.notificationsOpen = !persist.notificationsOpen; + } + } + + LazyLoader { + id: loader + activeAsync: persist.notificationsOpen + + PanelWindow { + id: notificationPanel + color: "red" + implicitWidth: 500 + exclusionMode: ExclusionMode.Ignore + // WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + anchors { + top: true + right: true + bottom: true + } + + ColumnLayout { + spacing: 10 + + anchors { + fill: parent + margins: 10 + } + + Text { + text: "Notifications: " + toastList.count + Layout.fillWidth: true + } + + ListView { + id: toastList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 5 + + model: ScriptModel { + values: { + const notifications = Notifications.notificationServer.trackedNotifications.values.concat(); + + const groupedByApp = notifications.reduce((groups, notification) => { + const appName = notification.appName; + + if (!groups[appName]) { + groups[appName] = { + appName: appName, + summaryGroups: {} + }; + } + + const summary = notification.summary; + const image = notification.image; + + if (!groups[appName].summaryGroups[summary]) { + groups[appName].summaryGroups[summary] = { + summary: summary, + image: image, + notifications: [] + }; + } + + groups[appName].summaryGroups[summary].notifications.push(notification); + + return groups; + }, {}); + + return Object.values(groupedByApp).map(appGroup => { + return { + appName: appGroup.appName, + summaryGroups: Object.values(appGroup.summaryGroups) + }; + }); + } + } + + delegate: Item { + id: toastWrapper + required property var modelData + width: ListView.view.width + height: toastContent.height + + Item { + id: toastContent + width: parent.width + height: contentColumn.implicitHeight + anchors.centerIn: parent + + ColumnLayout { + id: contentColumn + spacing: 2 + + anchors { + fill: parent + margins: 0 + } + + // Notification content + Repeater { + model: toastWrapper.modelData.summaryGroups + + delegate: Rectangle { + id: summaryGroup + required property var modelData + required property int index + Layout.fillWidth: true + Layout.preferredHeight: groupContent.implicitHeight + 24 + color: ShellSettings.colors["surface_container"] + antialiasing: true + + topLeftRadius: index === 0 ? 25 : 5 + topRightRadius: index === 0 ? 25 : 5 + bottomLeftRadius: index === (toastWrapper.modelData.summaryGroups.length - 1) ? 25 : 5 + bottomRightRadius: index === (toastWrapper.modelData.summaryGroups.length - 1) ? 25 : 5 + + ColumnLayout { + id: groupContent + spacing: 8 + + anchors { + fill: parent + margins: 12 + } + + RowLayout { + spacing: 12 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + ColumnLayout { + Layout.alignment: Qt.AlignTop + spacing: 0 + + Item { + id: imageContainer + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + visible: summaryGroup.modelData.image != "" + antialiasing: true + + Image { + id: notificationImage + anchors.fill: parent + source: summaryGroup.modelData.image + fillMode: Image.PreserveAspectCrop + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: notificationImage.width + height: notificationImage.height + radius: notificationImage.width / 2 + antialiasing: true + } + } + } + } + } + + // Content column + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 8 + + // Header row + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + text: summaryGroup.modelData.summary + font.pixelSize: 16 + font.weight: Font.Medium + color: ShellSettings.colors["on_surface"] + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + } + + Widgets.Separator {} + + Text { + text: "now" + font.pixelSize: 14 + color: ShellSettings.colors["on_surface_variant"] + Layout.alignment: Qt.AlignVCenter + } + } + + // Notification bodies + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Repeater { + model: summaryGroup.modelData.notifications + + delegate: ColumnLayout { + id: bodyDelegate + required property var modelData + required property int index + Layout.fillWidth: true + spacing: 0 + + Text { + Layout.fillWidth: true + text: bodyDelegate.modelData.body + font.pixelSize: 14 + color: ShellSettings.colors["on_surface_variant"] + wrapMode: Text.WordWrap + maximumLineCount: 4 + elide: Text.ElideRight + lineHeight: 1.3 + visible: bodyDelegate.modelData.body != "" + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + // HyprlanFocusGrab { + // id: grab + // windows: [notificationPanel] + // onCleared: { + // ipc.hide(); + // } + // } + } + } + + function init() { + } +} diff --git a/shell/notifications/Notifications.qml b/shell/notifications/Notifications.qml new file mode 100644 index 0000000..a76b632 --- /dev/null +++ b/shell/notifications/Notifications.qml @@ -0,0 +1,23 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.Notifications + +Singleton { + property alias notificationServer: notifServer + + NotificationServer { + id: notifServer + actionsSupported: true + persistenceSupported: true + } + + Connections { + target: notifServer + + function onNotification(notification) { + notification.tracked = true; + } + } +} diff --git a/shell/resources/battery/battery-charge.svg b/shell/resources/battery/battery-charge.svg new file mode 100644 index 0000000..39e9d3f --- /dev/null +++ b/shell/resources/battery/battery-charge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/shell/resources/battery/battery.svg b/shell/resources/battery/battery.svg new file mode 100644 index 0000000..66b337d --- /dev/null +++ b/shell/resources/battery/battery.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/shell/resources/control/controls-button.svg b/shell/resources/control/controls-button.svg new file mode 100644 index 0000000..1d50a9e --- /dev/null +++ b/shell/resources/control/controls-button.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shell/resources/control/lock.svg b/shell/resources/control/lock.svg new file mode 100644 index 0000000..db842ce --- /dev/null +++ b/shell/resources/control/lock.svg @@ -0,0 +1,2 @@ + + diff --git a/shell/resources/control/shutdown.svg b/shell/resources/control/shutdown.svg new file mode 100644 index 0000000..6d84ad9 --- /dev/null +++ b/shell/resources/control/shutdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/shell/resources/control/sleep.svg b/shell/resources/control/sleep.svg new file mode 100644 index 0000000..7206f9f --- /dev/null +++ b/shell/resources/control/sleep.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/shell/resources/general/down-arrow.svg b/shell/resources/general/down-arrow.svg new file mode 100644 index 0000000..6470d1a --- /dev/null +++ b/shell/resources/general/down-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/shell/resources/general/nixos.svg b/shell/resources/general/nixos.svg new file mode 100644 index 0000000..34e9baa --- /dev/null +++ b/shell/resources/general/nixos.svg @@ -0,0 +1 @@ + diff --git a/shell/resources/general/notification.svg b/shell/resources/general/notification.svg new file mode 100644 index 0000000..21a0f2f --- /dev/null +++ b/shell/resources/general/notification.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/shell/resources/general/pfp.png b/shell/resources/general/pfp.png new file mode 100644 index 0000000..113ad36 Binary files /dev/null and b/shell/resources/general/pfp.png differ diff --git a/shell/resources/general/placeholder.svg b/shell/resources/general/placeholder.svg new file mode 100644 index 0000000..0fae206 --- /dev/null +++ b/shell/resources/general/placeholder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/shell/resources/general/right-arrow.svg b/shell/resources/general/right-arrow.svg new file mode 100644 index 0000000..0b46dfb --- /dev/null +++ b/shell/resources/general/right-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/shell/resources/mask.png b/shell/resources/mask.png new file mode 100644 index 0000000..e6cac94 Binary files /dev/null and b/shell/resources/mask.png differ diff --git a/shell/resources/mpris/next.svg b/shell/resources/mpris/next.svg new file mode 100644 index 0000000..706815d --- /dev/null +++ b/shell/resources/mpris/next.svg @@ -0,0 +1,19 @@ + + + + + next [#998] + Created with Sketch. + + + + + + + + + + + + + diff --git a/shell/resources/mpris/pause.svg b/shell/resources/mpris/pause.svg new file mode 100644 index 0000000..f19b075 --- /dev/null +++ b/shell/resources/mpris/pause.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/shell/resources/mpris/play.svg b/shell/resources/mpris/play.svg new file mode 100644 index 0000000..849fa14 --- /dev/null +++ b/shell/resources/mpris/play.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/shell/resources/mpris/previous.svg b/shell/resources/mpris/previous.svg new file mode 100644 index 0000000..895139f --- /dev/null +++ b/shell/resources/mpris/previous.svg @@ -0,0 +1,19 @@ + + + + + previous [#999] + Created with Sketch. + + + + + + + + + + + + + diff --git a/shell/resources/mpris/shuffle.svg b/shell/resources/mpris/shuffle.svg new file mode 100644 index 0000000..4df19c7 --- /dev/null +++ b/shell/resources/mpris/shuffle.svg @@ -0,0 +1,6 @@ + + + +shuffle + + diff --git a/shell/resources/mpris/stop.svg b/shell/resources/mpris/stop.svg new file mode 100644 index 0000000..6d2870a --- /dev/null +++ b/shell/resources/mpris/stop.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/shell/resources/volume/microphone-full.svg b/shell/resources/volume/microphone-full.svg new file mode 100644 index 0000000..8d1a116 --- /dev/null +++ b/shell/resources/volume/microphone-full.svg @@ -0,0 +1,2 @@ + + diff --git a/shell/resources/volume/microphone-mute.svg b/shell/resources/volume/microphone-mute.svg new file mode 100644 index 0000000..8c2d3b5 --- /dev/null +++ b/shell/resources/volume/microphone-mute.svg @@ -0,0 +1,2 @@ + + diff --git a/shell/resources/volume/volume-full.svg b/shell/resources/volume/volume-full.svg new file mode 100644 index 0000000..5126fca --- /dev/null +++ b/shell/resources/volume/volume-full.svg @@ -0,0 +1,15 @@ + + + + volume-up-solid + + + + + + + + + + + diff --git a/shell/resources/volume/volume-mute.svg b/shell/resources/volume/volume-mute.svg new file mode 100644 index 0000000..3604983 --- /dev/null +++ b/shell/resources/volume/volume-mute.svg @@ -0,0 +1,16 @@ + + + + volume-off-solid + + + + + + + + + + + + diff --git a/shell/resources/wallpapers/pixelart0.jpg b/shell/resources/wallpapers/pixelart0.jpg new file mode 100644 index 0000000..7da7dba Binary files /dev/null and b/shell/resources/wallpapers/pixelart0.jpg differ diff --git a/shell/resources/wallpapers/wallhaven-0w3ej7.jpg b/shell/resources/wallpapers/wallhaven-0w3ej7.jpg new file mode 100644 index 0000000..990cc34 Binary files /dev/null and b/shell/resources/wallpapers/wallhaven-0w3ej7.jpg differ diff --git a/shell/resources/wallpapers/wallhaven-2yp6gg.png b/shell/resources/wallpapers/wallhaven-2yp6gg.png new file mode 100644 index 0000000..8c65a14 Binary files /dev/null and b/shell/resources/wallpapers/wallhaven-2yp6gg.png differ diff --git a/shell/resources/wallpapers/wallhaven-5g22q5.png b/shell/resources/wallpapers/wallhaven-5g22q5.png new file mode 100644 index 0000000..a405652 Binary files /dev/null and b/shell/resources/wallpapers/wallhaven-5g22q5.png differ diff --git a/shell/resources/wallpapers/wallhaven-5w9em7.jpg b/shell/resources/wallpapers/wallhaven-5w9em7.jpg new file mode 100644 index 0000000..5b9f714 Binary files /dev/null and b/shell/resources/wallpapers/wallhaven-5w9em7.jpg differ diff --git a/shell/resources/wallpapers/wallhaven-96y9qk.jpg b/shell/resources/wallpapers/wallhaven-96y9qk.jpg new file mode 100644 index 0000000..80a03c4 Binary files /dev/null and b/shell/resources/wallpapers/wallhaven-96y9qk.jpg differ diff --git a/shell/resources/wallpapers/wallhaven-od2lwm.jpg b/shell/resources/wallpapers/wallhaven-od2lwm.jpg new file mode 100644 index 0000000..93e1865 Binary files /dev/null and b/shell/resources/wallpapers/wallhaven-od2lwm.jpg differ diff --git a/shell/resources/wallpapers/wallhaven-zywwky.jpg b/shell/resources/wallpapers/wallhaven-zywwky.jpg new file mode 100644 index 0000000..1c0d406 Binary files /dev/null and b/shell/resources/wallpapers/wallhaven-zywwky.jpg differ diff --git a/shell/screencapture/Controller.qml b/shell/screencapture/Controller.qml new file mode 100644 index 0000000..f41b6f4 --- /dev/null +++ b/shell/screencapture/Controller.qml @@ -0,0 +1,72 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import QtQuick +import ".." + +Singleton { + id: root + + property bool windowOpen: false + + IpcHandler { + target: "screencapture" + + function screenshot(): void { + root.windowOpen = true; + } + } + + LazyLoader { + active: root.windowOpen + + PanelWindow { + id: focusedScreen + color: "transparent" + exclusionMode: ExclusionMode.Ignore + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.namespace: "shell:screencapture" + + anchors { + top: true + bottom: true + left: true + right: true + } + + Item { + anchors.fill: parent + focus: true + Keys.onEscapePressed: root.windowOpen = false + + SelectionRectangle { + id: selection + anchors.fill: parent + + onAreaSelected: selection => { + let screen = focusedScreen.screen; + const x = Math.floor(selection.x) + screen.x; + const y = Math.floor(selection.y) + screen.y; + const width = Math.floor(selection.width); + const height = Math.floor(selection.height); + + let position = `${x},${y} ${width}x${height}`; + let path = "/home/koss/Pictures/screenshot.png"; + + Quickshell.execDetached({ + command: ["grim", "-g", position, path] + }); + + root.windowOpen = false; + } + } + } + } + } + + function init() { + } +} diff --git a/shell/screencapture/Screenshot.qml b/shell/screencapture/Screenshot.qml new file mode 100644 index 0000000..34f719a --- /dev/null +++ b/shell/screencapture/Screenshot.qml @@ -0,0 +1,3 @@ +import QtQuick + +Image {} diff --git a/shell/screencapture/SelectionCutout.qml b/shell/screencapture/SelectionCutout.qml new file mode 100644 index 0000000..0303a46 --- /dev/null +++ b/shell/screencapture/SelectionCutout.qml @@ -0,0 +1,152 @@ +import QtQuick +import ".." + +Canvas { + id: root + anchors.fill: parent + + property color overlayColor: "#80000000" + property color borderColor: ShellSettings.colors["primary"] + property real borderWidth: 3 + property real handleSize: 16 + property var screen + + property real centerX: width / 2 + property real centerY: height / 2 + property real minWidth: 400 + property real minHeight: 300 + + // rect that holds positional data for the selection + property rect selectionRect: Qt.rect(centerX - minWidth / 2, centerY - minHeight / 2, minWidth, minHeight) + + // handle positions + property point topLeftHandle: Qt.point(selectionRect.x, selectionRect.y) + property point topRightHandle: Qt.point(selectionRect.x + selectionRect.width, selectionRect.y) + property point bottomLeftHandle: Qt.point(selectionRect.x, selectionRect.y + selectionRect.height) + property point bottomRightHandle: Qt.point(selectionRect.x + selectionRect.width, selectionRect.y + selectionRect.height) + + // dragging state + property int activeHandle: -1 + property point dragStart: Qt.point(0, 0) + property rect initialRect: Qt.rect(0, 0, 0, 0) + + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, width, height); + + // grey overlay + ctx.fillStyle = overlayColor; + ctx.fillRect(0, 0, width, height); + + // cut out the selection rectangle + ctx.globalCompositeOperation = "destination-out"; + ctx.fillRect(selectionRect.x, selectionRect.y, selectionRect.width, selectionRect.height); + ctx.globalCompositeOperation = "source-over"; + + // draw border + ctx.strokeStyle = borderColor; + ctx.lineWidth = borderWidth; + ctx.beginPath(); + ctx.moveTo(topLeftHandle.x, topLeftHandle.y); + ctx.lineTo(topRightHandle.x, topRightHandle.y); + ctx.lineTo(bottomRightHandle.x, bottomRightHandle.y); + ctx.lineTo(bottomLeftHandle.x, bottomLeftHandle.y); + ctx.closePath(); + ctx.stroke(); + + // draw handles + ctx.fillStyle = borderColor; + drawHandle(ctx, topLeftHandle); + drawHandle(ctx, topRightHandle); + drawHandle(ctx, bottomLeftHandle); + drawHandle(ctx, bottomRightHandle); + } + + function drawHandle(ctx, center) { + var radius = handleSize / 2; + ctx.beginPath(); + ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI); + ctx.fill(); + } + + function getHandleAt(x, y) { + var halfSize = handleSize / 2; + var handles = [topLeftHandle, topRightHandle, bottomLeftHandle, bottomRightHandle]; + + for (var i = 0; i < handles.length; i++) { + var handle = handles[i]; + if (x >= handle.x - halfSize && x <= handle.x + halfSize && y >= handle.y - halfSize && y <= handle.y + halfSize) { + return i; + } + } + return -1; + } + + function constrainRect(rect) { + // Ensure minimum size + var width = Math.max(rect.width, minWidth); + var height = Math.max(rect.height, minHeight); + + // Ensure within canvas bounds + var x = Math.max(0, Math.min(rect.x, root.width - width)); + var y = Math.max(0, Math.min(rect.y, root.height - height)); + + return Qt.rect(x, y, width, height); + } + + MouseArea { + anchors.fill: parent + + onPressed: function (mouse) { + activeHandle = root.getHandleAt(mouse.x, mouse.y); + if (root.activeHandle >= 0) { + dragStart = Qt.point(mouse.x, mouse.y); + initialRect = root.selectionRect; + } + } + + // kinda stupid, should maybe bind a mouse area to each handle I don't know + onPositionChanged: function (mouse) { + if (root.activeHandle < 0) + return; + + var dx = mouse.x - root.dragStart.x; + var dy = mouse.y - root.dragStart.y; + var newRect; + + switch (root.activeHandle) { + // top left + case 0: + var newX = Math.max(0, Math.min(root.initialRect.x + dx, root.initialRect.x + root.initialRect.width - root.minWidth)); + var newY = Math.max(0, Math.min(root.initialRect.y + dy, root.initialRect.y + root.initialRect.height - minHeight)); + newRect = Qt.rect(newX, newY, root.initialRect.width - (newX - root.initialRect.x), root.initialRect.height - (newY - root.initialRect.y)); + break; + // top right + case 1: + var newY = Math.max(0, Math.min(root.initialRect.y + dy, root.initialRect.y + root.initialRect.height - root.minHeight)); + var newWidth = Math.max(root.minWidth, Math.min(root.initialRect.width + dx, root.width - root.initialRect.x)); + newRect = Qt.rect(root.initialRect.x, newY, newWidth, root.initialRect.height - (newY - root.initialRect.y)); + break; + // bottom left + case 2: + var newX = Math.max(0, Math.min(root.initialRect.x + dx, root.initialRect.x + root.initialRect.width - minWidth)); + var newHeight = Math.max(root.minHeight, Math.min(root.initialRect.height + dy, root.height - root.initialRect.y)); + newRect = Qt.rect(newX, root.initialRect.y, root.initialRect.width - (newX - root.initialRect.x), newHeight); + break; + // bottom right + case 3: + var newWidth = Math.max(root.minWidth, Math.min(root.initialRect.width + dx, root.width - root.initialRect.x)); + var newHeight = Math.max(root.minHeight, Math.min(root.initialRect.height + dy, root.height - root.initialRect.y)); + newRect = Qt.rect(root.initialRect.x, root.initialRect.y, newWidth, newHeight); + break; + } + + selectionRect = root.constrainRect(newRect); + root.requestPaint(); + } + + onReleased: { + root.activeHandle = -1; + } + } +} diff --git a/shell/screencapture/SelectionRectangle.qml b/shell/screencapture/SelectionRectangle.qml new file mode 100644 index 0000000..0c0fc25 --- /dev/null +++ b/shell/screencapture/SelectionRectangle.qml @@ -0,0 +1,54 @@ +import QtQuick +import ".." + +Canvas { + id: root + + property color overlayColor: "#80000000" + property color outlineColor: ShellSettings.colors["primary"] + property rect selectionRect + property point startPosition + signal areaSelected(rect selection) + + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, width, height); + + // grey overlay + ctx.fillStyle = overlayColor; + ctx.fillRect(0, 0, width, height); + + // cut out the selection rectangle + ctx.globalCompositeOperation = "destination-out"; + ctx.fillRect(selectionRect.x, selectionRect.y, selectionRect.width, selectionRect.height); + ctx.globalCompositeOperation = "source-over"; + ctx.strokeStyle = outlineColor; + ctx.lineWidth = 2; + ctx.strokeRect(selectionRect.x, selectionRect.y, selectionRect.width, selectionRect.height); + } + + MouseArea { + anchors.fill: parent + + onPressed: mouse => { + root.startPosition = Qt.point(mouse.x, mouse.y); + } + + onPositionChanged: mouse => { + if (pressed) { + var x = Math.min(root.startPosition.x, mouse.x); + var y = Math.min(root.startPosition.y, mouse.y); + var width = Math.abs(mouse.x - root.startPosition.x); + var height = Math.abs(mouse.y - root.startPosition.y); + + root.selectionRect = Qt.rect(x, y, width, height); + root.requestPaint(); + } + } + + onReleased: mouse => { + root.visible = false; + root.areaSelected(root.selectionRect); + } + } +} diff --git a/shell/settings/Controller.qml b/shell/settings/Controller.qml new file mode 100644 index 0000000..e2fc879 --- /dev/null +++ b/shell/settings/Controller.qml @@ -0,0 +1,90 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Layouts +import "../widgets/" as Widgets +import "../" + +Singleton { + PersistentProperties { + id: persist + property bool windowOpen: false + } + + IpcHandler { + target: "settings" + + function open(): void { + persist.windowOpen = true; + } + + function close(): void { + persist.windowOpen = false; + } + + function toggle(): void { + persist.windowOpen = !persist.windowOpen; + } + } + + LazyLoader { + id: loader + activeAsync: persist.windowOpen + + FloatingWindow { + color: ShellSettings.colors["surface"] + implicitWidth: 840 + implicitHeight: 845 + + // onWidthChanged: { + // console.log("height: " + height); + // console.log("width: " + width); + // } + + maximumSize { + width: 840 + height: 845 + } + + minimumSize { + width: 840 + height: 845 + } + + onVisibleChanged: { + if (!visible) { + persist.windowOpen = false; + } + } + + ColumnLayout { + spacing: 20 + anchors.fill: parent + + StackLayout { + id: page + currentIndex: topBar.currentIndex + Layout.fillWidth: true + Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0 + + readonly property Item currentItem: children[currentIndex] + + WallpaperPicker {} + } + + Widgets.TopBar { + id: topBar + model: ["headphones", "tune"] + Layout.fillWidth: true + Layout.preferredHeight: 35 + } + } + } + } + + function init() { + } +} diff --git a/shell/settings/WallpaperPicker.qml b/shell/settings/WallpaperPicker.qml new file mode 100644 index 0000000..25d9ae0 --- /dev/null +++ b/shell/settings/WallpaperPicker.qml @@ -0,0 +1,135 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Qt5Compat.GraphicalEffects +import Qt.labs.folderlistmodel +import ".." + +ColumnLayout { + id: container + spacing: 5 + + + + // anchors { + // fill: parent + // margins: 10 + // } + + ClippingRectangle { + radius: 20 + Layout.preferredWidth: 464 + Layout.preferredHeight: 261 + Layout.alignment: Qt.AlignCenter + Layout.margins: 20 + + Image { + id: wallpaperImage + source: ShellSettings.settings.wallpaperUrl + fillMode: Image.PreserveAspectFit + + anchors { + fill: parent + } + } + } + + Rectangle { + color: ShellSettings.colors["surface_container"] + radius: 20 + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + + Rectangle { + color: ShellSettings.colors["surface_container_high"] + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + + GridView { + id: wallpaperGrid + cellWidth: 200 + cellHeight: 200 + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 10 + + model: FolderListModel { + id: folderModel + folder: Qt.resolvedUrl("root:resources/wallpapers") + nameFilters: ["*.jpg", "*.png"] + } + + delegate: Rectangle { + id: cell + required property var modelData + width: 200 + height: 200 + color: "transparent" + + Item { + anchors.fill: parent + + Rectangle { + id: border + visible: mouseArea.containsMouse + color: "transparent" + radius: 20 + + border { + color: ShellSettings.colors["primary"] + width: 2 + } + + anchors { + fill: parent + margins: 1 + } + } + + Image { + id: image + source: cell.modelData.fileUrl + fillMode: Image.PreserveAspectCrop + asynchronous: true + + sourceSize { + height: image.height + width: image.width + } + + anchors { + fill: parent + margins: 5 + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: cell.width + height: cell.height + radius: 20 + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + ShellSettings.settings.wallpaperUrl = cell.modelData.fileUrl; + } + } + } + } + } + } +} diff --git a/shell/shaders/mask.frag b/shell/shaders/mask.frag new file mode 100644 index 0000000..3f93a1a --- /dev/null +++ b/shell/shaders/mask.frag @@ -0,0 +1,25 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source; +layout(binding = 2) uniform sampler2D mask; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; +}; + +void main() { + vec4 sourceColor = texture(source, qt_TexCoord0); + vec4 maskColor = texture(mask, qt_TexCoord0); + + // Use the mask's luminance to determine opacity + float maskValue = dot(maskColor.rgb, vec3(0.299, 0.587, 0.114)); + + // Black areas of mask = transparent, white areas = opaque + sourceColor.a *= (1.0 - maskValue) * qt_Opacity; + + fragColor = sourceColor; +} diff --git a/shell/shaders/mask.frag.qsb b/shell/shaders/mask.frag.qsb new file mode 100644 index 0000000..5a3382a Binary files /dev/null and b/shell/shaders/mask.frag.qsb differ diff --git a/shell/shaders/vertexgradient.frag b/shell/shaders/vertexgradient.frag new file mode 100644 index 0000000..0b82457 --- /dev/null +++ b/shell/shaders/vertexgradient.frag @@ -0,0 +1,18 @@ +#version 440 + +layout(location = 0) in vec2 coord; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + vec4 topLeftColor; + vec4 topRightColor; + vec4 bottomLeftColor; + vec4 bottomRightColor; +} ubuf; + +void main() { + vec4 topColor = mix(ubuf.topLeftColor, ubuf.topRightColor, coord.x); + vec4 bottomColor = mix(ubuf.bottomLeftColor, ubuf.bottomRightColor, coord.x); + fragColor = mix(topColor, bottomColor, coord.y); +} diff --git a/shell/shaders/vertexgradient.frag.qsb b/shell/shaders/vertexgradient.frag.qsb new file mode 100644 index 0000000..4781cf9 Binary files /dev/null and b/shell/shaders/vertexgradient.frag.qsb differ diff --git a/shell/shaders/vertexgradient.vert b/shell/shaders/vertexgradient.vert new file mode 100644 index 0000000..f91120b --- /dev/null +++ b/shell/shaders/vertexgradient.vert @@ -0,0 +1,21 @@ +#version 440 + +layout(location = 0) in vec4 qt_Vertex; +layout(location = 1) in vec2 qt_MultiTexCoord0; + +layout(location = 0) out vec2 coord; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + vec4 topLeftColor; + vec4 topRightColor; + vec4 bottomLeftColor; + vec4 bottomRightColor; +} ubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() { + coord = qt_MultiTexCoord0; + gl_Position = ubuf.qt_Matrix * qt_Vertex; +} diff --git a/shell/shaders/vertexgradient.vert.qsb b/shell/shaders/vertexgradient.vert.qsb new file mode 100644 index 0000000..70a12d3 Binary files /dev/null and b/shell/shaders/vertexgradient.vert.qsb differ diff --git a/shell/shaders/wallpapertransition.frag b/shell/shaders/wallpapertransition.frag new file mode 100644 index 0000000..8982ba7 --- /dev/null +++ b/shell/shaders/wallpapertransition.frag @@ -0,0 +1,33 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + vec2 aspectRatio; + vec2 origin; +}; + +layout(binding = 1) uniform sampler2D fromImage; +layout(binding = 2) uniform sampler2D toImage; + +void main() { + vec2 uv = qt_TexCoord0; + + vec2 scaledUV = (uv - origin) * aspectRatio; + float distance = length(scaledUV); + + vec2 maxVec = max(origin, vec2(1.0) - origin) * aspectRatio; + float maxDistance = length(maxVec); + + float threshold = progress * maxDistance; + + if (distance < threshold) { + fragColor = texture(toImage, uv) * qt_Opacity; + } else { + fragColor = texture(fromImage, uv) * qt_Opacity; + } +} diff --git a/shell/shell.qml b/shell/shell.qml new file mode 100644 index 0000000..8527c2d --- /dev/null +++ b/shell/shell.qml @@ -0,0 +1,30 @@ +//@ pragma UseQApplication +//@ pragma IconTheme Papirus-Dark + +import Quickshell +import QtQuick +import "bar" +import "notifications" as Notifications +import "mpris" as Mpris +import "volume-osd" as VolumeOSD +import "settings" as Settings +import "launcher" as Launcher +import "lockscreen" as LockScreen +import "wallpaper" as Wallpaper +import "screencapture" as ScreenCapture + +ShellRoot { + Bar {} + Wallpaper.Controller {} + Notifications.Controller {} + VolumeOSD.Controller {} + + Component.onCompleted: { + Launcher.Controller.init(); + Settings.Controller.init(); + ScreenCapture.Controller.init(); + Mpris.Controller.init(); + Notifications.NotificationCenter.init(); + LockScreen.Controller.init(); + } +} diff --git a/shell/volume-osd/Controller.qml b/shell/volume-osd/Controller.qml new file mode 100644 index 0000000..b381574 --- /dev/null +++ b/shell/volume-osd/Controller.qml @@ -0,0 +1,102 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import ".." + +Scope { + id: root + + // Bind the pipewire node so its volume will be tracked + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + Connections { + target: Pipewire.defaultAudioSink?.audio + + function onVolumeChanged() { + root.shouldShowOsd = true; + hideTimer.restart(); + } + } + + property bool shouldShowOsd: false + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.shouldShowOsd = false + } + + LazyLoader { + active: root.shouldShowOsd + + PopupWindow { + implicitWidth: 50 + implicitHeight: 275 + color: "transparent" + + // An empty click mask prevents the window from blocking mouse events. + mask: Region {} + + Rectangle { + anchors.fill: parent + radius: 8 + color: { + let color = ShellSettings.colors["surface"]; + return Qt.rgba(color.r, color.g, color.b, 0.8); + } + + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + IconImage { + implicitSize: 30 + source: "root:resources/volume/volume-full.svg" + } + + Rectangle { + id: sliderBackground + Layout.fillWidth: true + implicitHeight: 10 + radius: 20 + color: { + let color = ShellSettings.colors["inverse_surface"]; + return Qt.rgba(color.r, color.g, color.b, 0.5); + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: sliderBackground.width + height: sliderBackground.height + radius: sliderBackground.radius + color: "black" + } + } + + Rectangle { + color: ShellSettings.colors["primary"] + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) + } + } + } + } + } + } +} diff --git a/shell/wallpaper/Controller.qml b/shell/wallpaper/Controller.qml new file mode 100644 index 0000000..443bacc --- /dev/null +++ b/shell/wallpaper/Controller.qml @@ -0,0 +1,45 @@ +import Quickshell +import QtQuick +import ".." + +Scope { + id: root + + LazyLoader { + loading: true + + Scope { + Variants { + model: Quickshell.screens + + PanelWindow { + required property var modelData + color: "black" + aboveWindows: false + screen: modelData + + anchors { + left: true + right: true + top: true + bottom: true + } + + Image { + source: ShellSettings.settings.wallpaperUrl + fillMode: Image.PreserveAspectCrop + anchors.fill: parent + } + } + } + + Connections { + target: ShellSettings.settings + + function onWallpaperUrlChanged() { + console.log("Switching wallpaper: " + ShellSettings.settings.wallpaperUrl); + } + } + } + } +} diff --git a/shell/wallpaper/matugen.toml b/shell/wallpaper/matugen.toml new file mode 100644 index 0000000..cf99df3 --- /dev/null +++ b/shell/wallpaper/matugen.toml @@ -0,0 +1,31 @@ +[config.custom_colors] + +[templates.kde] +input_path = "templates/BreezeDark.colors" +mode = "Dark" +output_path = "~/.config/kdeglobals" +# post_hook = "systemctl restart --user plasma-xdg-desktop-portal-kde.service" + +[templates.nvim] +input_path = "templates/nvim.json" +mode = "Dark" +output_path = "~/.local/share/nvim-colors.json" + +[templates.qt5ct] +input_path = "templates/qtct-colors.conf" +mode = "Dark" +output_path = "~/.config/qt5ct/colors/matugen.conf" + +[templates.qt6ct] +input_path = "templates/BreezeDark.colors" +mode = "Dark" +output_path = "~/.config/qt6ct/colors/BreezeDark.colors" + +[templates.hyprland] +input_path = 'templates/hyprland-colors.conf' +output_path = '~/.config/hypr/colors.conf' +post_hook = 'hyprctl reload' + +[templates.foot] +input_path = 'templates/foot.ini' +output_path = '~/.config/foot/foot.ini' diff --git a/shell/wallpaper/templates/BreezeDark.colors b/shell/wallpaper/templates/BreezeDark.colors new file mode 100644 index 0000000..b7a5fa6 --- /dev/null +++ b/shell/wallpaper/templates/BreezeDark.colors @@ -0,0 +1,211 @@ +# SPDX-FileCopyrightText: Andrew Lake +# SPDX-FileCopyrightText: Marco Martin +# SPDX-FileCopyrightText: Nate Graham +# SPDX-FileCopyrightText: Noah Davis +# SPDX-FileCopyrightText: Neal Gompa +# SPDX-FileCopyrightText: David Redondo +# SPDX-License-Identifier: LGPL-2.0-or-later + +[ColorEffects:Disabled] +Color={{colors.outline.default.red}},{{colors.outline.default.green}},{{colors.outline.default.blue}} +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=true +Color={{colors.outline_variant.default.red}},{{colors.outline_variant.default.green}},{{colors.outline_variant.default.blue}} +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +Enable=false +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Complementary] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Header] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Header][Inactive] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Selection] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Tooltip] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:View] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Window] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Icons] +Theme=breeze-dark + +[General] +ColorScheme=BreezeDark +Name=Breeze Dark +Name[ar]=نسيم داكن +Name[az]=Breeze - Tünd +Name[bg]=Breeze Тъмен +Name[bs]=Breeze tamna +Name[ca]=Brisa fosca +Name[ca@valencia]=Brisa fosca +Name[cs]=Breeze Tmavé +Name[da]=Breeze Dark +Name[de]=Breeze Dunkel +Name[el]=Breeze σκούρο +Name[en_GB]=Breeze Dark +Name[eo]=Breeze Dark +Name[es]=Brisa oscuro +Name[et]=Breeze tume +Name[eu]=Breeze iluna +Name[fi]=Tumma Breeze +Name[fr]=Brise sombre +Name[gl]=Brisa escura +Name[he]=בריזה כהה +Name[hi]=ब्रीज़ गहरा +Name[hu]=Breeze Dark +Name[ia]=Brisa obscure +Name[id]=Breeze Gelap +Name[is]=Breeze dökkt +Name[it]=Brezza scuro +Name[ja]=Breeze ダーク +Name[ka]=Breeze მუქი +Name[ko]=어두운 Breeze +Name[lt]=Breeze tamsus +Name[lv]=Breeze Dark +Name[nb]=Breeze mørk +Name[nl]=Breeze Dark +Name[nn]=Breeze mørk +Name[pa]=ਬਰੀਜ਼ ਗੂੜ੍ਹਾ +Name[pl]=Ciemna Bryza +Name[pt]=Brisa Escura +Name[pt_BR]=Breeze Dark +Name[ro]=Briză, întunecat +Name[ru]=Breeze, тёмный вариант +Name[sa]=वायुः अन्धकारः +Name[sk]=Tmavý vánok +Name[sl]=Sapica, temna +Name[sr]=Поветарац тамни +Name[sr@ijekavian]=Поветарац тамни +Name[sr@ijekavianlatin]=Povetarac tamni +Name[sr@latin]=Povetarac tamni +Name[sv]=Breeze mörk +Name[tg]=Насими торик +Name[tr]=Esinti Koyu +Name[uk]=Темна Breeze +Name[x-test]=xxBreeze Darkxx +Name[zh_CN]=Breeze 微风深色 +Name[zh_TW]=Breeze Dark +shadeSortColumn=true + +[KDE] +contrast=4 + +[WM] +activeBackground={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +activeBlend={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +activeForeground={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +inactiveBackground={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +inactiveBlend={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +inactiveForeground={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} diff --git a/shell/wallpaper/templates/foot.ini b/shell/wallpaper/templates/foot.ini new file mode 100644 index 0000000..1894b3c --- /dev/null +++ b/shell/wallpaper/templates/foot.ini @@ -0,0 +1,70 @@ +[colors] +foreground={{colors.inverse_surface.dark.hex_stripped}} +background={{colors.surface.dark.hex_stripped}} +16=f5a97f +17=b7bdf8 +alpha=1.000000 +alpha-mode=matching +bright0=5b6078 +bright1=ed8796 +bright2=a6da95 +bright3=eed49f +bright4=8aadf4 +bright5=f5bde6 +bright6=8bd5ca +bright7=cad3f5 +jump-labels=24273a f5a97f +regular0=494d64 +regular1=ed8796 +regular2=a6da95 +regular3=eed49f +regular4=8aadf4 +regular5=f5bde6 +regular6=8bd5ca +regular7=b8c0e0 +search-box-match=24273a a6da95 +search-box-no-match=24273a ed8796 +selection-background=8aadf4 +selection-foreground=24273a +urls=8aadf4 + +[cursor] +color=181818 cdcdcd + +[key-bindings] +clipboard-copy=Control+Shift+c XF86Copy +clipboard-paste=Control+Shift+v XF86Paste +font-decrease=Control+minus Control+KP_Subtract +font-increase=Control+plus Control+equal Control+KP_Add +font-reset=Control+0 Control+KP_0 +fullscreen=none +maximize=none +minimize=none +noop=none +pipe-command-output=[wl-copy] none +pipe-scrollback=[sh -c 'xurls | fuzzel | xargs -r firefox'] none +pipe-selected=[xargs -r firefox] none +pipe-visible=[sh -c 'xurls | fuzzel | xargs -r firefox'] none +primary-paste=Shift+Insert +prompt-next=Control+Shift+x +prompt-prev=Control+Shift+z +scrollback-down-half-page=none +scrollback-down-line=none +scrollback-down-page=Shift+Page_Down +scrollback-end=none +scrollback-home=none +scrollback-up-half-page=none +scrollback-up-line=none +scrollback-up-page=Shift+Page_Up +search-start=Control+Shift+r +show-urls-copy=none +show-urls-launch=Control+Shift+o +show-urls-persistent=none +spawn-terminal=Control+Shift+n +unicode-input=Control+Shift+u + +[main] +font=DejaVuSansM Nerd Font:size=14 +gamma-correct-blending=no +shell=zsh +term=xterm-256color diff --git a/shell/wallpaper/templates/hyprland-colors.conf b/shell/wallpaper/templates/hyprland-colors.conf new file mode 100644 index 0000000..d7e6821 --- /dev/null +++ b/shell/wallpaper/templates/hyprland-colors.conf @@ -0,0 +1,4 @@ +<* for name, value in colors *> +$image = {{image}} +${{name}} = rgba({{value.default.hex_stripped}}ff) +<* endfor *> diff --git a/shell/wallpaper/templates/nvim.json b/shell/wallpaper/templates/nvim.json new file mode 100644 index 0000000..d0bcfa7 --- /dev/null +++ b/shell/wallpaper/templates/nvim.json @@ -0,0 +1,20 @@ +{ + "colors": { + "base00": "{{colors.surface.dark.hex}}", + "base01": "{{colors.surface_container.dark.hex}}", + "base02": "{{colors.surface_container_high.dark.hex}}", + "base03": "{{colors.outline.dark.hex}}", + "base04": "{{colors.on_surface_variant.dark.hex}}", + "base05": "{{colors.on_surface.dark.hex}}", + "base06": "{{colors.inverse_surface.dark.hex}}", + "base07": "{{colors.inverse_on_surface.dark.hex}}", + "base08": "{{colors.primary_fixed.dark.hex}}", + "base09": "{{colors.tertiary.dark.hex}}", + "base0A": "{{colors.secondary.dark.hex}}", + "base0B": "{{colors.primary.dark.hex}}", + "base0C": "{{colors.tertiary.light.hex}}", + "base0D": "{{colors.primary.light.hex}}", + "base0E": "{{colors.secondary.light.hex}}", + "base0F": "{{colors.error.light.hex}}" + } +} diff --git a/shell/wallpaper/templates/qtct-colors.conf b/shell/wallpaper/templates/qtct-colors.conf new file mode 100644 index 0000000..848d620 --- /dev/null +++ b/shell/wallpaper/templates/qtct-colors.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_container_high.default.hex}}, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface.default.hex}}, {{colors.scrim.default.hex}}, {{colors.surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.secondary.default.hex}} +disabled_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_container_high.default.hex}}, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface.default.hex}}, {{colors.scrim.default.hex}}, {{colors.surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.secondary.default.hex}} +inactive_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_container_high.default.hex}}, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface.default.hex}}, {{colors.scrim.default.hex}}, {{colors.surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.secondary.default.hex}} diff --git a/shell/widgets/ColoredIcon.qml b/shell/widgets/ColoredIcon.qml new file mode 100644 index 0000000..d54dea2 --- /dev/null +++ b/shell/widgets/ColoredIcon.qml @@ -0,0 +1,32 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import Quickshell.Widgets +import ".." + +Item { + id: root + required property var source + property var implicitSize: 0 + property var color: "white" + readonly property real actualSize: Math.min(root.width, root.height) + + implicitWidth: implicitSize + implicitHeight: implicitSize + + IconImage { + anchors.fill: parent + source: root.source + layer.enabled: true + layer.effect: MultiEffect { + colorization: 1 + colorizationColor: root.color + } + } + + Rectangle { + color: root.color + anchors.fill: parent + } +} diff --git a/shell/widgets/FontIcon.qml b/shell/widgets/FontIcon.qml new file mode 100644 index 0000000..310383f --- /dev/null +++ b/shell/widgets/FontIcon.qml @@ -0,0 +1,34 @@ +import QtQuick +import ".." + +Text { + id: textIcon + + property real fill: 0 + + renderType: Text.NativeRendering + textFormat: Text.PlainText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + font { + family: "Material Symbols Outlined" + pointSize: Math.max(parent.height * 0.50, 11) + + variableAxes: { + "FILL": fill + } + } + + Behavior on fill { + NumberAnimation { + duration: 200 + } + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + } +} diff --git a/shell/widgets/FontIconButton.qml b/shell/widgets/FontIconButton.qml new file mode 100644 index 0000000..ebe264c --- /dev/null +++ b/shell/widgets/FontIconButton.qml @@ -0,0 +1,48 @@ +import QtQuick +import ".." + +MaterialButton { + id: root + + property real implicitSize + property string iconName: "" + property string activeIconColor: ShellSettings.colors["inverse_primary"] + property string inactiveIconColor: ShellSettings.colors["inverse_surface"] + + implicitWidth: this.implicitSize + implicitHeight: this.implicitSize + + Text { + id: textIcon + text: root.iconName + renderType: Text.NativeRendering + textFormat: Text.PlainText + color: root.containsMouse || root.checked ? root.activeIconColor : root.inactiveIconColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + + font { + family: "Material Symbols Outlined" + pointSize: Math.max(parent.height * 0.60, 11) + + variableAxes: { + "FILL": fill + } + } + + property real fill: !root.containsMouse && !root.checked ? 0 : 1 + + Behavior on fill { + NumberAnimation { + duration: 200 + } + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } +} diff --git a/shell/widgets/IconButton.qml b/shell/widgets/IconButton.qml new file mode 100644 index 0000000..16cba83 --- /dev/null +++ b/shell/widgets/IconButton.qml @@ -0,0 +1,51 @@ +import QtQuick +import Quickshell.Widgets +import qs + +Item { + id: root + property string source + property var implicitSize: 24 + property var padding: 0 + property var radius: 20 + property var activeRectangle: true + property var color: ShellSettings.colors.inactive_translucent + property var activeColor: ShellSettings.colors.active_translucent + signal clicked + + implicitWidth: implicitSize + implicitHeight: implicitSize + + Rectangle { + id: iconBackground + color: ShellSettings.colors.active_translucent + radius: root.radius + visible: iconButton.containsMouse && root.activeRectangle + anchors.fill: parent + } + + // Figure out a way to color images better + IconImage { + id: iconImage + source: root.source + visible: true + // color: { + // if (!activeRectangle) + // return root.color; + // + // return iconButton.containsMouse ? root.activeColor : root.color; + // } + + anchors { + fill: parent + margins: root.padding + } + } + + MouseArea { + id: iconButton + hoverEnabled: true + anchors.fill: parent + onPressed: root.clicked() + } +} diff --git a/shell/widgets/MaterialSlider.qml b/shell/widgets/MaterialSlider.qml new file mode 100644 index 0000000..4ea95dd --- /dev/null +++ b/shell/widgets/MaterialSlider.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import ".." + +Slider { + id: root + + value: 0.5 + from: 0.0 + to: 1.0 + + property string text + property Component icon + + background: Rectangle { + id: background + implicitWidth: parent.width + implicitHeight: parent.height + width: root.availableWidth + height: implicitHeight + x: root.leftPadding + y: root.topPadding + root.availableHeight / 2 - height / 2 + z: 0 + color: ShellSettings.colors["surface_container_highest"] + radius: height / 2 + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: background.implicitWidth + height: background.implicitHeight + radius: background.radius + color: "black" + } + } + + Rectangle { + id: visualPos + width: root.visualPosition * (root.availableWidth - root.height) + (root.height / 2) + height: parent.height + color: ShellSettings.colors["primary"] + } + + Text { + id: sliderText + text: root.text + visible: text !== "" + color: ShellSettings.colors["inverse_primary"] + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + + font { + pointSize: Math.max(handle.implicitHeight * 0.35, 11) + } + + anchors { + top: parent.top + bottom: parent.bottom + left: { + let visualWidth = (root.visualPosition * root.availableWidth); + if ((visualWidth / root.availableWidth) < 0.5) + return visualPos.right; + else + return parent.left; + } + right: { + let visualWidth = (root.visualPosition * root.availableWidth); + if ((visualWidth / root.availableWidth) > 0.5) + return visualPos.right; + else + return parent.right; + } + + leftMargin: 20 + rightMargin: 20 + } + } + } + + handle: Rectangle { + id: handle + color: ShellSettings.colors["primary"] + implicitWidth: root.height + implicitHeight: root.height + radius: width / 2 + + x: root.leftPadding + root.visualPosition * (root.availableWidth - width) + y: root.topPadding + root.availableHeight / 2 - height / 2 + // icon maybe + + Loader { + active: root.icon !== undefined + sourceComponent: root.icon + + anchors { + fill: parent + margins: 2 + } + } + } +} diff --git a/shell/widgets/RoundSlider.qml b/shell/widgets/RoundSlider.qml new file mode 100644 index 0000000..0317963 --- /dev/null +++ b/shell/widgets/RoundSlider.qml @@ -0,0 +1,55 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import ".." + +Slider { + id: slider + implicitHeight: 8 + property var accentColor: ShellSettings.colors["primary"] + + background: Rectangle { + id: sliderContainer + width: slider.availableWidth + height: slider.implicitHeight + color: ShellSettings.colors["inverse_surface"] + radius: 4 + anchors.verticalCenter: parent.verticalCenter + + layer.enabled: true + layer.effect: OpacityMask { + source: Rectangle { + width: sliderContainer.width + height: sliderContainer.height + radius: sliderContainer.radius + color: "white" + } + + maskSource: Rectangle { + width: sliderContainer.width + height: sliderContainer.height + radius: sliderContainer.radius + color: "black" + } + } + + Rectangle { + id: fill + width: slider.handle.width / 2 + slider.visualPosition * (sliderContainer.width - slider.handle.width) + height: sliderContainer.height + color: Qt.color(slider.accentColor ?? "purple").darker(1.2) + } + } + + handle: Rectangle { + id: handleRect + x: slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + width: 16 + height: 16 + radius: width / 2 + color: slider.pressed ? Qt.color(slider.accentColor ?? "purple").darker(1.5) : slider.accentColor ?? "purple" + } +} diff --git a/shell/widgets/Separator.qml b/shell/widgets/Separator.qml new file mode 100644 index 0000000..0e3fc25 --- /dev/null +++ b/shell/widgets/Separator.qml @@ -0,0 +1,9 @@ +import QtQuick +import ".." + +Rectangle { + color: ShellSettings.colors["active"] + radius: 5 + width: 3.5 + height: 15 +} diff --git a/shell/widgets/StyledMouseArea.qml b/shell/widgets/StyledMouseArea.qml new file mode 100644 index 0000000..d7c694a --- /dev/null +++ b/shell/widgets/StyledMouseArea.qml @@ -0,0 +1,24 @@ +import QtQuick +import qs + +MouseArea { + id: root + hoverEnabled: true + + property real radius: width / 2 + property bool checked: false + property var activeColor: ShellSettings.colors.active_translucent + property var inactiveColor: "transparent" + + Rectangle { + color: root.containsMouse || root.checked ? root.activeColor : root.inactiveColor + radius: root.radius + anchors.fill: parent + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } +} diff --git a/shell/widgets/StyledPopup.qml b/shell/widgets/StyledPopup.qml new file mode 100644 index 0000000..71883f5 --- /dev/null +++ b/shell/widgets/StyledPopup.qml @@ -0,0 +1,53 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import Quickshell.Hyprland +import ".." + +PopupWindow { + id: root + color: "transparent" + implicitWidth: container.width + implicitHeight: container.height + + default property alias contentItem: container.children + + function open() { + // root.anchor.rect.y = -root.implicitHeight; + root.visible = true; + grab.active = true; + // slideAnimation.start(); + } + + function hide() { + root.visible = false; + grab.active = false; + } + + // PropertyAnimation { + // id: slideAnimation + // target: root.anchor.rect + // property: "y" + // from: -root.implicitHeight // Off-screen position + // to: 0 // On-screen position + // duration: 300 // Animation duration in milliseconds + // } + + HyprlandFocusGrab { + id: grab + windows: [root] + onCleared: root.hide() + } + + WrapperRectangle { + id: container + margin: 5 + radius: 12 + color: ShellSettings.colors.surface_translucent + + border { + width: 1 + color: ShellSettings.colors.active_translucent + } + } +} diff --git a/shell/widgets/StyledRectangle.qml b/shell/widgets/StyledRectangle.qml new file mode 100644 index 0000000..4be0b57 --- /dev/null +++ b/shell/widgets/StyledRectangle.qml @@ -0,0 +1,13 @@ +import QtQuick +import qs + +Rectangle { + id: root + radius: 12 + color: ShellSettings.colors.surface_translucent + + border { + width: 1 + color: ShellSettings.colors.active_translucent + } +} diff --git a/shell/widgets/TopBar.qml b/shell/widgets/TopBar.qml new file mode 100644 index 0000000..048e2ac --- /dev/null +++ b/shell/widgets/TopBar.qml @@ -0,0 +1,94 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import ".." + +Item { + id: root + property alias model: buttonRepeater.model + property int currentIndex: 0 + + RowLayout { + id: buttonGroup + spacing: 0 + anchors.fill: parent + + Repeater { + id: buttonRepeater + + delegate: MouseArea { + id: button + hoverEnabled: true + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + + required property var modelData + required property int index + property bool checked: index === root.currentIndex + + onClicked: { + currentIndex = button.index; + root.updateSelectionBarPosition(); + } + + // Change to SVG Icon + FontIcon { + text: button.modelData + fill: { + if (button.checked) + return 1; + + return button.containsMouse ? 1 : 0; + } + color: button.checked ? ShellSettings.colors["primary"] : ShellSettings.colors["inverse_surface"] + anchors.fill: parent + anchors.bottomMargin: 5 + } + } + } + } + + Rectangle { + id: selectionBar + implicitWidth: 100 + implicitHeight: 3 + topLeftRadius: width / 2 + topRightRadius: width / 2 + color: ShellSettings.colors["primary"] + anchors.bottom: tabBar.top + + Behavior on x { + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: tabBar + implicitHeight: 1.5 + radius: width / 2 + color: ShellSettings.colors["surface_container"] + + anchors { + top: buttonGroup.bottom + left: parent.left + right: parent.right + } + } + + // Change to icons being greyed out by default but selected is full white + function updateSelectionBarPosition() { + if (buttonRepeater.count > 0) { + var buttonWidth = buttonGroup.width / buttonRepeater.count; + var targetX = currentIndex * buttonWidth + (buttonWidth - selectionBar.width) / 2; + selectionBar.x = targetX; + } + } + + Component.onCompleted: updateSelectionBarPosition() + onWidthChanged: updateSelectionBarPosition() +}