quickshell: launcher rises from the bottom frame; sliding pills in session menu and launcher list
- launcher moved into the bar window as a 5th SDF surface: melts out of the bottom frame edge, height morphs live as results filter - session menu selection is a gliding pill (battery-menu tech) - launcher selection uses an animated ListView highlight - HyprlandFocusGrab gives both panels click-outside dismissal - standalone Launcher.qml window removed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d2bcdad2fe
commit
641d5c7e63
1 changed files with 246 additions and 221 deletions
|
|
@ -69,6 +69,7 @@ in
|
||||||
vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none)
|
vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none)
|
||||||
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
||||||
vec4 session; // cx, cy, hw, hh — session menu (hw <= 0: none)
|
vec4 session; // cx, cy, hw, hh — session menu (hw <= 0: none)
|
||||||
|
vec4 launcher; // cx, cy, hw, hh — bottom launcher (hw <= 0: none)
|
||||||
vec4 fillColor; // straight (non-premultiplied) rgba
|
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||||
vec4 borderColor;
|
vec4 borderColor;
|
||||||
vec2 res;
|
vec2 res;
|
||||||
|
|
@ -101,6 +102,8 @@ in
|
||||||
d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK);
|
||||||
if (session.z > 0.5)
|
if (session.z > 0.5)
|
||||||
d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK);
|
||||||
|
if (launcher.z > 0.5)
|
||||||
|
d = smin(d, sdRoundedBox(p, launcher.xy, launcher.zw, panelR), meltK);
|
||||||
|
|
||||||
float fw = fwidth(d);
|
float fw = fwidth(d);
|
||||||
// 1 inside the union, 0 outside (antialiased)
|
// 1 inside the union, 0 outside (antialiased)
|
||||||
|
|
@ -134,7 +137,6 @@ in
|
||||||
singleton Theme 1.0 Theme.qml
|
singleton Theme 1.0 Theme.qml
|
||||||
singleton Commands 1.0 Commands.qml
|
singleton Commands 1.0 Commands.qml
|
||||||
Bar 1.0 Bar.qml
|
Bar 1.0 Bar.qml
|
||||||
Launcher 1.0 Launcher.qml
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -203,15 +205,11 @@ in
|
||||||
property var mainBar: null
|
property var mainBar: null
|
||||||
signal notificationReceived()
|
signal notificationReceived()
|
||||||
|
|
||||||
Launcher {
|
// Bound in hyprland.nix: Super+R → launcher (bottom of the
|
||||||
id: launcher
|
// bar window), Super+L → session menu (right edge).
|
||||||
}
|
|
||||||
|
|
||||||
// Bound in hyprland.nix: Super+R → app launcher,
|
|
||||||
// Super+L → session menu (in the bar window).
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "launcher"
|
target: "launcher"
|
||||||
function toggle(): void { launcher.toggle(); }
|
function toggle(): void { if (root.mainBar) root.mainBar.toggleLauncher(); }
|
||||||
function powermenu(): void { if (root.mainBar) root.mainBar.toggleSession(); }
|
function powermenu(): void { if (root.mainBar) root.mainBar.toggleSession(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,210 +246,6 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# App launcher + power menu (replaces anyrun). Full-screen transparent
|
|
||||||
# overlay with exclusive keyboard focus while open; Esc / click-outside
|
|
||||||
# closes. Apps come from Quickshell's DesktopEntries service.
|
|
||||||
"quickshell/Launcher.qml" = {
|
|
||||||
onChange = qsRestart;
|
|
||||||
text = ''
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
screen: Quickshell.screens[0]
|
|
||||||
WlrLayershell.namespace: "quickshell-launcher"
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
bottom: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (visible) { close(); return; }
|
|
||||||
search.text = "";
|
|
||||||
list.currentIndex = 0;
|
|
||||||
visible = true;
|
|
||||||
search.forceActiveFocus();
|
|
||||||
}
|
|
||||||
function close() {
|
|
||||||
visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function score(name, extra, q) {
|
|
||||||
let n = name.toLowerCase();
|
|
||||||
if (n.startsWith(q)) return 5;
|
|
||||||
if (n.includes(" " + q)) return 4;
|
|
||||||
if (n.includes(q)) return 3;
|
|
||||||
if (extra && extra.toLowerCase().includes(q)) return 2;
|
|
||||||
// Fuzzy: q as an in-order subsequence of n (vktop →
|
|
||||||
// vesktop); fewer skipped characters scores higher.
|
|
||||||
let qi = 0, gaps = 0, last = -1;
|
|
||||||
for (let i = 0; i < n.length && qi < q.length; i++) {
|
|
||||||
if (n[i] === q[qi]) {
|
|
||||||
if (last >= 0) gaps += i - last - 1;
|
|
||||||
last = i;
|
|
||||||
qi++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (qi === q.length) return 1 / (1 + gaps);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
property var entries: {
|
|
||||||
let q = search.text.toLowerCase().trim();
|
|
||||||
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
|
||||||
if (q === "") {
|
|
||||||
apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
return apps.slice(0, 8);
|
|
||||||
}
|
|
||||||
let scored = [];
|
|
||||||
for (let i = 0; i < apps.length; i++) {
|
|
||||||
let s = score(apps[i].name, apps[i].genericName + " " + apps[i].comment, q);
|
|
||||||
if (s > 0) scored.push({ app: apps[i], s: s });
|
|
||||||
}
|
|
||||||
scored.sort((a, b) => b.s - a.s || a.app.name.localeCompare(b.app.name));
|
|
||||||
return scored.slice(0, 8).map(x => x.app);
|
|
||||||
}
|
|
||||||
|
|
||||||
function activate(item) {
|
|
||||||
if (!item) return;
|
|
||||||
item.execute();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click outside the box closes
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: box
|
|
||||||
x: Math.round((parent.width - width) / 2)
|
|
||||||
y: Math.round(parent.height * 0.25)
|
|
||||||
width: 350
|
|
||||||
height: col.height + 16
|
|
||||||
radius: 8 // matches hyprland decoration.rounding
|
|
||||||
color: Theme.base00
|
|
||||||
border.width: Theme.borderWidth
|
|
||||||
border.color: Theme.base03
|
|
||||||
|
|
||||||
// Swallow clicks inside the box
|
|
||||||
MouseArea { anchors.fill: parent }
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: col
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 8
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 36
|
|
||||||
radius: 6
|
|
||||||
color: Theme.base01
|
|
||||||
|
|
||||||
TextInput {
|
|
||||||
id: search
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 12
|
|
||||||
anchors.rightMargin: 12
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
color: Theme.base05
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 13
|
|
||||||
clip: true
|
|
||||||
onTextChanged: list.currentIndex = 0
|
|
||||||
|
|
||||||
Keys.onEscapePressed: root.close()
|
|
||||||
Keys.onUpPressed: list.currentIndex = Math.max(0, list.currentIndex - 1)
|
|
||||||
Keys.onDownPressed: list.currentIndex = Math.min(root.entries.length - 1, list.currentIndex + 1)
|
|
||||||
Keys.onReturnPressed: root.activate(root.entries[list.currentIndex])
|
|
||||||
Keys.onEnterPressed: root.activate(root.entries[list.currentIndex])
|
|
||||||
Keys.onTabPressed: list.currentIndex = (list.currentIndex + 1) % Math.max(1, root.entries.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.fill: search
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
visible: search.text === ""
|
|
||||||
text: "Search"
|
|
||||||
color: Theme.base03
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 13
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: list
|
|
||||||
width: parent.width
|
|
||||||
height: contentHeight
|
|
||||||
interactive: false
|
|
||||||
model: root.entries
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
width: list.width
|
|
||||||
height: 32
|
|
||||||
radius: 6
|
|
||||||
color: list.currentIndex === index ? Theme.base02 : "transparent"
|
|
||||||
Behavior on color { ColorAnimation { duration: 100 } }
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Image {
|
|
||||||
visible: source != ""
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
sourceSize.width: 18
|
|
||||||
sourceSize.height: 18
|
|
||||||
source: Quickshell.iconPath(modelData.icon, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: modelData.name
|
|
||||||
color: Theme.base05
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 13
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: 270
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: list.currentIndex = index
|
|
||||||
onClicked: root.activate(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
"quickshell/Bar.qml" = {
|
"quickshell/Bar.qml" = {
|
||||||
onChange = qsRestart;
|
onChange = qsRestart;
|
||||||
text = ''
|
text = ''
|
||||||
|
|
@ -478,7 +272,7 @@ in
|
||||||
screen: modelData
|
screen: modelData
|
||||||
WlrLayershell.namespace: "quickshell-bar"
|
WlrLayershell.namespace: "quickshell-bar"
|
||||||
// Keyboard only while the session menu is open (arrow/Enter nav)
|
// Keyboard only while the session menu is open (arrow/Enter nav)
|
||||||
WlrLayershell.keyboardFocus: sessionMenu.open ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: sessionMenu.open || launcherPanel.open ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
|
|
@ -510,6 +304,12 @@ in
|
||||||
width: sessionMenu.visible ? sessionMenu.width : 0
|
width: sessionMenu.visible ? sessionMenu.width : 0
|
||||||
height: sessionMenu.visible ? sessionMenu.height : 0
|
height: sessionMenu.visible ? sessionMenu.height : 0
|
||||||
}
|
}
|
||||||
|
Region {
|
||||||
|
x: launcherPanel.visible ? launcherPanel.x : 0
|
||||||
|
y: launcherPanel.visible ? launcherPanel.y : 0
|
||||||
|
width: launcherPanel.visible ? launcherPanel.width : 0
|
||||||
|
height: launcherPanel.visible ? launcherPanel.height : 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -531,6 +331,10 @@ in
|
||||||
sessionMenu.toggle();
|
sessionMenu.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleLauncher() {
|
||||||
|
launcherPanel.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Shell chrome: bar, frame, panel and toast rendered as
|
// ── Shell chrome: bar, frame, panel and toast rendered as
|
||||||
// ONE signed-distance field (caelestia-style). Surfaces merge
|
// ONE signed-distance field (caelestia-style). Surfaces merge
|
||||||
// via circular smooth-min, and the 2px border is the distance
|
// via circular smooth-min, and the 2px border is the distance
|
||||||
|
|
@ -559,6 +363,11 @@ in
|
||||||
? Qt.vector4d((sessionMenu.x + sessRight) / 2, sessionMenu.y + sessionMenu.height / 2,
|
? Qt.vector4d((sessionMenu.x + sessRight) / 2, sessionMenu.y + sessionMenu.height / 2,
|
||||||
(sessRight - sessionMenu.x) / 2, sessionMenu.height / 2)
|
(sessRight - sessionMenu.x) / 2, sessionMenu.height / 2)
|
||||||
: Qt.vector4d(0, 0, 0, 0)
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
|
readonly property real launchBot: bar.height - Theme.frameWidth + 4
|
||||||
|
property vector4d launcher: launcherPanel.visible
|
||||||
|
? Qt.vector4d(launcherPanel.x + launcherPanel.width / 2, (launcherPanel.y + launchBot) / 2,
|
||||||
|
launcherPanel.width / 2, (launchBot - launcherPanel.y) / 2)
|
||||||
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
||||||
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
||||||
property vector2d res: Qt.vector2d(width, height)
|
property vector2d res: Qt.vector2d(width, height)
|
||||||
|
|
@ -652,6 +461,22 @@ in
|
||||||
radius: 8
|
radius: 8
|
||||||
color: Theme.base01
|
color: Theme.base01
|
||||||
|
|
||||||
|
// Sliding selection pill — same tech as the power
|
||||||
|
// profile selector; glides between the buttons.
|
||||||
|
Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: 8
|
||||||
|
color: Theme.base02
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.base03
|
||||||
|
x: sessionCol.x
|
||||||
|
y: sessionCol.y + sessionMenu.selIdx * 44
|
||||||
|
Behavior on y {
|
||||||
|
NumberAnimation { duration: 250; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: sessionCol
|
id: sessionCol
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
@ -660,20 +485,13 @@ in
|
||||||
Repeater {
|
Repeater {
|
||||||
model: sessionMenu.actions
|
model: sessionMenu.actions
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
id: sessBtn
|
id: sessBtn
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
readonly property bool selected: sessionMenu.selIdx === index
|
readonly property bool selected: sessionMenu.selIdx === index
|
||||||
width: 40
|
width: 40
|
||||||
height: 40
|
height: 40
|
||||||
radius: 8
|
|
||||||
// Same selection grammar as the power-profile pill
|
|
||||||
// and calendar "today": base02 surface, base03 border
|
|
||||||
color: selected ? Theme.base02 : "transparent"
|
|
||||||
border.width: selected ? 1 : 0
|
|
||||||
border.color: Theme.base03
|
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
@ -703,6 +521,213 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Launcher: rises out of the bottom frame edge (Super+R).
|
||||||
|
// Lives in the SDF field, so it melts into the frame and its
|
||||||
|
// height morphs live as results filter. Results sit above the
|
||||||
|
// search box; selection uses an animated ListView highlight.
|
||||||
|
Item {
|
||||||
|
id: launcherPanel
|
||||||
|
property bool open: false
|
||||||
|
readonly property real panelW: 420
|
||||||
|
property real targetH: 36 + launcherList.contentHeight
|
||||||
|
+ (launcherList.count > 0 ? 8 : 0) + 24
|
||||||
|
property real openH: open ? targetH : 0
|
||||||
|
Behavior on openH {
|
||||||
|
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
|
||||||
|
x: Math.round((bar.width - panelW) / 2)
|
||||||
|
y: bar.height - Theme.frameWidth - openH
|
||||||
|
width: panelW
|
||||||
|
height: openH
|
||||||
|
visible: openH > 0.5
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
open = !open;
|
||||||
|
if (open) {
|
||||||
|
searchInput.text = "";
|
||||||
|
launcherList.currentIndex = 0;
|
||||||
|
searchInput.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(item) {
|
||||||
|
if (!item) return;
|
||||||
|
item.execute();
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function score(name, extra, q) {
|
||||||
|
let n = name.toLowerCase();
|
||||||
|
if (n.startsWith(q)) return 5;
|
||||||
|
if (n.includes(" " + q)) return 4;
|
||||||
|
if (n.includes(q)) return 3;
|
||||||
|
if (extra && extra.toLowerCase().includes(q)) return 2;
|
||||||
|
// Fuzzy: q as an in-order subsequence of n (vktop →
|
||||||
|
// vesktop); fewer skipped characters scores higher.
|
||||||
|
let qi = 0, gaps = 0, last = -1;
|
||||||
|
for (let i = 0; i < n.length && qi < q.length; i++) {
|
||||||
|
if (n[i] === q[qi]) {
|
||||||
|
if (last >= 0) gaps += i - last - 1;
|
||||||
|
last = i;
|
||||||
|
qi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (qi === q.length) return 1 / (1 + gaps);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
property var entries: {
|
||||||
|
let q = searchInput.text.toLowerCase().trim();
|
||||||
|
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
||||||
|
if (q === "") {
|
||||||
|
apps.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return apps.slice(0, 8);
|
||||||
|
}
|
||||||
|
let scored = [];
|
||||||
|
for (let i = 0; i < apps.length; i++) {
|
||||||
|
let s = score(apps[i].name, apps[i].genericName + " " + apps[i].comment, q);
|
||||||
|
if (s > 0) scored.push({ app: apps[i], s: s });
|
||||||
|
}
|
||||||
|
scored.sort((a, b) => b.s - a.s || a.app.name.localeCompare(b.app.name));
|
||||||
|
return scored.slice(0, 8).map(x => x.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content anchored to the bottom so the grow reveals upward
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
opacity: launcherPanel.open ? 1 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 12
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: launcherPanel.panelW - 24
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: launcherList
|
||||||
|
width: parent.width
|
||||||
|
height: contentHeight
|
||||||
|
interactive: false
|
||||||
|
model: launcherPanel.entries
|
||||||
|
highlight: Rectangle {
|
||||||
|
radius: 6
|
||||||
|
color: Theme.base02
|
||||||
|
}
|
||||||
|
highlightMoveDuration: 200
|
||||||
|
highlightMoveVelocity: -1
|
||||||
|
highlightResizeDuration: 0
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
width: launcherList.width
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Image {
|
||||||
|
visible: source != ""
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
sourceSize.width: 18
|
||||||
|
sourceSize.height: 18
|
||||||
|
source: Quickshell.iconPath(modelData.icon, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: modelData.name
|
||||||
|
color: Theme.base05
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: 330
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onEntered: launcherList.currentIndex = index
|
||||||
|
onClicked: launcherPanel.activate(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 36
|
||||||
|
radius: 6
|
||||||
|
color: Theme.base01
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: searchIcon
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "search"
|
||||||
|
color: Theme.base04
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: searchInput
|
||||||
|
anchors.left: searchIcon.right
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: Theme.base05
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
clip: true
|
||||||
|
onTextChanged: launcherList.currentIndex = 0
|
||||||
|
|
||||||
|
Keys.onEscapePressed: launcherPanel.open = false
|
||||||
|
Keys.onUpPressed: launcherList.currentIndex = Math.max(0, launcherList.currentIndex - 1)
|
||||||
|
Keys.onDownPressed: launcherList.currentIndex = Math.min(launcherPanel.entries.length - 1, launcherList.currentIndex + 1)
|
||||||
|
Keys.onTabPressed: launcherList.currentIndex = (launcherList.currentIndex + 1) % Math.max(1, launcherPanel.entries.length)
|
||||||
|
Keys.onReturnPressed: launcherPanel.activate(launcherPanel.entries[launcherList.currentIndex])
|
||||||
|
Keys.onEnterPressed: launcherPanel.activate(launcherPanel.entries[launcherList.currentIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: searchIcon.right
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: searchInput.text === ""
|
||||||
|
text: "Search"
|
||||||
|
color: Theme.base03
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click-outside dismissal for the keyboard-grabbing panels
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
active: sessionMenu.open || launcherPanel.open
|
||||||
|
windows: [bar]
|
||||||
|
onCleared: {
|
||||||
|
sessionMenu.open = false;
|
||||||
|
launcherPanel.open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property var activeDropdown: null
|
property var activeDropdown: null
|
||||||
|
|
||||||
function closeAllDropdowns() {
|
function closeAllDropdowns() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue