pragma ComponentBehavior: Bound import Quickshell import Quickshell.Hyprland import Quickshell.Wayland import QtQuick import qs.HyprChrome.Theme // Primary application launcher — the one on SUPER_L. // // Migrated from widgets/launcher/ApplicationLauncher.qml. Two things changed in // the move, both because HyprChromeShell now owns the state its surfaces share: // // * no scrim of its own. The shell raises the single ChromeBackdrop for any // of its causes — expanded rail, polkit prompt, this — so opening the // launcher over an already-expanded rail reuses the scrim that is there // rather than laying a second dim on top of it. // * `active` is read by the shell, which uses it to raise that scrim, to // place this on the focused monitor, and to decide who gets the keyboard. // // The full-screen layer-shell adapter owns focus, DesktopEntries and execution; // AppLauncherContent stays an Item so the whole visual state can be rendered // headlessly (tests/AppLauncherHeadless.qml). Scope { id: root property bool active: false // Which output to appear on. Driven by the shell, which puts it on the // focused monitor — a launcher belongs where the user is looking, which is // not necessarily where the rail lives. property var screen: null function toggle() { root.active = !root.active; } function close() { root.active = false; } // The name is legacy: this was "variant 8" of eleven, and both SUPER_L (via // open_launcher.sh) and SUPER CTRL 8 still dispatch quickshell:launcher8. // Renaming it means editing hosts/terra/home/hyprland.nix AND the script // together, and neither takes effect until a deploy — so the shortcut would // be dead in the running session in between. Kept as-is deliberately. GlobalShortcut { name: "launcher8" description: "Toggle dense application command index" onPressed: root.toggle() } PanelWindow { id: win screen: root.screen visible: root.active WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore color: Theme.textAlpha(0) anchors { top: true left: true right: true bottom: true } property int selectedIndex: 0 function clampSelection(index) { if (model.apps.length === 0) return 0; return Math.max(0, Math.min(model.apps.length - 1, index)); } function move(delta) { const count = model.apps.length; if (count === 0) return; selectedIndex = ((selectedIndex + delta) % count + count) % count; } function launch(index) { if (model.launch(index)) root.active = false; } onVisibleChanged: { if (visible) { content.clearSearch(); selectedIndex = 0; content.focusSearch(); } } AppModel { id: model search: content.query } // Click-off dismissal. The scrim itself belongs to the shell and takes // no input (its mask is empty), so the catcher lives here: a // transparent full-surface MouseArea UNDER the content, which is what // keeps clicks on the launcher itself from closing it. MouseArea { anchors.fill: parent onClicked: root.close() } AppLauncherContent { id: content anchors.centerIn: parent width: 1080 height: 620 scale: Math.min(1, (parent.width - 64) / width, (parent.height - 64) / height) transformOrigin: Item.Center apps: model.apps selectedIndex: win.selectedIndex onSelectionRequested: index => win.selectedIndex = win.clampSelection(index) onMoveRequested: delta => win.move(delta) onLaunchRequested: index => win.launch(index) onDismissRequested: root.close() } } }