From a0192ee28093917cc36430fc2ea2d7090bae5207 Mon Sep 17 00:00:00 2001 From: Callie Peden Date: Tue, 7 Jul 2026 15:30:22 -0400 Subject: [PATCH] extract workspace/window pill strip into WindowList.qml --- Bar.qml | 471 +----------------------------------------------- WindowList.qml | 478 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 484 insertions(+), 465 deletions(-) create mode 100644 WindowList.qml diff --git a/Bar.qml b/Bar.qml index 3581ec9..082e647 100644 --- a/Bar.qml +++ b/Bar.qml @@ -1,7 +1,6 @@ import QtQuick 2.15 import Quickshell import Quickshell.Io -import Quickshell.Wayland import Quickshell.Services.SystemTray import Quickshell.Services.Pipewire @@ -26,414 +25,6 @@ PanelWindow { top: true } - property var activeWorkspace: niriWorkspacesList.find( - function(ws) { return ws.output === root.screen.name && ws.is_active }) || null - - // shared font metrics for measuring pill widths so the highlight knows where to slide - FontMetrics { - id: pillFm - font.family: "Inter" - font.pixelSize: 13 - font.weight: Font.Bold - } - - function pillWidth(appId) { - return pillFm.advanceWidth(appId ?? "") + 16 - } - - readonly property int stripHeight: 26 - - // What workspace's pills are currently shown in the "current" slot of the strip stack. - // Lags activeWorkspace.id during a slide, so the strip can render the OLD state - // while sliding to reveal the NEW workspace (rendered in the above/below slot). - property int displayedWorkspaceId: -1 - - property var displayedWorkspace: niriWorkspacesList.find( - function(ws) { return ws.id === displayedWorkspaceId }) || null - - property int aboveAdjWorkspaceId: { - if (!displayedWorkspace) return -1 - var ws = niriWorkspacesList.find(function(w) { - return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx - 1 - }) - return ws ? ws.id : -1 - } - - property int belowAdjWorkspaceId: { - if (!displayedWorkspace) return -1 - var ws = niriWorkspacesList.find(function(w) { - return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx + 1 - }) - return ws ? ws.id : -1 - } - - // Vertical translation of the strip stack during a transition. - // Steady state = 0. +stripHeight = above slot in view. -stripHeight = below slot in view. - property real transitionOffset: 0 - - Component.onCompleted: { - if (activeWorkspace) displayedWorkspaceId = activeWorkspace.id - } - - // Drive the slide animation from displayedWorkspaceId toward activeWorkspace.id. - // If a slide is already in flight, leave it alone; onStopped will re-invoke us to - // chain a new slide from the just-reached workspace to whatever the latest active - // workspace is. That way rapid switches turn into a sequence of slides instead of - // a single slide followed by a snap-cut. - function syncWorkspaceAnim() { - if (!activeWorkspace) return - var newId = activeWorkspace.id - if (displayedWorkspaceId === -1) { - displayedWorkspaceId = newId - return - } - if (displayedWorkspaceId === newId) return - if (slideAnim.running) return - var oldWs = niriWorkspacesList.find(function(ws) { return ws.id === displayedWorkspaceId }) - if (!oldWs || oldWs.output !== activeWorkspace.output) { - displayedWorkspaceId = newId - return - } - // niri slides screen DOWN when focusing workspace UP -> stack offset goes +. - slideAnim.goingUp = activeWorkspace.idx < oldWs.idx - slideAnim.targetWsId = newId - slideAnim.start() - } - - onActiveWorkspaceChanged: syncWorkspaceAnim() - - NumberAnimation { - id: slideAnim - target: root - property: "transitionOffset" - duration: 200 - easing.type: Easing.OutCubic - property bool goingUp: false - // The workspace this slide is animating *to*. Tracked explicitly so onStopped - // doesn't snap straight to the latest activeWorkspace (which can race ahead - // mid-flight during rapid switching). - property int targetWsId: -1 - to: goingUp ? root.stripHeight : -root.stripHeight - onStopped: { - // Land on this slide's intended target, then re-evaluate: if the active - // workspace has moved further while we were animating, syncWorkspaceAnim - // kicks off the next slide from here. - root.displayedWorkspaceId = targetWsId - root.transitionOffset = 0 - root.syncWorkspaceAnim() - } - } - - function windowsForWorkspace(wsId) { - var wins = niriWindowsList.filter(function(w) { return w.workspace_id === wsId }) - wins.sort(function(a, b) { - var ap = a.layout && a.layout.pos_in_scrolling_layout - var bp = b.layout && b.layout.pos_in_scrolling_layout - if (ap && bp) return ap[0] !== bp[0] ? ap[0] - bp[0] : ap[1] - bp[1] - return 0 - }) - return wins - } - - // workspaces above a given workspace, farthest -> closest - function workspacesAbove(wsId) { - var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) - if (!ws) return [] - return niriWorkspacesList - .filter(function(w) { return w.output === ws.output && w.idx < ws.idx }) - .sort(function(a, b) { return a.idx - b.idx }) - } - - // workspaces below a given workspace, closest -> farthest - function workspacesBelow(wsId) { - var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) - if (!ws) return [] - return niriWorkspacesList - .filter(function(w) { return w.output === ws.output && w.idx > ws.idx }) - .sort(function(a, b) { return a.idx - b.idx }) - } - - Process { - id: focusWindowProcess - property int targetId: 0 - command: ["niri", "msg", "action", "focus-window", "--id", targetId.toString()] - } - - Process { - id: focusWorkspaceUp - command: ["niri", "msg", "action", "focus-workspace-up"] - } - - Process { - id: focusWorkspaceDown - command: ["niri", "msg", "action", "focus-workspace-down"] - } - - // A full duplicate of the bar's left section, rendered for a given workspace. - // Includes: bg + focus highlight + current-workspace pills + ↑ + above pills + ↓ + below pills. - // Three instances live in the clipped viewport (above / current / below) and slide vertically - // on workspace change. - Component { - id: leftSectionComp - - Item { - id: section - property int displayWsId: -1 - property var displayWs: niriWorkspacesList.find(function(ws) { return ws.id === displayWsId }) || null - property bool isCurrent: displayWsId === (root.activeWorkspace ? root.activeWorkspace.id : -1) - - // Briefly suppresses the highlight's slide Behavior whenever this section's - // workspace changes (e.g., the Loader rebinds after a slide ends). Without this, - // the highlight would animate from the OLD workspace's focused position to the - // NEW one's across now-different pills, producing the "second" animation. - property bool _allowHighlightAnim: true - onDisplayWsIdChanged: { - _allowHighlightAnim = false - Qt.callLater(function() { section._allowHighlightAnim = true }) - } - - // pills for the workspace this section represents - property var currentPills: { - if (displayWsId < 0) return [] - return windowsForWorkspace(displayWsId).map(function(w) { - return Object.assign({}, w, {inViewport: w.is_focused, onCurrentWorkspace: section.isCurrent}) - }) - } - - // flat list of windows from all workspaces above this section's workspace - property var aboveWinsList: { - if (!displayWs) return [] - var result = [] - workspacesAbove(displayWsId).forEach(function(ws) { - result = result.concat(windowsForWorkspace(ws.id)) - }) - return result - } - - // flat list of windows from all workspaces below this section's workspace - property var belowWinsList: { - if (!displayWs) return [] - var result = [] - workspacesBelow(displayWsId).forEach(function(ws) { - result = result.concat(windowsForWorkspace(ws.id)) - }) - return result - } - - // focus highlight is meaningful only for the workspace that actually has the - // globally-focused window (niri marks exactly one window is_focused) - property int focusedIdx: { - for (var i = 0; i < currentPills.length; i++) { - if (currentPills[i].inViewport) return i - } - return -1 - } - - property real focusedX: { - if (focusedIdx < 0) return 0 - var x = 0 - for (var i = 0; i < focusedIdx; i++) { - x += pillWidth(currentPills[i].app_id) - } - return x - } - - property real focusedW: { - if (focusedIdx < 0) return 0 - return pillWidth(currentPills[focusedIdx].app_id) - } - - height: root.stripHeight - width: sectionRow.width - - // bg strip behind everything - Rectangle { - anchors.fill: parent - color: "#1a1a1a" - } - - // focus highlight. Slides between pills within a workspace, but snaps during - // workspace transitions (controlled by _allowHighlightAnim and slideAnim.running). - Rectangle { - visible: section.focusedIdx >= 0 - x: section.focusedX - width: section.focusedW - height: root.stripHeight - color: "#555" - border.color: "#888" - border.width: 1 - Behavior on x { - enabled: section._allowHighlightAnim && !slideAnim.running - NumberAnimation { duration: 180; easing.type: Easing.OutCubic } - } - Behavior on width { - enabled: section._allowHighlightAnim && !slideAnim.running - NumberAnimation { duration: 180; easing.type: Easing.OutCubic } - } - } - - Row { - id: sectionRow - spacing: 0 - height: root.stripHeight - - Repeater { - model: section.currentPills - delegate: windowPill - } - - // ↑ arrow with 1px bar-bg dividers - Item { - visible: section.aboveWinsList.length > 0 - width: visible ? 28 : 0 - height: parent.height - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: 1 - height: 26 - color: root.color - } - Rectangle { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - width: 1 - height: 26 - color: root.color - } - - Rectangle { - anchors.centerIn: parent - width: 26 - height: 26 - radius: 0 - color: upArea.containsMouse ? "#2a2a2a" : "transparent" - border.color: "transparent" - border.width: 1 - - Text { - anchors.centerIn: parent - text: "↑" - color: "#888" - font.family: "Inter" - font.pixelSize: 14 - font.weight: 700 - } - } - - MouseArea { - id: upArea - anchors.fill: parent - hoverEnabled: true - onClicked: focusWorkspaceUp.running = true - } - } - - Repeater { - model: section.aboveWinsList - delegate: windowPill - } - - // ↓ arrow with 1px bar-bg dividers - Item { - visible: section.belowWinsList.length > 0 - width: visible ? 28 : 0 - height: parent.height - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: 1 - height: 26 - color: root.color - } - Rectangle { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - width: 1 - height: 26 - color: root.color - } - - Rectangle { - anchors.centerIn: parent - width: 26 - height: 26 - radius: 0 - color: downArea.containsMouse ? "#2a2a2a" : "transparent" - border.color: "transparent" - border.width: 1 - - Text { - anchors.centerIn: parent - text: "↓" - color: "#888" - font.family: "Inter" - font.pixelSize: 14 - font.weight: 700 - } - } - - MouseArea { - id: downArea - anchors.fill: parent - hoverEnabled: true - onClicked: focusWorkspaceDown.running = true - } - } - - Repeater { - model: section.belowWinsList - delegate: windowPill - } - } - } - } - - Component { - id: windowPill - - Item { - id: windowItem - required property var modelData - height: parent.height - width: winLabel.implicitWidth + 16 - - Rectangle { - anchors.centerIn: parent - width: winLabel.implicitWidth + 16 - height: 26 - radius: 0 - color: (!windowItem.modelData.inViewport && hoverArea.containsMouse) ? "#2a2a2a" : "transparent" - border.color: "transparent" - border.width: 1 - - Text { - id: winLabel - anchors.centerIn: parent - text: windowItem.modelData.app_id ?? "" - color: windowItem.modelData.inViewport - ? "white" - : (windowItem.modelData.onCurrentWorkspace ? "#b0b0b0" : "#888") - font.family: "Inter" - font.pixelSize: 13 - font.weight: 700 - } - } - - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - focusWindowProcess.targetId = windowItem.modelData.id - focusWindowProcess.running = true - } - } - } - } - property bool idleInhibitActive: false Process { @@ -465,65 +56,15 @@ PanelWindow { bar: root } - // Clipped viewport showing one strip's worth of vertical space. Inside, a stack of - // three full leftSectionComp instances (above / current / below) is translated up/down - // on workspace change to reveal the adjacent section. - Item { - id: leftViewport - clip: true + // Left: sliding workspace/window pill strip. + WindowList { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: px(10) - height: root.stripHeight - width: Math.max( - aboveSectionLoader.item ? aboveSectionLoader.item.width : 0, - currentSectionLoader.item ? currentSectionLoader.item.width : 0, - belowSectionLoader.item ? belowSectionLoader.item.width : 0 - ) - - Item { - id: sectionStack - anchors.left: parent.left - y: root.transitionOffset - height: parent.height - width: parent.width - - Loader { - id: aboveSectionLoader - sourceComponent: leftSectionComp - y: -root.stripHeight - Binding { - target: aboveSectionLoader.item - property: "displayWsId" - value: root.aboveAdjWorkspaceId - when: aboveSectionLoader.item !== null - } - } - - Loader { - id: currentSectionLoader - sourceComponent: leftSectionComp - y: 0 - Binding { - target: currentSectionLoader.item - property: "displayWsId" - value: root.displayedWorkspaceId - when: currentSectionLoader.item !== null - } - } - - Loader { - id: belowSectionLoader - sourceComponent: leftSectionComp - y: root.stripHeight - Binding { - target: belowSectionLoader.item - property: "displayWsId" - value: root.belowAdjWorkspaceId - when: belowSectionLoader.item !== null - } - } - } + niriWindowsList: root.niriWindowsList + niriWorkspacesList: root.niriWorkspacesList + outputName: root.screen.name + barColor: root.color } // Right: idle inhibit + audio + tray + clock diff --git a/WindowList.qml b/WindowList.qml new file mode 100644 index 0000000..932df0b --- /dev/null +++ b/WindowList.qml @@ -0,0 +1,478 @@ +import QtQuick 2.15 +import Quickshell.Io + +// Left section of the bar: a vertically-sliding strip of workspace/window pills. +// Renders the currently displayed workspace's pills plus ↑/↓ affordances into adjacent +// workspaces, and animates a vertical slide when the active workspace changes. +Item { + id: windowList + + // --- inputs (fed by Bar / shell state) --- + property var niriWindowsList: [] + property var niriWorkspacesList: [] + property string outputName: "" // this monitor's niri output name + property color barColor: "black" // bar background, used for 1px pill dividers + + clip: true + height: stripHeight + width: Math.max( + aboveSectionLoader.item ? aboveSectionLoader.item.width : 0, + currentSectionLoader.item ? currentSectionLoader.item.width : 0, + belowSectionLoader.item ? belowSectionLoader.item.width : 0 + ) + + property var activeWorkspace: niriWorkspacesList.find( + function(ws) { return ws.output === windowList.outputName && ws.is_active }) || null + + // shared font metrics for measuring pill widths so the highlight knows where to slide + FontMetrics { + id: pillFm + font.family: "Inter" + font.pixelSize: 13 + font.weight: Font.Bold + } + + function pillWidth(appId) { + return pillFm.advanceWidth(appId ?? "") + 16 + } + + readonly property int stripHeight: 26 + + // What workspace's pills are currently shown in the "current" slot of the strip stack. + // Lags activeWorkspace.id during a slide, so the strip can render the OLD state + // while sliding to reveal the NEW workspace (rendered in the above/below slot). + property int displayedWorkspaceId: -1 + + property var displayedWorkspace: niriWorkspacesList.find( + function(ws) { return ws.id === displayedWorkspaceId }) || null + + property int aboveAdjWorkspaceId: { + if (!displayedWorkspace) return -1 + var ws = niriWorkspacesList.find(function(w) { + return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx - 1 + }) + return ws ? ws.id : -1 + } + + property int belowAdjWorkspaceId: { + if (!displayedWorkspace) return -1 + var ws = niriWorkspacesList.find(function(w) { + return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx + 1 + }) + return ws ? ws.id : -1 + } + + // Vertical translation of the strip stack during a transition. + // Steady state = 0. +stripHeight = above slot in view. -stripHeight = below slot in view. + property real transitionOffset: 0 + + Component.onCompleted: { + if (activeWorkspace) displayedWorkspaceId = activeWorkspace.id + } + + // Drive the slide animation from displayedWorkspaceId toward activeWorkspace.id. + // If a slide is already in flight, leave it alone; onStopped will re-invoke us to + // chain a new slide from the just-reached workspace to whatever the latest active + // workspace is. That way rapid switches turn into a sequence of slides instead of + // a single slide followed by a snap-cut. + function syncWorkspaceAnim() { + if (!activeWorkspace) return + var newId = activeWorkspace.id + if (displayedWorkspaceId === -1) { + displayedWorkspaceId = newId + return + } + if (displayedWorkspaceId === newId) return + if (slideAnim.running) return + var oldWs = niriWorkspacesList.find(function(ws) { return ws.id === displayedWorkspaceId }) + if (!oldWs || oldWs.output !== activeWorkspace.output) { + displayedWorkspaceId = newId + return + } + // niri slides screen DOWN when focusing workspace UP -> stack offset goes +. + slideAnim.goingUp = activeWorkspace.idx < oldWs.idx + slideAnim.targetWsId = newId + slideAnim.start() + } + + onActiveWorkspaceChanged: syncWorkspaceAnim() + + NumberAnimation { + id: slideAnim + target: windowList + property: "transitionOffset" + duration: 200 + easing.type: Easing.OutCubic + property bool goingUp: false + // The workspace this slide is animating *to*. Tracked explicitly so onStopped + // doesn't snap straight to the latest activeWorkspace (which can race ahead + // mid-flight during rapid switching). + property int targetWsId: -1 + to: goingUp ? windowList.stripHeight : -windowList.stripHeight + onStopped: { + // Land on this slide's intended target, then re-evaluate: if the active + // workspace has moved further while we were animating, syncWorkspaceAnim + // kicks off the next slide from here. + windowList.displayedWorkspaceId = targetWsId + windowList.transitionOffset = 0 + windowList.syncWorkspaceAnim() + } + } + + function windowsForWorkspace(wsId) { + var wins = niriWindowsList.filter(function(w) { return w.workspace_id === wsId }) + wins.sort(function(a, b) { + var ap = a.layout && a.layout.pos_in_scrolling_layout + var bp = b.layout && b.layout.pos_in_scrolling_layout + if (ap && bp) return ap[0] !== bp[0] ? ap[0] - bp[0] : ap[1] - bp[1] + return 0 + }) + return wins + } + + // workspaces above a given workspace, farthest -> closest + function workspacesAbove(wsId) { + var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) + if (!ws) return [] + return niriWorkspacesList + .filter(function(w) { return w.output === ws.output && w.idx < ws.idx }) + .sort(function(a, b) { return a.idx - b.idx }) + } + + // workspaces below a given workspace, closest -> farthest + function workspacesBelow(wsId) { + var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) + if (!ws) return [] + return niriWorkspacesList + .filter(function(w) { return w.output === ws.output && w.idx > ws.idx }) + .sort(function(a, b) { return a.idx - b.idx }) + } + + Process { + id: focusWindowProcess + property int targetId: 0 + command: ["niri", "msg", "action", "focus-window", "--id", targetId.toString()] + } + + Process { + id: focusWorkspaceUp + command: ["niri", "msg", "action", "focus-workspace-up"] + } + + Process { + id: focusWorkspaceDown + command: ["niri", "msg", "action", "focus-workspace-down"] + } + + // A full duplicate of the bar's left section, rendered for a given workspace. + // Includes: bg + focus highlight + current-workspace pills + ↑ + above pills + ↓ + below pills. + // Three instances live in the clipped viewport (above / current / below) and slide vertically + // on workspace change. + Component { + id: leftSectionComp + + Item { + id: section + property int displayWsId: -1 + property var displayWs: niriWorkspacesList.find(function(ws) { return ws.id === displayWsId }) || null + property bool isCurrent: displayWsId === (windowList.activeWorkspace ? windowList.activeWorkspace.id : -1) + + // Briefly suppresses the highlight's slide Behavior whenever this section's + // workspace changes (e.g., the Loader rebinds after a slide ends). Without this, + // the highlight would animate from the OLD workspace's focused position to the + // NEW one's across now-different pills, producing the "second" animation. + property bool _allowHighlightAnim: true + onDisplayWsIdChanged: { + _allowHighlightAnim = false + Qt.callLater(function() { section._allowHighlightAnim = true }) + } + + // pills for the workspace this section represents + property var currentPills: { + if (displayWsId < 0) return [] + return windowsForWorkspace(displayWsId).map(function(w) { + return Object.assign({}, w, {inViewport: w.is_focused, onCurrentWorkspace: section.isCurrent}) + }) + } + + // flat list of windows from all workspaces above this section's workspace + property var aboveWinsList: { + if (!displayWs) return [] + var result = [] + workspacesAbove(displayWsId).forEach(function(ws) { + result = result.concat(windowsForWorkspace(ws.id)) + }) + return result + } + + // flat list of windows from all workspaces below this section's workspace + property var belowWinsList: { + if (!displayWs) return [] + var result = [] + workspacesBelow(displayWsId).forEach(function(ws) { + result = result.concat(windowsForWorkspace(ws.id)) + }) + return result + } + + // focus highlight is meaningful only for the workspace that actually has the + // globally-focused window (niri marks exactly one window is_focused) + property int focusedIdx: { + for (var i = 0; i < currentPills.length; i++) { + if (currentPills[i].inViewport) return i + } + return -1 + } + + property real focusedX: { + if (focusedIdx < 0) return 0 + var x = 0 + for (var i = 0; i < focusedIdx; i++) { + x += pillWidth(currentPills[i].app_id) + } + return x + } + + property real focusedW: { + if (focusedIdx < 0) return 0 + return pillWidth(currentPills[focusedIdx].app_id) + } + + height: windowList.stripHeight + width: sectionRow.width + + // bg strip behind everything + Rectangle { + anchors.fill: parent + color: "#1a1a1a" + } + + // focus highlight. Slides between pills within a workspace, but snaps during + // workspace transitions (controlled by _allowHighlightAnim and slideAnim.running). + Rectangle { + visible: section.focusedIdx >= 0 + x: section.focusedX + width: section.focusedW + height: windowList.stripHeight + color: "#555" + border.color: "#888" + border.width: 1 + Behavior on x { + enabled: section._allowHighlightAnim && !slideAnim.running + NumberAnimation { duration: 180; easing.type: Easing.OutCubic } + } + Behavior on width { + enabled: section._allowHighlightAnim && !slideAnim.running + NumberAnimation { duration: 180; easing.type: Easing.OutCubic } + } + } + + Row { + id: sectionRow + spacing: 0 + height: windowList.stripHeight + + Repeater { + model: section.currentPills + delegate: windowPill + } + + // ↑ arrow with 1px bar-bg dividers + Item { + visible: section.aboveWinsList.length > 0 + width: visible ? 28 : 0 + height: parent.height + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 1 + height: 26 + color: windowList.barColor + } + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 1 + height: 26 + color: windowList.barColor + } + + Rectangle { + anchors.centerIn: parent + width: 26 + height: 26 + radius: 0 + color: upArea.containsMouse ? "#2a2a2a" : "transparent" + border.color: "transparent" + border.width: 1 + + Text { + anchors.centerIn: parent + text: "↑" + color: "#888" + font.family: "Inter" + font.pixelSize: 14 + font.weight: 700 + } + } + + MouseArea { + id: upArea + anchors.fill: parent + hoverEnabled: true + onClicked: focusWorkspaceUp.running = true + } + } + + Repeater { + model: section.aboveWinsList + delegate: windowPill + } + + // ↓ arrow with 1px bar-bg dividers + Item { + visible: section.belowWinsList.length > 0 + width: visible ? 28 : 0 + height: parent.height + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 1 + height: 26 + color: windowList.barColor + } + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 1 + height: 26 + color: windowList.barColor + } + + Rectangle { + anchors.centerIn: parent + width: 26 + height: 26 + radius: 0 + color: downArea.containsMouse ? "#2a2a2a" : "transparent" + border.color: "transparent" + border.width: 1 + + Text { + anchors.centerIn: parent + text: "↓" + color: "#888" + font.family: "Inter" + font.pixelSize: 14 + font.weight: 700 + } + } + + MouseArea { + id: downArea + anchors.fill: parent + hoverEnabled: true + onClicked: focusWorkspaceDown.running = true + } + } + + Repeater { + model: section.belowWinsList + delegate: windowPill + } + } + } + } + + Component { + id: windowPill + + Item { + id: windowItem + required property var modelData + height: parent.height + width: winLabel.implicitWidth + 16 + + Rectangle { + anchors.centerIn: parent + width: winLabel.implicitWidth + 16 + height: 26 + radius: 0 + color: (!windowItem.modelData.inViewport && hoverArea.containsMouse) ? "#2a2a2a" : "transparent" + border.color: "transparent" + border.width: 1 + + Text { + id: winLabel + anchors.centerIn: parent + text: windowItem.modelData.app_id ?? "" + color: windowItem.modelData.inViewport + ? "white" + : (windowItem.modelData.onCurrentWorkspace ? "#b0b0b0" : "#888") + font.family: "Inter" + font.pixelSize: 13 + font.weight: 700 + } + } + + MouseArea { + id: hoverArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + focusWindowProcess.targetId = windowItem.modelData.id + focusWindowProcess.running = true + } + } + } + } + + // Clipped viewport showing one strip's worth of vertical space. Inside, a stack of + // three full leftSectionComp instances (above / current / below) is translated up/down + // on workspace change to reveal the adjacent section. + Item { + id: sectionStack + anchors.left: parent.left + y: windowList.transitionOffset + height: parent.height + width: parent.width + + Loader { + id: aboveSectionLoader + sourceComponent: leftSectionComp + y: -windowList.stripHeight + Binding { + target: aboveSectionLoader.item + property: "displayWsId" + value: windowList.aboveAdjWorkspaceId + when: aboveSectionLoader.item !== null + } + } + + Loader { + id: currentSectionLoader + sourceComponent: leftSectionComp + y: 0 + Binding { + target: currentSectionLoader.item + property: "displayWsId" + value: windowList.displayedWorkspaceId + when: currentSectionLoader.item !== null + } + } + + Loader { + id: belowSectionLoader + sourceComponent: leftSectionComp + y: windowList.stripHeight + Binding { + target: belowSectionLoader.item + property: "displayWsId" + value: windowList.belowAdjWorkspaceId + when: belowSectionLoader.item !== null + } + } + } +} -- 2.51.2