taskbar: window previews

This commit is contained in:
end-4
2025-11-12 00:09:22 +01:00
parent a412688af2
commit 20e1f0e0bb
12 changed files with 390 additions and 78 deletions
@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 410 B

@@ -10,8 +10,16 @@ BarButton {
required property string iconName
property bool separateLightDark: false
leftInset: 2
rightInset: 2
implicitWidth: height - topInset - bottomInset + leftInset + rightInset
onDownChanged: {
scaleAnim.duration = root.down ? 150 : 200
scaleAnim.easing.bezierCurve = root.down ? Looks.transition.easing.bezierCurve.easeIn : Looks.transition.easing.bezierCurve.easeOut
contentItem.scale = root.down ? 5/6 : 1 // If/When we do dragging, the scale is 1.25
}
contentItem: Item {
id: contentItem
anchors.centerIn: root.background
@@ -19,12 +27,10 @@ BarButton {
implicitHeight: iconWidget.implicitHeight
implicitWidth: iconWidget.implicitWidth
scale: root.down ? 5/6 : 1 // If/When we do dragging, the scale is 1.25
Behavior on scale {
NumberAnimation {
duration: 90
id: scaleAnim
easing.type: Easing.BezierSpline
easing.bezierCurve: root.down ? Looks.transition.easing.bezierCurve.easeIn : Looks.transition.easing.bezierCurve.easeOut
}
}
@@ -32,6 +38,7 @@ BarButton {
id: iconWidget
anchors.centerIn: parent
iconName: root.iconName
separateLightDark: root.separateLightDark
}
}
}
@@ -5,11 +5,13 @@ import qs.modules.common
import qs.modules.waffle.looks
Kirigami.Icon {
id: iconWidget
id: root
required property string iconName
property bool separateLightDark: false
implicitWidth: 26
implicitHeight: 26
property real implicitSize: 26
implicitWidth: implicitSize
implicitHeight: implicitSize
roundToIconSize: false
source: `${Looks.iconsPath}/${root.iconName}${!root.separateLightDark ? "" : Looks.dark ? "-dark" : "-light"}.svg`
fallback: root.iconName
@@ -11,17 +11,9 @@ Button {
Layout.fillHeight: true
topInset: 4
bottomInset: 4
property color borderColor: ColorUtils.transparentize(Looks.colors.bg1Border, ((root.hovered && !root.down) || root.checked) ? Looks.fluentContentTransparency : 1)
Behavior on borderColor {
animation: Looks.transition.color.createObject(this)
}
onBorderColorChanged: {
borderCanvas.requestPaint();
}
background: Rectangle {
id: background
background: AcrylicRectangle {
shiny: ((root.hovered && !root.down) || root.checked)
color: {
if (root.down) {
return Looks.colors.bg1Active
@@ -31,48 +23,5 @@ Button {
return ColorUtils.transparentize(Looks.colors.bg1)
}
}
radius: Looks.radius.medium
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
// Top 1px border with color
Canvas {
id: borderCanvas
anchors.fill: parent
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
var borderColor = root.borderColor;
var r = background.radius;
var fadeLength = Math.max(1, r);
var fadeLengthPercent = fadeLength / width;
// Compute normalized stops
var leftFadeStop = fadeLengthPercent;
var rightFadeStop = 1 - fadeLengthPercent;
var grad = ctx.createLinearGradient(0, 0, width, 0);
grad.addColorStop(0, Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0));
grad.addColorStop(leftFadeStop, borderColor);
grad.addColorStop(rightFadeStop, borderColor);
grad.addColorStop(1, Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0));
ctx.strokeStyle = grad;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(r, 0.5);
ctx.lineTo(width - r, 0.5);
// Top-right curve
ctx.arcTo(width, 0.5, width, r + 0.5, r);
// Top-left curve
ctx.moveTo(width - r, 0.5);
ctx.arcTo(0, 0.5, 0, r + 0.5, r);
ctx.stroke();
}
}
}
}
@@ -3,15 +3,23 @@ import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
import Quickshell
AppButton {
id: root
required property var toplevel
readonly property bool isSeparator: toplevel.appId === "SEPARATOR"
readonly property var desktopEntry: DesktopEntries.heuristicLookup(toplevel.appId)
required property var appEntry
readonly property bool isSeparator: appEntry.appId === "SEPARATOR"
readonly property var desktopEntry: DesktopEntries.heuristicLookup(appEntry.appId)
Layout.fillHeight: true
signal hoverPreviewRequested()
iconName: toplevel.appId
iconName: AppSearch.guessIcon(appEntry.appId)
Timer {
running: root.hovered
interval: 250
onTriggered: {
root.hoverPreviewRequested()
}
}
}
@@ -0,0 +1,128 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import Quickshell
PopupWindow {
id: root
///////////////////// Properties ////////////////////
required property bool tasksHovered
property var appEntry
property Item anchorItem
//////////////////// Functions ////////////////////
function close() {
marginBehavior.enabled = false;
root.visible = false;
}
function open() {
marginBehavior.enabled = true;
root.visible = true;
}
function show(appEntry: var, button: Item) {
root.appEntry = appEntry;
root.anchorItem = button;
root.anchor.updateAnchor();
root.open();
}
///////////////////// Internals /////////////////////
readonly property bool bottom: Config.options.waffles.bar.bottom
property real visualMargin: 12
property alias ambientShadowWidth: ambientShadow.border.width
visible: false
color: "transparent"
implicitWidth: contentItem.implicitWidth + ambientShadowWidth + (visualMargin * 2)
implicitHeight: contentItem.implicitHeight + ambientShadowWidth + (visualMargin * 2)
anchor {
adjustment: PopupAdjustment.Slide
item: root.anchorItem
gravity: bottom ? Edges.Top : Edges.Bottom
edges: bottom ? Edges.Top : Edges.Bottom
}
Timer {
interval: 250
running: root.visible && !hoverChecker.containsMouse && !root.tasksHovered
onTriggered: {
root.close();
}
}
// Content
MouseArea {
id: hoverChecker
anchors.fill: parent
hoverEnabled: true
// Shadow
Rectangle {
id: ambientShadow
anchors {
fill: contentItem
margins: -border.width
}
border.color: ColorUtils.transparentize(Looks.colors.bg0Border, Looks.contentTransparency)
border.width: 1
color: "transparent"
radius: Looks.radius.large + border.width
}
Rectangle {
id: contentItem
property real sourceEdgeMargin: root.visible ? (root.ambientShadowWidth + root.visualMargin) : -root.implicitHeight
Behavior on sourceEdgeMargin {
id: marginBehavior
animation: Looks.transition.enter.createObject(this)
}
anchors {
left: parent.left
right: parent.right
top: root.bottom ? undefined : parent.top
bottom: root.bottom ? parent.bottom : undefined
margins: root.ambientShadowWidth + root.visualMargin
// Opening anim
bottomMargin: root.bottom ? sourceEdgeMargin : (root.ambientShadowWidth + root.visualMargin)
topMargin: root.bottom ? (root.ambientShadowWidth + root.visualMargin) : sourceEdgeMargin
}
color: Looks.colors.bg1
radius: Looks.radius.large
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: contentItem.width
height: contentItem.height
radius: contentItem.radius
}
}
// Testing
implicitHeight: Math.min(158, windowsRow.implicitHeight)
implicitWidth: windowsRow.implicitWidth
RowLayout {
id: windowsRow
anchors.fill: parent
Repeater {
model: ScriptModel {
values: root.appEntry?.toplevels ?? []
}
delegate: WindowPreview {
required property var modelData
toplevel: modelData
}
}
}
}
}
}
@@ -5,30 +5,41 @@ import qs.services
import qs.modules.common
import qs.modules.waffle.looks
Item {
MouseArea {
id: root
Layout.fillHeight: true
implicitHeight: row.implicitHeight
implicitWidth: row.implicitWidth
hoverEnabled: true
// Apps row
RowLayout {
id: row
anchors.fill: parent
spacing: 4
spacing: 0
Repeater {
// TODO: Include only apps (and windows) in current workspace only
model: ScriptModel {
objectProp: "appId"
values: TaskbarApps.apps.filter(app => app.appId !== "SEPARATOR")
}
delegate: TaskAppButton {
required property var modelData
toplevel: modelData
appEntry: modelData
onHoverPreviewRequested: {
previewPopup.show(appEntry, this)
}
}
}
}
// TODO: Previews popup
// Previews popup
TaskPreview {
id: previewPopup
tasksHovered: root.containsMouse
anchor.window: root.QsWindow.window
}
}
@@ -36,7 +36,6 @@ Rectangle {
BarGroupRow {
id: appsRow
spacing: 4
anchors.left: undefined
anchors.horizontalCenter: parent.horizontalCenter
@@ -0,0 +1,135 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import Quickshell
import Quickshell.Wayland
Button {
id: root
required property var toplevel
property real previewWidthConstraint: 200
property real previewHeightConstraint: 110
padding: 5
Layout.fillHeight: true
onClicked: {
root.toplevel.activate(); // TODO: make this work with those who disable focus on activate because telegram is abusive
}
background: Rectangle {
id: background
radius: Looks.radius.medium
color: root.down ? Looks.colors.bg2Active : (root.hovered ? Looks.colors.bg2Hover : ColorUtils.transparentize(Looks.colors.bg2))
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
}
contentItem: ColumnLayout {
id: contentItem
anchors.fill: parent
anchors.margins: root.padding
spacing: 5
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: false
spacing: 8
AppIcon {
id: appIcon
Layout.leftMargin: Looks.radius.large - root.padding + 2
Layout.alignment: Qt.AlignVCenter
iconName: AppSearch.guessIcon(root.toplevel.appId)
implicitSize: 16
}
Item {
id: appTitleContainer
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: closeButton.implicitHeight // Enforce height, because closeButton doesn't contribute when it's invisible
WText {
id: appTitleText
anchors.fill: parent
text: root.toplevel.title
elide: Text.ElideRight
font.pixelSize: Looks.font.pixelSize.large
font.weight: Looks.font.weight.thin
color: Looks.colors.fg1
}
}
CloseButton {
id: closeButton
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: Looks.radius.large - root.padding
Layout.topMargin: 0
implicitWidth: Math.max(screencopyView.implicitWidth, 80)
implicitHeight: screencopyView.implicitHeight
ScreencopyView {
id: screencopyView
anchors.centerIn: parent
captureSource: root.toplevel
live: true
paintCursor: true
constraintSize: Qt.size(root.previewWidthConstraint, root.previewHeightConstraint)
}
}
}
component CloseButton: Button {
id: reusableCloseButton
visible: root.hovered
Layout.leftMargin: 4
implicitHeight: 30
implicitWidth: 30
onClicked: {
root.toplevel.close();
}
Rectangle {
z: 0
color: "transparent"
anchors.fill: closeButtonBg
anchors.margins: -1
opacity: closeButtonBg.opacity
border.width: 1
radius: closeButtonBg.radius + 1
border.color: Looks.colors.bg2Border
}
background: Rectangle {
id: closeButtonBg
z: 1
opacity: reusableCloseButton.hovered ? 1 : 0
radius: Looks.radius.large - root.padding
color: reusableCloseButton.pressed ? Looks.colors.dangerActive : Looks.colors.danger
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
}
contentItem: FluentIcon {
z: 2
anchors.centerIn: parent
icon: "dismiss"
implicitSize: 10
}
}
}
@@ -0,0 +1,63 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Rectangle {
id: root
property bool shiny: true // Top border
property color borderColor: ColorUtils.transparentize(Looks.colors.bg1Border, shiny ? Looks.contentTransparency : 1)
color: Looks.colors.bg1Hover
radius: Looks.radius.medium
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
Behavior on borderColor {
animation: Looks.transition.color.createObject(this)
}
onBorderColorChanged: {
borderCanvas.requestPaint();
}
// Top 1px border with color
Canvas {
id: borderCanvas
anchors.fill: parent
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
var borderColor = root.borderColor;
var r = root.radius;
var fadeLength = Math.max(1, r);
var fadeLengthPercent = fadeLength / width;
// Compute normalized stops
var leftFadeStop = fadeLengthPercent;
var rightFadeStop = 1 - fadeLengthPercent;
var grad = ctx.createLinearGradient(0, 0, width, 0);
grad.addColorStop(0, Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0));
grad.addColorStop(leftFadeStop, borderColor);
grad.addColorStop(rightFadeStop, borderColor);
grad.addColorStop(1, Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0));
ctx.strokeStyle = grad;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(r, 0.5);
ctx.lineTo(width - r, 0.5);
// Top-right curve
ctx.arcTo(width, 0.5, width, r + 0.5, r);
// Top-left curve
ctx.moveTo(width - r, 0.5);
ctx.arcTo(0, 0.5, 0, r + 0.5, r);
ctx.stroke();
}
}
}
@@ -15,17 +15,24 @@ Singleton {
property string iconsPath: `${Directories.assetsPath}/icons/fluent`
property bool dark: Appearance.m3colors.darkmode
property real fluentBackgroundTransparency: 0.17
property real fluentContentTransparency: 0.3
property real backgroundTransparency: 0.17
property real contentTransparency: 0.25
colors: QtObject {
id: colors
property color bg0: root.dark ? "#1C1C1C" : "#EEEEEE"
property color bg0Border: root.dark ? "#404040" : "#BEBEBE"
property color bg1: root.dark ? "#2E2E2E" : "#F7F7F7"
property color bg1: root.dark ? "#2C2C2C" : "#F7F7F7"
property color bg1Hover: root.dark ? "#292929" : "#F7F7F7"
property color bg1Active: root.dark ? "#252525" : "#F3F3F3"
property color bg1Border: root.dark ? "#333333" : "#E9E9E9"
property color bg2: root.dark ? "#313131" : "#FBFBFB"
property color bg2Hover: root.dark ? "#383838" : "#FDFDFD"
property color bg2Active: root.dark ? "#333333" : "#FDFDFD"
property color bg2Border: root.dark ? "#464646" : "#EEEEEE"
property color fg: root.dark ? "#FFFFFF" : "#000000"
property color fg1: root.dark ? "#D1D1D1" : "#626262"
property color danger: "#C42B1C"
property color dangerActive: "#B62D1F"
property color brand: Appearance.m3colors.m3primary
}
@@ -44,12 +51,14 @@ Singleton {
property string ui: "Noto Sans"
}
property QtObject weight: QtObject { // Noto is not Segoe, so we might use slightly different weights
property int thin: Font.Normal
property int regular: Font.Medium
property int strong: Font.DemiBold
property int stronger: Font.Bold
}
property QtObject pixelSize: QtObject {
property real normal: 11
property real large: 15
}
}
@@ -57,15 +66,15 @@ Singleton {
id: transition
property QtObject easing: QtObject {
property QtObject bezierCurve: QtObject {
readonly property list<real> easeInOut: [0.42,0.00,0.58,1.00]
readonly property list<real> easeIn: [0,1,1,1]
readonly property list<real> easeOut: [1,0,1,1]
readonly property list<real> easeInOut: [0.42,0.00,0.58,1.00,1,1]
readonly property list<real> easeIn: [0,1,1,1,1,1]
readonly property list<real> easeOut: [1,0,1,1,1,1]
}
}
property Component color: Component {
ColorAnimation {
duration: 80
duration: 120
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
@@ -73,7 +82,7 @@ Singleton {
property Component opacity: Component {
NumberAnimation{
duration: 80
duration: 120
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
@@ -151,6 +151,6 @@ Singleton {
// Give up
return str;
return "application-x-executable";
}
}