Build the Panama Hyprland desktop
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
// One dock icon: the app, its running-window dots, hover scale and click
|
||||
// behaviour. Reproduces Dash-to-Dock's item: 48px icon, up to four dots
|
||||
// underneath, grow-on-hover, click to focus / cycle / launch.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// { entry: DesktopEntry|null, windows: [HyprlandToplevel], appId: string }
|
||||
// Built by DockBody so this file stays presentational.
|
||||
required property var app
|
||||
|
||||
readonly property DesktopEntry entry: app && app.entry ? app.entry : null
|
||||
readonly property var windows: app && app.windows ? app.windows : []
|
||||
readonly property string label: entry ? entry.name : (app && app.appId ? app.appId : "")
|
||||
readonly property bool running: windows.length > 0
|
||||
readonly property bool hovered: mouse.containsMouse
|
||||
|
||||
// Emitted so DockBody can drive the single shared tooltip.
|
||||
signal entered
|
||||
signal exited
|
||||
|
||||
// The icon may grow past the cell on hover; the cell itself stays a fixed
|
||||
// size so the row doesn't reflow.
|
||||
implicitWidth: Theme.dockIconSize
|
||||
implicitHeight: Theme.dockIconSize + dots.height + 4
|
||||
|
||||
// Desktop entries usually carry a freedesktop icon *name*, but some ship an
|
||||
// absolute path. iconPath() only understands names, so branch on it. The
|
||||
// `true` argument makes a missing icon return "" instead of a placeholder
|
||||
// that renders as a black square in some themes.
|
||||
readonly property string iconSource: {
|
||||
const name = entry && entry.icon ? entry.icon : (app && app.appId ? app.appId : "");
|
||||
if (!name)
|
||||
return "";
|
||||
if (name.startsWith("/"))
|
||||
return "file://" + name;
|
||||
return Quickshell.iconPath(name, true);
|
||||
}
|
||||
|
||||
Item {
|
||||
id: iconSlot
|
||||
width: Theme.dockIconSize
|
||||
height: Theme.dockIconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 0
|
||||
|
||||
// Grow upward out of the dock, the way GNOME's dash does, so the icon
|
||||
// never overlaps its own running dots.
|
||||
transformOrigin: Item.Bottom
|
||||
scale: root.hovered ? 1.15 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.durFast
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 1.6
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
visible: root.iconSource !== ""
|
||||
source: root.iconSource
|
||||
asynchronous: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
// Last resort for an app with no resolvable icon: an initial, which is
|
||||
// still recognisable, unlike the theme's broken-icon placeholder.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: root.iconSource === ""
|
||||
radius: Theme.cardRadius
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, Theme.activeAlpha)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.label ? root.label.charAt(0).toUpperCase() : "?"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Math.round(Theme.dockIconSize * 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Running indicators. Dash-to-Dock showed one dot per window up to four.
|
||||
Row {
|
||||
id: dots
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 3
|
||||
height: 4
|
||||
|
||||
Repeater {
|
||||
model: Math.min(root.windows.length, 4)
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: 4
|
||||
radius: 2
|
||||
border.width: 0 // QTBUG-137166: transparent rounded rects punch corners
|
||||
color: Theme.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
|
||||
onClicked: mev => {
|
||||
// Middle click always starts a new instance, as in GNOME.
|
||||
if (mev.button === Qt.MiddleButton) {
|
||||
root.launch();
|
||||
return;
|
||||
}
|
||||
if (root.running)
|
||||
root.focusNext();
|
||||
else
|
||||
root.launch();
|
||||
}
|
||||
}
|
||||
|
||||
function launch(): void {
|
||||
if (root.entry)
|
||||
root.entry.execute();
|
||||
}
|
||||
|
||||
// Clicking a running app cycles through its windows, matching GNOME's dash.
|
||||
function focusNext(): void {
|
||||
const wins = root.windows;
|
||||
if (wins.length === 0)
|
||||
return;
|
||||
|
||||
let next = wins[0];
|
||||
for (let i = 0; i < wins.length; i++) {
|
||||
if (wins[i].activated) {
|
||||
next = wins[(i + 1) % wins.length];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!next)
|
||||
return;
|
||||
|
||||
// Quickshell reports the address without the 0x prefix Hyprland's
|
||||
// window selector expects; normalise so either form works.
|
||||
if (next.address) {
|
||||
const addr = next.address.startsWith("0x") ? next.address : "0x" + next.address;
|
||||
Hyprland.dispatch(`hl.dsp.focus({ window = "address:${addr}" })`);
|
||||
} else if (next.wayland) {
|
||||
// No address yet (the toplevel is still being reported) — the
|
||||
// wlr handle can still raise it.
|
||||
next.wayland.activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user