From 65b7a8e9763cf1c54c0e14fcecfae5944f612a62 Mon Sep 17 00:00:00 2001 From: Leriart Date: Tue, 26 May 2026 07:18:18 -0600 Subject: [PATCH] overview: rewrite with fill tiling, unified input, live refresh + drag-drop, per-monitor positioning, animation fixes --- modules/bar/workspaces/CompositorData.qml | 30 + modules/dock/DockContent.qml | 12 +- modules/widgets/overview/Overview.qml | 455 +++++--------- modules/widgets/overview/OverviewWindow.qml | 196 +++--- .../widgets/overview/ScrollingWorkspace.qml | 589 +++++++++--------- 5 files changed, 574 insertions(+), 708 deletions(-) mode change 100644 => 100755 modules/widgets/overview/Overview.qml mode change 100644 => 100755 modules/widgets/overview/OverviewWindow.qml diff --git a/modules/bar/workspaces/CompositorData.qml b/modules/bar/workspaces/CompositorData.qml index 3772995d..269f68b8 100644 --- a/modules/bar/workspaces/CompositorData.qml +++ b/modules/bar/workspaces/CompositorData.qml @@ -20,6 +20,36 @@ Singleton { // No-op: state is now pushed inline via axctl subscribe events } + // Force refresh from hyprctl (used by overview after drag-and-drop) + property Process _refreshProcess: Process { + command: ["hyprctl", "clients", "-j"] + stdout: StdioCollector { + onStreamFinished: { + try { + var raw = JSON.parse(text); + if (raw && raw.length > 0) + root.refreshFromJson(raw); + } catch (e) {} + } + } + } + + function refreshFromHyprctl() { + _refreshProcess.running = true; + } + + function refreshFromJson(raw) { + root.windowList = raw; + let tempWinByAddress = {} + for (var i = 0; i < root.windowList.length; ++i) { + var win = root.windowList[i] + tempWinByAddress[win.address] = win + } + root.windowByAddress = tempWinByAddress + root.addresses = root.windowList.map((win) => win.address) + updateMaps() + } + function updateMaps() { let occupationMap = {} let windowsMap = {} diff --git a/modules/dock/DockContent.qml b/modules/dock/DockContent.qml index a6b31db3..ab7d3c33 100644 --- a/modules/dock/DockContent.qml +++ b/modules/dock/DockContent.qml @@ -520,8 +520,10 @@ Item { onClicked: { let visibilities = Visibilities.getForScreen(root.screen.name); - if (visibilities) { - visibilities.overview = !visibilities.overview; + if (visibilities && visibilities.overview) { + Visibilities.setActiveModule(""); + } else { + Visibilities.setActiveModule("overview"); } } @@ -654,8 +656,10 @@ Item { onClicked: { let visibilities = Visibilities.getForScreen(root.screen.name); - if (visibilities) { - visibilities.overview = !visibilities.overview; + if (visibilities && visibilities.overview) { + Visibilities.setActiveModule(""); + } else { + Visibilities.setActiveModule("overview"); } } diff --git a/modules/widgets/overview/Overview.qml b/modules/widgets/overview/Overview.qml old mode 100644 new mode 100755 index 6dad0e66..6c5d8c16 --- a/modules/widgets/overview/Overview.qml +++ b/modules/widgets/overview/Overview.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QtQuick.Effects import Quickshell import Quickshell.Wayland +import Quickshell.Io import qs.modules.globals import qs.modules.theme import qs.modules.components @@ -11,12 +12,25 @@ import qs.modules.bar.workspaces import qs.modules.services import qs.config - - Item { id: overviewRoot - // Cache config values to avoid repeated lookups + // ── Direct hyprctl query for fresh window data ── + property Process _hyprctlClients: Process { + command: ["hyprctl", "clients", "-j"] + stdout: StdioCollector { + onStreamFinished: { + try { + var raw = JSON.parse(text); + if (raw && raw.length > 0 && typeof CompositorData !== "undefined") + CompositorData.windowList = raw; + } catch (e) {} + } + } + } + function refreshFromHyprctl() { _hyprctlClients.running = true; } + + // Config readonly property real scale: Config.overview.scale readonly property int rows: Config.overview.rows readonly property int columns: Config.overview.columns @@ -25,271 +39,168 @@ Item { readonly property real workspacePadding: 8 readonly property color activeBorderColor: Styling.srItem("overprimary") - // Use the screen's monitor instead of focused monitor for multi-monitor support - property var currentScreen: null // This will be set from parent + property var currentScreen: null readonly property var monitor: currentScreen ? AxctlService.monitorFor(currentScreen) : AxctlService.focusedMonitor - readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 1 || 0) / workspacesShown) - - // Cache these references readonly property var windowList: CompositorData.windowList - readonly property var monitors: CompositorData.monitors + readonly property var allMonitors: CompositorData.monitors readonly property int monitorId: monitor?.id ?? -1 - readonly property var monitorData: monitors.find(m => m.id === monitorId) ?? null - + readonly property var monitorData: allMonitors.find(m => m.id === monitorId) ?? null readonly property string barPosition: Config.bar.position readonly property var barPanel: monitor ? Visibilities.getBarPanelForScreen(monitor.name) : null readonly property bool isBarPinned: barPanel ? barPanel.pinned : (Config.bar.pinnedOnStartup ?? true) readonly property int barReserved: isBarPinned ? (Config.showBackground ? 44 : 40) : 0 - // Search functionality (controlled from parent) - property string searchQuery: "" - property var matchingWindows: [] - property int selectedMatchIndex: 0 + // Workspace cell size (based on current screen's monitor) + readonly property real wsCellW: { + if (!monitorData) return 200; + var ro = (monitorData.transform % 2 === 1); + var ms = monitorData.scale || 1.0; + var w = ro ? (monitor?.height || 1920) : (monitor?.width || 1920); + var sw = (w / ms) * scale; + if (barPosition === "left" || barPosition === "right") sw -= barReserved * scale; + return Math.max(0, Math.round(sw)); + } + readonly property real wsCellH: { + if (!monitorData) return 150; + var ro = (monitorData.transform % 2 === 1); + var ms = monitorData.scale || 1.0; + var h = ro ? (monitor?.width || 1080) : (monitor?.height || 1080); + var sh = (h / ms) * scale; + if (barPosition === "top" || barPosition === "bottom") sh -= barReserved * scale; + return Math.max(0, Math.round(sh)); + } - // Reset search state - function resetSearch() { - searchQuery = ""; - matchingWindows = []; - selectedMatchIndex = 0; + // Drag state + property int draggingFromWorkspace: -1 + property int draggingTargetWorkspace: -1 + property int _refreshToken: 0 + function forceRefresh() { _refreshToken++; } + function refreshWithHyprctl() { + refreshFromHyprctl(); + forceRefresh(); } + Component.onCompleted: refreshWithHyprctl() - // Update matching windows when search query or window list changes + // Search + property string searchQuery: "" + property var matchingWindows: [] + property int selectedMatchIndex: 0 + function resetSearch() { searchQuery = ""; matchingWindows = []; selectedMatchIndex = 0; } onSearchQueryChanged: updateMatchingWindows() onWindowListChanged: updateMatchingWindows() - // Fuzzy match: checks if all characters of query appear in order in target - function fuzzyMatch(query, target) { - if (query.length === 0) - return true; - if (target.length === 0) - return false; - - let queryIndex = 0; - for (let i = 0; i < target.length && queryIndex < query.length; i++) { - if (target[i] === query[queryIndex]) { - queryIndex++; - } - } - return queryIndex === query.length; + function fuzzyMatch(q, t) { + if (q.length === 0) return true; + if (t.length === 0) return false; + var qi = 0; + for (var i = 0; i < t.length && qi < q.length; i++) { if (t[i] === q[qi]) qi++; } + return qi === q.length; } - - // Score a fuzzy match (higher is better) - function fuzzyScore(query, target) { - if (query.length === 0) - return 0; - if (target.length === 0) - return -1; - - // Exact match gets highest score - if (target.includes(query)) - return 1000 + (100 - target.length); - - // Check for fuzzy match - let queryIndex = 0; - let consecutiveMatches = 0; - let maxConsecutive = 0; - let score = 0; - - for (let i = 0; i < target.length && queryIndex < query.length; i++) { - if (target[i] === query[queryIndex]) { - queryIndex++; - consecutiveMatches++; - maxConsecutive = Math.max(maxConsecutive, consecutiveMatches); - // Bonus for matches at word boundaries - if (i === 0 || target[i - 1] === ' ' || target[i - 1] === '-' || target[i - 1] === '_') { - score += 10; - } - } else { - consecutiveMatches = 0; - } + function fuzzyScore(q, t) { + if (q.length === 0) return 0; + if (t.length === 0) return -1; + if (t.includes(q)) return 1000 + (100 - t.length); + var qi = 0, cons = 0, maxc = 0, score = 0; + for (var i = 0; i < t.length && qi < q.length; i++) { + if (t[i] === q[qi]) { qi++; cons++; maxc = Math.max(maxc, cons); + if (i === 0 || t[i-1] === ' ' || t[i-1] === '-' || t[i-1] === '_') score += 10; } + else cons = 0; } - - if (queryIndex !== query.length) - return -1; // No match - - return score + maxConsecutive * 5; + if (qi !== q.length) return -1; + return score + maxc * 5; } - function updateMatchingWindows() { - if (searchQuery.length === 0) { - matchingWindows = []; - selectedMatchIndex = 0; - return; - } - - const query = searchQuery.toLowerCase(); - const matches = windowList.filter(win => { - if (!win) - return false; - const title = (win.title || "").toLowerCase(); - const windowClass = (win.class || "").toLowerCase(); - return fuzzyMatch(query, title) || fuzzyMatch(query, windowClass); - }).map(win => ({ - window: win, - score: Math.max(fuzzyScore(query, (win.title || "").toLowerCase()), fuzzyScore(query, (win.class || "").toLowerCase())) - })).sort((a, b) => b.score - a.score).map(item => item.window); - - matchingWindows = matches; - selectedMatchIndex = matches.length > 0 ? 0 : -1; + if (searchQuery.length === 0) { matchingWindows = []; selectedMatchIndex = 0; return; } + var q = searchQuery.toLowerCase(); + var m = windowList.filter(function(w) { + if (!w) return false; + return fuzzyMatch(q, (w.title || "").toLowerCase()) || fuzzyMatch(q, (w.class || "").toLowerCase()); + }).map(function(w) { + return { window: w, score: Math.max(fuzzyScore(q, (w.title || "").toLowerCase()), fuzzyScore(q, (w.class || "").toLowerCase())) }; + }).sort(function(a,b) { return b.score - a.score; }).map(function(i) { return i.window; }); + matchingWindows = m; selectedMatchIndex = m.length > 0 ? 0 : -1; } - - function navigateToSelectedWindow() { - if (matchingWindows.length === 0 || selectedMatchIndex < 0) - return; - - const win = matchingWindows[selectedMatchIndex]; - if (!win) - return; - - // Close overview and focus the matched window - Visibilities.setActiveModule("", true); - Qt.callLater(() => { - AxctlService.dispatch(`focuswindow address:${win.address}`); - }); - } - - function selectNextMatch() { - if (matchingWindows.length === 0) - return; - selectedMatchIndex = (selectedMatchIndex + 1) % matchingWindows.length; - } - - function selectPrevMatch() { - if (matchingWindows.length === 0) - return; - selectedMatchIndex = (selectedMatchIndex - 1 + matchingWindows.length) % matchingWindows.length; - } - - function isWindowMatched(windowAddress) { - if (searchQuery.length === 0) - return false; - return matchingWindows.some(win => win?.address === windowAddress); - } - - function isWindowSelected(windowAddress) { - if (matchingWindows.length === 0 || selectedMatchIndex < 0) - return false; - return matchingWindows[selectedMatchIndex]?.address === windowAddress; - } - - // Pre-calculate workspace dimensions once - readonly property real workspaceImplicitWidth: { - if (!monitorData) - return 200; - const isRotated = (monitorData.transform % 2 === 1); - const monitorScale = monitorData.scale || 1.0; - const width = isRotated ? (monitor?.height || 1920) : (monitor?.width || 1920); - let scaledWidth = (width / monitorScale) * scale; - if (barPosition === "left" || barPosition === "right") { - scaledWidth -= barReserved * scale; + function isWindowMatched(addr) { return searchQuery.length > 0 && matchingWindows.some(function(w) { return w?.address === addr; }); } + function isWindowSelected(addr) { return matchingWindows.length > 0 && selectedMatchIndex >= 0 && matchingWindows[selectedMatchIndex]?.address === addr; } + + // ── Build filtered windows list (ALL monitors, workspaces 1..workspacesShown) ── + // Each window carries its OWN monitor data for correct positioning + readonly property var filteredWindows: { + var _ = overviewRoot._refreshToken; + var toplevels = ToplevelManager.toplevels.values; + var result = []; + var list = overviewRoot.windowList; + for (var i = 0; i < list.length; i++) { + var w = list[i]; + if (!w || !w.workspace || !w.workspace.id || w.workspace.id <= 0 || w.workspace.id > workspacesShown) + continue; + var winMon = overviewRoot.allMonitors.find(function(m) { return m.id === w.monitor; }); + result.push({ + windowData: w, + winMonData: winMon, + toplevel: (function() { + var cls = w.class || ""; + if (!cls) return null; + var cands = toplevels.filter(function(t) { return t.appId === cls; }); + if (cands.length <= 1) return cands[0] || null; + return cands.find(function(t) { return t.title === (w.title || ""); }) || cands[0]; + })() + }); } - return Math.max(0, Math.round(scaledWidth)); + return result; } - readonly property real workspaceImplicitHeight: { - if (!monitorData) - return 150; - const isRotated = (monitorData.transform % 2 === 1); - const monitorScale = monitorData.scale || 1.0; - const height = isRotated ? (monitor?.width || 1080) : (monitor?.height || 1080); - let scaledHeight = (height / monitorScale) * scale; - if (barPosition === "top" || barPosition === "bottom") { - scaledHeight -= barReserved * scale; - } - return Math.max(0, Math.round(scaledHeight)); - } - - property int draggingFromWorkspace: -1 - property int draggingTargetWorkspace: -1 - - implicitWidth: overviewBackground.implicitWidth - implicitHeight: overviewBackground.implicitHeight + implicitWidth: bg.implicitWidth + implicitHeight: bg.implicitHeight Item { - id: overviewBackground + id: bg anchors.centerIn: parent - implicitWidth: workspaceColumnLayout.implicitWidth - implicitHeight: workspaceColumnLayout.implicitHeight - ColumnLayout { - id: workspaceColumnLayout + id: mainGrid anchors.centerIn: parent spacing: workspaceSpacing Repeater { model: overviewRoot.rows delegate: RowLayout { - id: row - property int rowIndex: index spacing: workspaceSpacing - Repeater { model: overviewRoot.columns Rectangle { - id: workspace - property int colIndex: index - property int workspaceValue: overviewRoot.workspaceGroup * workspacesShown + rowIndex * overviewRoot.columns + colIndex + 1 - property color defaultWorkspaceColor: Colors.background - property color hoveredWorkspaceColor: Colors.surfaceContainer - property color hoveredBorderColor: Colors.outline - property bool hoveredWhileDragging: false - - implicitWidth: overviewRoot.workspaceImplicitWidth + workspacePadding - implicitHeight: overviewRoot.workspaceImplicitHeight + workspacePadding - color: "transparent" - radius: Styling.radius(2) - border.width: 2 - border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent" + id: cell + property int wsNum: rowIndex * overviewRoot.columns + index + 1 + readonly property bool isDropTarget: overviewRoot.draggingTargetWorkspace === wsNum + + implicitWidth: overviewRoot.wsCellW + workspacePadding + implicitHeight: overviewRoot.wsCellH + workspacePadding + color: "transparent"; radius: Styling.radius(2) + border.width: 2; border.color: isDropTarget ? Colors.outline : "transparent" clip: true - // Wallpaper background for each workspace TintedWallpaper { - id: workspaceWallpaper - anchors.fill: parent - radius: Styling.radius(2) + anchors.fill: parent; radius: Styling.radius(2) tintEnabled: GlobalStates.wallpaperManager ? GlobalStates.wallpaperManager.tintEnabled : false - - property string lockscreenFramePath: { - if (!GlobalStates.wallpaperManager) - return ""; - return GlobalStates.wallpaperManager.getLockscreenFramePath(GlobalStates.wallpaperManager.currentWallpaper); - } - - source: lockscreenFramePath ? "file://" + lockscreenFramePath : "" + property string lfp: GlobalStates.wallpaperManager ? GlobalStates.wallpaperManager.getLockscreenFramePath(GlobalStates.wallpaperManager.currentWallpaper) : "" + source: lfp ? "file://" + lfp : "" + } + Text { + anchors.centerIn: parent + text: String(wsNum) + font.family: Config.theme.font + font.pixelSize: Math.max(20, Math.round(wsCellH * 0.12)) + font.bold: true; color: Colors.onSurface; opacity: 0.3; z: 5 } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - if (overviewRoot.draggingTargetWorkspace === -1) { - // Only switch workspace, don't close overview - AxctlService.dispatch(`workspace ${workspaceValue}`); - } - } - onDoubleClicked: { - if (overviewRoot.draggingTargetWorkspace === -1) { - // Double click closes overview and switches workspace - Visibilities.setActiveModule(""); - AxctlService.dispatch(`workspace ${workspaceValue}`); - } - } + anchors.fill: parent; acceptedButtons: Qt.LeftButton + onClicked: AxctlService.dispatch("workspace " + wsNum) + onDoubleClicked: { Visibilities.setActiveModule(""); AxctlService.dispatch("workspace " + wsNum); } } - DropArea { anchors.fill: parent - onEntered: { - overviewRoot.draggingTargetWorkspace = workspaceValue; - if (overviewRoot.draggingFromWorkspace == overviewRoot.draggingTargetWorkspace) - return; - hoveredWhileDragging = true; - } - onExited: { - hoveredWhileDragging = false; - if (overviewRoot.draggingTargetWorkspace == workspaceValue) - overviewRoot.draggingTargetWorkspace = -1; - } + onEntered: overviewRoot.draggingTargetWorkspace = wsNum + onExited: { if (overviewRoot.draggingTargetWorkspace === wsNum) overviewRoot.draggingTargetWorkspace = -1; } } } } @@ -297,108 +208,52 @@ Item { } } + // Window overlay Item { - id: windowSpace + id: winLayer anchors.centerIn: parent - implicitWidth: workspaceColumnLayout.implicitWidth - implicitHeight: workspaceColumnLayout.implicitHeight - - // Pre-filter windows for this monitor and workspace group - readonly property var filteredWindowData: { - const minWs = overviewRoot.workspaceGroup * overviewRoot.workspacesShown; - const maxWs = (overviewRoot.workspaceGroup + 1) * overviewRoot.workspacesShown; - const monId = overviewRoot.monitorId; - const toplevels = ToplevelManager.toplevels.values; - - return overviewRoot.windowList.filter(win => { - const wsId = win?.workspace?.id; - return wsId > minWs && wsId <= maxWs && win.monitor === monId; - }).map(win => ({ - windowData: win, - toplevel: (() => { - const cls = win.class || ""; - if (!cls) return null; - const candidates = toplevels.filter(t => t.appId === cls); - if (candidates.length <= 1) return candidates[0] || null; - return candidates.find(t => t.title === (win.title || "")) || candidates[0]; - })() - })); - } + implicitWidth: mainGrid.implicitWidth + implicitHeight: mainGrid.implicitHeight Repeater { - model: windowSpace.filteredWindowData + model: overviewRoot.filteredWindows delegate: OverviewWindow { - id: window + id: win required property var modelData windowData: modelData.windowData toplevel: modelData.toplevel scale: overviewRoot.scale - availableWorkspaceWidth: overviewRoot.workspaceImplicitWidth - availableWorkspaceHeight: overviewRoot.workspaceImplicitHeight - monitorData: overviewRoot.monitorData + availableWorkspaceWidth: overviewRoot.wsCellW + availableWorkspaceHeight: overviewRoot.wsCellH + monitorData: modelData.winMonData || overviewRoot.monitorData barPosition: overviewRoot.barPosition barReserved: overviewRoot.barReserved - // Search highlighting + overviewRootRef: overviewRoot isSearchMatch: overviewRoot.isWindowMatched(windowData?.address) isSearchSelected: overviewRoot.isWindowSelected(windowData?.address) - property int workspaceColIndex: (windowData?.workspace.id - 1) % overviewRoot.columns - property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % overviewRoot.workspacesShown / overviewRoot.columns) - - xOffset: Math.round((overviewRoot.workspaceImplicitWidth + workspacePadding + workspaceSpacing) * workspaceColIndex + workspacePadding / 2) - yOffset: Math.round((overviewRoot.workspaceImplicitHeight + workspacePadding + workspaceSpacing) * workspaceRowIndex + workspacePadding / 2) + // Grid cell offset based on workspace ID + property int wCol: (windowData?.workspace.id - 1) % overviewRoot.columns + property int wRow: Math.floor((windowData?.workspace.id - 1) % overviewRoot.workspacesShown / overviewRoot.columns) + xOffset: Math.round((overviewRoot.wsCellW + workspacePadding + workspaceSpacing) * wCol + workspacePadding / 2) + yOffset: Math.round((overviewRoot.wsCellH + workspacePadding + workspaceSpacing) * wRow + workspacePadding / 2) onDragStarted: overviewRoot.draggingFromWorkspace = windowData?.workspace.id || -1 - onDragFinished: targetWorkspace => { + onDragFinished: function(targetWs) { overviewRoot.draggingFromWorkspace = -1; - if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { - AxctlService.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${windowData?.address}`); + if (targetWs > 0 && targetWs !== windowData?.workspace.id) { + AxctlService.dispatch("movetoworkspacesilent " + targetWs + ",address:" + (windowData?.address || "")); + Qt.callLater(function() { overviewRoot.refreshFromHyprctl(); }); + overviewRoot.forceRefresh(); } } onWindowClicked: { - // Close overview and focus the specific clicked window - // Skip generic focus restoration since we're handling it specifically Visibilities.setActiveModule("", true); - Qt.callLater(() => { - AxctlService.dispatch(`focuswindow address:${windowData.address}`); - }); - } - onWindowClosed: { - AxctlService.dispatch(`closewindow address:${windowData.address}`); - } - } - } - - Rectangle { - id: focusedWorkspaceIndicator - property int activeWorkspaceInGroup: (monitor?.activeWorkspace?.id || 1) - (overviewRoot.workspaceGroup * overviewRoot.workspacesShown) - property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / overviewRoot.columns) - property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % overviewRoot.columns - - x: Math.round((overviewRoot.workspaceImplicitWidth + workspacePadding + workspaceSpacing) * activeWorkspaceColIndex) - y: Math.round((overviewRoot.workspaceImplicitHeight + workspacePadding + workspaceSpacing) * activeWorkspaceRowIndex) - width: Math.round(overviewRoot.workspaceImplicitWidth + workspacePadding) - height: Math.round(overviewRoot.workspaceImplicitHeight + workspacePadding) - color: "transparent" - radius: Styling.radius(2) - border.width: 2 - border.color: overviewRoot.activeBorderColor - - Behavior on x { - enabled: Config.animDuration > 0 - NumberAnimation { - duration: Config.animDuration - easing.type: Easing.OutQuart - } - } - Behavior on y { - enabled: Config.animDuration > 0 - NumberAnimation { - duration: Config.animDuration - easing.type: Easing.OutQuart + Qt.callLater(function() { AxctlService.dispatch("focuswindow address:" + windowData.address); }); } + onWindowClosed: { AxctlService.dispatch("closewindow address:" + windowData.address); } } } } diff --git a/modules/widgets/overview/OverviewWindow.qml b/modules/widgets/overview/OverviewWindow.qml old mode 100644 new mode 100755 index 8943a6ee..10c3ff0a --- a/modules/widgets/overview/OverviewWindow.qml +++ b/modules/widgets/overview/OverviewWindow.qml @@ -28,6 +28,9 @@ Item { property string barPosition: "top" property int barReserved: 0 + // Reference to overview root (for drag state) + property Item overviewRootRef: null + // Search highlighting property bool isSearchMatch: false property bool isSearchSelected: false @@ -41,8 +44,9 @@ Item { readonly property real initX: { if (useOverridePosition && overrideX >= 0) return overrideX; - - let base = (windowData?.at?.[0] || 0) - (monitorData?.x || 0); + // hyprctl clients -j returns ABSOLUTE coordinates + var mx = (monitorData && monitorData.x !== undefined) ? monitorData.x : 0; + var base = (windowData?.at?.[0] || 0) - mx; if (barPosition === "left") base -= barReserved; return Math.round(Math.max(base * scale, 0) + xOffset); @@ -50,17 +54,27 @@ Item { readonly property real initY: { if (useOverridePosition && overrideY >= 0) return overrideY; - let base = (windowData?.at?.[1] || 0) - (monitorData?.y || 0); + // hyprctl clients -j returns ABSOLUTE coordinates + var my = (monitorData && monitorData.y !== undefined) ? monitorData.y : 0; + var base = (windowData?.at?.[1] || 0) - my; if (barPosition === "top") base -= barReserved; return Math.round(Math.max(base * scale, 0) + yOffset); } - readonly property real targetWindowWidth: Math.round((windowData?.size[0] || 100) * scale) - readonly property real targetWindowHeight: Math.round((windowData?.size[1] || 100) * scale) + // Use real window size when available (>200px), otherwise fill 85% of workspace cell + readonly property real targetWindowWidth: Math.round(Math.min( + (windowData?.size[0] > 200 ? windowData.size[0] : Math.round(availableWorkspaceWidth * 0.85 / scale)) * scale, + availableWorkspaceWidth)) + readonly property real targetWindowHeight: Math.round(Math.min( + (windowData?.size[1] > 200 ? windowData.size[1] : Math.round(availableWorkspaceHeight * 0.85 / scale)) * scale, + availableWorkspaceHeight)) readonly property bool compactMode: targetWindowHeight < 60 || targetWindowWidth < 60 readonly property string iconPath: AppSearch.guessIcon(windowData?.class || "") readonly property int calculatedRadius: Styling.radius(-2) + // Drag tracking + property bool _isDragging: false + signal dragStarted signal dragFinished(int targetWorkspace) signal windowClicked @@ -87,11 +101,13 @@ Item { } } - // Watch for windowData changes to reset override when real data updates + // Watch for windowData changes: reset override and sync position onWindowDataChanged: { - if (useOverridePosition) { + if (useOverridePosition) resetOverrideTimer.restart(); - } + // Re-apply position after data refresh (drag.target broke the binding) + x = initX; + y = initY; } Behavior on x { @@ -137,6 +153,23 @@ Item { live: GlobalStates.overviewOpen visible: Config.performance.windowPreview } + + // Retry capture periodically when preview has no content + Timer { + id: retryPreviewTimer + interval: 600 + running: GlobalStates.overviewOpen && Config.performance.windowPreview + repeat: true + onTriggered: { + if (!windowPreview.hasContent && root.toplevel) { + // Toggle capture source to force retry + windowPreview.captureSource = null; + Qt.callLater(function() { + windowPreview.captureSource = Config.performance.windowPreview && GlobalStates.overviewOpen ? root.toplevel : null; + }); + } + } + } } // Background rectangle with rounded corners @@ -144,10 +177,12 @@ Item { id: previewBackground anchors.fill: parent radius: root.calculatedRadius - color: pressed ? Colors.surfaceBright : hovered ? Colors.surface : Colors.background - border.color: root.isSearchSelected ? Colors.tertiary : root.isSearchMatch ? Styling.srItem("overprimary") : Styling.srItem("overprimary") - border.width: root.isSearchSelected ? 3 : root.isSearchMatch ? 2 : (hovered ? 2 : 0) + color: pressed ? Colors.surfaceBright : hovered ? Colors.surface : Colors.surfaceContainer + border.color: root.isSearchSelected ? Colors.tertiary : root.isSearchMatch ? Styling.srItem("overprimary") : Colors.outlineVariant + border.width: root.isSearchSelected ? 3 : root.isSearchMatch ? 2 : (hovered ? 2 : 1) visible: !windowPreview.hasContent || !Config.performance.windowPreview + opacity: !windowPreview.hasContent ? 1.0 : 0.0 + Behavior on opacity { NumberAnimation { duration: 150 } } Behavior on color { enabled: Config.animDuration > 0 @@ -249,120 +284,63 @@ Item { onEntered: { root.hovered = true; - // Only focus window on hover if it's in the current workspace - if (root.windowData) { - // Get current active workspace from AxctlService - let currentWorkspace = AxctlService.focusedMonitor?.activeWorkspace?.id; - let windowWorkspace = root.windowData?.workspace?.id; - - // Only focus if the window is in the current workspace - if (currentWorkspace && windowWorkspace && currentWorkspace === windowWorkspace) { - AxctlService.dispatch(`focuswindow address:${windowData.address}`); - } - } } onExited: root.hovered = false onPressed: mouse => { root.pressed = true; + root._isDragging = false; root.Drag.active = true; root.Drag.source = root; root.dragStarted(); } - onReleased: mouse => { - const overviewRoot = parent.parent.parent.parent; - let targetWorkspace = overviewRoot.draggingTargetWorkspace; + onPositionChanged: { + if (!root._isDragging && root.Drag.active) + root._isDragging = true; + } + onReleased: mouse => { root.pressed = false; root.Drag.active = false; - if (mouse.button === Qt.LeftButton) { - // If targetWorkspace is -1, calculate it from current position - if (targetWorkspace === -1) { - // Calculate which workspace we're over based on position - const workspaceColIndex = Math.floor((root.x - root.xOffset + root.availableWorkspaceWidth / 2) / (root.availableWorkspaceWidth + overviewRoot.workspacePadding + overviewRoot.workspaceSpacing)); - const workspaceRowIndex = Math.floor((root.y - root.yOffset + root.availableWorkspaceHeight / 2) / (root.availableWorkspaceHeight + overviewRoot.workspacePadding + overviewRoot.workspaceSpacing)); - - if (workspaceColIndex >= 0 && workspaceColIndex < overviewRoot.columns && - workspaceRowIndex >= 0 && workspaceRowIndex < overviewRoot.rows) { - targetWorkspace = overviewRoot.workspaceGroup * overviewRoot.workspacesShown + - workspaceRowIndex * overviewRoot.columns + workspaceColIndex + 1; - } else { - // Out of bounds, default to current workspace - targetWorkspace = windowData?.workspace.id; - } + if (mouse.button === Qt.LeftButton && root._isDragging) { + // Calculate target workspace from MOUSE position (more accurate than window pos) + // Mouse in winLayer coordinates = window.pos + mouse.offset + var ov = root.overviewRootRef; + var targetWs = -1; + + if (ov && ov.columns && ov.rows) { + // Mouse position in winLayer coordinates (winLayer == grid coords) + var mx = root.x + mouse.x; + var my = root.y + mouse.y; + // Cell dimensions (grid spacing + cell size + padding) + var cw = root.availableWorkspaceWidth + ov.workspacePadding + ov.workspaceSpacing; + var ch = root.availableWorkspaceHeight + ov.workspacePadding + ov.workspaceSpacing; + // Cell index from mouse position (cells start at padding/2) + var colIdx = Math.floor((mx - ov.workspacePadding / 2) / cw); + var rowIdx = Math.floor((my - ov.workspacePadding / 2) / ch); + + if (colIdx >= 0 && colIdx < ov.columns && rowIdx >= 0 && rowIdx < ov.rows) + targetWs = rowIdx * ov.columns + colIdx + 1; } - root.dragFinished(targetWorkspace); - overviewRoot.draggingTargetWorkspace = -1; - - // Check if moving to different workspace - if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { - // Moving to different workspace - if (windowData?.floating && (root.x !== root.initX || root.y !== root.initY)) { - // Calculate position in the target workspace - // Get target workspace offset - const targetColIndex = (targetWorkspace - 1) % overviewRoot.columns; - const targetRowIndex = Math.floor((targetWorkspace - 1) % overviewRoot.workspacesShown / overviewRoot.columns); - const targetXOffset = Math.round((overviewRoot.workspaceImplicitWidth + overviewRoot.workspacePadding + overviewRoot.workspaceSpacing) * targetColIndex + overviewRoot.workspacePadding / 2); - const targetYOffset = Math.round((overviewRoot.workspaceImplicitHeight + overviewRoot.workspacePadding + overviewRoot.workspaceSpacing) * targetRowIndex + overviewRoot.workspacePadding / 2); - - // Calculate relative position in target workspace - const relativeX = root.x - targetXOffset; - const relativeY = root.y - targetYOffset; - - // Convert to percentage - const percentageX = Math.round((relativeX / root.availableWorkspaceWidth) * 100); - const percentageY = Math.round((relativeY / root.availableWorkspaceHeight) * 100); - - // Move to workspace and set position - AxctlService.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${windowData?.address}`); - AxctlService.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${windowData?.address}`); - - // Force immediate window data update - CompositorData.updateWindowList(); - } else { - // Just move workspace without repositioning - AxctlService.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${windowData?.address}`); - - // Force immediate window data update - CompositorData.updateWindowList(); - } - - // Reset position in overview - root.x = root.initX; - root.y = root.initY; - } else if (windowData?.floating && (root.x !== root.initX || root.y !== root.initY)) { - // Dropped on same workspace and floating - reposition - const relativeX = root.x - root.xOffset; - const relativeY = root.y - root.yOffset; - - const percentageX = Math.round((relativeX / root.availableWorkspaceWidth) * 100); - const percentageY = Math.round((relativeY / root.availableWorkspaceHeight) * 100); - - const draggedX = root.x; - const draggedY = root.y; - - AxctlService.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${windowData?.address}`); - - // Force immediate window data update - CompositorData.updateWindowList(); - - // Set override position for immediate visual update - root.overrideX = draggedX; - root.overrideY = draggedY; - root.useOverridePosition = true; - - root.x = draggedX; - root.y = draggedY; - - resetOverrideTimer.restart(); - } else { - // Reset position for non-floating or non-moved windows - root.x = root.initX; - root.y = root.initY; - } + // If grid calculation failed, try DropArea state + if (targetWs <= 0 && ov) + targetWs = ov.draggingTargetWorkspace; + + // If still nothing, stay on current workspace + if (targetWs <= 0) + targetWs = windowData?.workspace?.id || -1; + + // Signal the delegate handles the move + refresh + root.dragFinished(targetWs); + if (ov) ov.draggingTargetWorkspace = -1; + + // Don't dispatch here — the delegate's onDragFinished handles it + // Just reset visual position (will be updated when data refreshes) + root.x = root.initX; + root.y = root.initY; } } diff --git a/modules/widgets/overview/ScrollingWorkspace.qml b/modules/widgets/overview/ScrollingWorkspace.qml index 3047e559..98e1f490 100644 --- a/modules/widgets/overview/ScrollingWorkspace.qml +++ b/modules/widgets/overview/ScrollingWorkspace.qml @@ -20,7 +20,7 @@ Item { required property real workspaceWidth required property real workspaceHeight required property real workspacePadding - required property real scale_ + property real scale_: 0 // legacy, no longer used (uniformScale is computed from monitor+viewport) required property int monitorId required property var monitorData required property string barPosition @@ -50,43 +50,57 @@ Item { readonly property real viewportWidth: workspaceWidth / 3 readonly property real viewportOffset: viewportWidth // Offset to center third - // Filter windows for this workspace and monitor + // Filter windows for this workspace and monitor. + // Defensive: if workspace or monitor metadata is missing, still show the window. readonly property var workspaceWindows: { return windowList.filter(win => { - return (win && win.workspace ? win.workspace.id : null) === workspaceId && win.monitor === monitorId; + if (!win) return false; + const wsOk = win.workspace?.id === workspaceId || win.workspace?.id === undefined; + const monOk = monitorId < 0 || win.monitor === undefined || win.monitor === monitorId; + return wsOk && monOk; }); } - // Calculate content bounds based on actual window positions - // Windows are positioned relative to monitor, scaled, then offset by viewportOffset + // Monitor effective dimensions for bounds calculation + readonly property real monitorEffW: { + const md = root.monitorData; + if (!md) return 1920; + const ro = (md.transform % 2 === 1); + const mw = ro ? (md.height || 1080) : (md.width || 1920); + return mw > 0 ? mw : 1920; + } + readonly property real monitorEffH: { + const md = root.monitorData; + if (!md) return 1080; + const ro = (md.transform % 2 === 1); + const mh = ro ? (md.width || 1920) : (md.height || 1080); + return mh > 0 ? mh : 1080; + } + + // ── Pure proportion-based content bounds ── readonly property var contentBounds: { if (workspaceWindows.length === 0) { - return { - minX: 0, - maxX: 0, - hasOverflow: false - }; + return { minX: 0, maxX: 0, hasOverflow: false }; } - let minX = Infinity; - let maxX = -Infinity; + const gutter = 0.02; + const evpw = root.viewportWidth * (1 - gutter); + let minX = Infinity, maxX = -Infinity; for (const win of workspaceWindows) { - // Calculate window position the same way as in the delegate - let baseX = ((win && win.at && win.at[0] !== undefined ? win.at[0] : 0) || 0) - ((monitorData && monitorData.x !== undefined ? monitorData.x : 0) || 0); - if (barPosition === "left") - baseX -= barReserved; - const scaledX = baseX * scale_; - const winWidth = ((win && win.size && win.size[0] !== undefined ? win.size[0] : 100) || 100) * scale_; + const mx = (monitorData && monitorData.x !== undefined ? monitorData.x : 0) || 0; + let baseX = ((win && win.at && win.at[0] !== undefined ? win.at[0] : 0) || 0) - mx; + if (barPosition === "left") baseX -= barReserved; + const relX = Math.max(0, Math.min(1, baseX / root.monitorEffW)); + const wSize = (win && win.size && win.size[0] !== undefined ? win.size[0] : 0) || 0; + const relW = wSize > 200 ? Math.max(0.05, Math.min(1, wSize / root.monitorEffW)) : 0.85; + const scaledX = relX * evpw + root.viewportWidth * gutter / 2 + root.viewportOffset; + const winWidth = relW * evpw; minX = Math.min(minX, scaledX); maxX = Math.max(maxX, scaledX + winWidth); } - // The full workspace width is 3x viewport (workspaceWidth = viewportWidth * 3) - // Content in local coords spans from minX to maxX - // The full scrollable area in local coords is [-viewportWidth, 2*viewportWidth] - // Overflow exists only if content extends beyond the full workspace width const hasOverflow = minX < -viewportWidth || maxX > (viewportWidth * 2); return { @@ -184,6 +198,18 @@ Item { color: Colors.background opacity: 0.3 } + + // Workspace number label + Text { + anchors.centerIn: parent + text: String(root.workspaceId) + font.family: Config.theme.font + font.pixelSize: Math.max(24, Math.round(workspaceHeight * 0.15)) + font.bold: true + color: Colors.onSurface + opacity: 0.5 + z: 5 + } } // Border indicator for drag target @@ -201,6 +227,7 @@ Item { id: windowsContainer anchors.fill: parent anchors.margins: root.workspacePadding + clip: true // Horizontal scroll handler - right-click drag MouseArea { @@ -266,7 +293,10 @@ Item { TapHandler { acceptedButtons: Qt.LeftButton onDoubleTapped: { - AxctlService.dispatch(`workspace ${root.workspaceId}`); + if (root.overviewRoot && root.overviewRoot.wsProcess) { + root.overviewRoot.wsProcess.command = ["hyprctl", "dispatch", "workspace", String(root.workspaceId)]; + root.overviewRoot.wsProcess.running = true; + } Visibilities.setActiveModule("", true); } } @@ -284,8 +314,16 @@ Item { const cls = windowData.class || ""; if (!cls) return null; const candidates = toplevels.filter(t => t.appId === cls); - if (candidates.length <= 1) return candidates[0] || null; - return candidates.find(t => t.title === (windowData.title || "")) || candidates[0]; + if (candidates.length === 0) return null; + // Try exact title match first + const titleMatch = candidates.find(t => t.title === (windowData.title || "")); + if (titleMatch) return titleMatch; + // Try partial title match + const wt = (windowData.title || "").toLowerCase(); + const partial = candidates.find(t => { const tt = (t.title || "").toLowerCase(); return wt.includes(tt) || tt.includes(wt); }); + if (partial) return partial; + // Return null to avoid same-class windows sharing a toplevel + return null; } // Override position tracking for immediate visual update @@ -293,25 +331,111 @@ Item { property real overrideBaseY: -1 property bool useOverridePosition: false - // Position calculations relative to center viewport + readonly property real viewportWidth: root.viewportWidth + readonly property real viewportHeight: root.workspaceHeight - root.workspacePadding * 2 + + // ── Pure proportion-based coordinates (0.0..1.0) ── + readonly property real gutter: 0.02 + readonly property real effectiveVpW: viewportWidth * (1 - gutter) + readonly property real effectiveVpH: viewportHeight * (1 - gutter) + + readonly property real relX: { + const mx = monitorData?.x ?? 0; + let base = (windowData?.at?.[0] ?? 0) - mx; + if (barPosition === "left") base -= barReserved; + return Math.max(0, Math.min(1, root.monitorEffW > 0 ? base / root.monitorEffW : 0)); + } + readonly property real relY: { + const my = monitorData?.y ?? 0; + let base = (windowData?.at?.[1] ?? 0) - my; + if (barPosition === "top") base -= barReserved; + return Math.max(0, Math.min(1, root.monitorEffH > 0 ? base / root.monitorEffH : 0)); + } + readonly property real relW: { + var w = windowData?.size?.[0] ?? 0; + return w > 200 && root.monitorEffW > 0 + ? Math.max(0.05, Math.min(1, w / root.monitorEffW)) + : 0.85; + } + readonly property real relH: { + var h = windowData?.size?.[1] ?? 0; + return h > 200 && root.monitorEffH > 0 + ? Math.max(0.05, Math.min(1, h / root.monitorEffH)) + : 0.85; + } + // Fill dimensions: extend to neighbor without overlapping. + // Must match the same coordinate system as relX/relY + // (bar-adjusted, rotation-aware) for consistency. + readonly property real fillW: { + var neighbors = root.workspaceWindows; + if (!neighbors || neighbors.length <= 1) return relW; + var mx = monitorData?.x ?? 0; + var my = monitorData?.y ?? 0; + var ax = (windowData?.at?.[0] ?? 0) - mx; + var ay = (windowData?.at?.[1] ?? 0) - my; + if (barPosition === "left") ax -= barReserved; + if (barPosition === "top") ay -= barReserved; + var aw = windowData?.size?.[0] ?? root.monitorEffW; + var ah = windowData?.size?.[1] ?? root.monitorEffH; + var limit = root.monitorEffW - (barPosition === "left" || barPosition === "right" ? barReserved : 0); + for (var n = 0; n < neighbors.length; n++) { + var nb = neighbors[n]; + if (!nb || nb.address === (windowData?.address ?? "")) continue; + var bx = (nb.at?.[0] ?? 0) - mx; + var by = (nb.at?.[1] ?? 0) - my; + if (barPosition === "left") bx -= barReserved; + if (barPosition === "top") by -= barReserved; + var bw = nb.size?.[0] ?? root.monitorEffW; + var bh = nb.size?.[1] ?? root.monitorEffH; + var nbContained = (bx >= ax && by >= ay && bx + bw <= ax + aw && by + bh <= ay + ah); + if (!nbContained && bx > ax && by < ay + ah && by + bh > ay) + limit = Math.min(limit, bx); + } + var effW = root.monitorEffW - (barPosition === "left" || barPosition === "right" ? barReserved : 0); + var neighborW = effW > 0 ? (limit - ax) / effW : 1; + return Math.max(relW, Math.max(0.05, Math.min(1, neighborW))); + } + readonly property real fillH: { + var neighbors = root.workspaceWindows; + if (!neighbors || neighbors.length <= 1) return relH; + var mx = monitorData?.x ?? 0; + var my = monitorData?.y ?? 0; + var ax = (windowData?.at?.[0] ?? 0) - mx; + var ay = (windowData?.at?.[1] ?? 0) - my; + if (barPosition === "left") ax -= barReserved; + if (barPosition === "top") ay -= barReserved; + var aw = windowData?.size?.[0] ?? root.monitorEffW; + var ah = windowData?.size?.[1] ?? root.monitorEffH; + var limit = root.monitorEffH - (barPosition === "top" || barPosition === "bottom" ? barReserved : 0); + for (var n = 0; n < neighbors.length; n++) { + var nb = neighbors[n]; + if (!nb || nb.address === (windowData?.address ?? "")) continue; + var bx = (nb.at?.[0] ?? 0) - mx; + var by = (nb.at?.[1] ?? 0) - my; + if (barPosition === "left") bx -= barReserved; + if (barPosition === "top") by -= barReserved; + var bw = nb.size?.[0] ?? root.monitorEffW; + var bh = nb.size?.[1] ?? root.monitorEffH; + var nbContained = (bx >= ax && by >= ay && bx + bw <= ax + aw && by + bh <= ay + ah); + if (!nbContained && by > ay && bx < ax + aw && bx + bw > ax) + limit = Math.min(limit, by); + } + var effH = root.monitorEffH - (barPosition === "top" || barPosition === "bottom" ? barReserved : 0); + var neighborH = effH > 0 ? (limit - ay) / effH : 1; + return Math.max(relH, Math.max(0.05, Math.min(1, neighborH))); + } + readonly property real baseX: { - if (useOverridePosition && overrideBaseX >= 0) - return overrideBaseX; - let base = ((windowData && windowData.at && windowData.at[0] !== undefined ? windowData.at[0] : 0) || 0) - ((monitorData && monitorData.x !== undefined ? monitorData.x : 0) || 0); - if (barPosition === "left") - base -= barReserved; - return (base * scale_) + root.viewportOffset + root.horizontalScrollOffset; + if (useOverridePosition && overrideBaseX >= 0) return overrideBaseX; + return Math.round(relX * effectiveVpW + viewportWidth * gutter / 2 + root.viewportOffset + root.horizontalScrollOffset); } readonly property real baseY: { - if (useOverridePosition && overrideBaseY >= 0) - return overrideBaseY; - let base = ((windowData && windowData.at && windowData.at[1] !== undefined ? windowData.at[1] : 0) || 0) - ((monitorData && monitorData.y !== undefined ? monitorData.y : 0) || 0); - if (barPosition === "top") - base -= barReserved; - return Math.max(base * scale_, 0); + if (useOverridePosition && overrideBaseY >= 0) return overrideBaseY; + return Math.round(relY * effectiveVpH + viewportHeight * gutter / 2); } - readonly property real targetWidth: Math.round(((windowData && windowData.size && windowData.size[0] !== undefined ? windowData.size[0] : 100) || 100) * scale_) - readonly property real targetHeight: Math.round(((windowData && windowData.size && windowData.size[1] !== undefined ? windowData.size[1] : 100) || 100) * scale_) + + readonly property real targetWidth: Math.max(24, Math.round(fillW * effectiveVpW)) + readonly property real targetHeight: Math.max(24, Math.round(fillH * effectiveVpH)) readonly property bool compactMode: targetHeight < 60 || targetWidth < 60 readonly property string iconPath: AppSearch.guessIcon((windowData && windowData.class !== undefined ? windowData.class : "") || "") readonly property int calculatedRadius: Styling.radius(-2) @@ -332,10 +456,30 @@ Item { property point pressPos: Qt.point(0, 0) readonly property real dragThreshold: 5 - Drag.active: dragging - Drag.source: windowDelegate - Drag.hotSpot.x: width / 2 - Drag.hotSpot.y: height / 2 + // Entry / hover / close animations + property bool _entered: false + property bool _closing: false + Component.onCompleted: _entered = true + + readonly property real hoverScale: !dragging && hovered && !_closing ? 1.03 : 1.0 + scale: _closing ? 0.3 : (_entered ? hoverScale : 0.85) + + Behavior on scale { + enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 + NumberAnimation { + duration: _closing ? (Config.animDuration !== undefined ? Config.animDuration : 200) * 0.4 : (Config.animDuration !== undefined ? Config.animDuration : 200) * 0.6 + easing.type: _closing ? Easing.InBack : Easing.OutBack + } + } + + opacity: _closing ? 0.0 : (_entered ? 1.0 : 0.0) + Behavior on opacity { + enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 + NumberAnimation { + duration: _closing ? (Config.animDuration !== undefined ? Config.animDuration : 200) * 0.3 : (Config.animDuration !== undefined ? Config.animDuration : 200) * 0.4 + easing.type: Easing.OutQuart + } + } // Timer to reset override position after AxctlService update Timer { @@ -353,161 +497,164 @@ Item { } } - Behavior on x { - enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 && !windowDelegate.dragging && !windowDelegate.useOverridePosition - NumberAnimation { - duration: (Config.animDuration !== undefined ? Config.animDuration : 0) - easing.type: Easing.OutQuart - } - } - Behavior on y { - enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 && !windowDelegate.dragging && !windowDelegate.useOverridePosition - NumberAnimation { - duration: (Config.animDuration !== undefined ? Config.animDuration : 0) - easing.type: Easing.OutQuart - } - } + // ═══════════════════════════════════════════════════ + // VISUAL: Clean dark card, no white background + // ═══════════════════════════════════════════════════ + // ── Live window preview: render at source size, Scale to fill card ── ClippingRectangle { + id: swClipRect anchors.fill: parent radius: windowDelegate.calculatedRadius antialiasing: true color: "transparent" - border.color: Colors.background + border.color: "transparent" border.width: 0 ScreencopyView { id: windowPreview - anchors.fill: parent + width: Math.max(1, (windowData && windowData.size && windowData.size[0] !== undefined ? windowData.size[0] : 0) || 640) + height: Math.max(1, (windowData && windowData.size && windowData.size[1] !== undefined ? windowData.size[1] : 0) || 480) captureSource: Config.performance.windowPreview && GlobalStates.overviewOpen ? windowDelegate.toplevel : null live: GlobalStates.overviewOpen visible: Config.performance.windowPreview + + transform: Scale { + origin.x: 0; origin.y: 0 + xScale: swClipRect.width / windowPreview.width + yScale: swClipRect.height / windowPreview.height + } } } - // Background when no preview + // ── Dark fallback card ── Rectangle { - id: previewBackground + id: fallbackCard anchors.fill: parent radius: windowDelegate.calculatedRadius - color: windowDelegate.dragging ? Colors.surfaceBright : windowDelegate.hovered ? Colors.surface : Colors.background - border.color: windowDelegate.isSelected ? Colors.tertiary : windowDelegate.isMatched ? Styling.srItem("overprimary") : Styling.srItem("overprimary") - border.width: windowDelegate.isSelected ? 3 : windowDelegate.isMatched ? 2 : (windowDelegate.hovered ? 2 : 0) - visible: !Config.performance.windowPreview - - Behavior on color { + color: Qt.rgba(Colors.surfaceContainer.r, Colors.surfaceContainer.g, Colors.surfaceContainer.b, 0.35) + visible: !windowPreview.hasContent || !Config.performance.windowPreview + border.color: windowDelegate.isSelected ? Colors.tertiary + : windowDelegate.isMatched ? Styling.srItem("overprimary") + : windowDelegate.hovered ? Qt.rgba(Colors.onSurface.r, Colors.onSurface.g, Colors.onSurface.b, 0.25) + : Qt.rgba(Colors.onSurface.r, Colors.onSurface.g, Colors.onSurface.b, 0.10) + border.width: windowDelegate.isSelected ? 2 : windowDelegate.isMatched ? 2 : 1 + Behavior on border.color { enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 - ColorAnimation { - duration: (Config.animDuration !== undefined ? Config.animDuration : 0) / 2 - } + ColorAnimation { duration: (Config.animDuration !== undefined ? Config.animDuration : 0) / 2 } } } - // Icon + // ── App icon ── Image { mipmap: true id: windowIcon - readonly property real iconSize: Math.round(Math.min(windowDelegate.targetWidth, windowDelegate.targetHeight) * (windowDelegate.compactMode ? 0.6 : 0.35)) + readonly property real iconSize: Math.round(Math.min(windowDelegate.targetWidth, windowDelegate.targetHeight) * (windowDelegate.compactMode ? 0.55 : 0.30)) anchors.centerIn: parent width: iconSize height: iconSize source: Quickshell.iconPath(windowDelegate.iconPath, "image-missing") sourceSize: Qt.size(iconSize, iconSize) asynchronous: true - visible: !Config.performance.windowPreview - z: 10 + visible: !windowPreview.hasContent || !Config.performance.windowPreview + opacity: 0.7 + z: 2 } - // Overlay when preview is available (only show on interaction) + // ── Hover / selection border ── Rectangle { - id: previewOverlay + id: borderOverlay anchors.fill: parent radius: windowDelegate.calculatedRadius - color: windowDelegate.dragging ? Qt.rgba(Colors.surfaceContainerHighest.r, Colors.surfaceContainerHighest.g, Colors.surfaceContainerHighest.b, 0.5) : windowDelegate.hovered ? Qt.rgba(Colors.surfaceContainer.r, Colors.surfaceContainer.g, Colors.surfaceContainer.b, 0.2) : "transparent" - border.color: windowDelegate.isSelected ? Colors.tertiary : windowDelegate.isMatched ? Styling.srItem("overprimary") : Styling.srItem("overprimary") + color: "transparent" + border.color: windowDelegate.isSelected ? Colors.tertiary + : windowDelegate.isMatched ? Styling.srItem("overprimary") + : windowDelegate.hovered ? Styling.srItem("overprimary") + : "transparent" border.width: windowDelegate.isSelected ? 3 : windowDelegate.isMatched ? 2 : (windowDelegate.hovered ? 2 : 0) - visible: Config.performance.windowPreview && (windowDelegate.hovered || windowDelegate.dragging || windowDelegate.isMatched || windowDelegate.isSelected) - z: 5 + z: 3 + Behavior on border.color { + enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 + ColorAnimation { duration: (Config.animDuration !== undefined ? Config.animDuration : 0) / 2 } + } + Behavior on border.width { + enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 + NumberAnimation { duration: (Config.animDuration !== undefined ? Config.animDuration : 0) / 2 } + } } - // Corner icon when preview available + // ── Hover tint ── + Rectangle { + anchors.fill: parent + radius: windowDelegate.calculatedRadius + color: windowDelegate.dragging ? Qt.rgba(1, 1, 1, 0.10) : windowDelegate.hovered ? Qt.rgba(1, 1, 1, 0.05) : "transparent" + z: 1 + Behavior on color { + enabled: (Config.animDuration !== undefined ? Config.animDuration : 0) > 0 + ColorAnimation { duration: (Config.animDuration !== undefined ? Config.animDuration : 0) / 2 } + } + } + + // ── Corner icon (when preview active) ── Image { mipmap: true visible: windowPreview.hasContent && !windowDelegate.compactMode && Config.performance.windowPreview anchors.bottom: parent.bottom anchors.right: parent.right - anchors.margins: 4 - width: 16 - height: 16 + anchors.margins: 3 + width: 14 + height: 14 source: Quickshell.iconPath(windowDelegate.iconPath, "image-missing") - sourceSize: Qt.size(16, 16) + sourceSize: Qt.size(14, 14) asynchronous: true - opacity: 0.8 - z: 10 + opacity: 0.6 + z: 4 } - // XWayland indicator + // ── XWayland indicator ── Rectangle { visible: (windowDelegate.windowData && windowDelegate.windowData.xwayland !== undefined ? windowDelegate.windowData.xwayland : false) || false anchors.top: parent.top anchors.right: parent.right anchors.margins: 2 - width: 6 - height: 6 + width: 5 + height: 5 radius: 3 color: Colors.error - z: 10 + z: 4 } + // ═══════════════════════════════════════════════════════ + // RIGHT-CLICK DRAG TO MOVE WINDOW BETWEEN WORKSPACES + // Left clicks pass through to workspace cells below. + // ═══════════════════════════════════════════════════════ MouseArea { id: dragArea anchors.fill: parent hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton - drag.target: windowDelegate.dragging ? windowDelegate : null - drag.threshold: 0 - - // Right-click drag state for horizontal scroll - property real rightDragStartX: 0 - property real rightScrollStartOffset: 0 + acceptedButtons: Qt.RightButton onEntered: windowDelegate.hovered = true onExited: windowDelegate.hovered = false onPressed: mouse => { - if (mouse.button === Qt.LeftButton) { - windowDelegate.pressPos = Qt.point(mouse.x, mouse.y); - windowDelegate.initX = windowDelegate.x; - windowDelegate.initY = windowDelegate.y; - } else if (mouse.button === Qt.RightButton && root.contentBounds.hasOverflow) { - rightDragStartX = mouse.x; - rightScrollStartOffset = root.horizontalScrollOffset; - root.isScrollDragging = true; - } + if (mouse.button !== Qt.RightButton) return; + windowDelegate.pressPos = Qt.point(mouse.x, mouse.y); + windowDelegate.initX = windowDelegate.x; + windowDelegate.initY = windowDelegate.y; } onPositionChanged: mouse => { - // Handle right-click drag for horizontal scroll - if (root.isScrollDragging && (mouse.buttons & Qt.RightButton) && root.contentBounds.hasOverflow) { - const delta = mouse.x - rightDragStartX; - root.horizontalScrollOffset = root.clampHorizontalScroll(rightScrollStartOffset + delta); + if (!(mouse.buttons & Qt.RightButton)) return; - } - if (!(mouse.buttons & Qt.LeftButton)) - return; - - // Check if we should start dragging if (!windowDelegate.dragging) { const dx = mouse.x - windowDelegate.pressPos.x; const dy = mouse.y - windowDelegate.pressPos.y; const distance = Math.sqrt(dx * dx + dy * dy); - if (distance > windowDelegate.dragThreshold) { - // Start dragging windowDelegate.dragging = true; root.draggingFromWorkspace = root.workspaceId; - // Reparent to drag overlay if (root.dragOverlay) { windowDelegate.originalParent = windowDelegate.parent; @@ -516,191 +663,43 @@ Item { windowDelegate.x = globalPos.x; windowDelegate.y = globalPos.y; } + } else { + return; } - } else { - // Update target workspace indicator while dragging - if (root.overviewRoot && root.overviewRoot.getWorkspaceAtY) { - const globalPos = dragArea.mapToItem(null, mouse.x, mouse.y); - const targetWs = root.overviewRoot.getWorkspaceAtY(globalPos.y); - if (targetWs !== -1 && targetWs !== root.workspaceId) { - root.draggingTargetWorkspace = targetWs; - } else { - root.draggingTargetWorkspace = -1; - } - } + } + + // Update target workspace while dragging + if (root.overviewRoot && root.overviewRoot.getWorkspaceAtY) { + const globalPos = dragArea.mapToItem(null, mouse.x, mouse.y); + const targetWs = root.overviewRoot.getWorkspaceAtY(globalPos.y); + root.draggingTargetWorkspace = (targetWs !== -1 && targetWs !== root.workspaceId) ? targetWs : -1; } } onReleased: mouse => { - if (mouse.button === Qt.LeftButton) { - if (windowDelegate.dragging) { - windowDelegate.dragging = false; - - // Calculate target workspace from cursor position - let targetWs = root.workspaceId; // Default to current workspace - if (root.overviewRoot && root.overviewRoot.getWorkspaceAtY) { - const globalPos = dragArea.mapToItem(null, mouse.x, mouse.y); - const calculatedWs = root.overviewRoot.getWorkspaceAtY(globalPos.y); - if (calculatedWs !== -1) { - targetWs = calculatedWs; - } - } + if (mouse.button !== Qt.RightButton) return; - if (targetWs !== root.workspaceId) { - // Moving to different workspace - if ((windowDelegate.windowData && windowDelegate.windowData.floating !== undefined ? windowDelegate.windowData.floating : false)) { - // Calculate position for floating window in target workspace - const draggedX = windowDelegate.x; - const draggedY = windowDelegate.y; - - const workspaceGlobalPos = windowsContainer.mapToItem(root.dragOverlay, 0, 0); - const relativeX = draggedX - workspaceGlobalPos.x; - const relativeY = draggedY - workspaceGlobalPos.y; - - const workspaceX = relativeX - root.horizontalScrollOffset - root.viewportOffset; - const workspaceY = relativeY; - - const monitorWidth = ((monitorData && monitorData.width !== undefined ? monitorData.width : 1920) || 1920) / ((monitorData && monitorData.scale !== undefined ? monitorData.scale : 1.0) || 1.0); - const monitorHeight = ((monitorData && monitorData.height !== undefined ? monitorData.height : 1080) || 1080) / ((monitorData && monitorData.scale !== undefined ? monitorData.scale : 1.0) || 1.0); - - let adjustedMonitorWidth = monitorWidth; - let adjustedMonitorHeight = monitorHeight; - if (barPosition === "left" || barPosition === "right") { - adjustedMonitorWidth -= barReserved; - } - if (barPosition === "top" || barPosition === "bottom") { - adjustedMonitorHeight -= barReserved; - } - - const actualX = workspaceX / scale_; - const actualY = workspaceY / scale_; - - const percentageX = Math.round((actualX / adjustedMonitorWidth) * 100); - const percentageY = Math.round((actualY / adjustedMonitorHeight) * 100); - - // Move to workspace and set position - AxctlService.dispatch(`movetoworkspacesilent ${targetWs}, address:${(windowDelegate.windowData && windowDelegate.windowData.address !== undefined ? windowDelegate.windowData.address : "")}`); - AxctlService.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${(windowDelegate.windowData && windowDelegate.windowData.address !== undefined ? windowDelegate.windowData.address : "")}`); - - // Force immediate window data update - CompositorData.updateWindowList(); - } else { - // Just move workspace without repositioning for tiled windows - AxctlService.dispatch(`movetoworkspacesilent ${targetWs}, address:${(windowDelegate.windowData && windowDelegate.windowData.address !== undefined ? windowDelegate.windowData.address : "")}`); - - // Force immediate window data update - CompositorData.updateWindowList(); - } - - // Restore original parent and reset position - if (windowDelegate.originalParent) { - windowDelegate.parent = windowDelegate.originalParent; - windowDelegate.originalParent = null; - } - windowDelegate.x = windowDelegate.initX; - windowDelegate.y = windowDelegate.initY; - - } else if ((windowDelegate.windowData && windowDelegate.windowData.floating !== undefined ? windowDelegate.windowData.floating : false) && (windowDelegate.x !== windowDelegate.initX || windowDelegate.y !== windowDelegate.initY)) { - // Dropped on same workspace and window is floating - reposition it - // The window is currently in the drag overlay with global coordinates - - // Store current drag position - const draggedX = windowDelegate.x; - const draggedY = windowDelegate.y; - - // Get the workspace container position - const workspaceGlobalPos = windowsContainer.mapToItem(root.dragOverlay, 0, 0); - - // Calculate position relative to workspace - const relativeX = draggedX - workspaceGlobalPos.x; - const relativeY = draggedY - workspaceGlobalPos.y; - - // Remove horizontal scroll offset to get actual position in workspace - const workspaceX = relativeX - root.horizontalScrollOffset - root.viewportOffset; - const workspaceY = relativeY; - - // Convert to percentage of workspace dimensions (in scaled space) - const monitorWidth = ((monitorData && monitorData.width !== undefined ? monitorData.width : 1920) || 1920) / ((monitorData && monitorData.scale !== undefined ? monitorData.scale : 1.0) || 1.0); - const monitorHeight = ((monitorData && monitorData.height !== undefined ? monitorData.height : 1080) || 1080) / ((monitorData && monitorData.scale !== undefined ? monitorData.scale : 1.0) || 1.0); - - // Adjust for bar reserved space - let adjustedMonitorWidth = monitorWidth; - let adjustedMonitorHeight = monitorHeight; - if (barPosition === "left" || barPosition === "right") { - adjustedMonitorWidth -= barReserved; - } - if (barPosition === "top" || barPosition === "bottom") { - adjustedMonitorHeight -= barReserved; - } - - // Convert from scaled overview space to actual position - const actualX = workspaceX / scale_; - const actualY = workspaceY / scale_; - - // Calculate percentage - const percentageX = Math.round((actualX / adjustedMonitorWidth) * 100); - const percentageY = Math.round((actualY / adjustedMonitorHeight) * 100); - - // Dispatch movewindowpixel command - AxctlService.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${(windowDelegate.windowData && windowDelegate.windowData.address !== undefined ? windowDelegate.windowData.address : "")}`); - - // Force immediate window data update - CompositorData.updateWindowList(); - - // Restore original parent - if (windowDelegate.originalParent) { - windowDelegate.parent = windowDelegate.originalParent; - windowDelegate.originalParent = null; - } - - // Set override position for immediate visual update - // Calculate what baseX/baseY should be at the dropped position - windowDelegate.overrideBaseX = relativeX; - windowDelegate.overrideBaseY = relativeY; - windowDelegate.useOverridePosition = true; - - // Force position to dropped location - windowDelegate.x = relativeX; - windowDelegate.y = relativeY; - - // Start timer to clear override - resetOverrideTimer.restart(); - } else { - // Not a floating window or didn't move - restore original parent and position - if (windowDelegate.originalParent) { - windowDelegate.parent = windowDelegate.originalParent; - windowDelegate.originalParent = null; - } - windowDelegate.x = windowDelegate.initX; - windowDelegate.y = windowDelegate.initY; - } + if (windowDelegate.dragging) { + windowDelegate.dragging = false; + const targetWs = root.draggingTargetWorkspace !== -1 ? root.draggingTargetWorkspace : root.workspaceId; - root.draggingFromWorkspace = -1; - root.draggingTargetWorkspace = -1; + if (targetWs !== root.workspaceId && windowDelegate.windowData) { + AxctlService.dispatch(`movetoworkspacesilent ${targetWs}, address:${windowDelegate.windowData.address || ""}`); + Qt.callLater(function() { CompositorData.refreshFromHyprctl(); }); } - } else if (mouse.button === Qt.RightButton) { - root.isScrollDragging = false; - } - } - onClicked: mouse => { - if (!windowDelegate.windowData) - return; - if (mouse.button === Qt.LeftButton && !windowDelegate.dragging) { - AxctlService.dispatch(`focuswindow address:${windowDelegate.windowData.address}`); - } else if (mouse.button === Qt.MiddleButton) { - AxctlService.dispatch(`closewindow address:${windowDelegate.windowData.address}`); - } - } + // Restore original parent and re-bind position. + // Setting x/y directly breaks the baseX/baseY bindings; + // Qt.binding restores them so the thumbnail follows window data. + if (windowDelegate.originalParent) { + windowDelegate.parent = windowDelegate.originalParent; + windowDelegate.originalParent = null; + } + windowDelegate.x = Qt.binding(function() { return windowDelegate.baseX; }); + windowDelegate.y = Qt.binding(function() { return windowDelegate.baseY; }); - onDoubleClicked: mouse => { - if (!windowDelegate.windowData) - return; - if (mouse.button === Qt.LeftButton) { - Visibilities.setActiveModule("", true); - Qt.callLater(() => { - AxctlService.dispatch(`focuswindow address:${windowDelegate.windowData.address}`); - }); + root.draggingFromWorkspace = -1; + root.draggingTargetWorkspace = -1; } } }