diff --git a/Modules/CustomTextField.qml b/Modules/CustomTextField.qml new file mode 100644 index 0000000..c3594bb --- /dev/null +++ b/Modules/CustomTextField.qml @@ -0,0 +1,82 @@ +import Quickshell +import QtQuick +import QtQuick.Controls + +TextField { + id: root + color: "white" + horizontalAlignment: Text.AlignLeft + echoMode: TextInput.Normal + placeholderText: qsTr("Search applications...") + background: null + renderType: TextInput.NativeRendering + + font.family: "Rubik" + + cursorDelegate: Rectangle { + id: cursor + + property bool disableBlink + + implicitWidth: 2 + color: "white" + radius: 2 + + Connections { + target: root + + function onCursorPositionChanged(): void { + if ( root.activeFocus && root.cursorVisible ) { + cursor.opacity = 1; + cursor.disableBlink = true; + enableBlink.restart(); + } + } + } + + Timer { + id: enableBlink + + interval: 100 + onTriggered: cursor.disableBlink = false + } + + Timer { + running: root.activeFocus && root.cursorVisible && !cursor.disableBlink + repeat: true + triggeredOnStart: true + interval: 500 + onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 + } + + Binding { + when: !root.activeFocus || !root.cursorVisible + cursor.opacity: 0 + } + + Behavior on opacity { + Anim { + duration: 200 + } + } + } + + Keys.onPressed: { + if ( event.key === Qt.Key_Down ) { + appListView.decrementCurrentIndex(); + event.accepted = true; + } else if ( event.key === Qt.Key_Up ) { + appListView.incrementCurrentIndex(); + event.accepted = true; + } else if ( event.key === Qt.Key_Return || event.key === Qt.Key_Enter ) { + if ( appListView.currentItem ) { + Search.launch(appListView.currentItem.modelData); + launcherWindow.visible = false; + } + event.accepted = true; + } else if ( event.key === Qt.Key_Escape ) { + closeAnim.start(); + event.accepted = true; + } + } +} diff --git a/Modules/Launcher.qml b/Modules/Launcher.qml index ea0b568..42c572a 100644 --- a/Modules/Launcher.qml +++ b/Modules/Launcher.qml @@ -3,6 +3,7 @@ import Quickshell.Wayland import Quickshell.Hyprland import QtQuick import QtQuick.Controls +import QtQuick.Effects import QtQuick.Layouts import qs @@ -38,30 +39,133 @@ Scope { appid: "z-cast" name: "toggle-launcher" onPressed: { - launcherWindow.visible = !launcherWindow.visible; + if ( !launcherWindow.visible ) { + openAnim.start(); + } else if ( launcherWindow.visible ) { + closeAnim.start(); + } focusGrab.active = true; searchInput.forceActiveFocus(); } } - mask: Region { item: backgroundRect } + // mask: Region { item: backgroundRect } + + Rectangle { + id: shadowRect + anchors { + top: appListView.count > 0 ? appListRect.top : backgroundRect.top + bottom: backgroundRect.bottom + left: appListRect.left + right: appListRect.right + } + layer.enabled: true + radius: 8 + color: "black" + visible: false + } + + MultiEffect { + id: effects + source: shadowRect + anchors.fill: shadowRect + shadowBlur: 2.0 + shadowEnabled: true + shadowOpacity: 1 + shadowColor: "black" + maskSource: shadowRect + maskEnabled: true + maskInverted: true + autoPaddingEnabled: true + } Rectangle { id: backgroundRect - anchors.top: parent.top - anchors.topMargin: 200 + anchors.bottom: parent.bottom + anchors.bottomMargin: -1 implicitHeight: mainLayout.childrenRect.height + 20 implicitWidth: 600 x: Math.round(( parent.width - width ) / 2 ) color: "#d01a1a1a" - radius: 8 + opacity: 1 border.color: "#444444" border.width: 1 - Behavior on implicitHeight { + ParallelAnimation { + id: openAnim Anim { + target: appListRect duration: MaterialEasing.expressiveFastSpatialTime easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + property: "implicitHeight" + from: 40 + to: appListView.implicitHeight + 20 + } + Anim { + target: appListRect + duration: 50 + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: backgroundRect + duration: 50 + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: effects + duration: 50 + property: "opacity" + from: 0 + to: 1 + } + onStarted: { + launcherWindow.visible = true; + } + } + + ParallelAnimation { + id: closeAnim + Anim { + target: appListRect + duration: MaterialEasing.expressiveFastSpatialTime + easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + property: "implicitHeight" + from: appListView.implicitHeight + to: 40 + } + SequentialAnimation { + PauseAnimation { duration: 120 } + + ParallelAnimation { + Anim { + target: backgroundRect + duration: 50 + property: "opacity" + from: 1 + to: 0 + } + Anim { + target: appListRect + duration: 50 + property: "opacity" + from: 1 + to: 0 + } + Anim { + target: effects + duration: 50 + property: "opacity" + from: 1 + to: 0 + } + } + } + onStopped: { + launcherWindow.visible = false; } } @@ -70,258 +174,247 @@ Scope { anchors.fill: parent anchors.margins: 10 spacing: 5 + clip: true - TextField { + CustomTextField { id: searchInput implicitHeight: 30 implicitWidth: parent.width - color: "white" - horizontalAlignment: Text.AlignLeft - echoMode: TextInput.Normal - placeholderText: qsTr("Search applications...") - background: null + } + } + } - cursorDelegate: Rectangle { - id: cursor + Rectangle { + id: appListRect + x: Math.round(( parent.width - width ) / 2 ) + implicitWidth: backgroundRect.implicitWidth + implicitHeight: appListView.implicitHeight + 20 + anchors.bottom: backgroundRect.top + anchors.bottomMargin: -1 + color: backgroundRect.color + topRightRadius: 8 + topLeftRadius: 8 + border.color: backgroundRect.border.color - property bool disableBlink + Behavior on implicitHeight { + Anim { + duration: MaterialEasing.expressiveFastSpatialTime + easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + } + } - implicitWidth: 2 - color: "white" - radius: 2 + Item { + anchors.fill: parent + anchors.margins: 10 + visible: appListView.count > 0 + clip: true + ListView { + id: appListView + anchors.fill: parent + model: ScriptModel { + id: appModel - Connections { - target: searchInput - - function onCursorPositionChanged(): void { - if ( searchInput.activeFocus && searchInput.cursorVisible ) { - cursor.opacity = 1; - cursor.disableBlink = true; - enableBlink.restart(); - } - } + onValuesChanged: { + appListView.currentIndex = 0; } + } - Timer { - id: enableBlink + verticalLayoutDirection: ListView.BottomToTop + implicitHeight: Math.min( count, 20 ) * 48 - interval: 100 - onTriggered: cursor.disableBlink = false - } + preferredHighlightBegin: 0 + preferredHighlightEnd: appListRect.height + highlightFollowsCurrentItem: false + highlightRangeMode: ListView.ApplyRange + focus: true + highlight: Rectangle { + radius: 4 + color: "#FFFFFF" + opacity: 0.08 - Timer { - running: searchInput.activeFocus && searchInput.cursorVisible && !cursor.disableBlink - repeat: true - triggeredOnStart: true - interval: 500 - onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 - } + y: appListView.currentItem?.y + implicitWidth: appListView.width + implicitHeight: appListView.currentItem?.implicitHeight ?? 0 - Binding { - when: !searchInput.activeFocus || !searchInput.cursorVisible - cursor.opacity: 0 - } - - Behavior on opacity { + Behavior on y { Anim { - duration: 200 + duration: MaterialEasing.expressiveDefaultSpatialTime + easing.bezierCurve: MaterialEasing.expressiveFastSpatial } } } - Keys.onPressed: { - if ( event.key === Qt.Key_Down ) { - appListView.incrementCurrentIndex(); - event.accepted = true; - } else if ( event.key === Qt.Key_Up ) { - appListView.decrementCurrentIndex(); - event.accepted = true; - } else if ( event.key === Qt.Key_Return || event.key === Qt.Key_Enter ) { - if ( appListView.currentItem ) { - Search.launch(appListView.currentItem.modelData); - launcherWindow.visible = false; + property list search: Search.search( searchInput.text ) + + state: search.length === 0 ? "noresults" : "apps" + + states: [ + State { + name: "apps" + PropertyChanges { + appModel.values: Search.search(searchInput.text) + appListView.delegate: appItem } - event.accepted = true; - } else if ( event.key === Qt.Key_Escape ) { - launcherWindow.visible = false; - event.accepted = true; + }, + State { + name: "noresults" + PropertyChanges { + appModel.values: [1] + appListView.delegate: noResultsItem + } + } + ] + + Component { + id: appItem + AppItem { } } - } - Rectangle { - id: separator - implicitWidth: parent.width - implicitHeight: 1 - color: "#444444" - } + Component { + id: noResultsItem + Item { + width: appListView.width + height: 48 + Text { + id: icon + anchors.verticalCenter: parent.verticalCenter + property real fill: 0 + text: "\ue000" + color: "#cccccc" + renderType: Text.NativeRendering + font.pointSize: 28 + font.family: "Material Symbols Outlined" + font.variableAxes: ({ + FILL: fill.toFixed(1), + GRAD: -25, + opsz: fontInfo.pixelSize, + wght: fontInfo.weight + }) + } - Rectangle { - id: appListRect - implicitWidth: parent.width - implicitHeight: appListView.implicitHeight - color: "transparent" - clip: true - ListView { - id: appListView - anchors.fill: parent - model: ScriptModel { - id: appModel - - onValuesChanged: { - appListView.currentIndex = 0; + Text { + anchors.left: icon.right + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + text: "No results found" + color: "#cccccc" + renderType: Text.NativeRendering + + font.pointSize: 12 + font.family: "Rubik" } } + } - implicitHeight: Math.min( count, 15 ) * 48 - - preferredHighlightBegin: 0 - preferredHighlightEnd: appListRect.height - highlightFollowsCurrentItem: false - highlightRangeMode: ListView.ApplyRange - focus: true - highlight: Rectangle { - radius: 4 - color: "#FFFFFF" - opacity: 0.08 - - y: appListView.currentItem?.y - implicitWidth: appListView.width - implicitHeight: appListView.currentItem?.implicitHeight ?? 0 - - Behavior on y { + transitions: Transition { + SequentialAnimation { + ParallelAnimation { Anim { - duration: MaterialEasing.expressiveDefaultSpatialTime - easing.bezierCurve: MaterialEasing.expressiveFastSpatial + target: appListView + property: "opacity" + from: 1 + to: 0 + duration: 200 + easing.bezierCurve: MaterialEasing.standardAccel + } + Anim { + target: appListView + property: "scale" + from: 1 + to: 0.9 + duration: 200 + easing.bezierCurve: MaterialEasing.standardAccel } } - } - - state: "apps" - - states: [ - State { - name: "apps" - PropertyChanges { - appModel.values: Search.search(searchInput.text) - appListView.delegate: appItem + PropertyAction { + targets: [model, appListView] + properties: "values,delegate" + } + ParallelAnimation { + Anim { + target: appListView + property: "opacity" + from: 0 + to: 1 + duration: 200 + easing.bezierCurve: MaterialEasing.standardDecel + } + Anim { + target: appListView + property: "scale" + from: 0.9 + to: 1 + duration: 200 + easing.bezierCurve: MaterialEasing.standardDecel } } - ] - - Component { - id: appItem - AppItem { + PropertyAction { + targets: [appListView.add, appListView.remove] + property: "enabled" + value: true } } + } - transitions: Transition { - SequentialAnimation { - ParallelAnimation { - Anim { - target: appListView - property: "opacity" - from: 1 - to: 0 - duration: 200 - easing.bezierCurve: MaterialEasing.standardAccel - } - Anim { - target: appListView - property: "scale" - from: 1 - to: 0.9 - duration: 200 - easing.bezierCurve: MaterialEasing.standardAccel - } - } - PropertyAction { - targets: [model, appListView] - properties: "values,delegate" - } - ParallelAnimation { - Anim { - target: appListView - property: "opacity" - from: 0 - to: 1 - duration: 200 - easing.bezierCurve: MaterialEasing.standardDecel - } - Anim { - target: appListView - property: "scale" - from: 0.9 - to: 1 - duration: 200 - easing.bezierCurve: MaterialEasing.standardDecel - } - } - PropertyAction { - targets: [appListView.add, appListView.remove] - property: "enabled" - value: true - } - } + add: Transition { + enabled: !appListView.state + Anim { + properties: "opacity" + from: 0 + to: 1 } - add: Transition { - Anim { - properties: "opacity" - from: 0 - to: 1 - } + Anim { + properties: "scale" + from: 0.95 + to: 1 + } + } - Anim { - properties: "scale" - from: 0.95 - to: 1 - } + remove: Transition { + enabled: !appListView.state + Anim { + properties: "opacity" + from: 1 + to: 0 } - remove: Transition { - Anim { - properties: "opacity" - from: 1 - to: 0 - } - - Anim { - properties: "scale" - from: 1 - to: 0.95 - } + Anim { + properties: "scale" + from: 1 + to: 0.95 } + } - move: Transition { - Anim { - property: "y" - } - Anim { - properties: "opacity,scale" - to: 1 - } + move: Transition { + Anim { + property: "y" } - - addDisplaced: Transition { - Anim { - property: "y" - duration: 200 - } - Anim { - properties: "opacity,scale" - to: 1 - } + Anim { + properties: "opacity,scale" + to: 1 } + } - displaced: Transition { - Anim { - property: "y" - } - Anim { - properties: "opacity,scale" - to: 1 - } + addDisplaced: Transition { + Anim { + property: "y" + duration: 200 + } + Anim { + properties: "opacity,scale" + to: 1 + } + } + + displaced: Transition { + Anim { + property: "y" + } + Anim { + properties: "opacity,scale" + to: 1 } } } diff --git a/Modules/Search.qml b/Modules/Search.qml index b84a6f8..74a3d99 100644 --- a/Modules/Search.qml +++ b/Modules/Search.qml @@ -21,10 +21,39 @@ Searcher { } function search(search: string): list { - keys = ["name"]; - weights = [1]; + const prefix = ">"; + if (search.startsWith(`${prefix}i `)) { + keys = ["id", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}c `)) { + keys = ["categories", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}d `)) { + keys = ["comment", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}e `)) { + keys = ["execString", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}w `)) { + keys = ["startupClass", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}g `)) { + keys = ["genericName", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}k `)) { + keys = ["keywords", "name"]; + weights = [0.9, 0.1]; + } else { + keys = ["name"]; + weights = [1]; - const results = query(search).map(e => e.entry); + if (!search.startsWith(`${prefix}t `)) + return query(search).map(e => e.entry); + } + + const results = query(search.slice(prefix.length + 2)).map(e => e.entry); + if (search.startsWith(`${prefix}t `)) + return results.filter(a => a.runInTerminal); return results; }