last 7 months of qs changes

This commit is contained in:
outfoxxed 2025-01-06 00:13:19 -08:00
parent 2c64563ade
commit 4b90113a54
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
103 changed files with 3467 additions and 1415 deletions
.gitignore
modules/user/modules
greetd
quickshell
default.nix
lockscreen
shell
HyprlandIpc.qmlPopupSurface.qmlSelectionArea.qmlSelectionLayer.qmlShellGlobals.qmlShellIpc.qmlShortcut.qmlSlideView.qmlSlideViewItem.qml
bar
components
greeter.qml
icons
launcher
lock
notifications
screenshot
selection
shaders

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.envrc
.direnv/

View file

@ -0,0 +1,55 @@
{ inputs, system, config, lib, pkgs, ... }: let
hyprlandPackage = config.home-manager.users.${config.main-user}.wayland.windowManager.hyprland.package;
hyprlandConfig = pkgs.writeText "greetd-hyprland-config" ''
# for some reason pkill is way faster than dispatching exit, to the point greetd thinks the greeter died.
exec-once = quickshell -c greeter >& qslog.txt && pkill Hyprland
input {
kb_layout = us
sensitivity = 0
follow_mouse = 1
# mouse_refocus = false - #6393
accel_profile = flat
}
decoration {
blur {
enabled = no
}
}
animations {
enabled = no
}
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
background_color = 0x000000
key_press_enables_dpms = true
mouse_move_enables_dpms = true
}
${config.hyprland-session.extraConfigStatic}
'';
in {
services.greetd = {
enable = true;
restart = false;
settings.default_session.command = "${lib.getExe hyprlandPackage} -c ${hyprlandConfig}";
};
# needed for hyprland cache dir
users.users.greeter = {
home = "/home/greeter";
createHome = true;
};
home-manager.users.greeter = {
home.stateVersion = config.system.stateVersion;
imports = [
../../../theme/home.nix # also fixes cursor
../quickshell # set up quickshell manifest and such
];
};
}

View file

@ -1,16 +1,29 @@
{ inputs, pkgs, lib, system, impurity, ... }: let
{ config, inputs, pkgs, lib, system, impurity, ... }: let
inherit (inputs) quickshell;
# hack because the greeter user cant access /home/admin
maybeLink = path: if config.home.username == "admin" then impurity.link path else path;
in {
home.packages = with pkgs; [
qt6.qtimageformats # amog
qt6.qt5compat # shader fx
quickshell.packages.${system}.default
pamtester # lockscreen
(quickshell.packages.${system}.default.override (prevqs: {
debug = true;
qt6 = prevqs.qt6.overrideScope (_: prevqt: {
qtdeclarative = prevqt.qtdeclarative.overrideAttrs (prev: {
cmakeBuildType = "Debug";
dontStrip = true;
});
});
breakpad = prevqs.breakpad.override rec {
stdenv = pkgs.gcc13Stdenv;
};
}))
grim imagemagick # screenshot
];
xdg.configFile."quickshell/manifest.conf".text = lib.generators.toKeyValue {} {
shell = "${impurity.link ./shell}";
lockscreen = "${impurity.link ./lockscreen}";
shell = "${maybeLink ./.}/shell";
greeter = "${maybeLink ./.}/shell/greeter.qml";
};
}

View file

@ -1,43 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
property int status: AuthContext.Status.FirstLogin
signal unlocked();
enum Status {
FirstLogin,
Authenticating,
LoginFailed
}
property string password
property var pamtester: Process {
property bool failed: true
command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
onStarted: this.write(`${password}\n`)
stdout: SplitParser {
// fails go to stderr
onRead: pamtester.failed = false
}
onExited: {
if (failed) {
status = AuthContext.Status.LoginFailed;
} else {
unlocked();
}
}
}
function tryLogin(password: string) {
this.password = password
status = AuthContext.Status.Authenticating;
pamtester.running = true;
}
}

View file

@ -1,17 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
property var time: new Date();
property string text;
Timer {
interval: 10000
running: true
repeat: true
onTriggered: time = new Date()
}
}

View file

@ -1,131 +0,0 @@
import QtQuick
import QtQuick.Controls.Basic
import Quickshell.Io
Item {
required property AuthContext context
Item {
anchors.centerIn: parent
Text {
id: timeText
anchors {
bottom: entryBox.top
bottomMargin: 100
horizontalCenter: parent.horizontalCenter
}
font {
pointSize: 120
hintingPreference: Font.PreferFullHinting
family: "Noto Sans"
}
color: "white"
text: {
const hours = LockGlobals.time.getHours().toString().padStart(2, '0');
const minutes = LockGlobals.time.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
}
Text {
anchors {
top: timeText.bottom
topMargin: -20
horizontalCenter: parent.horizontalCenter
}
font.pointSize: 40
color: "#50ffffff"
text: "Locked"
}
TextField {
id: entryBox
anchors.centerIn: parent
width: 600
font.pointSize: 24
enabled: context.status != AuthContext.Status.Authenticating
focus: true
horizontalAlignment: TextInput.AlignHCenter
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
onCursorVisibleChanged: {
if (cursorVisible) cursorVisible = false;
}
cursorVisible: false
color: "white"
background: Rectangle {
color: "#20ffffff"
border.color: "#30ffffff"
radius: height / 2
}
text: LockGlobals.text
onTextChanged: {
LockGlobals.text = text
}
onAccepted: {
if (text != "") context.tryLogin(text)
}
onEnabledChanged: {
if (enabled) text = ""
}
}
Text {
id: status
color: "white"
font.pointSize: 24
anchors {
horizontalCenter: entryBox.horizontalCenter
top: entryBox.bottom
topMargin: 40
}
text: {
switch (context.status) {
case AuthContext.Status.FirstLogin: return ""
case AuthContext.Status.Authenticating: return "Authenticating"
case AuthContext.Status.LoginFailed: return "Login Failed"
}
}
}
}
Button {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 20
}
contentItem: Text {
text: "Turn off Monitors"
color: "#aaeeffff"
}
onClicked: dpms.running = true
background: Rectangle {
color: "#20ffffff"
border.color: "#30ffffff"
radius: height / 2
}
Process {
id: dpms
command: [ "hyprctl", "dispatch", "dpms" ]
}
}
}

View file

@ -1,36 +0,0 @@
//@ pragma NativeTextRendering
import QtQuick
import Quickshell
import Quickshell.Wayland
import ".."
ShellRoot {
AuthContext {
id: authContext
onUnlocked: lock.locked = false
}
WlSessionLock {
id: lock
locked: true
onLockedChanged: {
if (!locked) Qt.quit();
}
WlSessionLockSurface {
id: surface
BackgroundImage {
anchors.fill: parent
screen: surface.screen
}
Lockscreen {
anchors.fill: parent
context: authContext
}
}
}
}

View file

@ -1,39 +0,0 @@
import QtQuick
import Quickshell
import ".."
ShellRoot {
AuthContext {
id: authContext
onUnlocked: Qt.quit()
}
FloatingWindow {
BackgroundImage {
anchors.fill: parent
screen: Quickshell.screens.filter(s => s.name == "eDP-1")[0]
}
Lockscreen {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
right: parent.horizontalCenter
}
context: authContext
}
Lockscreen {
anchors {
left: parent.horizontalCenter
top: parent.top
bottom: parent.bottom
right: parent.right
}
context: authContext
}
}
}

View file

@ -1,26 +0,0 @@
pragma Singleton
import Quickshell
import Quickshell.Io
Singleton {
signal windowOpened(address: string, workspace: string, klass: string, title: string);
Socket {
connected: true
path: `/tmp/hypr/${Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")}/.socket2.sock`
parser: SplitParser {
onRead: message => {
const [type, body] = message.split(">>");
const args = body.split(",");
switch (type) {
case "openwindow":
windowOpened(args[0], args[1], args[2], args[3])
break;
}
}
}
}
}

View file

@ -1,81 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
WlrLayershell {
id: root
required property var bar;
property var popup: null;
property list<variant> overlays: [];
property variant activeOverlay: null;
property variant lastActiveOverlay: null;
onActiveOverlayChanged: {
if (lastActiveOverlay != null && lastActiveOverlay != activeOverlay) {
lastActiveOverlay.expanded = false;
}
lastActiveOverlay = activeOverlay;
}
readonly property rect barRect: {
void [width, height];
this.contentItem.mapFromItem(bar, 0, 0, bar.width, bar.height)
}
readonly property real overlayXOffset: barRect.x + barRect.width + 10
exclusionMode: ExclusionMode.Ignore
color: "transparent"
namespace: "shell:bar"
Variants {
id: masks
model: overlays
Region {
required property var modelData;
item: modelData.widget
}
}
mask: Region {
regions: masks.instances
}
anchors {
left: true
top: true
bottom: true
}
width: {
const extents = overlays
.filter(overlay => !overlay.fullyCollapsed)
.map(overlay => overlayXOffset + overlay.expandedWidth);
return Math.max(barRect.x + barRect.width, ...extents);
}
function connectOverlay(overlay: variant) {
overlay.widget.parent = this.contentItem
overlays.push(overlay);
}
function disconnectOverlay(overlay: variant) {
// Splice seems to make it undefined as an intermediary step
// which breaks bindings.
overlays = overlays.filter(o => o != overlay);
}
function expandedPosition(overlay: variant): rect {
const rect = overlay.collapsedLayerRect;
const idealY = rect.y + (rect.height / 2) - (overlay.expandedHeight / 2)
const y = Math.max(barRect.y, Math.min((barRect.y + barRect.height) - overlay.expandedHeight, idealY));
return Qt.rect(overlayXOffset, y, overlay.expandedWidth, overlay.expandedHeight);
}
}

View file

@ -1,27 +0,0 @@
import QtQuick
Item {
id: root
required property var screen;
required property var selectionArea;
signal selectionComplete(x: real, y: real, width: real, height: real)
MouseArea {
anchors.fill: parent
onPressed: {
selectionArea.startX = mouseX;
selectionArea.startY = mouseY;
selectionArea.endX = mouseX;
selectionArea.endY = mouseY;
selectionArea.startSelection(false);
}
onPositionChanged: {
selectionArea.endX = mouseX;
selectionArea.endY = mouseY;
}
onReleased: selectionArea.endSelection();
}
}

View file

@ -1,99 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
WlrLayershell {
signal selectionComplete(x: real, y: real, width: real, height: real)
color: "transparent"
visible: selectionArea.selecting || selectionArea.initializing
exclusionMode: ExclusionMode.Ignore
layer: WlrLayer.Overlay
namespace: "termspawner"
anchors {
left: true
right: true
top: true
bottom: true
}
property var selectionArea: area
Rectangle {
id: area
property bool selecting: false
property bool initializing: false
property bool locked: false
property real startX: 0
property real startY: 0
property real endX: 0
property real endY: 0
readonly property bool bigEnough: width > 300 && height > 150
border.color: bigEnough ? "#ee33ccff" : "#aa595959"
border.width: 1
radius: 5
color: "#66001017"
visible: selecting
x: Math.min(startX, endX) - border.width
y: Math.min(startY, endY) - border.width
width: Math.max(startX, endX) - x + border.width * 2
height: Math.max(startY, endY) - y + border.width * 2
function startSelection(initialize: bool) {
locked = false;
if (!initialize) {
selecting = true;
return;
}
initializing = true
if (selecting) {
area.startX = mouseArea.mouseX;
area.startY = mouseArea.mouseY;
area.endX = mouseArea.mouseX;
area.endY = mouseArea.mouseY;
}
}
function endSelection() {
const wasSelecting = selecting;
initializing = false;
if (wasSelecting && bigEnough) {
locked = true;
selectionComplete(x + 1, y + 1, width - 2, height - 2);
} else {
selecting = false;
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
if (area.initializing) {
if (!containsMouse) {
area.initializing = false;
return;
}
area.startX = mouseX;
area.startY = mouseY;
area.initializing = false;
area.selecting = true;
}
if (!selectionArea.locked) {
area.endX = mouseX;
area.endY = mouseY;
}
}
}
}

View file

@ -24,16 +24,6 @@ Singleton {
curve.type: Easing.InQuart
}
property var time: new Date();
Timer {
interval: 1000
running: true
repeat: true
onTriggered: time = new Date()
}
function interpolateColors(x: real, a: color, b: color): color {
const xa = 1.0 - x;
return Qt.rgba(a.r * xa + b.r * x, a.g * xa + b.g * x, a.b * xa + b.b * x, a.a * xa + b.a * x);

View file

@ -1,19 +1,14 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
Singleton {
readonly property alias termSelect: termSelectBind.pressed;
signal screenshot();
Shortcut {
name: "screenshot"
onPressed: screenshot()
}
Shortcut {
id: termSelectBind
name: "termselect"
IpcHandler {
target: "screenshot"
function takeScreenshot() { screenshot(); }
}
}

View file

@ -1,5 +0,0 @@
import Quickshell.Hyprland
GlobalShortcut {
appid: "shell"
}

View file

@ -0,0 +1,112 @@
import QtQuick
// kind of like a lighter StackView which handles replacement better.
Item {
id: root
property Component enterTransition: XAnimator {
from: root.width
duration: 3000
}
property Component exitTransition: XAnimator {
to: target.x - target.width
duration: 3000
}
property bool animate: this.visible;
onAnimateChanged: {
if (!this.animate) this.finishAnimations();
}
property Component itemComponent: SlideViewItem {}
property SlideViewItem activeItem: null;
property Item pendingItem: null;
property bool pendingNoAnim: false;
property list<SlideViewItem> removingItems;
readonly property bool animating: activeItem?.activeAnimation != null
function replace(component: Component, defaults: var, noanim: bool) {
this.pendingNoAnim = noanim;
if (component) {
const props = defaults ?? {};
props.parent = null;
props.width = Qt.binding(() => this.width);
props.height = Qt.binding(() => this.height);
const item = component.createObject(this, props);
if (pendingItem) pendingItem.destroy();
pendingItem = item;
const ready = item?.svReady ?? true;
if (ready) finishPending();
} else {
finishPending(); // remove
}
}
Connections {
target: pendingItem
function onSvReadyChanged() {
if (pendingItem.svReady) {
root.finishPending();
}
}
}
function finishPending() {
const noanim = this.pendingNoAnim || !this.animate;
if (this.activeItem) {
if (noanim) {
this.activeItem.destroyAll();
this.activeItem = null;
} else {
removingItems.push(this.activeItem);
this.activeItem.animationCompleted.connect(item => root.removeItem(item));
this.activeItem.stopIfRunning();
this.activeItem.createAnimation(exitTransition);
this.activeItem = null;
}
}
if (!this.animate) finishAnimations();
if (this.pendingItem) {
pendingItem.parent = this;
this.activeItem = itemComponent.createObject(this, { item: this.pendingItem });
this.pendingItem = null;
if (!noanim) {
this.activeItem.createAnimation(enterTransition);
}
}
}
function removeItem(item: SlideViewItem) {
item.destroyAll();
for (const i = 0; i !== this.removingItems.length; i++) {
if (this.removingItems[i] === item) {
removingItems.splice(i, 1);
break;
}
}
}
function finishAnimations() {
this.removingItems.forEach(item => item.destroyAll())
this.removingItems = [];
if (this.activeItem) {
this.activeItem.finishIfRunning();
}
}
Component.onDestruction: {
this.removingItems.forEach(item => item.destroyAll());
this.activeItem?.destroyAll();
this.pendingItem?.destroy();
}
}

View file

@ -0,0 +1,47 @@
import Quickshell
import QtQuick
QtObject {
id: root
required property Item item;
property Animation activeAnimation: null;
signal animationCompleted(self: SlideViewItem);
property Connections __animConnection: Connections {
target: activeAnimation
function onStopped() {
root.activeAnimation.destroy();
root.animationCompleted(root);
}
}
function createAnimation(component: Component) {
this.stopIfRunning();
this.activeAnimation = component.createObject(this, { target: this.item });
this.activeAnimation.running = true;
}
function stopIfRunning() {
if (this.activeAnimation) {
this.activeAnimation.stop();
this.activeAnimation = null;
}
}
function finishIfRunning() {
if (this.activeAnimation) {
// animator types dont handle complete correctly.
this.activeAnimation.complete();
this.activeAnimation.stop();
this.item.x = 0;
this.item.y = 0;
this.activeAnimation = null;
}
}
function destroyAll() {
this.item.destroy();
this.destroy();
}
}

View file

@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
@ -5,7 +6,8 @@ import Quickshell
import "systray" as SysTray
import "audio" as Audio
import "mpris" as Mpris
import "workspaces" as Workspaces
import "power" as Power
import "root:notifications" as Notifs
BarContainment {
id: root
@ -13,6 +15,7 @@ BarContainment {
property bool isSoleBar: Quickshell.screens.length == 1;
ColumnLayout {
anchors {
left: parent.left
right: parent.right
@ -21,24 +24,32 @@ BarContainment {
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Loader {
active: isSoleBar
Layout.preferredHeight: active ? implicitHeight : 0;
Notifs.NotificationWidget {
Layout.fillWidth: true
sourceComponent: Workspaces.Widget {
bar: root
wsBaseIndex: 1
}
bar: root
}
Workspaces.Widget {
bar: root
Layout.fillWidth: true
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
hideWhenEmpty: isSoleBar
ColumnLayout {
spacing: 0
Loader {
active: root.isSoleBar
Layout.preferredHeight: active ? implicitHeight : 0;
Layout.fillWidth: true
sourceComponent: Workspaces {
bar: root
wsBaseIndex: 1
}
}
Workspaces {
bar: root
Layout.fillWidth: true
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
hideWhenEmpty: root.isSoleBar
}
}
}
}
@ -65,8 +76,15 @@ BarContainment {
Layout.fillWidth: true
}
ClockWidget {
Power.Power {
bar: root
Layout.fillWidth: true
}
ClockWidget {
bar: root
Layout.fillWidth: true
}
}
}

View file

@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Effects
FullwidthMouseArea {
id: root
property bool showPressed: mouseArea.pressed;
property real baseMargin: 0;
property bool directScale: false;
readonly property Item contentItem: mContentItem;
default property alias contentItemData: mContentItem.data;
property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } }
property real targetMargins: root.showPressed ? 3 : 0;
Behavior on targetMargins { SmoothedAnimation { velocity: 25 } }
hoverEnabled: true
Item {
id: mContentItem
anchors.fill: parent;
anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins);
scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0;
opacity: root.enabled ? 1.0 : 0.5;
Behavior on opacity { SmoothedAnimation { velocity: 5 } }
layer.enabled: root.targetBrightness != 0
layer.effect: MultiEffect { brightness: root.targetBrightness / 1000 }
}
}

View file

@ -1,8 +1,8 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:."
import "root:lock" as Lock
import ".."
import "../lock" as Lock
PanelWindow {
id: root
@ -20,8 +20,8 @@ PanelWindow {
exclusiveZone: width - margins.left
color: "transparent"
WlrLayershell.namespace: "shell:bar"
WlrLayershell.namespace: "shell:bar"
readonly property var tooltip: tooltip;
Tooltip {

View file

@ -1,31 +0,0 @@
import QtQuick
BarWidgetInner {
implicitHeight: 50
SequentialAnimation on implicitHeight {
loops: Animation.Infinite
PropertyAnimation { to: 70; duration: 1000 }
PropertyAnimation { to: 40; duration: 1000 }
}
property int len: 1
Text {
anchors.centerIn: parent
text: `8${'='.repeat(len)}D`
font.pointSize: 16
color: "white"
PropertyAnimation on rotation {
loops: Animation.Infinite
to: 365
duration: 1000
}
}
MouseArea {
anchors.fill: parent
onClicked: len += 1;
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
BarButton {
id: root
required property string image;
property alias cache: imageComponent.cache;
property alias asynchronous: imageComponent.asynchronous;
property bool scaleIcon: !asynchronous
Image {
id: imageComponent
anchors.fill: parent
source: root.image
sourceSize.width: scaleIcon ? width : (root.width - baseMargin)
sourceSize.height: scaleIcon ? height : (root.height - baseMargin)
cache: false
}
}

View file

@ -1,41 +1,67 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import ".."
OverlayWidget {
expandedWidth: 600
expandedHeight: 600
BarWidgetInner {
id: root
required property var bar;
BarWidgetInner {
implicitHeight: layout.implicitHeight
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
spacing: 0
SystemClock {
id: clock
precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes;
}
anchors {
right: parent.right
left: parent.left
BarButton {
id: button
anchors.fill: parent
fillWindowWidth: true
acceptedButtons: Qt.NoButton
ColumnLayout {
id: layout
spacing: 0
anchors {
right: parent.right
left: parent.left
}
Text {
Layout.alignment: Qt.AlignHCenter
text: {
const hours = clock.hours.toString().padStart(2, '0')
const minutes = clock.minutes.toString().padStart(2, '0')
return `${hours}\n${minutes}`
}
font.pointSize: 18
color: "white"
}
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Loader {
active: tooltip.visible
sourceComponent: Label {
text: {
// SystemClock can send an update slightly (<50ms) before the
// time changes. We use its readout so the widget and tooltip match.
const hours = clock.hours.toString().padStart(2, '0');
const minutes = clock.minutes.toString().padStart(2, '0');
const seconds = clock.seconds.toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}\n` + new Date().toLocaleString(Qt.locale("en_US"), "dddd, MMMM d, yyyy");
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: ShellGlobals.time.getHours().toString().padStart(2, '0')
font.pointSize: 18
color: "#a0ffffff"
}
Text {
Layout.alignment: Qt.AlignHCenter
text: ShellGlobals.time.getMinutes().toString().padStart(2, '0')
font.pointSize: 18
color: "#a0ffffff"
}
}
MouseArea {
anchors.fill: parent
onClicked: expanded = !expanded
}
}
}

View file

@ -1,97 +0,0 @@
import QtQuick
import Quickshell
import ".."
Item {
required property var bar;
required property real expandedWidth;
required property real expandedHeight;
required default property Item widget;
property bool expanded: false;
onExpandedChanged: {
animateTo(expanded ? 1.0 : 0.0)
if (expanded) popupSurface.activeOverlay = this
}
readonly property bool fullyCollapsed: animationProgress == 0.0;
onFullyCollapsedChanged: {
if (fullyCollapsed && popupSurface.activeOverlay == this) {
popupSurface.activeOverlay = null;
}
/*if (fullyCollapsed) {
widget.x = Qt.binding(() => this.x)
}*/
}
readonly property rect collapsedLayerRect: {
void [barWindow.windowTransform, popupSurface.windowTransform, y, parent.y];
return this.mapToItem(popupSurface.contentItem, 0, 0, width, height);
}
onCollapsedLayerRectChanged: console.log(`clr: ${collapsedLayerRect}`)
onLayerRectChanged: console.log(`lr: ${layerRect}`)
onYChanged: console.log(`y: ${y}`)
readonly property rect expandedLayerRect: bar.widgetSurface.expandedPosition(this)
readonly property rect layerRect: {
const [p, xCurve, yCurve] = [animationProgress, ShellGlobals.popoutXCurve, ShellGlobals.popoutYCurve];
return Qt.rect(
xCurve.interpolate(p, collapsedLayerRect.x, expandedLayerRect.x),
yCurve.interpolate(p, collapsedLayerRect.y, expandedLayerRect.y),
xCurve.interpolate(p, collapsedLayerRect.width, expandedLayerRect.width),
yCurve.interpolate(p, collapsedLayerRect.height, expandedLayerRect.height),
);
}
implicitWidth: widget.implicitWidth
implicitHeight: widget.implicitHeight
Component.onCompleted: {
popupSurface.connectOverlay(this);
widget.x = Qt.binding(() => layerRect.x);
widget.y = Qt.binding(() => layerRect.y);
widget.width = Qt.binding(() => layerRect.width);
widget.height = Qt.binding(() => layerRect.height);
}
Component.onDestruction: {
popupSurface.disconnectOverlay(this)
}
function animateTo(target: real) {
animationProgressInternal = target * 1000
}
property real animationProgress: animationProgressInternal * 0.001
property real animationProgressInternal: 0.0 // animations seem to only have int precision
Behavior on animationProgressInternal {
SmoothedAnimation { velocity: 3000 }
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPressed: expanded = false
Rectangle {
anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutline
border.width: 1
radius: 5
color: "transparent"
opacity: mouseArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
SmoothedAnimation { velocity: 4 }
}
}
}
}

View file

@ -0,0 +1,53 @@
import QtQuick
Item {
id: root
property bool fillWindowWidth: false;
property real extraVerticalMargin: 0;
property alias mouseArea: mouseArea;
property alias hoverEnabled: mouseArea.hoverEnabled;
property alias acceptedButtons: mouseArea.acceptedButtons;
property alias pressedButtons: mouseArea.pressedButtons;
property alias containsMouse: mouseArea.containsMouse;
property alias isPressed: mouseArea.pressed;
signal clicked(event: MouseEvent);
signal entered();
signal exited();
signal pressed(event: MouseEvent);
signal released(event: MouseEvent);
signal wheel(event: WheelEvent);
MouseArea {
id: mouseArea
anchors {
fill: parent
// not much point in finding exact values
leftMargin: root.fillWindowWidth ? -50 : 0
rightMargin: root.fillWindowWidth ? -50 : 0
topMargin: -root.extraVerticalMargin
bottomMargin: -root.extraVerticalMargin
}
Component.onCompleted: {
this.clicked.connect(root.clicked);
//this.entered.connect(root.entered);
//this.exited.connect(root.exited);
//this.pressed.connect(root.pressed);
this.released.connect(root.released);
//this.wheel.connect(root.wheel);
}
// for some reason MouseArea.pressed is both a prop and signal so connect doesn't work
onPressed: event => root.pressed(event);
// connecting to onwheel seems to implicitly accept it. undo that.
onWheel: event => {
event.accepted = false;
root.wheel(event);
}
}
}

View file

@ -1,17 +0,0 @@
import QtQuick
Item {
default property Item item;
property int expandedWidth;
property int expandedHeight;
implicitHeight: item.implicitHeight
implicitWidth: item.implicitWidth
Component.onCompleted: {
item.width = Qt.binding(() => this.width)
item.height = Qt.binding(() => this.height)
}
children: [ item ]
}

View file

@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/"
Scope {
id: root
@ -11,11 +12,14 @@ Scope {
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip;
property TooltipItem lastActiveItem: null;
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem;
property real hangTime: lastActiveItem?.hangTime ?? 0;
property Item tooltipItem: null;
onActiveItemChanged: {
if (activeItem != null) {
hangTimer.stop();
activeItem.targetVisible = true;
if (tooltipItem) {
@ -24,10 +28,12 @@ Scope {
}
if (lastActiveItem != null && lastActiveItem != activeItem) {
lastActiveItem.targetVisible = false;
if (activeItem != null) lastActiveItem.targetVisible = false;
else if (root.hangTime == 0) doLastHide();
else hangTimer.start();
}
lastActiveItem = activeItem;
if (activeItem != null) lastActiveItem = activeItem;
}
function setItem(item: TooltipItem) {
@ -46,28 +52,59 @@ Scope {
}
}
function doLastHide() {
lastActiveItem.targetVisible = false;
}
function onHidden(item: TooltipItem) {
if (item == lastActiveItem) {
lastActiveItem = null;
}
}
Timer {
id: hangTimer
interval: root.hangTime
onTriggered: doLastHide();
}
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0;
Behavior on scaleMul { SmoothedAnimation { velocity: 5 } }
LazyLoader {
id: popupLoader
activeAsync: activeItem != null
activeAsync: shownItem != null
PopupWindow {
id: popup
parentWindow: bar
relativeX: bar.tooltipXOffset
relativeY: 0
height: bar.height
width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth)
anchor {
window: bar
rect.x: bar.tooltipXOffset
rect.y: tooltipItem.highestAnimY
adjustment: PopupAdjustment.None
}
HyprlandWindow.opacity: root.scaleMul
//height: bar.height
width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
height: {
const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY
//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
return h
}
visible: true
color: "transparent"
//color: "#20000000"
mask: Region {
item: (activeItem?.hoverable ?? false) ? tooltipItem : null
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
}
HyprlandFocusGrab {
active: activeItem?.isMenu ?? false
windows: [ popup, bar ]
windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ]
onActiveChanged: {
if (!active && activeItem?.isMenu) {
activeMenu.close()
@ -75,30 +112,76 @@ Scope {
}
}
/*Rectangle {
color: "#10ff0000"
//y: tooltipItem.highestAnimY
height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
width: parent.width
}
Rectangle {
color: "#1000ff00"
//y: tooltipItem.highestAnimY
height: popup.height
width: parent.width
}*/
Item {
id: tooltipItem
Component.onCompleted: {
root.tooltipItem = this;
if (root.activeItem) {
root.activeItem.parent = this;
if (root.shownItem) {
root.shownItem.parent = this;
}
//highestAnimY = targetY - targetHeight / 2;
//lowestAnimY = targetY + targetHeight / 2;
}
transform: Scale {
origin.x: 0
origin.y: tooltipItem.height / 2
xScale: 0.9 + scaleMul * 0.1
yScale: xScale
}
clip: width != targetWidth || height != targetHeight
// bkg
BarWidgetInner {
anchors.fill: parent
color: ShellGlobals.colors.bar
}
readonly property var targetWidth: shownItem?.implicitWidth ?? 0;
readonly property var targetHeight: shownItem?.implicitHeight ?? 0;
property var largestAnimWidth: 0;
property var highestAnimY: 0; // unused due to reposition timing issues
property var lowestAnimY: bar.height;
onTargetWidthChanged: {
if (targetWidth > largestAnimWidth) {
largestAnimWidth = targetWidth;
}
}
readonly property var targetWidth: activeItem?.implicitWidth ?? 0;
readonly property var targetHeight: activeItem?.implicitHeight ?? 0;
onTargetYChanged: updateYBounds();
onTargetHeightChanged: updateYBounds();
function updateYBounds() {
if (targetY - targetHeight / 2 < highestAnimY) {
//highestAnimY = targetY - targetHeight / 2
}
property var lastTargetWidthTracker: 0;
property var lastTargetWidth: 0;
onTargetWidthChanged: {
lastTargetWidth = lastTargetWidthTracker;
lastTargetWidthTracker = targetWidth;
if (targetY + targetHeight / 2 > lowestAnimY) {
//lowestAnimY = targetY + targetHeight / 2
}
}
readonly property real targetY: {
if (activeItem == null) return 0;
const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
return bar.boundedY(target, activeItem.implicitHeight / 2);
if (shownItem == null) return 0;
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
return bar.boundedY(target, shownItem.implicitHeight / 2);
}
property var w: -1
@ -107,15 +190,24 @@ Scope {
property var y1: -1
property var y2: -1
y: y1
y: y1 - popup.anchor.rect.y
height: y2 - y1
SmoothedAnimation {
target: tooltipItem;
property: "y1";
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
onAnyAnimsRunningChanged: {
if (!anyAnimsRunning) {
largestAnimWidth = targetWidth
//highestAnimY = y1;
//lowestAnimY = y2;
}
}
SmoothedAnimation on y1 {
id: y1Anim
to: tooltipItem.targetY - tooltipItem.targetHeight / 2;
onToChanged: {
if (tooltipItem.y1 == -1 || !(activeItem?.animateSize ?? true)) {
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y1 = to;
} else {
@ -125,12 +217,11 @@ Scope {
}
}
SmoothedAnimation {
target: tooltipItem
property: "y2"
SmoothedAnimation on y2 {
id: y2Anim
to: tooltipItem.targetY + tooltipItem.targetHeight / 2;
onToChanged: {
if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) {
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y2 = to;
} else {
@ -140,13 +231,11 @@ Scope {
}
}
SmoothedAnimation {
SmoothedAnimation on w {
id: widthAnim
target: tooltipItem
property: "w"
to: tooltipItem.targetWidth;
onToChanged: {
if (tooltipItem.w == -1) {
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.w = to;
} else {

View file

@ -1,12 +1,12 @@
import QtQuick
import Quickshell
import "root:/"
Item {
id: root
required property var tooltip;
required property Item owner;
property bool isMenu: false;
property list<QtObject> grabWindows;
property bool hoverable: isMenu;
property bool animateSize: true;
property bool show: false;
@ -17,47 +17,35 @@ Item {
signal close();
readonly property alias contentItem: contentItem;
default property alias data: contentItem.data;
property Component backgroundComponent: BarWidgetInner {
color: ShellGlobals.colors.bar
anchors.fill: parent
}
property Component backgroundComponent: null
onShowChanged: {
if (show) {
hangTimer.stop();
tooltip.setItem(this);
} else if (hangTime == 0) {
tooltip.removeItem(this);
} else hangTimer.start();
}
Timer {
id: hangTimer
interval: hangTime
onTriggered: tooltip.removeItem(root);
if (show) tooltip.setItem(this);
else tooltip.removeItem(this);
}
property bool targetVisible: false
property real targetOpacity: 0
opacity: targetOpacity / 1000
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
Behavior on targetOpacity {
id: opacityAnimation
SmoothedAnimation { velocity: 5000 }
SmoothedAnimation { velocity: 5 }
}
function snapOpacity(opacity: real) {
opacityAnimation.enabled = false;
targetOpacity = opacity * 1000
targetOpacity = opacity;
opacityAnimation.enabled = true;
}
onTargetVisibleChanged: {
if (targetVisible) {
visible = true;
targetOpacity = 1000;
targetOpacity = 1;
} else {
close()
targetOpacity = 0;
@ -68,20 +56,21 @@ Item {
if (!targetVisible && targetOpacity == 0) {
visible = false;
this.parent = null;
if (tooltip) tooltip.onHidden(this);
}
}
anchors.fill: parent
visible: false
clip: true
implicitHeight: contentItem.implicitHeight + 10
implicitWidth: contentItem.implicitWidth + 10
//clip: true
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
readonly property Item item: contentItem;
Loader {
anchors.fill: parent
active: root.visible || root.preloadBackground
active: root.backgroundComponent && (root.visible || root.preloadBackground)
asynchronous: !root.visible && root.preloadBackground
sourceComponent: backgroundComponent
}
@ -91,7 +80,7 @@ Item {
anchors.fill: parent
anchors.margins: 5
implicitHeight: childrenRect.height
implicitWidth: childrenRect.width
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
}
}

View file

@ -1,17 +1,21 @@
pragma ComponentBehavior: Bound;
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import ".."
import "root:."
MouseArea {
FullwidthMouseArea {
id: root
required property var bar;
required property int wsBaseIndex;
property int wsCount: 10;
property bool hideWhenEmpty: false;
implicitHeight: column.implicitHeight + 10;
fillWindowWidth: true
acceptedButtons: Qt.NoButton
onWheel: event => {
@ -29,9 +33,6 @@ MouseArea {
property int existsCount: 0;
visible: !hideWhenEmpty || existsCount > 0;
property real animPos: 0;
Behavior on animPos { SmoothedAnimation { velocity: 100 } }
// destructor takes care of nulling
signal workspaceAdded(workspace: HyprlandWorkspace);
@ -45,20 +46,22 @@ MouseArea {
}
Repeater {
model: 10
model: root.wsCount
MouseArea {
FullwidthMouseArea {
id: wsItem
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
Layout.fillWidth: true
implicitHeight: 15
fillWindowWidth: true
required property int index;
property int wsIndex: wsBaseIndex + index;
property int wsIndex: root.wsBaseIndex + index;
property HyprlandWorkspace workspace: null;
property bool exists: workspace != null;
property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace;
property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace;
onActiveChanged: {
if (active) root.currentIndex = wsIndex;
@ -78,21 +81,21 @@ MouseArea {
}
}
property real animActive: active ? 100 : 0
Behavior on animActive { NumberAnimation { duration: 100 } }
property real animActive: active ? 1 : 0
Behavior on animActive { NumberAnimation { duration: 150 } }
property real animExists: exists ? 100 : 0
property real animExists: exists ? 1 : 0
Behavior on animExists { NumberAnimation { duration: 100 } }
Rectangle {
anchors.centerIn: parent
height: 10
width: parent.width
scale: 1 + animActive * 0.003
scale: 1 + wsItem.animActive * 0.3
radius: height / 2
border.color: ShellGlobals.colors.widgetOutline
border.width: 1
color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
}
}
}

View file

@ -12,14 +12,20 @@ ClickableIcon {
implicitHeight: width;
acceptedButtons: Qt.LeftButton | Qt.RightButton;
showPressed: mixerOpen
fillWindowWidth: true
showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton)
onPressed: event => {
event.accepted = true;
if (event.button === Qt.RightButton) {
mixerOpen = !mixerOpen;
}
}
onClicked: event => {
event.accepted = true;
if (event.button === Qt.LeftButton) {
event.accepted = true;
node.audio.muted = !node.audio.muted;
} else if (event.button === Qt.RightButton) {
mixerOpen = !mixerOpen;
}
}
@ -71,7 +77,16 @@ ClickableIcon {
sourceComponent: Mixer {
width: 550
trackedNode: node
nodeList: Pipewire.nodes.values.filter(n => n.audio && !n.isStream && n.isSink == node.isSink)
nodeImage: root.image
onSelected: n => {
if (node.isSink) {
Pipewire.preferredDefaultAudioSink = n;
} else {
Pipewire.preferredDefaultAudioSource = n;
}
}
}
}
}

View file

@ -1,12 +1,18 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import ".."
import "../.."
ColumnLayout {
id: root
required property PwNode trackedNode;
required property string nodeImage;
required property list<PwNode> nodeList;
signal selected(node: PwNode);
PwNodeLinkTracker {
id: linkTracker
@ -15,10 +21,13 @@ ColumnLayout {
PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
MixerEntry {
MixerEntry/*WithSelect*/ {
id: nodeEntry
node: trackedNode
//nodeList: root.nodeList
image: nodeImage
Component.onCompleted: this.selected.connect(root.selected);
}
Rectangle {
@ -49,7 +58,7 @@ ColumnLayout {
// special cases :(
if (icon == "firefox") icon = "firefox-devedition";
return `image://icon/${icon}`
return Quickshell.iconPath(icon)
}
}
}

View file

@ -1,51 +1,11 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
RowLayout {
MixerEntryBase {
id: root
required property PwNode node;
required property string image;
property int state: PwLinkState.Unlinked;
PwObjectTracker { objects: [ node ] }
ClickableIcon {
image: root.image
asynchronous: true
implicitHeight: 40
implicitWidth: height
}
ColumnLayout {
RowLayout {
Item {
implicitHeight: title.implicitHeight
Layout.fillWidth: true
Text {
id: title
color: "white"
anchors.fill: parent
elide: Text.ElideRight
text: {
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
const mediaName = node.properties["media.name"];
return mediaName != undefined ? `${name} - ${mediaName}` : name;
}
}
}
}
VolumeSlider {
//Layout.fillHeight: true
Layout.fillWidth: true
implicitWidth: 200
value: node.audio.volume
onValueChanged: node.audio.volume = value
}
headerComponent: Text {
color: "white"
elide: Text.ElideRight
text: root.getNodeName(root.node)
}
}

View file

@ -0,0 +1,50 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
RowLayout {
id: root
required property PwNode node;
required property string image;
required property Item headerComponent;
property int state: PwLinkState.Unlinked;
function getNodeName(node: PwNode): string {
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
const mediaName = node.properties["media.name"];
return mediaName != undefined ? `${name} - ${mediaName}` : name + node.id;
}
PwObjectTracker { objects: [ node ] }
ClickableIcon {
image: root.image
asynchronous: true
implicitHeight: 40
implicitWidth: height
}
ColumnLayout {
Item {
id: container
Layout.fillWidth: true
implicitWidth: headerComponent.implicitWidth
implicitHeight: headerComponent.implicitHeight
children: [ headerComponent ]
Binding { root.headerComponent.anchors.fill: container }
}
VolumeSlider {
Layout.fillWidth: true
value: node.audio.volume
onValueChanged: node.audio.volume = value
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.Pipewire
MixerEntryBase {
id: root
required property list<PwNode> nodeList;
signal selected(node: PwNode);
headerComponent: ComboBox {
model: nodeList.map(node => root.getNodeName(node));
currentIndex: nodeList.findIndex(node => node == root.node)
onActivated: index => root.selected(nodeList[index])
}
}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M806-56 677.67-184.33q-27 18.66-58 32.16-31 13.5-64.34 21.17v-68.67q20-6.33 38.84-13.66 18.83-7.34 35.5-19l-154.34-155V-160l-200-200h-160v-240H262L51.33-810.67 98.67-858l754.66 754L806-56Zm-26.67-232-48-48q19-33 28.17-69.62 9.17-36.61 9.17-75.38 0-100.22-58.34-179.11Q652-739 555.33-762.33V-831q124 28 202 125.5t78 224.5q0 51.67-14.16 100.67-14.17 49-41.84 92.33Zm-134-134-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5t-7.5 28.5Zm-170-170-104-104 104-104v208Zm-66.66 270v-131.33l-80-80H182v106.66h122L408.67-322Zm-40-171.33Z"/></svg>

Before

(image error) Size: 650 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M560-131v-68.67q94.67-27.33 154-105 59.33-77.66 59.33-176.33 0-98.67-59-176.67-59-78-154.33-104.66V-831q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm426.67 45.33v-332Q599-628 629.5-582T660-480q0 55-30.83 100.83-30.84 45.84-82.5 64.5ZM413.33-634l-104 100.67H186.67v106.66h122.66l104 101.34V-634Zm-96 154Z"/></svg>

Before

(image error) Size: 474 B

View file

@ -82,7 +82,7 @@ BarWidgetInner {
x: blurRadius
width: blur.width - blurRadius * 2
height: blur.height
clip: true
GaussianBlur {
source: blurSource
x: -parent.x
@ -139,23 +139,27 @@ BarWidgetInner {
readonly property Rectangle overlay: overlayItem;
Rectangle {
id: overlayItem
visible: false
visible: true
anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutlineSeparate
border.width: 0//1
radius: 0//root.radius
radius: root.radius
color: "transparent"
Rectangle {
anchors.fill: parent
radius: root.radius
color: "transparent"
border.color: ShellGlobals.colors.widgetOutlineSeparate;
border.width: 1
}
}
// slightly offset on the corners :/
layer.enabled: true
layer.effect: ShaderEffect {
fragmentShader: "radial_clip.frag.qsb"
// +1 seems to match Rectangle
property real radius: root.radius + 1
property size size: Qt.size(root.width, root.height)
property real borderWidth: 1//.5
property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
property color tint: overlayItem.color
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
radius: root.radius
}
}
}

View file

@ -1,9 +1,11 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQml.Models
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import Quickshell.Hyprland
import "../.."
Singleton {
@ -15,51 +17,58 @@ Singleton {
property bool __reverse: false;
property var activeTrack;
Component.onCompleted: {
for (const player of Mpris.players.values) {
if (player.playbackState == MprisPlaybackState.Playing) {
if (root.trackedPlayer == null) {
root.trackedPlayer = player;
Instantiator {
model: Mpris.players;
Connections {
required property MprisPlayer modelData;
target: modelData;
Component.onCompleted: {
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData;
}
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
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 (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0];
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData;
}
}
}
Connections {
target: activePlayer
function onTrackChanged() {
function onPostTrackChanged() {
root.updateTrack();
}
}
// Change the tracked player when one changes playback state or is created in a playing state.
Connections {
target: Mpris.players;
function onTrackArtUrlChanged() {
console.log("arturl:", activePlayer.trackArtUrl)
//root.updateTrack();
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
// cantata likes to send cover updates *BEFORE* updating the track info.
// as such, art url changes shouldn't be able to break the reverse animation
const r = root.__reverse;
root.updateTrack();
root.__reverse = r;
function onObjectInsertedPost(player: MprisPlayer) {
if (player.playbackState === MprisPlaybackState.Playing) {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
}
function onObjectRemovedPre() {
console.log(`trackedPlayer: ${root.trackedPlayer}`)
if (root.trackedPlayer == null) {
for (const player of Mpris.players.values) {
if (player.playbackState === MprisPlaybackState.Playing) {
root.trackedPlayer = player;
break;
}
}
}
}
}
@ -67,27 +76,23 @@ Singleton {
onActivePlayerChanged: this.updateTrack();
function updateTrack() {
const metadata = this.activePlayer?.metadata ?? {};
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
this.activeTrack = {
artUrl: metadata["mpris:artUrl"] ?? "",
title: metadata["xesam:title"] ?? "",
artist: metadata["xesam:artist"] ?? "",
uniqueId: this.activePlayer?.uniqueId ?? 0,
artUrl: this.activePlayer?.trackArtUrl ?? "",
title: this.activePlayer?.trackTitle || "Unknown Title",
artist: this.activePlayer?.trackArtist || "Unknown Artist",
album: this.activePlayer?.trackAlbum || "Unknown Album",
};
this.trackChanged(__reverse);
this.__reverse = false;
}
property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing;
property bool canPlay: this.activePlayer?.canPlay ?? false;
function play() {
if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
}
property bool canPause: this.activePlayer?.canPause ?? false;
function pause() {
if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying;
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false;
function togglePlaying() {
if (this.canTogglePlaying) this.activePlayer.togglePlaying();
}
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
@ -125,7 +130,7 @@ Singleton {
}
function setActivePlayer(player: MprisPlayer) {
const targetPlayer = player ?? MprisPlayer.players[0];
const targetPlayer = player ?? Mpris.players[0];
console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
if (targetPlayer && this.activePlayer) {
@ -138,31 +143,17 @@ Singleton {
this.trackedPlayer = targetPlayer;
}
Shortcut {
name: "music-pauseall";
onPressed: {
for (let i = 0; i < Mpris.players.length; i++) {
const player = Mpris.players[i];
if (player.canPause) player.playbackState = MprisPlaybackState.Paused;
IpcHandler {
target: "mpris"
function pauseAll(): void {
for (const player of Mpris.players.values) {
if (player.canPause) player.pause();
}
}
}
Shortcut {
name: "music-playpause";
onPressed: {
if (root.isPlaying) root.pause();
else root.play();
}
}
Shortcut {
name: "music-previous";
onPressed: root.previous();
}
Shortcut {
name: "music-next";
onPressed: root.next();
function playPause(): void { root.togglePlaying(); }
function previous(): void { root.previous(); }
function next(): void { root.next(); }
}
}

View file

@ -1,7 +0,0 @@
import Quickshell
import Quickshell.Services.Mpris
Scope {
required property MprisPlayer player;
}

View file

@ -1,2 +0,0 @@
import QtQuick
import

View file

@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@ -7,9 +9,10 @@ import Quickshell.Services.Mpris
import ".."
import "../.."
MouseArea {
FullwidthMouseArea {
id: root
hoverEnabled: true
fillWindowWidth: true
required property var bar;
implicitHeight: column.implicitHeight + 10
@ -27,12 +30,12 @@ MouseArea {
property alias widgetOpen: persist.widgetOpen;
acceptedButtons: Qt.RightButton
onClicked: widgetOpen = !widgetOpen
onPressed: widgetOpen = !widgetOpen
onWheel: event => {
event.accepted = true;
if (MprisController.canChangeVolume) {
this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
}
}
@ -42,7 +45,7 @@ MouseArea {
id: widget
anchors.fill: parent
property real scaleMul: root.pressed || widgetOpen ? 100 : 1
property real scaleMul: widgetOpen ? 100 : 1
Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
@ -56,8 +59,10 @@ MouseArea {
BackgroundArt {
id: bkg
anchors.fill: parent
overlay.color: "#30000000"
function updateArt(reverse: bool) {
console.log("update art", MprisController.activeTrack.artUrl)
this.setArt(MprisController.activeTrack.artUrl, reverse, false)
}
@ -86,6 +91,7 @@ MouseArea {
implicitHeight: width
scaleIcon: false
baseMargin: 3
hoverEnabled: false
enabled: MprisController.canGoPrevious;
onClicked: MprisController.previous();
}
@ -95,11 +101,9 @@ MouseArea {
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay;
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
hoverEnabled: false
enabled: MprisController.canTogglePlaying;
onClicked: MprisController.togglePlaying();
}
ClickableIcon {
@ -108,51 +112,162 @@ MouseArea {
implicitHeight: width
scaleIcon: false
baseMargin: 3
hoverEnabled: false
enabled: MprisController.canGoNext;
onClicked: MprisController.next();
}
}
property var tooltip: TooltipItem {
property Scope positionInfo: Scope {
id: positionInfo
property var player: root.activePlayer;
property int position: Math.floor(player.position);
property int length: Math.floor(player.length);
FrameAnimation {
id: posTracker;
running: positionInfo.player.isPlaying && (tooltip.visible || rightclickMenu.visible);
onTriggered: positionInfo.player.positionChanged();
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
show: root.containsMouse
//implicitHeight: root.height - 10
//implicitWidth: childrenRect.width
/*ColumnLayout {
ColumnLayout {
visible: MprisController.activePlayer != null
Label { text: MprisController.activeTrack?.title ?? "" }
Label {
text: {
const artist = MprisController.activeTrack?.artist ?? "";
const album = MprisController.activeTrack?.album ?? "";
return artist + (album ? ` - ${album}` : "");
}
}
Label { text: MprisController.activePlayer?.identity ?? "" }
}
Label {
visible: MprisController.activePlayer == null
text: "No media playing"
}
Rectangle { implicitHeight: 10; color: "white"; Layout.fillWidth: true }
}*/
contentItem.anchors.margins: 0
Item {
implicitWidth: 200
implicitHeight: 100
}
id: ttcontent
width: parent.width
height: Math.max(parent.height, implicitHeight)
implicitWidth: cl.implicitWidth + 10
implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0)
/*Loader {
active: tooltip.visible
ColumnLayout {
id: cl
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 5
}
sourceComponent: ColumnLayout {
height: root.height - 10
RowLayout {
Image {
Layout.fillHeight: true
source: mainPlayer.metadata["mpris:artUrl"] ?? ""
//visible: MprisController.activePlayer != null
FontMetrics { id: fontmetrics }
component FullheightLabel: Item {
implicitHeight: fontmetrics.height
implicitWidth: label.implicitWidth
property alias text: label.text
cache: false
fillMode: Image.PreserveAspectCrop
sourceSize.width: height
sourceSize.height: height
}
Label {
text: mainPlayer.identity
id: label
anchors.verticalCenter: parent.verticalCenter
}
}
Slider {
Layout.fillWidth: true
FullheightLabel {
visible: MprisController.activePlayer != null
text: MprisController.activeTrack?.title ?? ""
}
FullheightLabel {
visible: MprisController.activePlayer != null
text: MprisController.activeTrack?.artist ?? ""
/*text: {
const artist = MprisController.activeTrack?.artist ?? "";
const album = MprisController.activeTrack?.album ?? "";
return artist + (album ? ` - ${album}` : "");
}*/
}
Label {
text: {
if (!MprisController.activePlayer) return "No media playing";
return MprisController.activePlayer?.identity + " - "
+ positionInfo.timeStr(positionInfo.position) + " / "
+ positionInfo.timeStr(positionInfo.length);
}
}
}
}*/
Rectangle {
id: ttprect
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
color: "#30ceffff"
implicitHeight: 8
visible: MprisController.activePlayer != null
Rectangle {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
color: "#80ceffff"
width: parent.width * (root.activePlayer.position / root.activePlayer.length)
}
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: ttcontent.width
height: ttcontent.height
bottomLeftRadius: 5
bottomRightRadius: 5
}
}
}
}
property var rightclickMenu: TooltipItem {
@ -182,6 +297,7 @@ MouseArea {
target: MprisController
function onTrackChanged(reverse: bool) {
console.log(`track changed: rev: ${reverse}`)
popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
}
}
@ -191,71 +307,41 @@ MouseArea {
}
}
contentItem {
implicitWidth: 500
implicitHeight: 650
}
Loader {
active: rightclickMenu.visible
width: 500
height: 650
active: rightclickMenu.visible
sourceComponent: ColumnLayout {
property var player: activePlayer;
anchors.fill: parent;
property int position: 0;
property int length: 0;
FrameAnimation {
id: posTracker;
running: player.playbackState == MprisPlaybackState.Playing && widgetOpen;
onTriggered: player.positionChanged();
}
Connections {
target: player
function onPositionChanged() {
const newPosition = Math.floor(player.position);
if (newPosition != position) position = newPosition;
}
function onLengthChanged() {
const newLength = Math.floor(player.length);
if (newLength != length) length = newLength;
}
}
property var player: root.activePlayer;
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
trackStack.updateTrack(reverse, false);
length = Math.floor(player.length);
}
}
Component.onCompleted: {
position = Math.floor(player.position);
length = Math.floor(player.length);
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
Item {
id: playerSelectorContainment
Layout.fillWidth: true
implicitHeight: playerSelector.implicitHeight + 20
implicitWidth: playerSelector.implicitWidth
ScrollView {
RowLayout { //ScrollView {
id: playerSelector
anchors.centerIn: parent
width: Math.min(implicitWidth, playerSelectorContainment.width)
RowLayout {
//RowLayout {
Repeater {
model: Mpris.players
@ -281,8 +367,7 @@ MouseArea {
source: {
const entry = DesktopEntries.byId(modelData.desktopEntry);
console.log(`ent ${entry} id ${modelData.desktopEntry}`)
if (!entry) return "image://icon/";
return `image://icon/${entry.icon}`;
return Quickshell.iconPath(entry?.icon);
}
//asynchronous: true
@ -292,18 +377,18 @@ MouseArea {
}
}
}
}
//}
}
}
}
Item {
Layout.fillWidth: true
Layout.bottomMargin: 10
Layout.bottomMargin: 20
Label {
anchors.centerIn: parent
text: activePlayer.identity
text: root.activePlayer.identity
}
}
@ -311,7 +396,13 @@ MouseArea {
id: trackStack
Layout.fillWidth: true
implicitHeight: 400
clip: animating || (lastFlicked?.contentX ?? 0) != 0
// inverse of default tooltip margin - 1px for border
Layout.leftMargin: -4
Layout.rightMargin: -4
property Flickable lastFlicked;
property bool reverse: false;
Component.onCompleted: updateTrack(false, true);
@ -333,14 +424,16 @@ MouseArea {
// but may take longer if the image is huge.
readonly property bool svReady: img.status === Image.Ready;
contentWidth: width + 1
onDragStarted: trackStack.lastFlicked = this
onDragEnded: {
return;
//return;
console.log(`dragend ${contentX}`)
if (Math.abs(contentX) > 75) {
if (contentX < 0) MprisController.previous();
else if (contentX > 0) MprisController.next();
}
}
ColumnLayout {
id: trackContent
width: flickable.width
@ -348,7 +441,7 @@ MouseArea {
Item {
Layout.fillWidth: true
implicitHeight: 300//img.implicitHeight
implicitHeight: 302//img.implicitHeight
implicitWidth: img.implicitWidth
Image {
@ -362,27 +455,50 @@ MouseArea {
sourceSize.height: 300
sourceSize.width: 300
layer.enabled: true
layer.effect: OpacityMask {
cached: true
maskSource: Rectangle {
width: img.width
height: img.height
radius: 5
}
}
}
}
Item {
component CenteredText: Item {
Layout.fillWidth: true
Layout.topMargin: 20
property alias text: label.text
property alias font: label.font
Label {
id: label
visible: text != ""
anchors.centerIn: parent
text: track.title
elide: Text.ElideRight
width: Math.min(parent.width - 20, implicitWidth)
}
}
Item {
Layout.fillWidth: true
CenteredText {
Layout.topMargin: 20
text: track.title
font.pointSize: albumLabel.font.pointSize + 1
}
Label {
anchors.centerIn: parent
text: track.artist
}
CenteredText {
id: albumLabel
Layout.topMargin: 18
text: track.album
opacity: 0.8
}
CenteredText {
Layout.topMargin: 25
text: track.artist
}
Item { Layout.fillHeight: true }
@ -462,11 +578,8 @@ MouseArea {
implicitWidth: 80
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
enabled: MprisController.canTogglePlaying;
onClicked: MprisController.togglePlaying();
}
ClickableIcon {
@ -491,33 +604,70 @@ MouseArea {
}
RowLayout {
Layout.margins: 5
Label {
Layout.preferredWidth: lengthLabel.implicitWidth
text: timeStr(position)
text: positionInfo.timeStr(positionInfo.position)
}
MediaSlider {
id: slider
property bool bindSlider: true;
property real boundAnimStart: 0;
property real boundAnimFactor: 1;
property real lastPosition: 0;
property real lastLength: 0;
property real boundPosition: {
const ppos = player.position / player.length;
const bpos = boundAnimStart;
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
}
NumberAnimation {
id: boundAnim
target: slider
property: "boundAnimFactor"
from: 0
to: 1
duration: 600
easing.type: Easing.OutExpo
}
Connections {
target: player
function onPositionChanged() {
if (false && player.position == 0 && slider.lastPosition != 0 && !boundAnim.running) {
slider.boundAnimStart = slider.lastPosition / slider.lastLength;
boundAnim.start();
}
slider.lastPosition = player.position;
slider.lastLength = player.length;
}
}
Layout.fillWidth: true
property var bindSlider: true;
enabled: player.canSeek
from: 0
to: player.length
to: 1
onPressedChanged: {
if (!pressed) player.position = value;
if (!pressed) player.position = value * player.length;
bindSlider = !pressed;
}
Binding {
when: slider.bindSlider
slider.value: player.position
slider.value: slider.boundPosition
}
}
Label {
id: lengthLabel
text: timeStr(length)
text: positionInfo.timeStr(positionInfo.length)
}
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import Quickshell.Services.UPower
import "root:."
Item {
id: root
required property UPowerDevice device;
property real scale: 1;
readonly property bool isCharging: root.device.state == UPowerDeviceState.Charging;
readonly property bool isPluggedIn: isCharging || root.device.state == UPowerDeviceState.PendingCharge;
readonly property bool isLow: root.device.percentage <= 0.20;
width: 35 * root.scale
height: 35 * root.scale
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 4 * root.scale
}
width: 13 * root.scale
height: 23 * root.device.percentage * root.scale
radius: 2 * root.scale
color: root.isPluggedIn ? "#359040"
: ShellGlobals.interpolateColors(Math.min(1.0, Math.min(0.5, root.device.percentage) * 2), "red", "white")
}
Image {
id: img
anchors.fill: parent;
source: root.isCharging ? "root:icons/battery-charging.svg"
: root.isPluggedIn ? "root:icons/battery-plus.svg"
: root.isLow ? "root:icons/battery-warning.svg"
: "root:icons/battery-empty.svg"
sourceSize.width: parent.width
sourceSize.height: parent.height
visible: true
}
}

View file

@ -0,0 +1,224 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Widgets
import ".."
import "root:."
import "root:components"
import "power"
BarWidgetInner {
id: root
required property var bar;
readonly property var chargeState: UPower.displayDevice.state
readonly property bool isCharging: chargeState == UPowerDeviceState.Charging;
readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge;
readonly property real percentage: UPower.displayDevice.percentage
readonly property bool isLow: percentage <= 0.20
readonly property UPowerDevice batteryDevice: UPower.devices.values
.find(device => device.isLaptopBattery);
function statusStr() {
return root.isPluggedIn ? `Plugged in, ${root.isCharging ? "Charging" : "Not Charging"}`
: "Discharging";
}
property bool showMenu: false;
implicitHeight: width
color: isLow ? "#45ff6060" : ShellGlobals.colors.widget
BarButton {
id: button
anchors.fill: parent
baseMargin: 5
fillWindowWidth: true
acceptedButtons: Qt.RightButton
directScale: true
showPressed: root.showMenu
onPressed: {
root.showMenu = !root.showMenu
}
BatteryIcon {
device: UPower.displayDevice
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Loader {
active: tooltip.visible
sourceComponent: Label {
text: {
const status = root.statusStr();
const percentage = Math.round(root.percentage * 100);
let str = `${percentage}% - ${status}`;
return str;
}
}
}
}
property TooltipItem rightclickMenu: TooltipItem {
id: rightclickMenu
tooltip: bar.tooltip
owner: root
isMenu: true
show: root.showMenu
onClose: root.showMenu = false
Loader {
active: rightclickMenu.visible
sourceComponent: ColumnLayout {
spacing: 10
FontMetrics { id: fm }
component SmallLabel: Label {
font.pointSize: fm.font.pointSize * 0.8
color: "#d0eeffff"
}
RowLayout {
IconImage {
source: "root:icons/gauge.svg"
implicitSize: 32
}
ColumnLayout {
spacing: 0
Label { text: "Power Profile" }
OptionSlider {
values: ["Power Save", "Balanced", "Performance"]
index: PowerProfiles.profile
onIndexChanged: PowerProfiles.profile = this.index;
implicitWidth: 350
}
}
}
RowLayout {
IconImage {
Layout.alignment: Qt.AlignTop
source: "root:icons/battery-empty.svg"
implicitSize: 32
}
ColumnLayout {
spacing: 0
RowLayout {
Label { text: "Battery" }
Item { Layout.fillWidth: true }
Label {
text: `${root.statusStr()} -`
color: "#d0eeffff"
}
Label { text: `${Math.round(root.percentage * 100)}%` }
}
ProgressBar {
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.fillWidth: true
value: UPower.displayDevice.percentage
}
RowLayout {
visible: remainingTimeLbl.text !== ""
SmallLabel { text: "Time remaining" }
Item { Layout.fillWidth: true }
SmallLabel {
id: remainingTimeLbl
text: {
const device = UPower.displayDevice;
const time = device.timeToEmpty || device.timeToFull;
if (time === 0) return "";
const minutes = Math.floor(time / 60).toString().padStart(2, '0');
return `${minutes} minutes`
}
}
}
RowLayout {
visible: root.batteryDevice.healthSupported
SmallLabel { text: "Health" }
Item { Layout.fillWidth: true }
SmallLabel {
text: `${Math.floor((root.batteryDevice?.healthPercentage ?? 0))}%`
}
}
}
}
Repeater {
model: ScriptModel {
// external devices
values: UPower.devices.values.filter(device => !device.powerSupply)
}
RowLayout {
required property UPowerDevice modelData;
IconImage {
Layout.alignment: Qt.AlignTop
source: {
switch (modelData.type) {
case UPowerDeviceType.Headset: return "root:icons/headset.svg";
}
return Quickshell.iconPath(modelData.iconName)
}
implicitSize: 32
}
ColumnLayout {
spacing: 0
RowLayout {
Label { text: modelData.model }
Item { Layout.fillWidth: true }
Label { text: `${Math.round(modelData.percentage * 100)}%` }
}
ProgressBar {
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.fillWidth: true
value: modelData.percentage
}
RowLayout {
visible: modelData.healthSupported
SmallLabel { text: "Health" }
Item { Layout.fillWidth: true }
SmallLabel {
text: `${Math.floor(modelData.healthPercentage)}%`
}
}
}
}
}
}
}
}
}

View file

@ -6,7 +6,7 @@ Item {
property bool expanded: false;
readonly property bool open: progress != 0;
readonly property bool animating: internalProgress != -1 && internalProgress != 101;
readonly property bool animating: internalProgress != (expanded ? 101 : -1);
implicitHeight: 16
implicitWidth: 16

View file

@ -1,14 +1,15 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.DBusMenu
import "../.."
MouseArea {
id: root
required property var entry;
required property QsMenuEntry entry;
property alias expanded: childrenRevealer.expanded;
property bool animating: childrenRevealer.animating || childrenList.animating;
property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false);
// appears it won't actually create the handler when only used from MenuItemList.
onExpandedChanged: {}
onAnimatingChanged: {}
@ -22,7 +23,7 @@ MouseArea {
onClicked: {
if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded
else {
entry.click();
entry.triggered();
if (entry.toggleType == ToggleButtonType.None) close();
}
}
@ -34,19 +35,21 @@ MouseArea {
spacing: 0
RowLayout {
id: innerRow
Item {
implicitWidth: 22
implicitHeight: 22
MenuCheckBox {
anchors.centerIn: parent
visible: entry.toggleType == ToggleButtonType.CheckBox
visible: entry.buttonType == QsMenuButtonType.CheckBox
checkState: entry.checkState
}
MenuRadioButton {
anchors.centerIn: parent
visible: entry.toggleType == ToggleButtonType.RadioButton
visible: entry.buttonType == QsMenuButtonType.RadioButton
checkState: entry.checkState
}
@ -59,7 +62,7 @@ MouseArea {
}
Text {
text: entry.cleanLabel
text: entry.text
color: entry.enabled ? "white" : "#bbbbbb"
}
@ -68,25 +71,32 @@ MouseArea {
implicitWidth: 22
implicitHeight: 22
Image {
IconImage {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: entry.icon != ""
source: entry.icon
sourceSize.height: parent.height
sourceSize.width: parent.height
visible: source != ""
implicitSize: parent.height
}
}
}
Item {
Loader {
id: childMenuLoader
Layout.fillWidth: true
implicitHeight: childrenList.implicitHeight * childrenRevealer.progress
Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
readonly property real widthDifference: {
Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
}
Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
active: root.expanded || root.animating
clip: true
MenuItemList {
sourceComponent: MenuView {
id: childrenList
items: entry.children
menu: entry
onClose: root.close()
anchors {

View file

@ -6,17 +6,19 @@ import "../.."
ColumnLayout {
id: root
required property var items;
property alias menu: menuView.menu;
property Item animatingItem: null;
property bool animating: animatingItem != null;
signal close();
signal submenuExpanded(item: var);
QsMenuOpener { id: menuView }
spacing: 0
Repeater {
model: items
model: menuView.children;
Loader {
required property var modelData;

View file

@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
@ -5,102 +7,102 @@ import Quickshell
import Quickshell.Services.SystemTray
import ".."
OverlayWidget {
BarWidgetInner {
id: root
expandedWidth: 600
expandedHeight: 800
required property var bar;
implicitHeight: column.implicitHeight + 10
BarWidgetInner {
implicitHeight: column.implicitHeight + 10
ColumnLayout {
id: column
implicitHeight: childrenRect.height
spacing: 5
ColumnLayout {
id: column
implicitHeight: childrenRect.height
spacing: 5
anchors {
fill: parent
margins: 5
}
anchors {
fill: parent
margins: 5
}
Repeater {
model: SystemTray.items;
Repeater {
model: SystemTray.items;
Item {
id: item
required property SystemTrayItem modelData;
Item {
required property var modelData;
readonly property alias menu: menuWatcher.menu;
property bool targetMenuOpen: false;
SystemTrayMenuWatcher {
id: menuWatcher;
trayItem: modelData;
Layout.fillWidth: true
implicitHeight: width
ClickableIcon {
id: mouseArea
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: height
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
image: item.modelData.icon
showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
fillWindowWidth: true
extraVerticalMargin: column.spacing / 2
onClicked: event => {
event.accepted = true;
if (event.button == Qt.LeftButton) {
item.modelData.activate();
} else if (event.button == Qt.MiddleButton) {
item.modelData.secondaryActivate();
}
}
property bool targetMenuOpen: false;
onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen
Layout.fillWidth: true
implicitHeight: width
ClickableIcon {
id: mouseArea
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
onPressed: event => {
if (event.button == Qt.RightButton && item.modelData.hasMenu) {
item.targetMenuOpen = !item.targetMenuOpen;
}
width: height
}
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onWheel: event => {
event.accepted = true;
const points = event.angleDelta.y / 120
item.modelData.scroll(points, false);
}
image: modelData.icon
showPressed: targetMenuOpen
property var tooltip: TooltipItem {
tooltip: root.bar.tooltip
owner: mouseArea
onClicked: event => {
event.accepted = true;
show: mouseArea.containsMouse
if (event.button == Qt.LeftButton) {
modelData.activate();
} else if (event.button == Qt.MiddleButton) {
modelData.secondaryActivate();
} else if (event.button == Qt.RightButton && menu != null) {
targetMenuOpen = !targetMenuOpen;
}
Text {
id: tooltipText
text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
color: "white"
}
}
onWheel: event => {
event.accepted = true;
const points = event.angleDelta.y / 120
modelData.scroll(points, false);
}
property var rightclickMenu: TooltipItem {
id: rightclickMenu
tooltip: root.bar.tooltip
owner: mouseArea
property var tooltip: TooltipItem {
tooltip: bar.tooltip
owner: mouseArea
isMenu: true
show: item.targetMenuOpen
animateSize: !(menuContentLoader?.item?.animating ?? false)
show: mouseArea.containsMouse
onClose: item.targetMenuOpen = false;
Text {
id: tooltipText
text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id
color: "white"
}
}
Loader {
id: menuContentLoader
active: item.targetMenuOpen || rightclickMenu.visible || mouseArea.containsMouse
property var rightclickMenu: TooltipItem {
tooltip: bar.tooltip
owner: mouseArea
isMenu: true
show: targetMenuOpen && menu.showChildren
animateSize: !rightclickItems.animating
onClose: targetMenuOpen = false;
MenuItemList {
id: rightclickItems
items: menu == null ? [] : menu.children
onClose: targetMenuOpen = false;
sourceComponent: MenuView {
menu: item.modelData.menu
onClose: item.targetMenuOpen = false;
}
}
}

View file

@ -0,0 +1,90 @@
import QtQuick
import Quickshell
Scope {
id: root
required property MouseArea target;
property real velocityX: 0;
property real velocityY: 0;
signal flickStarted();
signal flickCompleted();
property real dragStartX: 0;
property real dragStartY: 0;
property real dragDeltaX: 0;
property real dragDeltaY: 0;
property real dragEndX: 0;
property real dragEndY: 0;
property var sampleIdx: -1
property var tSamples: []
property var xSamples: []
property var ySamples: []
ElapsedTimer { id: velocityTimer }
function resetSamples() {
velocityTimer.restart();
sampleIdx = -1;
tSamples = [];
xSamples = [];
ySamples = [];
}
function sample() {
const deltaT = velocityTimer.elapsed();
sampleIdx++;
if (sampleIdx > 5) {
sampleIdx = 0;
}
tSamples[sampleIdx] = deltaT;
xSamples[sampleIdx] = dragDeltaX;
ySamples[sampleIdx] = dragDeltaY;
}
function updateVelocity() {
let firstIdx = sampleIdx + 1;
if (firstIdx > tSamples.length - 1) firstIdx = 0;
const deltaT = tSamples[sampleIdx] - tSamples[firstIdx];
const deltaX = xSamples[sampleIdx] - xSamples[firstIdx];
const deltaY = ySamples[sampleIdx] - ySamples[firstIdx];
root.velocityX = deltaX / deltaT;
root.velocityY = deltaY / deltaT;
}
Connections {
target: root.target;
function onPressed(event: MouseEvent) {
root.resetSamples();
root.dragDeltaX = 0;
root.dragDeltaY = 0;
root.dragStartX = event.x;
root.dragStartY = event.y;
root.flickStarted();
}
function onReleased(event: MouseEvent) {
root.dragDeltaX = event.x - root.dragStartX;
root.dragDeltaY = event.y - root.dragStartY;
root.dragEndX = event.x;
root.dragEndY = event.y;
root.sample();
root.updateVelocity();
root.flickCompleted();
}
function onPositionChanged(event: MouseEvent) {
root.dragDeltaX = event.x - root.dragStartX;
root.dragDeltaY = event.y - root.dragStartY;
root.sample();
}
}
}

View file

@ -0,0 +1,99 @@
pragma ComponentBehavior: Bound;
import QtQuick
Item {
id: root
property list<string> values;
property int index: 0;
implicitWidth: 300
implicitHeight: 40
MouseArea {
id: mouseArea
anchors.fill: parent
property real halfHandle: handle.width / 2;
property real activeWidth: groove.width - handle.width;
property real valueOffset: mouseArea.halfHandle + (root.index / (root.values.length - 1)) * mouseArea.activeWidth;
Repeater {
model: root.values
Item {
id: delegate
required property int index;
required property string modelData;
anchors.top: groove.bottom
anchors.topMargin: 2
x: mouseArea.halfHandle + (delegate.index / (root.values.length - 1)) * mouseArea.activeWidth
Rectangle {
id: mark
color: "#60eeffff"
width: 1
height: groove.height
}
Text {
anchors.top: mark.bottom
x: delegate.index === 0 ? -4
: delegate.index === root.values.length - 1 ? -this.width + 4
: -(this.width / 2);
text: delegate.modelData
color: "#a0eeffff"
}
}
}
Rectangle {
id: grooveFill
anchors {
left: groove.left
top: groove.top
bottom: groove.bottom
}
radius: 5
color: "#80ceffff"
width: mouseArea.valueOffset
}
Rectangle {
id: groove
anchors {
left: parent.left
right: parent.right
}
y: 5
implicitHeight: 7
color: "transparent"
border.color: "#20eeffff"
border.width: 1
radius: 5
}
Rectangle {
id: handle
anchors.verticalCenter: groove.verticalCenter
height: 15
width: height
radius: height * 0.5
x: mouseArea.valueOffset - width * 0.5
}
}
Binding {
when: mouseArea.pressed
root.index: Math.max(0, Math.min(root.values.length - 1, Math.round((mouseArea.mouseX / root.width) * (root.values.length - 1))));
restoreMode: Binding.RestoreBinding
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
Item {
id: root
property real from: 0.0
property real to: 1.0
property real value: 0.0
implicitHeight: 7
implicitWidth: 200
Rectangle {
id: grooveFill
anchors {
left: groove.left
top: groove.top
bottom: groove.bottom
}
radius: 5
color: "#80ceffff"
width: root.width * ((root.value - root.from) / (root.to - root.from))
}
Rectangle {
id: groove
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
height: 7
color: "transparent"
border.color: "#20eeffff"
border.width: 1
radius: 5
}
}

View file

@ -0,0 +1,36 @@
import QtQuick
Item {
id: root
onChildrenChanged: recalc();
Instantiator {
model: root.children
Connections {
required property Item modelData;
target: modelData;
function onImplicitHeightChanged() {
root.recalc();
}
function onImplicitWidthChanged() {
root.recalc();
}
}
}
function recalc() {
let y = 0
let w = 0
for (const child of this.children) {
child.y = y;
y += child.implicitHeight
if (child.implicitWidth > w) w = child.implicitWidth;
}
this.implicitHeight = y;
this.implicitWidth = w;
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Greetd
import ".."
import "lock"
ShellRoot {
GreeterContext {
id: context
onLaunch: {
lock.locked = false;
Greetd.launch(["hyprland"]);
}
}
WlSessionLock {
id: lock
locked: true
WlSessionLockSurface {
id: lockSurface
BackgroundImage {
id: backgroundImage
anchors.fill: parent
screen: lockSurface.screen
asynchronous: true
}
LockContent {
anchors.fill: parent
state: context.state
}
}
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M150.81,131.79a8,8,0,0,1,.35,7.79l-16,32a8,8,0,0,1-14.32-7.16L131.06,144H112a8,8,0,0,1-7.16-11.58l16-32a8,8,0,1,1,14.32,7.16L124.94,128H144A8,8,0,0,1,150.81,131.79ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>

After

(image error) Size: 499 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>

After

(image error) Size: 348 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Zm-28,76H136V112a8,8,0,0,0-16,0v20H100a8,8,0,0,0,0,16h20v20a8,8,0,0,0,16,0V148h20a8,8,0,0,0,0-16Z"></path></svg>

After

(image error) Size: 443 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M120,136V96a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,24a12,12,0,1,0,12,12A12,12,0,0,0,128,160ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>

After

(image error) Size: 424 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216Z"></path></svg>

After

(image error) Size: 348 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M53.92,34.62A8,8,0,1,0,42.08,45.38L58.82,63.8A79.59,79.59,0,0,0,48,104c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.8a40,40,0,0,0,78.4,0h15.44l19.44,21.38a8,8,0,1,0,11.84-10.76ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a63.65,63.65,0,0,1,6.26-27.62L168.09,184Zm166-4.73a8.13,8.13,0,0,1-2.93.55,8,8,0,0,1-7.44-5.08C196.35,156.19,192,129.75,192,104A64,64,0,0,0,96.43,48.31a8,8,0,0,1-7.9-13.91A80,80,0,0,1,208,104c0,35.35,8.05,58.59,10.52,64.88A8,8,0,0,1,214,179.25Z"></path></svg>

After

(image error) Size: 641 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a64,64,0,1,1,128,0c0,36.05,8.28,66.73,16,80Z"></path></svg>

After

(image error) Size: 424 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"></path></svg>

Before

(image error) Size: 445 B

After

(image error) Size: 445 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M72,128a134.63,134.63,0,0,1-14.16,60.47,8,8,0,1,1-14.32-7.12A118.8,118.8,0,0,0,56,128,71.73,71.73,0,0,1,83,71.8,8,8,0,1,1,93,84.29,55.76,55.76,0,0,0,72,128Zm56-8a8,8,0,0,0-8,8,184.12,184.12,0,0,1-23,89.1,8,8,0,0,0,14,7.76A200.19,200.19,0,0,0,136,128,8,8,0,0,0,128,120Zm0-32a40,40,0,0,0-40,40,8,8,0,0,0,16,0,24,24,0,0,1,48,0,214.09,214.09,0,0,1-20.51,92A8,8,0,1,0,146,226.83,230,230,0,0,0,168,128,40,40,0,0,0,128,88Zm0-64A104.11,104.11,0,0,0,24,128a87.76,87.76,0,0,1-5,29.33,8,8,0,0,0,15.09,5.33A103.9,103.9,0,0,0,40,128a88,88,0,0,1,176,0,282.24,282.24,0,0,1-5.29,54.45,8,8,0,0,0,6.3,9.4,8.22,8.22,0,0,0,1.55.15,8,8,0,0,0,7.84-6.45A298.37,298.37,0,0,0,232,128,104.12,104.12,0,0,0,128,24ZM94.4,152.17A8,8,0,0,0,85,158.42a151,151,0,0,1-17.21,45.44,8,8,0,0,0,13.86,8,166.67,166.67,0,0,0,19-50.25A8,8,0,0,0,94.4,152.17ZM128,56a72.85,72.85,0,0,0-9,.56,8,8,0,0,0,2,15.87A56.08,56.08,0,0,1,184,128a252.12,252.12,0,0,1-1.92,31A8,8,0,0,0,189,168a8.39,8.39,0,0,0,1,.06,8,8,0,0,0,7.92-7,266.48,266.48,0,0,0,2-33A72.08,72.08,0,0,0,128,56Zm57.93,128.25a8,8,0,0,0-9.75,5.75c-1.46,5.69-3.15,11.4-5,17a8,8,0,0,0,5,10.13,7.88,7.88,0,0,0,2.55.42,8,8,0,0,0,7.58-5.46c2-5.92,3.79-12,5.35-18.05A8,8,0,0,0,185.94,184.26Z"></path></svg>

After

(image error) Size: 1.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M207.06,72.67A111.24,111.24,0,0,0,128,40h-.4C66.07,40.21,16,91,16,153.13V176a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V152A111.25,111.25,0,0,0,207.06,72.67ZM224,176H119.71l54.76-75.3a8,8,0,0,0-12.94-9.42L99.92,176H32V153.13c0-3.08.15-6.12.43-9.13H56a8,8,0,0,0,0-16H35.27c10.32-38.86,44-68.24,84.73-71.66V80a8,8,0,0,0,16,0V56.33A96.14,96.14,0,0,1,221,128H200a8,8,0,0,0,0,16h23.67c.21,2.65.33,5.31.33,8Z"></path></svg>

After

(image error) Size: 523 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M201.89,54.66A103.43,103.43,0,0,0,128.79,24H128A104,104,0,0,0,24,128v56a24,24,0,0,0,24,24H64a24,24,0,0,0,24-24V144a24,24,0,0,0-24-24H40.36A88.12,88.12,0,0,1,190.54,65.93,87.39,87.39,0,0,1,215.65,120H192a24,24,0,0,0-24,24v40a24,24,0,0,0,24,24h24a24,24,0,0,1-24,24H136a8,8,0,0,0,0,16h56a40,40,0,0,0,40-40V128A103.41,103.41,0,0,0,201.89,54.66ZM64,136a8,8,0,0,1,8,8v40a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V136Zm128,56a8,8,0,0,1-8-8V144a8,8,0,0,1,8-8h24v56Z"></path></svg>

After

(image error) Size: 570 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"></path></svg>

After

(image error) Size: 252 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"></path></svg>

Before

(image error) Size: 353 B

After

(image error) Size: 354 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"></path></svg>

Before

(image error) Size: 386 B

After

(image error) Size: 386 B

View file

@ -2,7 +2,7 @@
<svg
width="32"
height="32"
fill="#ffffff"
fill="#f0f0f0"
viewBox="0 0 256 256"
data-darkreader-inline-fill=""
version="1.1"

Before

(image error) Size: 580 B

After

(image error) Size: 580 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"></path></svg>

Before

(image error) Size: 521 B

After

(image error) Size: 521 B

View file

@ -2,7 +2,7 @@
<svg
width="32"
height="32"
fill="#ffffff"
fill="#f0f0f0"
viewBox="0 0 256 256"
data-darkreader-inline-fill=""
version="1.1"

Before

(image error) Size: 957 B

After

(image error) Size: 957 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Zm-88,40a8,8,0,0,0,8-8V104a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,1,0,7.16,14.31l4.42-2.21V152A8,8,0,0,0,136,160Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Zm-88,40a8,8,0,0,0,8-8V104a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,1,0,7.16,14.31l4.42-2.21V152A8,8,0,0,0,136,160Z"></path></svg>

Before

(image error) Size: 627 B

After

(image error) Size: 627 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"></path></svg>

Before

(image error) Size: 415 B

After

(image error) Size: 415 B

View file

@ -2,7 +2,7 @@
<svg
width="32"
height="32"
fill="#ffffff"
fill="#f0f0f0"
viewBox="0 0 256 256"
data-darkreader-inline-fill=""
version="1.1"

Before

(image error) Size: 847 B

After

(image error) Size: 847 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M237.66,178.34a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,200,208V192a72.15,72.15,0,0,1-57.65-30.14l-41.72-58.4A56.1,56.1,0,0,0,55.06,80H32a8,8,0,0,1,0-16H55.06a72.12,72.12,0,0,1,58.59,30.15l41.72,58.4A56.08,56.08,0,0,0,200,176V160a8,8,0,0,1,13.66-5.66ZM143,107a8,8,0,0,0,11.16-1.86l1.2-1.67A56.08,56.08,0,0,1,200,80V96a8,8,0,0,0,13.66,5.66l24-24a8,8,0,0,0,0-11.32l-24-24A8,8,0,0,0,200,48V64a72.15,72.15,0,0,0-57.65,30.14l-1.2,1.67A8,8,0,0,0,143,107Zm-30,42a8,8,0,0,0-11.16,1.86l-1.2,1.67A56.1,56.1,0,0,1,55.06,176H32a8,8,0,0,0,0,16H55.06a72.12,72.12,0,0,0,58.59-30.15l1.2-1.67A8,8,0,0,0,113,149Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M237.66,178.34a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,200,208V192a72.15,72.15,0,0,1-57.65-30.14l-41.72-58.4A56.1,56.1,0,0,0,55.06,80H32a8,8,0,0,1,0-16H55.06a72.12,72.12,0,0,1,58.59,30.15l41.72,58.4A56.08,56.08,0,0,0,200,176V160a8,8,0,0,1,13.66-5.66ZM143,107a8,8,0,0,0,11.16-1.86l1.2-1.67A56.08,56.08,0,0,1,200,80V96a8,8,0,0,0,13.66,5.66l24-24a8,8,0,0,0,0-11.32l-24-24A8,8,0,0,0,200,48V64a72.15,72.15,0,0,0-57.65,30.14l-1.2,1.67A8,8,0,0,0,143,107Zm-30,42a8,8,0,0,0-11.16,1.86l-1.2,1.67A56.1,56.1,0,0,1,55.06,176H32a8,8,0,0,0,0,16H55.06a72.12,72.12,0,0,0,58.59-30.15l1.2-1.67A8,8,0,0,0,113,149Z"></path></svg>

Before

(image error) Size: 787 B

After

(image error) Size: 787 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,47.88V208.12a16,16,0,0,1-24.43,13.43L64,146.77V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0v69.23L183.57,34.45A15.95,15.95,0,0,1,208,47.88Z"></path></svg>

Before

(image error) Size: 335 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,40V216a8,8,0,0,1-16,0V146.77L72.43,221.55A15.95,15.95,0,0,1,48,208.12V47.88A15.95,15.95,0,0,1,72.43,34.45L192,109.23V40a8,8,0,0,1,16,0Z"></path></svg>

Before

(image error) Size: 339 B

View file

@ -0,0 +1,281 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Services.SystemTray
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 {
width: 450
height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:launcher"
Rectangle {
//anchors.fill: parent
height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10)
Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
width: 450
color: ShellGlobals.colors.bar
radius: 5
border.color: ShellGlobals.colors.barOutline
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
anchors.bottomMargin: 0
spacing: 0
Rectangle {
id: searchContainer
Layout.fillWidth: true
implicitHeight: searchbox.implicitHeight + 10
color: "#30c0ffff"
radius: 3
border.color: "#50ffffff"
RowLayout {
id: searchbox
anchors.fill: parent
anchors.margins: 5
IconImage {
implicitSize: parent.height
source: "root:icons/magnifying-glass.svg"
}
TextInput {
id: search
Layout.fillWidth: true
color: "white"
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: 5
color: "#20e0ffff"
border.color: "#30ffffff"
border.width: 1
}
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: "#f0f0f0"
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
}
}
function init() {}
}

View file

@ -3,8 +3,10 @@ pragma Singleton
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.Pam
import ".."
import "../.."
@ -61,18 +63,15 @@ Singleton {
root.oldWorkspaces = ({});
}
Shortcut {
name: "lock"
onPressed: {
if (root.locked) root.locked = false;
else root.locked = true;
}
IpcHandler {
target: "lockscreen"
function lock(): void { root.locked = true; }
}
LazyLoader {
id: lockContextLoader
LockContext {
SessionLockContext {
onUnlocked: root.locked = false;
}
}
@ -82,7 +81,7 @@ Singleton {
onSecureChanged: {
if (secure) {
Qt.callLater(() => root.workspaceLockAnimation());
root.workspaceLockAnimation();
}
}
@ -108,7 +107,7 @@ Singleton {
LockContent {
id: lockContent
context: lockContextLoader.item;
state: lockContextLoader.item.state;
visible: false
width: lockSurface.width
@ -128,7 +127,6 @@ Singleton {
onVisibleChanged: {
if (visible) {
lockContent.y = -lockSurface.height
console.log(`y ${lockContent.y}`)
lockContent.visible = true;
lockAnim.running = true;
}

View file

@ -0,0 +1,37 @@
import QtQuick
import Quickshell
import Quickshell.Services.Greetd
Scope {
id: root
signal launch();
property LockState state: LockState {
onTryPasswordUnlock: {
this.isUnlocking = true;
Greetd.createSession("admin");
}
}
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.error = "Invalid password";
root.state.failed = true;
root.state.isUnlocking = false;
}
function onReadyToLaunch() {
root.state.isUnlocking = false;
root.launch();
}
}
}

View file

@ -44,6 +44,8 @@ Item {
anchors.margins: 15
source: root.icon
sourceSize.width: width
sourceSize.height: height
}
}
}

View file

@ -1,113 +1,202 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import ".."
Item {
id: root
required property LockContext context;
required property LockState state;
property real focusAnim: focusAnimInternal * 0.001
property int focusAnimInternal: Window.active ? 1000 : 0
Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } }
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height / 2 + textBox.height
id: sep
MouseArea {
anchors.fill: parent
hoverEnabled: true
implicitHeight: 6
implicitWidth: 800
radius: height / 2
color: ShellGlobals.colors.widget
}
property real startMoveX: 0
property real startMoveY: 0
ColumnLayout {
implicitWidth: sep.implicitWidth
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: sep.top
spacing: 0
Text {
id: timeText
Layout.alignment: Qt.AlignHCenter
font {
pointSize: 120
hintingPreference: Font.PreferFullHinting
family: "Noto Sans"
}
color: "white"
renderType: Text.NativeRendering
text: {
const hours = ShellGlobals.time.getHours().toString().padStart(2, '0');
const minutes = ShellGlobals.time.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
// prevents wakeups from bumping the mouse
onPositionChanged: event => {
if (root.state.fadedOut) {
if (root.state.mouseMoved()) {
const xOffset = Math.abs(event.x - startMoveX);
const yOffset = Math.abs(event.y - startMoveY);
const distanceSq = (xOffset * xOffset) + (yOffset * yOffset);
if (distanceSq > (100 * 100)) root.state.fadeIn();
} else {
startMoveX = event.x;
startMoveY = event.y;
}
}
}
Item {
Layout.alignment: Qt.AlignHCenter
implicitHeight: childrenRect.height * focusAnim
implicitWidth: sep.implicitWidth
clip: true
id: content
width: parent.width
height: parent.height
y: root.state.fadeOutMul * (height / 2 + childrenRect.height)
TextInput {
id: textBox
focus: true
width: parent.width
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height / 2 + textBox.height
id: sep
color: enabled ?
root.context.failed ? "#ffa0a0" : "white"
: "#80ffffff";
implicitHeight: 6
implicitWidth: 800
radius: height / 2
color: ShellGlobals.colors.widget
}
font.pointSize: 24
horizontalAlignment: TextInput.AlignHCenter
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
ColumnLayout {
implicitWidth: sep.implicitWidth
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: sep.top
spacing: 0
onTextChanged: root.context.currentText = text;
SystemClock {
id: clock
precision: SystemClock.Minutes
}
Window.onActiveChanged: {
if (Window.active) {
text = root.context.currentText;
Text {
id: timeText
Layout.alignment: Qt.AlignHCenter
font {
pointSize: 120
hintingPreference: Font.PreferFullHinting
family: "Noto Sans"
}
color: "white"
renderType: Text.NativeRendering
text: {
const hours = clock.hours.toString().padStart(2, '0');
const minutes = clock.minutes.toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
}
onAccepted: {
if (text != "") root.context.tryUnlock();
}
Item {
Layout.alignment: Qt.AlignHCenter
implicitHeight: textBox.height * focusAnim
implicitWidth: sep.implicitWidth
clip: true
enabled: !root.context.isUnlocking;
TextInput {
id: textBox
focus: true
width: parent.width
color: enabled ?
root.state.failed ? "#ffa0a0" : "white"
: "#80ffffff";
font.pointSize: 24
horizontalAlignment: TextInput.AlignHCenter
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
cursorVisible: text != ""
onCursorVisibleChanged: cursorVisible = text != ""
onTextChanged: {
root.state.currentText = text;
cursorVisible = text != ""
}
Window.onActiveChanged: {
if (Window.active) {
text = root.state.currentText;
}
}
Connections {
target: root.state
function onCurrentTextChanged() {
textBox.text = root.state.currentText;
}
}
onAccepted: {
if (text != "") root.state.tryPasswordUnlock();
}
enabled: !root.state.isUnlocking;
}
Text {
anchors.fill: textBox
font: textBox.font
color: root.state.failed ? "#ffa0a0" : "#80ffffff";
horizontalAlignment: TextInput.AlignHCenter
visible: !textBox.cursorVisible
text: root.state.failed ? root.state.error
: root.state.fprintAvailable ? "Touch sensor or enter password" : "Enter password";
}
Rectangle {
Layout.fillHeight: true
implicitWidth: height
color: "transparent"
visible: root.state.fprintAvailable
anchors {
right: textBox.right
top: textBox.top
bottom: textBox.bottom
}
Image {
anchors.fill: parent
anchors.margins: 5
source: "root:icons/fingerprint.svg"
sourceSize.width: width
sourceSize.height: height
}
}
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: sep.bottom
implicitHeight: (75 + 30) * focusAnim
implicitWidth: sep.implicitWidth
clip: true
RowLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.topMargin: 50
spacing: 0
LockButton {
icon: "root:icons/monitor.svg"
onClicked: root.state.fadeOut();
}
LockButton {
icon: "root:icons/pause.svg"
show: root.state.mediaPlaying;
onClicked: root.state.pauseMedia();
}
}
}
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: sep.bottom
implicitHeight: (childrenRect.height + 30) * focusAnim
implicitWidth: sep.implicitWidth
clip: true
RowLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.topMargin: 50
spacing: 0
LockButton {
icon: "root:icons/monitor.svg"
onClicked: root.context.dpms();
}
LockButton {
icon: "root:icons/pause.svg"
show: context.mediaPlaying;
onClicked: root.context.pauseMedia();
}
}
Rectangle {
id: darkenOverlay
anchors.fill: parent
color: "black"
opacity: root.state.fadeOutMul
visible: opacity != 0
}
}

View file

@ -1,57 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import Quickshell.Services.Mpris
Scope {
id: root
signal unlocked();
property string currentText: "";
readonly property alias isUnlocking: pamtester.running;
property bool failed: false;
onCurrentTextChanged: failed = false;
readonly property bool mediaPlaying: Mpris.players.values.some(player => {
return player.playbackState === MprisPlaybackState.Playing && player.canPause;
});
function pauseMedia() {
Mpris.players.values.forEach(player => {
if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
player.playbackState = MprisPlaybackState.Paused;
}
});
}
function dpms() {
Hyprland.dispatch(`dpms`);
}
Process {
id: pamtester
property bool failed: true
command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
onStarted: this.write(`${currentText}\n`)
stdout: SplitParser {
// fails go to stderr
onRead: pamtester.failed = false
}
onExited: {
if (failed) {
root.failed = true;
} else {
root.unlocked();
}
}
}
function tryUnlock() {
pamtester.running = true;
}
}

View file

@ -0,0 +1,70 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Mpris
Scope {
signal tryPasswordUnlock();
property string currentText: "";
property string error: "";
property bool isUnlocking: false;
property bool failed: false;
property bool fprintAvailable: false;
property bool fadedOut: false
property real fadeOutMul: 0
NumberAnimation on fadeOutMul {
id: fadeAnim
duration: 600
easing.type: Easing.BezierSpline
easing.bezierCurve: [0.0, 0.75, 0.15, 1.0, 1.0, 1.0]
onStopped: {
if (fadedOut) Hyprland.dispatch("dpms off");
}
}
onCurrentTextChanged: {
failed = false;
error = "";
if (fadedOut) {
fadeIn();
}
}
function fadeOut() {
if (fadedOut) return;
fadedOut = true;
fadeAnim.to = 1;
fadeAnim.restart();
}
function fadeIn() {
if (!fadedOut) return;
Hyprland.dispatch("dpms on");
fadedOut = false;
fadeAnim.to = 0;
fadeAnim.restart();
}
ElapsedTimer { id: mouseTimer }
// returns if mouse move should be continued, false should restart
function mouseMoved(): bool {
return mouseTimer.restart() < 0.2;
}
readonly property bool mediaPlaying: Mpris.players.values.some(player => {
return player.playbackState === MprisPlaybackState.Playing && player.canPause;
});
function pauseMedia() {
Mpris.players.values.forEach(player => {
if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
player.playbackState = MprisPlaybackState.Paused;
}
});
}
}

View file

@ -0,0 +1,44 @@
import Quickshell
import Quickshell.Services.Pam
Scope {
id: root
signal unlocked();
property LockState state: LockState {
onTryPasswordUnlock: {
root.state.isUnlocking = true;
pam.start();
}
}
PamContext {
id: pam
configDirectory: "pam"
config: "password.conf"
onPamMessage: {
if (this.responseRequired) {
this.respond(root.state.currentText);
} else if (this.messageIsError) {
root.state.currentText = "";
root.state.failed = true;
root.state.error = this.message;
} // else ignore
}
onCompleted: status => {
const success = status == PamResult.Success;
if (!success) {
root.state.currentText = "";
root.state.error = "Invalid password";
}
root.state.failed = !success;
root.state.isUnlocking = false;
if (success) root.unlocked();
}
}
}

View file

@ -0,0 +1 @@
auth required /run/current-system/sw/lib/security/pam_fprintd.so

View file

@ -0,0 +1 @@
auth required pam_unix.so

View file

@ -0,0 +1,39 @@
import QtQuick
Canvas {
id: root
property real ringFill: 1.0
onRingFillChanged: requestPaint();
renderStrategy: Canvas.Cooperative
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.lineWidth = 2;
ctx.strokeStyle = "#70ffffff";
ctx.beginPath();
const half = Math.round(root.width / 2);
const start = -Math.PI * 0.5;
const endM = ringFill == 0.0 || ringFill == 1.0 ? ringFill : 1.0 - ringFill
ctx.arc(half, half, half - ctx.lineWidth, start, start + 2 * Math.PI * endM, true);
ctx.stroke();
const xMin = Math.min(root.width * 0.3);
const xMax = Math.max(root.width * 0.7);
ctx.strokeStyle = "white";
ctx.beginPath();
ctx.moveTo(xMin, xMin);
ctx.lineTo(xMax, xMax);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(xMax, xMin);
ctx.lineTo(xMin, xMax);
ctx.stroke();
}
}

View file

@ -0,0 +1,35 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
TrackedNotification {
id: root
required property Notification notif;
renderComponent: StandardNotificationRenderer {
notif: root.notif
backer: root
}
function handleDiscard() {
if (!lock.retained) notif.dismiss();
root.discarded();
}
function handleDismiss() {
//handleDiscard();
}
RetainableLock {
id: lock
object: root.notif
locked: true
onRetainedChanged: {
if (retained) root.discard();
}
}
expireTimeout: notif.expireTimeout
}

View file

@ -0,0 +1,243 @@
import QtQuick
import Quickshell
import "../components"
Item {
id: root
enum FlingState {
Inert,
Returning,
Flinging,
Dismissing
}
implicitWidth: display.implicitWidth
// note: can be 0, use ZHVStack
implicitHeight: (display.implicitHeight + padding * 2) * display.meshFactor
z: 1.0 - display.meshFactor
property var view;
property Item contentItem;
property real padding: 5;
property real edgeXOffset;
property bool canOverlap: display.rotation > 2 || Math.abs(display.displayY) > 10 || display.displayX < -60
property bool canDismiss: display.state != FlickableNotification.Dismissing && display.state != FlickableNotification.Flinging;
property alias displayContainer: displayContainer;
signal leftViewBounds();
signal dismissed();
signal discarded();
signal startedFlick();
function playEntry(delay: real) {
if (display.state != FlickableNotification.Flinging) {
display.displayX = -display.width + edgeXOffset
root.playReturn(delay);
}
}
function playDismiss(delay: real) {
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
display.state = FlickableNotification.Dismissing;
display.animationDelay = delay;
}
}
function playDiscard(delay: real) {
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
display.velocityX = 500;
display.velocityY = 1500;
display.state = FlickableNotification.Flinging;
display.animationDelay = delay;
}
}
function playReturn(delay: real) {
if (display.state != FlickableNotification.Flinging) {
display.state = FlickableNotification.Returning;
display.animationDelay = delay;
}
}
MouseArea {
id: mouseArea
width: display.width
height: display.height
enabled: display.state == FlickableNotification.Inert || display.state == FlickableNotification.Returning
FlickMonitor {
id: flickMonitor
target: mouseArea
onDragDeltaXChanged: {
const delta = dragDeltaX;
display.displayX = delta < 0 ? delta : Math.pow(delta, 0.8);
display.updateMeshFactor(true);
updateDragY();
}
onDragDeltaYChanged: {
updateDragY();
display.state = FlickableNotification.Inert;
}
function updateDragY() {
//const xMul = 1//dragDeltaX < 0 ? 0 : Math.min(1, Math.pow(dragDeltaX / 200, 0.8));
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
const xMul = d
const targetY = dragDeltaY;
display.displayY = root.padding + targetY * xMul;
}
onFlickStarted: {
display.initialAnimComplete = true;
root.startedFlick();
}
onFlickCompleted: {
display.releaseY = dragEndY;
if (velocityX > 1000 || (velocityX > -100 && display.displayX > display.width * 0.4)) {
display.velocityX = Math.max(velocityX * 0.8, 1000);
display.velocityY = velocityY * 0.6;
display.state = FlickableNotification.Flinging;
root.discarded();
} else if (velocityX < -1500 || (velocityX < 100 && display.displayX < -(display.width * 0.4))) {
display.velocityX = Math.min(velocityX * 0.8, -700)
display.velocityY = 0
display.state = FlickableNotification.Dismissing;
root.dismissed();
} else {
display.velocityX = 0;
display.velocityY = 0;
display.state = FlickableNotification.Returning;
}
}
}
Item {
id: displayContainer
layer.enabled: view && view.topNotification == root
opacity: layer.enabled ? 0 : 1 // shader ignores it
width: Math.ceil(display.width + display.xPadding * 2)
height: Math.ceil(display.height + display.yPadding * 2)
x: Math.floor(display.targetContainmentX)
y: Math.floor(display.targetContainmentY)
Item {
id: display
//anchors.centerIn: parent
x: xPadding + (targetContainmentX - displayContainer.x)
y: yPadding + (targetContainmentY - displayContainer.y)
//visible: meshFactor > 0.95
children: [root.contentItem]
implicitWidth: root.contentItem?.width ?? 0
implicitHeight: root.contentItem?.height ?? 0
property var state: FlickableNotification.Inert;
property real meshFactor: 1;
property real velocityX;
property real velocityY;
property real releaseY;
property real animationDelay;
property bool initialAnimComplete;
property real displayX;
property real displayY;
property real tiltSize: Math.max(width, height) * 1.2;
property real xPadding: (tiltSize - width) / 2;
property real yPadding: (tiltSize - height) / 2;
property real targetContainmentX: display.displayX - display.xPadding
property real targetContainmentY: root.padding + display.displayY - display.yPadding
function updateMeshFactor(canRemesh: bool) {
let meshFactor = (display.implicitWidth - Math.abs(display.displayX)) / display.implicitWidth;
meshFactor = 0.8 + (meshFactor * 0.2);
meshFactor = Math.max(0, meshFactor);
if (canRemesh) this.meshFactor = meshFactor;
else this.meshFactor = Math.min(this.meshFactor, meshFactor);
}
function unmesh(delta: real) {
if (meshFactor > 0) {
this.meshFactor = Math.max(0, this.meshFactor - delta * 5);
}
}
rotation: display.displayX < 0 ? 0 : display.displayX * (initialAnimComplete ? 0.1 : 0.02)
property real lastX;
FrameAnimation {
function dampingVelocity(currentVelocity, delta) {
const spring = 1.0;
const damping = 0.1;
const springForce = spring * delta;
const dampingForce = -damping * currentVelocity;
return currentVelocity + (springForce + dampingForce);
}
running: display.state != FlickableNotification.Inert
onTriggered: {
let frameTime = this.frameTime;
if (display.animationDelay != 0) {
const usedDelay = Math.min(display.animationDelay, frameTime);
frameTime -= usedDelay;
display.animationDelay -= usedDelay;
if (frameTime == 0) return;
}
if (display.state == FlickableNotification.Flinging) {
display.velocityY += frameTime * 100000 * (1 / display.velocityX * 100);
//display.velocityX -= display.velocityX * 0.98 * frameTime
display.unmesh(frameTime);
} else if (display.state == FlickableNotification.Dismissing) {
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
display.displayY = root.padding + display.releaseY * d;
display.velocityY = 0;
display.velocityX += frameTime * -20000;
if (display.displayX + display.width > 0) display.updateMeshFactor(false);
else display.unmesh(frameTime);
} else {
const deltaX = 0 - display.displayX;
const deltaY = root.padding - display.displayY;
display.velocityX = dampingVelocity(display.velocityX, deltaX);
display.velocityY = dampingVelocity(display.velocityY, deltaY);
if (Math.abs(display.velocityX) < 0.01 && Math.abs(deltaX) < 1
&& Math.abs(display.velocityY) < 0.01 && Math.abs(deltaY) < 1) {
display.state = FlickableNotification.Inert;
display.displayX = 0;
display.displayY = root.padding;
display.velocityX = 0;
display.velocityY = 0;
display.initialAnimComplete = true;
}
display.updateMeshFactor(true);
}
display.displayX += display.velocityX * frameTime;
display.displayY += display.velocityY * frameTime;
// todo: actually base this on the viewport
if (display.displayX > 10000 || display.displayY > 10000 || (display.displayX + display.width < root.edgeXOffset && display.meshFactor == 0) || display.displayY < -10000) root.leftViewBounds();
}
}
}
}
}
}

View file

@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import "../components"
import "../shaders" as Shaders
Item {
id: root
property list<Item> notifications: [];
property list<Item> heightStack: [];
property alias stack: stack;
property alias topNotification: stack.topNotification;
function addNotificationInert(notification: TrackedNotification): Item {
const harness = stack._harnessComponent.createObject(stack, {
backer: notification,
view: root,
});
harness.contentItem = notification.renderComponent.createObject(harness);
notifications = [...notifications, harness];
heightStack = [harness, ...heightStack];
return harness;
}
function addNotification(notification: TrackedNotification) {
const harness = root.addNotificationInert(notification);
harness.playEntry(0);
}
function dismissAll() {
let delay = 0;
for (const notification of root.notifications) {
if (!notification.canDismiss) continue;
notification.playDismiss(delay);
notification.dismissed();
delay += 0.025;
}
}
function discardAll() {
let delay = 0;
for (const notification of root.notifications) {
if (!notification.canDismiss) continue;
notification.playDismiss(delay);
notification.discarded();
delay += 0.025;
}
}
function addSet(notifications: list<TrackedNotification>) {
let delay = 0;
for (const notification of notifications) {
if (notification.visualizer) {
notification.visualizer.playReturn(delay);
} else {
const harness = root.addNotificationInert(notification);
harness.playEntry(delay);
}
delay += 0.025;
}
}
Item {
anchors.fill: parent
layer.enabled: stack.topNotification != null
layer.effect: Shaders.MaskedOverlay {
overlayItem: stack.topNotification?.displayContainer ?? null
overlayPos: Qt.point(stack.x + stack.topNotification.x + overlayItem.x, stack.y + stack.topNotification.y + overlayItem.y)
}
ZHVStack {
id: stack
property Item topNotification: {
if (root.heightStack.length < 2) return null;
const top = root.heightStack[0] ?? null;
return top && top.canOverlap ? top : null;
};
property Component _harnessComponent: FlickableNotification {
id: notification
required property TrackedNotification backer;
edgeXOffset: -stack.x
onDismissed: backer.handleDismiss();
onDiscarded: backer.handleDiscard();
onLeftViewBounds: {
root.notifications = root.notifications.filter(n => n != this);
root.heightStack = root.heightStack.filter(n => n != this);
this.destroy();
}
onStartedFlick: {
root.heightStack = [this, ...root.heightStack.filter(n => n != this)];
}
Component.onCompleted: backer.visualizer = this;
Connections {
target: backer
function onDismiss() {
notification.playDismiss(0);
notification.dismissed();
}
function onDiscard() {
notification.playDismiss(0);
notification.discarded();
}
}
}
}
}
}

View file

@ -0,0 +1,74 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
Singleton {
id: root
property list<TrackedNotification> notifications;
property Component notifComponent: DaemonNotification {}
property bool showTrayNotifs: false;
property bool dnd: false;
property bool hasNotifs: root.notifications.length != 0
property var lastHoveredNotif;
property var overlay;
signal notif(notif: TrackedNotification);
signal showAll(notifications: list<TrackedNotification>);
signal dismissAll(notifications: list<TrackedNotification>);
signal discardAll(notifications: list<TrackedNotification>);
NotificationServer {
imageSupported: true
actionsSupported: true
actionIconsSupported: true
onNotification: notification => {
notification.tracked = true;
const notif = root.notifComponent.createObject(null, { notif: notification });
root.notifications = [...root.notifications, notif];
root.notif(notif);
}
}
Instantiator {
model: root.notifications
Connections {
required property TrackedNotification modelData;
target: modelData;
function onDiscarded() {
root.notifications = root.notifications.filter(n => n != target);
modelData.untrack();
}
function onDiscard() {
if (!modelData.visualizer) modelData.discarded();
}
}
}
onShowTrayNotifsChanged: {
if (showTrayNotifs) {
for (const notif of root.notifications) {
notif.inTray = true;
}
root.showAll(root.notifications);
} else {
root.dismissAll(root.notifications);
}
}
function sendDiscardAll() {
root.discardAll(root.notifications);
}
}

View file

@ -0,0 +1,39 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
PanelWindow {
WlrLayershell.namespace: "shell:notifications"
exclusionMode: ExclusionMode.Ignore
color: "transparent"
anchors {
left: true
top: true
bottom: true
right: true
}
property Component notifComponent: DaemonNotification {}
NotificationDisplay {
id: display
anchors.fill: parent
stack.y: 5 + 55//(NotificationManager.showTrayNotifs ? 55 : 0)
stack.x: 72
}
visible: display.stack.children.length != 0
mask: Region { item: display.stack }
Component.onCompleted: {
NotificationManager.overlay = this;
NotificationManager.notif.connect(display.addNotification);
NotificationManager.showAll.connect(display.addSet);
NotificationManager.dismissAll.connect(display.dismissAll);
NotificationManager.discardAll.connect(display.discardAll);
}
}

View file

@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Controls
import "root:bar"
BarWidgetInner {
id: root
required property var bar;
property bool controlsOpen: false;
onControlsOpenChanged: NotificationManager.showTrayNotifs = controlsOpen;
Connections {
target: NotificationManager
function onHasNotifsChanged() {
if (!NotificationManager.hasNotifs) {
root.controlsOpen = false;
}
}
}
implicitHeight: width
BarButton {
id: button
anchors.fill: parent
baseMargin: 8
fillWindowWidth: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
showPressed: root.controlsOpen || (pressedButtons & ~Qt.RightButton)
Image {
anchors.fill: parent
source: NotificationManager.hasNotifs
? "root:icons/bell-fill.svg"
: "root:icons/bell.svg"
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
onPressed: event => {
if (event.button == Qt.RightButton && NotificationManager.hasNotifs) {
root.controlsOpen = !root.controlsOpen;
}
}
}
property var tooltip: TooltipItem {
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Label {
anchors.verticalCenter: parent.verticalCenter
text: {
const count = NotificationManager.notifications.length;
return count == 0 ? "No notifications"
: count == 1 ? "1 notification"
: `${count} notifications`;
}
}
}
property var rightclickMenu: TooltipItem {
tooltip: bar.tooltip
owner: root
isMenu: true
grabWindows: [NotificationManager.overlay]
show: root.controlsOpen
onClose: root.controlsOpen = false
Item {
implicitWidth: 440
implicitHeight: root.implicitHeight - 10
MouseArea {
id: closeArea
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
implicitWidth: 30
implicitHeight: 30
hoverEnabled: true
onPressed: {
NotificationManager.sendDiscardAll()
}
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: width * 0.5
antialiasing: true
color: "#60ffffff"
opacity: closeArea.containsMouse ? 1 : 0
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
}
CloseButton {
anchors.fill: parent
ringFill: root.backer.timePercentage
}
}
}
}
}

View file

@ -0,0 +1,203 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Notifications
import ".."
Rectangle {
id: root
required property Notification notif;
required property var backer;
color: notif.urgency == NotificationUrgency.Critical ? "#30ff2030" : "#30c0ffff"
radius: 5
implicitWidth: 450
implicitHeight: c.implicitHeight
HoverHandler {
onHoveredChanged: {
backer.pauseCounter += hovered ? 1 : -1;
}
}
Rectangle {
id: border
anchors.fill: parent
color: "transparent"
border.width: 2
border.color: ShellGlobals.colors.widgetOutline
radius: root.radius
}
ColumnLayout {
id: c
anchors.fill: parent
spacing: 0
ColumnLayout {
Layout.margins: 10
RowLayout {
Image {
visible: source != ""
source: notif.appIcon ? Quickshell.iconPath(notif.appIcon) : ""
fillMode: Image.PreserveAspectFit
antialiasing: true
sourceSize.width: 30
sourceSize.height: 30
Layout.preferredWidth: 30
Layout.preferredHeight: 30
}
Label {
visible: text != ""
text: notif.summary
font.pointSize: 20
elide: Text.ElideRight
Layout.maximumWidth: root.implicitWidth - 100 // QTBUG-127649
}
Item { Layout.fillWidth: true }
MouseArea {
id: closeArea
Layout.preferredWidth: 30
Layout.preferredHeight: 30
hoverEnabled: true
onPressed: root.backer.discard();
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: width * 0.5
antialiasing: true
color: "#60ffffff"
opacity: closeArea.containsMouse ? 1 : 0
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
}
CloseButton {
anchors.fill: parent
ringFill: root.backer.timePercentage
}
}
}
Item {
Layout.topMargin: 3
visible: bodyLabel.text != "" || notifImage.visible
implicitWidth: bodyLabel.width
implicitHeight: Math.max(notifImage.size, bodyLabel.implicitHeight)
Image {
id: notifImage
readonly property int size: visible ? 14 * 8 : 0
y: bodyLabel.y + bodyLabel.topPadding
visible: source != ""
source: notif.image
fillMode: Image.PreserveAspectFit
cache: false
antialiasing: true
width: size
height: size
sourceSize.width: size
sourceSize.height: size
}
Label {
id: bodyLabel
width: root.implicitWidth - 20
text: notif.body
wrapMode: Text.Wrap
onLineLaidOut: line => {
if (!notifImage.visible) return;
const isize = notifImage.size + 6;
if (line.y + line.height <= notifImage.y + isize) {
line.x += isize;
line.width -= isize;
}
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: root.border.width
spacing: 0
visible: notif.actions.length != 0
Rectangle {
height: border.border.width
Layout.fillWidth: true
color: border.border.color
antialiasing: true
}
RowLayout {
spacing: 0
Repeater {
model: notif.actions
Item {
required property NotificationAction modelData;
required property int index;
Layout.fillWidth: true
implicitHeight: 35
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: -implicitWidth * 0.5
}
visible: index != 0
implicitWidth: root.border.width
color: ShellGlobals.colors.widgetOutline
antialiasing: true
}
MouseArea {
id: actionArea
anchors.fill: parent
onClicked: {
modelData.invoke();
}
Rectangle {
anchors.fill: parent
color: actionArea.pressed && actionArea.containsMouse ? "#20000000" : "transparent"
}
RowLayout {
anchors.centerIn: parent
Image {
visible: notif.hasActionIcons
source: Quickshell.iconPath(modelData.identifier)
fillMode: Image.PreserveAspectFit
antialiasing: true
sourceSize.height: 25
sourceSize.width: 25
}
Label { text: modelData.text }
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,5 @@
import QtQuick
TrackedNotification {
renderComponent: StandardNotificationRenderer {}
}

View file

@ -0,0 +1,65 @@
import QtQuick
import Quickshell
Scope {
id: root
required property Component renderComponent;
property bool inTray: false;
property bool destroyOnInvisible: false;
property int visualizerCount: 0;
property FlickableNotification visualizer;
signal dismiss();
signal discard();
signal discarded();
function handleDismiss() {}
function handleDiscard() {}
onVisualizerChanged: {
if (!visualizer) {
expireAnim.stop();
timePercentage = 1;
}
if (!visualizer && destroyOnInvisible) this.destroy();
}
function untrack() {
destroyOnInvisible = true;
if (!visualizer) this.destroy();
}
property int expireTimeout: -1
property real timePercentage: 1
property int pauseCounter: 0
readonly property bool shouldPause: root.pauseCounter != 0 || (NotificationManager.lastHoveredNotif?.pauseCounter ?? 0) != 0
onPauseCounterChanged: {
if (pauseCounter > 0) {
NotificationManager.lastHoveredNotif = this;
}
}
NumberAnimation on timePercentage {
id: expireAnim
running: expireTimeout != 0
paused: running && root.shouldPause && to == 0
duration: expireTimeout == -1 ? 10000 : expireTimeout
to: 0
onFinished: {
if (!inTray) root.dismiss();
}
}
onInTrayChanged: {
if (inTray) {
expireAnim.stop();
expireAnim.duration = 300 * (1 - timePercentage);
expireAnim.to = 1;
expireAnim.start();
}
}
}

View file

@ -0,0 +1,95 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../components"
ShellRoot {
Component {
id: demoNotif
FlickableNotification {
contentItem: Rectangle {
color: "white"
border.color: "blue"
border.width: 2
radius: 10
width: 400
height: 150
}
onLeftViewBounds: this.destroy()
}
}
property Component testComponent: TrackedNotification {
id: notification
renderComponent: Rectangle {
color: "white"
border.color: "blue"
border.width: 2
radius: 10
width: 400
height: 150
ColumnLayout {
Button {
text: "dismiss"
onClicked: notification.dismiss();
}
Button {
text: "discard"
onClicked: notification.discard();
}
}
}
function handleDismiss() {
console.log(`dismiss (sub)`)
}
function handleDiscard() {
console.log(`discard (sub)`)
}
Component.onDestruction: console.log(`destroy (sub)`)
};
property Component realComponent: DaemonNotification {
id: dn
}
Daemon {
onNotification: notification => {
notification.tracked = true;
const o = realComponent.createObject(null, { notif: notification });
display.addNotification(o);
}
}
FloatingWindow {
color: "transparent"
ColumnLayout {
x: 5
Button {
visible: false
text: "add notif"
onClicked: {
//const notif = demoNotif.createObject(stack);
//stack.children = [...stack.children, notif];
const notif = testComponent.createObject(null);
display.addNotification(notif);
}
}
//ZHVStack { id: stack }
NotificationDisplay { id: display }
}
}
}

View file

@ -1,4 +1,5 @@
// very bad code DO NOT COPY
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
@ -25,7 +26,7 @@ Scope {
Process {
id: grimProc
command: ["grim", "-l", "0", path]
command: ["grim", "-l", "0", root.path]
onExited: code => {
if (code == 0) {
root.visible = true
@ -40,12 +41,11 @@ Scope {
id: magickProc
command: [
"magick",
path,
"-crop",
`${selection.w}x${selection.h}+${selection.x}+${selection.y}`,
"-quality",
"70",
path,
root.path,
"-crop", `${selection.w}x${selection.h}+${selection.x}+${selection.y}`,
"-quality", "70",
"-page", "0x0+0+0", // removes page size and shot position
root.path,
]
onExited: wlCopy.running = true;
@ -53,14 +53,14 @@ Scope {
Process {
id: wlCopy
command: ["sh", "-c", `wl-copy < '${path}'`]
command: ["sh", "-c", `wl-copy < '${root.path}'`]
onExited: shootingComplete = true;
onExited: root.shootingComplete = true;
}
Process {
id: cleanupProc
command: ["rm", path]
command: ["rm", root.path]
}
QtObject {

View file

@ -1,13 +0,0 @@
import Quickshell
import Quickshell.Wayland
PanelWindow {
visible: false
anchors {
left: true
right: true
top: true
bottom: true
}
}

View file

@ -0,0 +1,25 @@
import QtQuick
ShaderEffect {
property Item overlayItem;
property point overlayPos: Qt.point(overlayItem.x, overlayItem.y);
fragmentShader: Qt.resolvedUrl("masked_overlay.frag.qsb")
property point pOverlayPos: Qt.point(
overlayPos.x / width,
overlayPos.y / height
);
property point pOverlaySize: Qt.point(
overlayItem.width / width,
overlayItem.height / height
);
property point pMergeInset: Qt.point(
3 / width,
3 / height
);
property real pMergeCutoff: 0.15
}

View file

@ -0,0 +1,40 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 1) out vec4 fragColor;
layout(binding = 1) uniform sampler2D source;
layout(binding = 2) uniform sampler2D overlayItem;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
vec2 pOverlayPos;
vec2 pOverlaySize;
vec2 pMergeInset;
float pMergeCutoff;
};
void main() {
vec2 overlayCoord = (qt_TexCoord0 - pOverlayPos) / pOverlaySize;
if (overlayCoord.x >= 0 && overlayCoord.y >= 0 && overlayCoord.x < 1 && overlayCoord.y < 1) {
fragColor = texture(overlayItem, overlayCoord);
if (fragColor.a != 0) {
vec4 baseColor = texture(source, qt_TexCoord0);
// imperfect but visually good enough for now. if more is needed we'll probably need a mask tex
if (baseColor.a != 0
&& fragColor.a < pMergeCutoff
&& (texture(overlayItem, overlayCoord + vec2(0, pMergeInset.y)).a == 0
|| texture(overlayItem, overlayCoord + vec2(pMergeInset.x, 0)).a == 0
|| texture(overlayItem, overlayCoord + vec2(0, -pMergeInset.y)).a == 0
|| texture(overlayItem, overlayCoord + vec2(-pMergeInset.x, 0)).a == 0)) {
fragColor += baseColor * (1 - fragColor.a);
}
fragColor *= qt_Opacity;
return;
}
}
fragColor = texture(source, qt_TexCoord0) * qt_Opacity;
}

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