From 2c4a09a467babdbbc30a479012266c7d76df9e05 Mon Sep 17 00:00:00 2001
From: Rachel Powers <508861+Ryex@users.noreply.github.com>
Date: Thu, 31 Oct 2024 16:57:44 -0700
Subject: [PATCH] feat: pullin Kirigami primitives module, add
BusyIndicator.qml
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
---
launcher/Application.cpp | 6 +
launcher/qml/CMakeLists.txt | 4 +
.../prismlauncher/desktop/BusyIndicator.qml | 129 +++
.../qml/org/prismlauncher/desktop/Button.qml | 9 +-
.../org/prismlauncher/desktop/CMakeLists.txt | 2 +
.../org/prismlauncher/platform/CMakeLists.txt | 39 +
.../{pqcstyle => platform}/MnemonicData.cpp | 0
.../{pqcstyle => platform}/MnemonicData.h | 0
.../qml/org/prismlauncher/platform/Units.cpp | 360 ++++++++
.../qml/org/prismlauncher/platform/Units.h | 280 +++++++
.../org/prismlauncher/pqcstyle/CMakeLists.txt | 5 +-
.../pqcstyle/PQuickStyleItem.cpp | 33 +-
.../org/prismlauncher/pqcstyle/ScrollBar.cpp | 2 +-
.../qml/org/prismlauncher/pqcstyle/ToolBar.h | 3 -
.../prismlauncher/primitives/CMakeLists.txt | 103 +++
.../org/prismlauncher/primitives/README.md | 13 +
.../prismlauncher/primitives/Separator.qml | 53 ++
.../primitives/ShadowedImage.qml | 153 ++++
.../qml/org/prismlauncher/primitives/icon.cpp | 773 ++++++++++++++++++
.../qml/org/prismlauncher/primitives/icon.h | 306 +++++++
.../scenegraph/managedtexturenode.cpp | 61 ++
.../scenegraph/managedtexturenode.h | 52 ++
.../scenegraph/paintedrectangleitem.cpp | 57 ++
.../scenegraph/paintedrectangleitem.h | 43 +
.../shadowedborderrectanglematerial.cpp | 65 ++
.../shadowedborderrectanglematerial.h | 38 +
.../shadowedbordertexturematerial.cpp | 62 ++
.../shadowedbordertexturematerial.h | 34 +
.../scenegraph/shadowedrectanglematerial.cpp | 95 +++
.../scenegraph/shadowedrectanglematerial.h | 53 ++
.../scenegraph/shadowedrectanglenode.cpp | 207 +++++
.../scenegraph/shadowedrectanglenode.h | 79 ++
.../scenegraph/shadowedtexturematerial.cpp | 62 ++
.../scenegraph/shadowedtexturematerial.h | 40 +
.../scenegraph/shadowedtexturenode.cpp | 86 ++
.../scenegraph/shadowedtexturenode.h | 44 +
.../prismlauncher/primitives/shaders/sdf.glsl | 240 ++++++
.../primitives/shaders/sdf_lowpower.glsl | 240 ++++++
.../shaders/shadowedborderrectangle.frag | 56 ++
.../shadowedborderrectangle_lowpower.frag | 38 +
.../shaders/shadowedbordertexture.frag | 62 ++
.../shadowedbordertexture_lowpower.frag | 46 ++
.../primitives/shaders/shadowedrectangle.frag | 46 ++
.../primitives/shaders/shadowedrectangle.vert | 22 +
.../shaders/shadowedrectangle_lowpower.frag | 32 +
.../primitives/shaders/shadowedtexture.frag | 53 ++
.../shaders/shadowedtexture_lowpower.frag | 38 +
.../primitives/shaders/uniforms.glsl | 20 +
.../primitives/shadowedrectangle.cpp | 365 +++++++++
.../primitives/shadowedrectangle.h | 370 +++++++++
.../primitives/shadowedtexture.cpp | 89 ++
.../primitives/shadowedtexture.h | 46 ++
52 files changed, 5070 insertions(+), 44 deletions(-)
create mode 100644 launcher/qml/org/prismlauncher/desktop/BusyIndicator.qml
create mode 100644 launcher/qml/org/prismlauncher/platform/CMakeLists.txt
rename launcher/qml/org/prismlauncher/{pqcstyle => platform}/MnemonicData.cpp (100%)
rename launcher/qml/org/prismlauncher/{pqcstyle => platform}/MnemonicData.h (100%)
create mode 100644 launcher/qml/org/prismlauncher/platform/Units.cpp
create mode 100644 launcher/qml/org/prismlauncher/platform/Units.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/CMakeLists.txt
create mode 100644 launcher/qml/org/prismlauncher/primitives/README.md
create mode 100644 launcher/qml/org/prismlauncher/primitives/Separator.qml
create mode 100644 launcher/qml/org/prismlauncher/primitives/ShadowedImage.qml
create mode 100644 launcher/qml/org/prismlauncher/primitives/icon.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/icon.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/sdf.glsl
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/sdf_lowpower.glsl
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle_lowpower.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture_lowpower.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.vert
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle_lowpower.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture_lowpower.frag
create mode 100644 launcher/qml/org/prismlauncher/primitives/shaders/uniforms.glsl
create mode 100644 launcher/qml/org/prismlauncher/primitives/shadowedrectangle.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/shadowedrectangle.h
create mode 100644 launcher/qml/org/prismlauncher/primitives/shadowedtexture.cpp
create mode 100644 launcher/qml/org/prismlauncher/primitives/shadowedtexture.h
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index dd45b9de07..d62ec3b2f9 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -177,7 +177,9 @@ static const QLatin1String liveCheckFile("live.check");
PixmapCache* PixmapCache::s_instance = nullptr;
// PREVENT LINKER FORM OPTIMIZING OUT QML MODULES
+void qml_register_types_org_prismlauncher_platform();
void qml_register_types_org_prismlauncher_pqcstyle();
+void qml_register_types_org_prismlauncher_primitives();
void qml_register_types_org_prismlauncher_desktop();
void qml_register_types_org_prismlauncher_data();
void qml_register_types_org_prismlauncher_ui();
@@ -186,8 +188,12 @@ void qml_register_types_org_prismlauncher_ui();
// TO PREVENT LINKER FORM OPTIMISING THEM OUT
void preventQmlLinkerOpt()
{
+ volatile auto org_prismlauncher_platform_registration = &qml_register_types_org_prismlauncher_platform;
+ Q_UNUSED(org_prismlauncher_platform_registration);
volatile auto org_prismlauncher_pqcstyle_registration = &qml_register_types_org_prismlauncher_pqcstyle;
Q_UNUSED(org_prismlauncher_pqcstyle_registration);
+ volatile auto org_prismlauncher_primitives_registration = &qml_register_types_org_prismlauncher_primitives;
+ Q_UNUSED(org_prismlauncher_primitives_registration);
volatile auto org_prismlauncher_desktop_registration = &qml_register_types_org_prismlauncher_desktop;
Q_UNUSED(org_prismlauncher_desktop_registration);
volatile auto org_prismlauncher_data_registration = &qml_register_types_org_prismlauncher_data;
diff --git a/launcher/qml/CMakeLists.txt b/launcher/qml/CMakeLists.txt
index de8fb8184f..96acc7af8b 100644
--- a/launcher/qml/CMakeLists.txt
+++ b/launcher/qml/CMakeLists.txt
@@ -23,14 +23,18 @@
qt_policy(SET QTP0001 NEW)
+add_subdirectory(org/prismlauncher/platform)
add_subdirectory(org/prismlauncher/pqcstyle)
+add_subdirectory(org/prismlauncher/primitives)
add_subdirectory(org/prismlauncher/desktop)
add_subdirectory(org/prismlauncher/data)
add_subdirectory(org/prismlauncher/ui)
target_link_libraries(Launcher_logic
PUBLIC
+ org_prismlauncher_platform
org_prismlauncher_pqcstyle
+ org_prismlauncher_primitives
org_prismlauncher_desktop
org_prismlauncher_data
org_prismlauncher_ui
diff --git a/launcher/qml/org/prismlauncher/desktop/BusyIndicator.qml b/launcher/qml/org/prismlauncher/desktop/BusyIndicator.qml
new file mode 100644
index 0000000000..d46160db77
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/desktop/BusyIndicator.qml
@@ -0,0 +1,129 @@
+
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright ©: 2018 Oleg Chernovskiy
+ * Copyright ©: 2022 ivan tkachenko
+ *
+ * Licensed under LGPL-3.0-only OR GPL-2.0-or-later
+ *
+ * https://community.kde.org/Policies/Licensing_Policy
+ */
+import QtQuick
+import QtQuick.Templates as T
+import org.prismlauncher.pqcstyle as PrismStyle
+import org.prismlauncher.platform as Platform
+import org.prismlauncher.primitives as Primitives
+
+T.BusyIndicator {
+ id: control
+
+ implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+ implicitContentWidth + leftPadding + rightPadding)
+ implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+ implicitContentHeight + topPadding + bottomPadding)
+
+ // BusyIndicator doesn't need padding since it has no background.
+ // A Control containing a BusyIndicator can have padding instead
+ // (e.g., a ToolBar, a Page or maybe a widget in a Plasma panel).
+ padding: 0
+
+ hoverEnabled: false
+
+ contentItem: Item {
+ /* Binding on `visible` implicitly takes care of `control.visible`,
+ * `control.running` and `opacity > 0` at once.
+ * Also, don't animate at all if the user has disabled animations,
+ * and don't animate when window is hidden (which somehow does not
+ * affect items' visibility).
+ */
+ readonly property bool animationShouldBeRunning:
+ visible
+ && Window.visibility !== Window.Hidden
+ && PrismStyle.Units.longDuration > 1
+
+ /* implicitWidth and implicitHeight won't work unless they come
+ * from a child of the contentItem. No idea why.
+ */
+ implicitWidth: Platform.Units.gridUnit * 2
+ implicitHeight: Platform.Units.gridUnit * 2
+
+ // We can't bind directly to opacity, as Animator won't update its value immediately.
+ visible: control.running || opacityAnimator.running
+ opacity: control.running ? 1 : 0
+ Behavior on opacity {
+ enabled: Platform.Units.shortDuration > 0
+ OpacityAnimator {
+ id: opacityAnimator
+ duration: Platform.Units.shortDuration
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ // sync all busy animations such that they start at a common place in the rotation
+ onAnimationShouldBeRunningChanged: startOrStopAnimation();
+
+ function startOrStopAnimation() {
+ if (rotationAnimator.running === animationShouldBeRunning) {
+ return;
+ }
+ if (animationShouldBeRunning) {
+ const date = new Date;
+ const ms = date.valueOf();
+ const startAngle = ((ms % rotationAnimator.duration) / rotationAnimator.duration) * 360;
+ rotationAnimator.from = startAngle;
+ rotationAnimator.to = startAngle + 360
+ }
+ rotationAnimator.running = animationShouldBeRunning;
+ }
+
+ Primitives.Icon {
+ /* Do not use `anchors.fill: parent` in here or else
+ * the aspect ratio won't always be 1:1.
+ */
+ anchors.centerIn: parent
+ width: Math.min(parent.width, parent.height)
+ height: width
+
+ source: "process-working-symbolic"
+ smooth: true
+
+ RotationAnimator on rotation {
+ id: rotationAnimator
+ from: 0
+ to: 360
+ // Not using a standard duration value because we don't want the
+ // animation to spin faster or slower based on the user's animation
+ // scaling preferences; it doesn't make sense in this context
+ duration: 2000
+ loops: Animation.Infinite
+ // Initially false, will be set as appropriate after
+ // initialization. Can't be bound declaratively due to the
+ // procedural nature of to/from adjustments: order of
+ // assignments is crucial, as animator won't use new to/from
+ // values while running.
+ running: false
+ }
+ }
+
+ Component.onCompleted: startOrStopAnimation();
+ }
+}
diff --git a/launcher/qml/org/prismlauncher/desktop/Button.qml b/launcher/qml/org/prismlauncher/desktop/Button.qml
index c09cd5f1f7..e2dc13f440 100644
--- a/launcher/qml/org/prismlauncher/desktop/Button.qml
+++ b/launcher/qml/org/prismlauncher/desktop/Button.qml
@@ -31,6 +31,7 @@
import QtQuick
import QtQuick.Templates as T
import org.prismlauncher.pqcstyle as PrismStyle
+import org.prismlauncher.platform as Platform
T.Button {
id: controlRoot
@@ -45,13 +46,13 @@ T.Button {
hoverEnabled: Qt.styleHints.useHoverEffects
- PrismStyle.MnemonicData.enabled: enabled && visible
- PrismStyle.MnemonicData.controlType: PrismStyle.MnemonicData.ActionElement
- PrismStyle.MnemonicData.label: display !== T.AbstractButton.IconOnly ? text : ""
+ Platform.MnemonicData.enabled: enabled && visible
+ Platform.MnemonicData.controlType: Platform.MnemonicData.ActionElement
+ Platform.MnemonicData.label: display !== T.AbstractButton.IconOnly ? text : ""
Shortcut {
//in case of explicit & the button manages it by itself
enabled: !(RegExp(/\&[^\&]/).test(controlRoot.text))
- sequence: controlRoot.PrismStyle.MnemonicData.sequence
+ sequence: controlRoot.Platform.MnemonicData.sequence
onActivated: controlRoot.clicked()
}
background: PrismStyle.PStyleButton {
diff --git a/launcher/qml/org/prismlauncher/desktop/CMakeLists.txt b/launcher/qml/org/prismlauncher/desktop/CMakeLists.txt
index 8124b2e59a..bfbe05a8c1 100644
--- a/launcher/qml/org/prismlauncher/desktop/CMakeLists.txt
+++ b/launcher/qml/org/prismlauncher/desktop/CMakeLists.txt
@@ -23,6 +23,7 @@ qt_add_qml_module(org_prismlauncher_desktop
STATIC
IMPORTS
"org.prismlauncher.pqcstyle"
+ "org.prismlauncher.platform"
QML_FILES ${DESKTOP_QML_SOURCES}
)
set(DESKTOP_QML_SOURCES
@@ -35,4 +36,5 @@ target_link_libraries(org_prismlauncher_desktop
PUBLIC
Qt${QT_VERSION_MAJOR}::QuickControls2
org_prismlauncher_pqcstyle
+ org_prismlauncher_platform
)
diff --git a/launcher/qml/org/prismlauncher/platform/CMakeLists.txt b/launcher/qml/org/prismlauncher/platform/CMakeLists.txt
new file mode 100644
index 0000000000..a6d28cb040
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/platform/CMakeLists.txt
@@ -0,0 +1,39 @@
+
+# SPDX-FileCopyrightText: 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+#
+# SPDX-License-Identifier: GPL-3.0-only
+#
+# Prism Launcher - Minecraft Launcher
+# Copyright (C) 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+qt_add_qml_module(org_prismlauncher_platform
+ URI "org.prismlauncher.platform"
+ VERSION 1.0
+ STATIC
+)
+
+set(PLATFORM_CPP_SOURCES
+ "Units.cpp"
+ "Units.h"
+ "MnemonicData.h"
+ "MnemonicData.cpp"
+
+)
+
+target_sources(org_prismlauncher_platform PRIVATE ${PLATFORM_CPP_SOURCES})
+target_link_libraries(org_prismlauncher_platform
+ PUBLIC
+ Qt${QT_VERSION_MAJOR}::Quick
+)
diff --git a/launcher/qml/org/prismlauncher/pqcstyle/MnemonicData.cpp b/launcher/qml/org/prismlauncher/platform/MnemonicData.cpp
similarity index 100%
rename from launcher/qml/org/prismlauncher/pqcstyle/MnemonicData.cpp
rename to launcher/qml/org/prismlauncher/platform/MnemonicData.cpp
diff --git a/launcher/qml/org/prismlauncher/pqcstyle/MnemonicData.h b/launcher/qml/org/prismlauncher/platform/MnemonicData.h
similarity index 100%
rename from launcher/qml/org/prismlauncher/pqcstyle/MnemonicData.h
rename to launcher/qml/org/prismlauncher/platform/MnemonicData.h
diff --git a/launcher/qml/org/prismlauncher/platform/Units.cpp b/launcher/qml/org/prismlauncher/platform/Units.cpp
new file mode 100644
index 0000000000..c348f1beed
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/platform/Units.cpp
@@ -0,0 +1,360 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright © 2020 Jonah Brüchert
+ * Copyright © 2015 Marco Martin
+ *
+ * Licensed under LGPL-2.0-or-later
+ *
+ * https://community.kde.org/Policies/Licensing_Policy
+ */
+
+/*
+ * Modified from https://invent.kde.org/frameworks/kirigami/-/blob/master/src/platform/units.h * under GPL-3.0
+ */
+
+#include "Units.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+namespace PrismLauncher {
+namespace Platform {
+
+class UnitsPrivate {
+ Q_DISABLE_COPY(UnitsPrivate)
+
+ public:
+ explicit UnitsPrivate(Units* units)
+ // Cache font so we don't have to go through QVariant and property every time
+ : fontMetrics(QFontMetricsF(QGuiApplication::font()))
+ , gridUnit(18)
+ , smallSpacing(4)
+ , mediumSpacing(6)
+ , largeSpacing(8)
+ , veryLongDuration(400)
+ , longDuration(200)
+ , shortDuration(100)
+ , veryShortDuration(50)
+ , humanMoment(2000)
+ , toolTipDelay(700)
+ , cornerRadius(5)
+ , iconSizes(new IconSizes(units))
+ {}
+
+ // Font metrics used for Units.
+ // TextMetrics uses QFontMetricsF internally, so this should do the same
+ QFontMetricsF fontMetrics;
+
+ // units
+ int gridUnit;
+ int smallSpacing;
+ int mediumSpacing;
+ int largeSpacing;
+
+ // durations
+ int veryLongDuration;
+ int longDuration;
+ int shortDuration;
+ int veryShortDuration;
+ int humanMoment;
+ int toolTipDelay;
+ qreal cornerRadius;
+
+ IconSizes* const iconSizes;
+
+ // To prevent overriding custom set units if the font changes
+ bool customUnitsSet = false;
+};
+
+Units::~Units() = default;
+
+Units::Units(QObject* parent) : QObject(parent), d(std::make_unique(this))
+{
+ qGuiApp->installEventFilter(this);
+}
+
+int Units::gridUnit() const
+{
+ return d->gridUnit;
+}
+
+void Units::setGridUnit(int size)
+{
+ if (d->gridUnit == size) {
+ return;
+ }
+
+ d->gridUnit = size;
+ d->customUnitsSet = true;
+ Q_EMIT gridUnitChanged();
+}
+
+int Units::smallSpacing() const
+{
+ return d->smallSpacing;
+}
+
+void Units::setSmallSpacing(int size)
+{
+ if (d->smallSpacing == size) {
+ return;
+ }
+
+ d->smallSpacing = size;
+ d->customUnitsSet = true;
+ Q_EMIT smallSpacingChanged();
+}
+
+int Units::mediumSpacing() const
+{
+ return d->mediumSpacing;
+}
+
+void Units::setMediumSpacing(int size)
+{
+ if (d->mediumSpacing == size) {
+ return;
+ }
+
+ d->mediumSpacing = size;
+ d->customUnitsSet = true;
+ Q_EMIT mediumSpacingChanged();
+}
+
+int Units::largeSpacing() const
+{
+ return d->largeSpacing;
+}
+
+void Units::setLargeSpacing(int size)
+{
+ if (d->largeSpacing) {
+ return;
+ }
+
+ d->largeSpacing = size;
+ d->customUnitsSet = true;
+ Q_EMIT largeSpacingChanged();
+}
+
+int Units::veryLongDuration() const
+{
+ return d->veryLongDuration;
+}
+
+void Units::setVeryLongDuration(int duration)
+{
+ if (d->veryLongDuration == duration) {
+ return;
+ }
+
+ d->veryLongDuration = duration;
+ Q_EMIT veryLongDurationChanged();
+}
+
+int Units::longDuration() const
+{
+ return d->longDuration;
+}
+
+void Units::setLongDuration(int duration)
+{
+ if (d->longDuration == duration) {
+ return;
+ }
+
+ d->longDuration = duration;
+ Q_EMIT longDurationChanged();
+}
+
+int Units::shortDuration() const
+{
+ return d->shortDuration;
+}
+
+void Units::setShortDuration(int duration)
+{
+ if (d->shortDuration == duration) {
+ return;
+ }
+
+ d->shortDuration = duration;
+ Q_EMIT shortDurationChanged();
+}
+
+int Units::veryShortDuration() const
+{
+ return d->veryShortDuration;
+}
+
+void Units::setVeryShortDuration(int duration)
+{
+ if (d->veryShortDuration == duration) {
+ return;
+ }
+
+ d->veryShortDuration = duration;
+ Q_EMIT veryShortDurationChanged();
+}
+
+int Units::humanMoment() const
+{
+ return d->humanMoment;
+}
+
+void Units::setHumanMoment(int duration)
+{
+ if (d->humanMoment == duration) {
+ return;
+ }
+
+ d->humanMoment = duration;
+ Q_EMIT humanMomentChanged();
+}
+
+int Units::toolTipDelay() const
+{
+ return d->toolTipDelay;
+}
+
+void Units::setToolTipDelay(int delay)
+{
+ if (d->toolTipDelay == delay) {
+ return;
+ }
+
+ d->toolTipDelay = delay;
+ Q_EMIT toolTipDelayChanged();
+}
+
+qreal Units::cornerRadius() const
+{
+ return d->cornerRadius;
+}
+
+void Units::setcornerRadius(qreal cornerRadius)
+{
+ if (d->cornerRadius == cornerRadius) {
+ return;
+ }
+
+ d->cornerRadius = cornerRadius;
+ Q_EMIT cornerRadiusChanged();
+}
+
+Units* Units::create(QQmlEngine* qmlEngine, [[maybe_unused]] QJSEngine* jsEngine)
+{ // Fall back to the default units implementation
+ return new Units(qmlEngine);
+}
+
+bool Units::eventFilter([[maybe_unused]] QObject* watched, QEvent* event)
+{
+ if (event->type() == QEvent::ApplicationFontChange) {
+ d->fontMetrics = QFontMetricsF(qGuiApp->font());
+
+ if (d->customUnitsSet) {
+ return false;
+ }
+
+ Q_EMIT d->iconSizes->sizeForLabelsChanged();
+ }
+ return false;
+}
+
+IconSizes* Units::iconSizes() const
+{
+ return d->iconSizes;
+}
+
+IconSizes::IconSizes(Units* units) : QObject(units), m_units(units) {}
+
+int IconSizes::roundedIconSize(int size) const
+{
+ if (size < 16) {
+ return size;
+ }
+
+ if (size < 22) {
+ return 16;
+ }
+
+ if (size < 32) {
+ return 22;
+ }
+
+ if (size < 48) {
+ return 32;
+ }
+
+ if (size < 64) {
+ return 48;
+ }
+
+ return size;
+}
+
+int IconSizes::sizeForLabels() const
+{
+ // gridUnit is the height of textMetrics
+ return roundedIconSize(m_units->d->fontMetrics.height());
+}
+
+int IconSizes::small() const
+{
+ return 16;
+}
+
+int IconSizes::smallMedium() const
+{
+ return 22;
+}
+
+int IconSizes::medium() const
+{
+ return 32;
+}
+
+int IconSizes::large() const
+{
+ return 48;
+}
+
+int IconSizes::huge() const
+{
+ return 64;
+}
+
+int IconSizes::enormous() const
+{
+ return 128;
+}
+
+} // namespace Platform
+} // namespace PrismLauncher
diff --git a/launcher/qml/org/prismlauncher/platform/Units.h b/launcher/qml/org/prismlauncher/platform/Units.h
new file mode 100644
index 0000000000..cca126b232
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/platform/Units.h
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright © 2020 Jonah Brüchert
+ * Copyright © 2015 Marco Martin
+ *
+ * Licensed under LGPL-2.0-or-later
+ *
+ * https://community.kde.org/Policies/Licensing_Policy
+ */
+
+/*
+ * Modified from https://invent.kde.org/frameworks/kirigami/-/blob/master/src/platform/units.h * under GPL-3.0
+ */
+
+#pragma once
+
+#include
+
+#include
+#include
+
+class QQmlEngine;
+
+namespace PrismLauncher {
+namespace Platform {
+
+class Units;
+class UnitsPrivate;
+
+/**
+ * @class IconSizes units.h
+ *
+ * Provides access to platform-dependent icon sizing
+ */
+class IconSizes : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Grouped Property")
+
+ Q_PROPERTY(int sizeForLabels READ sizeForLabels NOTIFY sizeForLabelsChanged FINAL)
+ Q_PROPERTY(int small READ small NOTIFY smallChanged FINAL)
+ Q_PROPERTY(int smallMedium READ smallMedium NOTIFY smallMediumChanged FINAL)
+ Q_PROPERTY(int medium READ medium NOTIFY mediumChanged FINAL)
+ Q_PROPERTY(int large READ large NOTIFY largeChanged FINAL)
+ Q_PROPERTY(int huge READ huge NOTIFY hugeChanged FINAL)
+ Q_PROPERTY(int enormous READ enormous NOTIFY enormousChanged FINAL)
+
+ public:
+ IconSizes(Units* units);
+
+ int sizeForLabels() const;
+ int small() const;
+ int smallMedium() const;
+ int medium() const;
+ int large() const;
+ int huge() const;
+ int enormous() const;
+
+ Q_INVOKABLE int roundedIconSize(int size) const;
+
+ private:
+ float iconScaleFactor() const;
+
+ Units* m_units;
+
+ Q_SIGNALS:
+ void sizeForLabelsChanged();
+ void smallChanged();
+ void smallMediumChanged();
+ void mediumChanged();
+ void largeChanged();
+ void hugeChanged();
+ void enormousChanged();
+};
+
+/**
+ * @class Units units.h
+ *
+ * A set of values to define semantically sizes and durations.
+ */
+class Units : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_SINGLETON
+
+ friend class IconSizes;
+
+ /**
+ * The fundamental unit of space that should be used for sizes, expressed in pixels.
+ */
+ Q_PROPERTY(int gridUnit READ gridUnit NOTIFY gridUnitChanged FINAL)
+
+ /**
+ * units.iconSizes provides access to platform-dependent icon sizing
+ *
+ * The icon sizes provided are normalized for different DPI, so icons
+ * will scale depending on the DPI.
+ *
+ * * sizeForLabels (the largest icon size that fits within fontMetrics.height) @since 5.80 @since org.kde.kirigami 2.16
+ * * small
+ * * smallMedium
+ * * medium
+ * * large
+ * * huge
+ * * enormous
+ */
+ Q_PROPERTY(Kirigami::Platform::IconSizes* iconSizes READ iconSizes CONSTANT FINAL)
+
+ /**
+ * This property holds the amount of spacing that should be used between smaller UI elements,
+ * such as a small icon and a label in a button.
+ */
+ Q_PROPERTY(int smallSpacing READ smallSpacing NOTIFY smallSpacingChanged FINAL)
+
+ /**
+ * This property holds the amount of spacing that should be used between medium UI elements,
+ * such as buttons and text fields in a toolbar.
+ */
+ Q_PROPERTY(int mediumSpacing READ mediumSpacing NOTIFY mediumSpacingChanged FINAL)
+
+ /**
+ * This property holds the amount of spacing that should be used between bigger UI elements,
+ * such as a large icon and a heading in a card.
+ */
+ Q_PROPERTY(int largeSpacing READ largeSpacing NOTIFY largeSpacingChanged FINAL)
+
+ /**
+ * units.veryLongDuration should be used for specialty animations that benefit
+ * from being even longer than longDuration.
+ */
+ Q_PROPERTY(int veryLongDuration READ veryLongDuration NOTIFY veryLongDurationChanged FINAL)
+
+ /**
+ * units.longDuration should be used for longer, screen-covering animations, for opening and
+ * closing of dialogs and other "not too small" animations
+ */
+ Q_PROPERTY(int longDuration READ longDuration NOTIFY longDurationChanged FINAL)
+
+ /**
+ * units.shortDuration should be used for short animations, such as accentuating a UI event,
+ * hover events, etc..
+ */
+ Q_PROPERTY(int shortDuration READ shortDuration NOTIFY shortDurationChanged FINAL)
+
+ /**
+ * units.veryShortDuration should be used for elements that should have a hint of smoothness,
+ * but otherwise animate near instantly.
+ */
+ Q_PROPERTY(int veryShortDuration READ veryShortDuration NOTIFY veryShortDurationChanged FINAL)
+
+ /**
+ * Time in milliseconds equivalent to the theoretical human moment, which can be used
+ * to determine whether how long to wait until the user should be informed of something,
+ * or can be used as the limit for how long something should wait before being
+ * automatically initiated.
+ *
+ * Some examples:
+ *
+ * - When the user types text in a search field, wait no longer than this duration after
+ * the user completes typing before starting the search
+ * - When loading data which would commonly arrive rapidly enough to not require interaction,
+ * wait this long before showing a spinner
+ *
+ * This might seem an arbitrary number, but given the psychological effect that three
+ * seconds seems to be what humans consider a moment (and in the case of waiting for
+ * something to happen, a moment is that time when you think "this is taking a bit long,
+ * isn't it?"), the idea is to postpone for just before such a conceptual moment. The reason
+ * for the two seconds, rather than three, is to function as a middle ground: Not long enough
+ * that the user would think that something has taken too long, for also not so fast as to
+ * happen too soon.
+ *
+ * See also
+ * https://www.psychologytoday.com/blog/all-about-addiction/201101/tick-tock-tick-hugs-and-life-in-3-second-intervals
+ * (the actual paper is hidden behind an academic paywall and consequently not readily
+ * available to us, so the source will have to be the blog entry above)
+ *
+ * \note This should __not__ be used as an animation duration, as it is deliberately not scaled according
+ * to the animation settings. This is specifically for determining when something has taken too long and
+ * the user should expect some kind of feedback. See veryShortDuration, shortDuration, longDuration, and
+ * veryLongDuration for animation duration choices.
+ *
+ */
+ Q_PROPERTY(int humanMoment READ humanMoment NOTIFY humanMomentChanged FINAL)
+
+ /**
+ * time in ms by which the display of tooltips will be delayed.
+ *
+ * @sa ToolTip.delay property
+ */
+ Q_PROPERTY(int toolTipDelay READ toolTipDelay NOTIFY toolTipDelayChanged FINAL)
+
+ /**
+ * Corner radius value shared by buttons and other rectangle elements
+ *
+ */
+ Q_PROPERTY(qreal cornerRadius READ cornerRadius NOTIFY cornerRadiusChanged FINAL)
+
+ public:
+ ~Units() override;
+
+ int gridUnit() const;
+ void setGridUnit(int size);
+
+ int smallSpacing() const;
+ void setSmallSpacing(int size);
+
+ int mediumSpacing() const;
+ void setMediumSpacing(int size);
+
+ int largeSpacing() const;
+ void setLargeSpacing(int size);
+
+ int veryLongDuration() const;
+ void setVeryLongDuration(int duration);
+
+ int longDuration() const;
+ void setLongDuration(int duration);
+
+ int shortDuration() const;
+ void setShortDuration(int duration);
+
+ int veryShortDuration() const;
+ void setVeryShortDuration(int duration);
+
+ int humanMoment() const;
+ void setHumanMoment(int duration);
+
+ int toolTipDelay() const;
+ void setToolTipDelay(int delay);
+
+ qreal cornerRadius() const;
+ void setcornerRadius(qreal cornerRadius);
+
+ IconSizes* iconSizes() const;
+
+ static Units* create(QQmlEngine* qmlEngine, QJSEngine* jsEngine);
+
+ Q_SIGNALS:
+ void gridUnitChanged();
+ void smallSpacingChanged();
+ void mediumSpacingChanged();
+ void largeSpacingChanged();
+ void veryLongDurationChanged();
+ void longDurationChanged();
+ void shortDurationChanged();
+ void veryShortDurationChanged();
+ void humanMomentChanged();
+ void toolTipDelayChanged();
+ void wheelScrollLinesChanged();
+ void cornerRadiusChanged();
+
+ protected:
+ explicit Units(QObject* parent = nullptr);
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+ private:
+ std::unique_ptr d;
+};
+
+} // namespace Platform
+} // namespace PrismLauncher
diff --git a/launcher/qml/org/prismlauncher/pqcstyle/CMakeLists.txt b/launcher/qml/org/prismlauncher/pqcstyle/CMakeLists.txt
index 5ef65de6fe..a71f55dc51 100644
--- a/launcher/qml/org/prismlauncher/pqcstyle/CMakeLists.txt
+++ b/launcher/qml/org/prismlauncher/pqcstyle/CMakeLists.txt
@@ -21,9 +21,8 @@ qt_add_qml_module(org_prismlauncher_pqcstyle
URI "org.prismlauncher.pqcstyle"
VERSION 1.0
STATIC
- SOURCES ${PQCSTYLE_CPP_SOURCES}
)
-target_include_directories(org_prismlauncher_pqcstyle PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/org.prismlauncher.pqcstyle")
+target_include_directories(org_prismlauncher_pqcstyle PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..)
set(PQCSTYLE_CPP_SOURCES
"PQuickStyleItem.h"
@@ -31,8 +30,6 @@ set(PQCSTYLE_CPP_SOURCES
"PStylePalette.cpp"
"PStylePalette.h"
"PQuickPadding.h"
- "MnemonicData.h"
- "MnemonicData.cpp"
# items
"Button.cpp"
diff --git a/launcher/qml/org/prismlauncher/pqcstyle/PQuickStyleItem.cpp b/launcher/qml/org/prismlauncher/pqcstyle/PQuickStyleItem.cpp
index 54a770b0d4..5332e8b924 100644
--- a/launcher/qml/org/prismlauncher/pqcstyle/PQuickStyleItem.cpp
+++ b/launcher/qml/org/prismlauncher/pqcstyle/PQuickStyleItem.cpp
@@ -152,39 +152,8 @@ PQuickStyleItem::PQuickStyleItem(QQuickItem* parent)
PQuickStyleItem::~PQuickStyleItem()
{
- if (const QStyleOptionButton* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionViewItem* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionHeader* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionToolButton* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionToolBar* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionTab* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionFrame* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionFocusRect* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionTabWidgetFrame* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionMenuItem* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionComboBox* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionSpinBox* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionSlider* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionProgressBar* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else if (const QStyleOptionGroupBox* aux = qstyleoption_cast(m_styleoption)) {
- delete aux;
- } else {
+ if (m_styleoption)
delete m_styleoption;
- }
m_styleoption = nullptr;
}
diff --git a/launcher/qml/org/prismlauncher/pqcstyle/ScrollBar.cpp b/launcher/qml/org/prismlauncher/pqcstyle/ScrollBar.cpp
index fbe5635a69..f55deaad3f 100644
--- a/launcher/qml/org/prismlauncher/pqcstyle/ScrollBar.cpp
+++ b/launcher/qml/org/prismlauncher/pqcstyle/ScrollBar.cpp
@@ -75,7 +75,7 @@ void PStyleScrollBar::doInitStyleOption()
setTransient(PQuickStyleItem::style()->styleHint(QStyle::SH_ScrollBar_Transient, m_styleoption));
}
-QSize PStyleScrollBar::getContentSize(int width, int height)
+QSize PStyleScrollBar::getContentSize(int, int)
{
QSize size;
const auto opt = qstyleoption_cast(m_styleoption);
diff --git a/launcher/qml/org/prismlauncher/pqcstyle/ToolBar.h b/launcher/qml/org/prismlauncher/pqcstyle/ToolBar.h
index beeb9d7655..cdee98bb67 100644
--- a/launcher/qml/org/prismlauncher/pqcstyle/ToolBar.h
+++ b/launcher/qml/org/prismlauncher/pqcstyle/ToolBar.h
@@ -48,7 +48,4 @@ class PStyleToolBar : public PQuickStyleItem {
void doPaint(QPainter* painter) override;
QSize getContentSize(int width, int height) override;
-
- protected:
- qreal baselineOffset() const override;
};
diff --git a/launcher/qml/org/prismlauncher/primitives/CMakeLists.txt b/launcher/qml/org/prismlauncher/primitives/CMakeLists.txt
new file mode 100644
index 0000000000..9f213b5100
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/CMakeLists.txt
@@ -0,0 +1,103 @@
+
+# SPDX-FileCopyrightText: 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+#
+# SPDX-License-Identifier: GPL-3.0-only
+#
+# Prism Launcher - Minecraft Launcher
+# Copyright (C) 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+qt_add_qml_module(org_prismlauncher_primitives
+ URI "org.prismlauncher.primitives"
+ VERSION 1.0
+ STATIC
+ IMPORTS
+ QtQuick
+ org.prismlauncher.platform
+)
+
+target_sources(org_prismlauncher_primitives PRIVATE
+ icon.cpp
+ icon.h
+ shadowedrectangle.cpp
+ shadowedrectangle.h
+ shadowedtexture.cpp
+ shadowedtexture.h
+
+ scenegraph/managedtexturenode.cpp
+ scenegraph/managedtexturenode.h
+ scenegraph/paintedrectangleitem.cpp
+ scenegraph/paintedrectangleitem.h
+ scenegraph/shadowedborderrectanglematerial.cpp
+ scenegraph/shadowedborderrectanglematerial.h
+ scenegraph/shadowedbordertexturematerial.cpp
+ scenegraph/shadowedbordertexturematerial.h
+ scenegraph/shadowedrectanglematerial.cpp
+ scenegraph/shadowedrectanglematerial.h
+ scenegraph/shadowedrectanglenode.cpp
+ scenegraph/shadowedrectanglenode.h
+ scenegraph/shadowedtexturematerial.cpp
+ scenegraph/shadowedtexturematerial.h
+ scenegraph/shadowedtexturenode.cpp
+ scenegraph/shadowedtexturenode.h
+)
+
+qt_target_qml_sources(org_prismlauncher_primitives
+ QML_FILES
+ Separator.qml
+ ShadowedImage.qml
+)
+
+target_include_directories(org_prismlauncher_primitives PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..)
+
+target_link_libraries(org_prismlauncher_primitives
+ PUBLIC
+ Qt${QT_VERSION_MAJOR}::Quick
+ org_prismlauncher_platform
+)
+
+if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
+ set(_extra_options DEBUGINFO)
+else()
+ set(_extra_options PRECOMPILE OPTIMIZED)
+endif()
+
+qt_add_shaders(org_prismlauncher_primitives "shaders"
+ BATCHABLE
+ PREFIX "/qt/qml/org/prismlauncher/primitives/shaders"
+ FILES
+ shaders/shadowedrectangle.vert
+ shaders/shadowedrectangle.frag
+ shaders/shadowedrectangle_lowpower.frag
+ shaders/shadowedborderrectangle.frag
+ shaders/shadowedborderrectangle_lowpower.frag
+ shaders/shadowedtexture.frag
+ shaders/shadowedtexture_lowpower.frag
+ shaders/shadowedbordertexture.frag
+ shaders/shadowedbordertexture_lowpower.frag
+ OUTPUTS
+ shadowedrectangle.vert.qsb
+ shadowedrectangle.frag.qsb
+ shadowedrectangle_lowpower.frag.qsb
+ shadowedborderrectangle.frag.qsb
+ shadowedborderrectangle_lowpower.frag.qsb
+ shadowedtexture.frag.qsb
+ shadowedtexture_lowpower.frag.qsb
+ shadowedbordertexture.frag.qsb
+ shadowedbordertexture_lowpower.frag.qsb
+ ${_extra_options}
+ OUTPUT_TARGETS _out_targets
+)
+
+
diff --git a/launcher/qml/org/prismlauncher/primitives/README.md b/launcher/qml/org/prismlauncher/primitives/README.md
new file mode 100644
index 0000000000..417990f0e0
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/README.md
@@ -0,0 +1,13 @@
+# Kirigami Primitives Module
+
+This module contains types considered primitives, things that provide some basic
+capability like rendering a certain shape. They don't require styling or at most
+read a color value from the platform.
+
+# What goes here
+
+The following criteria should be used to determine if a type belongs here:
+
+- Types used as building blocks for other types.
+- Types are allowed to depend only on QtQuick.
+- Types are only allowed to depend on the Platform submodule.
diff --git a/launcher/qml/org/prismlauncher/primitives/Separator.qml b/launcher/qml/org/prismlauncher/primitives/Separator.qml
new file mode 100644
index 0000000000..ddcede299b
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/Separator.qml
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Marco Martin
+ * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+import QtQuick
+
+import org.kde.kirigami.platform as Platform
+
+/**
+ * @brief A visual separator.
+ *
+ * Useful for splitting one set of items from another.
+ *
+ * @inherit QtQuick.Rectangle
+ */
+Rectangle {
+ id: root
+ implicitHeight: 1
+ implicitWidth: 1
+ Accessible.role: Accessible.Separator
+
+ enum Weight {
+ Light,
+ Normal
+ }
+
+ /**
+ * @brief This property holds the visual weight of the separator.
+ *
+ * Weight options:
+ * * ``Kirigami.Separator.Weight.Light``
+ * * ``Kirigami.Separator.Weight.Normal``
+ *
+ * default: ``Kirigami.Separator.Weight.Normal``
+ *
+ * @since 5.72
+ * @since org.kde.kirigami 2.12
+ */
+ property int weight: Separator.Weight.Normal
+
+ /* TODO: If we get a separator color role, change this to
+ * mix weights lower than Normal with the background color
+ * and mix weights higher than Normal with the text color.
+ */
+ color: Platform.ColorUtils.linearInterpolation(
+ Platform.Theme.backgroundColor,
+ Platform.Theme.textColor,
+ weight === Separator.Weight.Light ? Platform.Theme.lightFrameContrast : Platform.Theme.frameContrast
+ )
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/ShadowedImage.qml b/launcher/qml/org/prismlauncher/primitives/ShadowedImage.qml
new file mode 100644
index 0000000000..a122389027
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/ShadowedImage.qml
@@ -0,0 +1,153 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ * SPDX-FileCopyrightText: 2022 Carl Schwan
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+import QtQuick
+import org.kde.kirigami as Kirigami
+
+/**
+ * @brief An image with a shadow.
+ *
+ * This item will render a image, with a shadow below it. The rendering is done
+ * using distance fields, which provide greatly improved performance. The shadow is
+ * rendered outside of the item's bounds, so the item's width and height are the
+ * don't include the shadow.
+ *
+ * Example usage:
+ * @code
+ * import org.kde.kirigami
+ *
+ * ShadowedImage {
+ * source: 'qrc:/myKoolGearPicture.png'
+ *
+ * radius: 20
+ *
+ * shadow.size: 20
+ * shadow.xOffset: 5
+ * shadow.yOffset: 5
+ *
+ * border.width: 2
+ * border.color: Kirigami.Theme.textColor
+ *
+ * corners.topLeftRadius: 4
+ * corners.topRightRadius: 5
+ * corners.bottomLeftRadius: 2
+ * corners.bottomRightRadius: 10
+ * }
+ * @endcode
+ *
+ * @since 5.69
+ * @since 2.12
+ * @inherit Item
+ */
+Item {
+ id: root
+
+//BEGIN properties
+ /**
+ * @brief This property holds the color that will be underneath the image.
+ *
+ * This will be visible if the image has transparancy.
+ *
+ * @see org::kde::kirigami::ShadowedRectangle::radius
+ * @property color color
+ */
+ property alias color: shadowRectangle.color
+
+ /**
+ * @brief This propery holds the corner radius of the image.
+ * @see org::kde::kirigami::ShadowedRectangle::radius
+ * @property real radius
+ */
+ property alias radius: shadowRectangle.radius
+
+ /**
+ * @brief This property holds shadow's properties group.
+ * @see org::kde::kirigami::ShadowedRectangle::shadow
+ * @property org::kde::kirigami::ShadowedRectangle::ShadowGroup shadow
+ */
+ property alias shadow: shadowRectangle.shadow
+
+ /**
+ * @brief This propery holds the border's properties of the image.
+ * @see org::kde::kirigami::ShadowedRectangle::border
+ * @property org::kde::kirigami::ShadowedRectangle::BorderGroup border
+ */
+ property alias border: shadowRectangle.border
+
+ /**
+ * @brief This propery holds the corner radius properties of the image.
+ * @see org::kde::kirigami::ShadowedRectangle::corners
+ * @property org::kde::kirigami::ShadowedRectangle::CornersGroup corners
+ */
+ property alias corners: shadowRectangle.corners
+
+ /**
+ * @brief This propery holds the source of the image.
+ * @brief QtQuick.Image::source
+ */
+ property alias source: image.source
+
+ /**
+ * @brief This property sets whether this image should be loaded asynchronously.
+ *
+ * Set this to false if you want the main thread to load the image, which
+ * blocks it until the image is loaded. Setting this to true loads the
+ * image in a separate thread which is useful when maintaining a responsive
+ * user interface is more desirable than having images immediately visible.
+ *
+ * @see QtQuick.Image::asynchronous
+ * @property bool asynchronous
+ */
+ property alias asynchronous: image.asynchronous
+
+ /**
+ * @brief This property defines what happens when the source image has a different
+ * size than the item.
+ * @see QtQuick.Image::fillMode
+ * @property int fillMode
+ */
+ property alias fillMode: image.fillMode
+
+ /**
+ * @brief This property holds whether the image uses mipmap filtering when scaled
+ * or transformed.
+ * @see QtQuick.Image::mipmap
+ * @property bool mipmap
+ */
+ property alias mipmap: image.mipmap
+
+ /**
+ * @brief This property holds the scaled width and height of the full-frame image.
+ * @see QtQuick.Image::sourceSize
+ */
+ property alias sourceSize: image.sourceSize
+
+ /**
+ * @brief This property holds the status of image loading.
+ * @see QtQuick.Image::status
+ * @since 6.5
+ */
+ readonly property alias status: image.status
+//END properties
+
+ Image {
+ id: image
+ anchors.fill: parent
+ }
+
+ ShaderEffectSource {
+ id: textureSource
+ sourceItem: image
+ hideSource: !shadowRectangle.softwareRendering
+ }
+
+ Kirigami.ShadowedTexture {
+ id: shadowRectangle
+ anchors.fill: parent
+ source: (image.status === Image.Ready && !softwareRendering) ? textureSource : null
+ }
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/icon.cpp b/launcher/qml/org/prismlauncher/primitives/icon.cpp
new file mode 100644
index 0000000000..c214aceff8
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/icon.cpp
@@ -0,0 +1,773 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright © 2011 Marco Martin
+ * Copyright © 2014 Aleix Pol Gonzalez
+ * Copyright © 2020 Carson Black
+ *
+ * Licensed under LGPL-2.0-or-later
+ */
+
+#include "icon.h"
+#include "scenegraph/managedtexturenode.h"
+
+#include "platform/Units.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache)
+
+Icon::Icon(QQuickItem* parent) : QQuickItem(parent), m_active(false), m_selected(false), m_isMask(false)
+{
+ setFlag(ItemHasContents, true);
+ // Using 32 because Icon used to redefine implicitWidth and implicitHeight and hardcode them to 32
+ setImplicitSize(32, 32);
+
+ connect(this, &QQuickItem::smoothChanged, this, &QQuickItem::polish);
+ connect(this, &QQuickItem::enabledChanged, this, [this]() { polish(); });
+}
+
+Icon::~Icon() {}
+
+void Icon::componentComplete()
+{
+ QQuickItem::componentComplete();
+
+ QQmlEngine* engine = qmlEngine(this);
+ Q_ASSERT(engine);
+ m_units = engine->singletonInstance("org.kde.kirigami.platform", "Units");
+ Q_ASSERT(m_units);
+ m_animation = new QPropertyAnimation(this);
+ connect(m_animation, &QPropertyAnimation::valueChanged, this, &Icon::valueChanged);
+ connect(m_animation, &QPropertyAnimation::finished, this, [this]() {
+ m_oldIcon = QImage();
+ m_textureChanged = true;
+ update();
+ });
+ m_animation->setTargetObject(this);
+ m_animation->setEasingCurve(QEasingCurve::InOutCubic);
+ m_animation->setDuration(m_units->longDuration());
+ connect(m_units, &PrismLauncher::Platform::Units::longDurationChanged, m_animation,
+ [this]() { m_animation->setDuration(m_units->longDuration()); });
+ updatePaintedGeometry();
+}
+
+void Icon::setSource(const QVariant& icon)
+{
+ if (m_source == icon) {
+ return;
+ }
+ m_source = icon;
+
+ if (m_networkReply) {
+ // if there was a network query going on, interrupt it
+ m_networkReply->close();
+ }
+ m_loadedImage = QImage();
+ setStatus(Loading);
+
+ polish();
+ Q_EMIT sourceChanged();
+ Q_EMIT validChanged();
+}
+
+QVariant Icon::source() const
+{
+ return m_source;
+}
+
+void Icon::setActive(const bool active)
+{
+ if (active == m_active) {
+ return;
+ }
+ m_active = active;
+ polish();
+ Q_EMIT activeChanged();
+}
+
+bool Icon::active() const
+{
+ return m_active;
+}
+
+bool Icon::valid() const
+{
+ // TODO: should this be return m_status == Ready?
+ // Consider an empty URL invalid, even though isNull() will say false
+ if (m_source.canConvert() && m_source.toUrl().isEmpty()) {
+ return false;
+ }
+
+ return !m_source.isNull();
+}
+
+void Icon::setSelected(const bool selected)
+{
+ if (selected == m_selected) {
+ return;
+ }
+ m_selected = selected;
+ polish();
+ Q_EMIT selectedChanged();
+}
+
+bool Icon::selected() const
+{
+ return m_selected;
+}
+
+void Icon::setIsMask(bool mask)
+{
+ if (m_isMask == mask) {
+ return;
+ }
+
+ m_isMask = mask;
+ polish();
+ Q_EMIT isMaskChanged();
+}
+
+bool Icon::isMask() const
+{
+ return m_isMask;
+}
+
+void Icon::setColor(const QColor& color)
+{
+ if (m_color == color) {
+ return;
+ }
+
+ m_color = color;
+ polish();
+ Q_EMIT colorChanged();
+}
+
+QColor Icon::color() const
+{
+ return m_color;
+}
+
+QSGNode* Icon::createSubtree(qreal initialOpacity)
+{
+ auto opacityNode = new QSGOpacityNode{};
+ opacityNode->setFlag(QSGNode::OwnedByParent, true);
+ opacityNode->setOpacity(initialOpacity);
+
+ auto* mNode = new ManagedTextureNode;
+
+ mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon, QQuickWindow::TextureCanUseAtlas));
+
+ opacityNode->appendChildNode(mNode);
+
+ return opacityNode;
+}
+
+void Icon::updateSubtree(QSGNode* node, qreal opacity)
+{
+ auto opacityNode = static_cast(node);
+ opacityNode->setOpacity(opacity);
+
+ auto textureNode = static_cast(opacityNode->firstChild());
+ textureNode->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest);
+}
+
+QSGNode* Icon::updatePaintNode(QSGNode* node, QQuickItem::UpdatePaintNodeData* /*data*/)
+{
+ if (m_source.isNull() || qFuzzyIsNull(width()) || qFuzzyIsNull(height())) {
+ delete node;
+ return nullptr;
+ }
+
+ if (!node) {
+ node = new QSGNode{};
+ }
+
+ if (m_animation && m_animation->state() == QAbstractAnimation::Running) {
+ if (node->childCount() < 2) {
+ node->appendChildNode(createSubtree(0.0));
+ m_textureChanged = true;
+ }
+
+ // Rather than doing a perfect crossfade, first fade in the new texture
+ // then fade out the old texture. This is done to avoid the underlying
+ // color bleeding through when both textures are at ~0.5 opacity, which
+ // causes flickering if the two textures are very similar.
+ updateSubtree(node->firstChild(), 2.0 - m_animValue * 2.0);
+ updateSubtree(node->lastChild(), m_animValue * 2.0);
+ } else {
+ if (node->childCount() == 0) {
+ node->appendChildNode(createSubtree(1.0));
+ m_textureChanged = true;
+ }
+
+ if (node->childCount() > 1) {
+ auto toRemove = node->firstChild();
+ node->removeChildNode(toRemove);
+ delete toRemove;
+ }
+
+ updateSubtree(node->firstChild(), 1.0);
+ }
+
+ if (m_textureChanged) {
+ auto mNode = static_cast(node->lastChild()->firstChild());
+ mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon, QQuickWindow::TextureCanUseAtlas));
+ m_textureChanged = false;
+ m_sizeChanged = true;
+ }
+
+ if (m_sizeChanged) {
+ const QSizeF iconPixSize(m_icon.width() / m_devicePixelRatio, m_icon.height() / m_devicePixelRatio);
+ const QSizeF itemPixSize = QSizeF((size() * m_devicePixelRatio).toSize()) / m_devicePixelRatio;
+ QRectF nodeRect(QPoint(0, 0), itemPixSize);
+
+ if (itemPixSize.width() != 0 && itemPixSize.height() != 0) {
+ if (iconPixSize != itemPixSize) {
+ // At this point, the image will already be scaled, but we need to output it in
+ // the correct aspect ratio, painted centered in the viewport. So:
+ QRectF destination(QPointF(0, 0), QSizeF(m_icon.size()).scaled(m_paintedSize, Qt::KeepAspectRatio));
+ destination.moveCenter(nodeRect.center());
+ destination.moveTopLeft(QPointF(destination.topLeft().toPoint() * m_devicePixelRatio) / m_devicePixelRatio);
+ nodeRect = destination;
+ }
+ }
+
+ // Adjust the final node on the pixel grid
+ QPointF globalPixelPos = mapToScene(nodeRect.topLeft()) * m_devicePixelRatio;
+ QPointF posAdjust =
+ QPointF(globalPixelPos.x() - std::round(globalPixelPos.x()), globalPixelPos.y() - std::round(globalPixelPos.y()));
+ nodeRect.moveTopLeft(nodeRect.topLeft() - posAdjust);
+
+ for (int i = 0; i < node->childCount(); ++i) {
+ auto mNode = static_cast(node->childAtIndex(i)->firstChild());
+ mNode->setRect(nodeRect);
+ }
+
+ m_sizeChanged = false;
+ }
+
+ return node;
+}
+
+void Icon::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry)
+{
+ QQuickItem::geometryChange(newGeometry, oldGeometry);
+ if (newGeometry.size() != oldGeometry.size()) {
+ m_sizeChanged = true;
+ updatePaintedGeometry();
+ polish();
+ }
+}
+
+void Icon::handleRedirect(QNetworkReply* reply)
+{
+ QNetworkAccessManager* qnam = reply->manager();
+ if (reply->error() != QNetworkReply::NoError) {
+ return;
+ }
+ const QUrl possibleRedirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+ if (!possibleRedirectUrl.isEmpty()) {
+ const QUrl redirectUrl = reply->url().resolved(possibleRedirectUrl);
+ if (redirectUrl == reply->url()) {
+ // no infinite redirections thank you very much
+ reply->deleteLater();
+ return;
+ }
+ reply->deleteLater();
+ QNetworkRequest request(possibleRedirectUrl);
+ request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
+ m_networkReply = qnam->get(request);
+ connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() { handleFinished(m_networkReply); });
+ }
+}
+
+void Icon::handleFinished(QNetworkReply* reply)
+{
+ if (!reply) {
+ return;
+ }
+
+ reply->deleteLater();
+ if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
+ handleRedirect(reply);
+ return;
+ }
+
+ m_loadedImage = QImage();
+
+ const QString filename = reply->url().fileName();
+ if (!m_loadedImage.load(reply, filename.mid(filename.indexOf(QLatin1Char('.'))).toLatin1().constData())) {
+ // broken image from data, inform the user of this with some useful broken-image thing...
+ m_loadedImage = iconPixmap(QIcon::fromTheme(m_fallback));
+ }
+
+ polish();
+}
+
+void Icon::updatePolish()
+{
+ QQuickItem::updatePolish();
+
+ if (window()) {
+ m_devicePixelRatio = window()->effectiveDevicePixelRatio();
+ }
+
+ if (m_source.isNull()) {
+ setStatus(Ready);
+ updatePaintedGeometry();
+ update();
+ return;
+ }
+
+ const QSize itemSize(width(), height());
+ if (itemSize.width() != 0 && itemSize.height() != 0) {
+ const QSize size = itemSize;
+
+ if (m_animation) {
+ m_animation->stop();
+ m_oldIcon = m_icon;
+ }
+
+ switch (m_source.userType()) {
+ case QMetaType::QPixmap:
+ m_icon = m_source.value().toImage();
+ break;
+ case QMetaType::QImage:
+ m_icon = m_source.value();
+ break;
+ case QMetaType::QBitmap:
+ m_icon = m_source.value().toImage();
+ break;
+ case QMetaType::QIcon: {
+ m_icon = iconPixmap(m_source.value());
+ break;
+ }
+ case QMetaType::QUrl:
+ case QMetaType::QString:
+ m_icon = findIcon(size);
+ break;
+ case QMetaType::QBrush:
+ // todo: fill here too?
+ case QMetaType::QColor:
+ m_icon = QImage(size, QImage::Format_Alpha8);
+ m_icon.fill(m_source.value());
+ break;
+ default:
+ break;
+ }
+
+ if (m_icon.isNull()) {
+ m_icon = QImage(size, QImage::Format_Alpha8);
+ m_icon.fill(Qt::transparent);
+ }
+
+ const QColor tintColor = //
+ !m_color.isValid() || m_color == Qt::transparent //
+ ? (m_selected ? qApp->palette().color(QPalette::HighlightedText) //
+ : qApp->palette().color(QPalette::Text))
+ : m_color;
+
+ // TODO: initialize m_isMask with icon.isMask()
+ if (tintColor.alpha() > 0 && isMask()) {
+ QPainter p(&m_icon);
+ p.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ p.fillRect(m_icon.rect(), tintColor);
+ p.end();
+ }
+ }
+
+ // don't animate initial setting
+ bool animated = m_animated && !m_oldIcon.isNull() && !m_sizeChanged && !m_blockNextAnimation;
+
+ if (animated && m_animation) {
+ m_animValue = 0.0;
+ m_animation->setStartValue((qreal)0);
+ m_animation->setEndValue((qreal)1);
+ m_animation->start();
+ } else {
+ if (m_animation) {
+ m_animation->stop();
+ }
+ m_animValue = 1.0;
+ m_blockNextAnimation = false;
+ }
+ m_textureChanged = true;
+ updatePaintedGeometry();
+ update();
+}
+
+QImage Icon::findIcon(const QSize& size)
+{
+ QImage img;
+ QString iconSource = m_source.toString();
+
+ if (iconSource.startsWith(QLatin1String("image://"))) {
+ QUrl iconUrl(iconSource);
+ QString iconProviderId = iconUrl.host();
+ // QUrl path has the "/" prefix while iconId does not
+ QString iconId = iconUrl.path().remove(0, 1);
+
+ QSize actualSize;
+ auto engine = qmlEngine(this);
+ if (!engine) {
+ return img;
+ }
+ QQuickImageProvider* imageProvider = dynamic_cast(engine->imageProvider(iconProviderId));
+ if (!imageProvider) {
+ return img;
+ }
+ switch (imageProvider->imageType()) {
+ case QQmlImageProviderBase::Image:
+ img = imageProvider->requestImage(iconId, &actualSize, size);
+ if (!img.isNull()) {
+ setStatus(Ready);
+ }
+ break;
+ case QQmlImageProviderBase::Pixmap:
+ img = imageProvider->requestPixmap(iconId, &actualSize, size).toImage();
+ if (!img.isNull()) {
+ setStatus(Ready);
+ }
+ break;
+ case QQmlImageProviderBase::ImageResponse: {
+ if (!m_loadedImage.isNull()) {
+ setStatus(Ready);
+ return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation);
+ }
+ QQuickAsyncImageProvider* provider = dynamic_cast(imageProvider);
+ auto response = provider->requestImageResponse(iconId, size);
+ connect(response, &QQuickImageResponse::finished, this, [iconId, response, this]() {
+ if (response->errorString().isEmpty()) {
+ QQuickTextureFactory* textureFactory = response->textureFactory();
+ if (textureFactory) {
+ m_loadedImage = textureFactory->image();
+ delete textureFactory;
+ }
+ if (m_loadedImage.isNull()) {
+ // broken image from data, inform the user of this with some useful broken-image thing...
+ m_loadedImage = iconPixmap(QIcon::fromTheme(m_fallback));
+ setStatus(Error);
+ } else {
+ setStatus(Ready);
+ }
+ polish();
+ }
+ response->deleteLater();
+ });
+ // Temporary icon while we wait for the real image to load...
+ img = iconPixmap(QIcon::fromTheme(m_placeholder));
+ break;
+ }
+ case QQmlImageProviderBase::Texture: {
+ QQuickTextureFactory* textureFactory = imageProvider->requestTexture(iconId, &actualSize, size);
+ if (textureFactory) {
+ img = textureFactory->image();
+ }
+ if (img.isNull()) {
+ // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image
+ // thing...
+ img = iconPixmap(QIcon::fromTheme(m_fallback));
+ setStatus(Error);
+ } else {
+ setStatus(Ready);
+ }
+ break;
+ }
+ case QQmlImageProviderBase::Invalid:
+ // will have to investigate this more
+ setStatus(Error);
+ break;
+ }
+ } else if (iconSource.startsWith(QLatin1String("http://")) || iconSource.startsWith(QLatin1String("https://"))) {
+ if (!m_loadedImage.isNull()) {
+ setStatus(Ready);
+ return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation);
+ }
+ const auto url = m_source.toUrl();
+ QQmlEngine* engine = qmlEngine(this);
+ QNetworkAccessManager* qnam;
+ if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) {
+ QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
+ m_networkReply = qnam->get(request);
+ connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() { handleFinished(m_networkReply); });
+ }
+ // Temporary icon while we wait for the real image to load...
+ img = iconPixmap(QIcon::fromTheme(m_placeholder));
+ } else {
+ if (iconSource.startsWith(QLatin1String("qrc:/"))) {
+ iconSource = iconSource.mid(3);
+ } else if (iconSource.startsWith(QLatin1String("file:/"))) {
+ iconSource = QUrl(iconSource).path();
+ }
+
+ const QIcon icon = loadFromTheme(iconSource);
+
+ if (!icon.isNull()) {
+ img = iconPixmap(icon);
+ setStatus(Ready);
+ }
+ }
+
+ if (!iconSource.isEmpty() && img.isNull()) {
+ setStatus(Error);
+ img = iconPixmap(QIcon::fromTheme(m_fallback));
+ }
+ return img;
+}
+
+QIcon::Mode Icon::iconMode() const
+{
+ if (!isEnabled()) {
+ return QIcon::Disabled;
+ } else if (m_selected) {
+ return QIcon::Selected;
+ } else if (m_active) {
+ return QIcon::Active;
+ }
+ return QIcon::Normal;
+}
+
+QString Icon::fallback() const
+{
+ return m_fallback;
+}
+
+void Icon::setFallback(const QString& fallback)
+{
+ if (m_fallback != fallback) {
+ m_fallback = fallback;
+ Q_EMIT fallbackChanged(fallback);
+ }
+}
+
+QString Icon::placeholder() const
+{
+ return m_placeholder;
+}
+
+void Icon::setPlaceholder(const QString& placeholder)
+{
+ if (m_placeholder != placeholder) {
+ m_placeholder = placeholder;
+ Q_EMIT placeholderChanged(placeholder);
+ }
+}
+
+void Icon::setStatus(Status status)
+{
+ if (status == m_status) {
+ return;
+ }
+
+ m_status = status;
+ Q_EMIT statusChanged();
+}
+
+Icon::Status Icon::status() const
+{
+ return m_status;
+}
+
+qreal Icon::paintedWidth() const
+{
+ return std::round(m_paintedSize.width());
+}
+
+qreal Icon::paintedHeight() const
+{
+ return std::round(m_paintedSize.height());
+}
+
+QSize Icon::iconSizeHint() const
+{
+ if (!m_roundToIconSize) {
+ return QSize(width(), height());
+ } else if (m_units) {
+ return QSize(m_units->iconSizes()->roundedIconSize(std::min(width(), height())),
+ m_units->iconSizes()->roundedIconSize(std::min(width(), height())));
+ } else {
+ return QSize(std::min(width(), height()), std::min(width(), height()));
+ }
+}
+
+QImage Icon::iconPixmap(const QIcon& icon) const
+{
+ const QSize actualSize = icon.actualSize(iconSizeHint());
+ QIcon sourceIcon = icon;
+
+ // if we have a non-default theme we need to load the icon with
+ // the right colors
+ const QQmlEngine* engine = qmlEngine(this);
+ if (engine && !engine->property("_kirigamiTheme").toString().isEmpty()) {
+ const QString iconName = icon.name();
+ if (!iconName.isEmpty() && QIcon::hasThemeIcon(iconName)) {
+ sourceIcon = loadFromTheme(iconName);
+ }
+ }
+
+ return sourceIcon.pixmap(actualSize, m_devicePixelRatio, iconMode(), QIcon::On).toImage();
+}
+
+QIcon Icon::loadFromTheme(const QString& iconName) const
+{
+ // const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent //
+ // ? (m_selected ? qApp->palette().color(QPalette::HighlightedText) //
+ // : qApp->palette().color(QPalette::Text)) //
+ // : m_color;
+ return QIcon::fromTheme(iconName);
+}
+
+void Icon::updatePaintedGeometry()
+{
+ QSizeF newSize;
+ if (!m_icon.width() || !m_icon.height()) {
+ newSize = { 0, 0 };
+ } else {
+ qreal roundedWidth = m_units ? m_units->iconSizes()->roundedIconSize(std::min(width(), height())) : 32;
+ roundedWidth = std::round(roundedWidth * m_devicePixelRatio) / m_devicePixelRatio;
+
+ if (QSizeF roundedSize(roundedWidth, roundedWidth); size() == roundedSize) {
+ m_paintedSize = roundedSize;
+ m_textureChanged = true;
+ update();
+ Q_EMIT paintedAreaChanged();
+ return;
+ }
+ if (m_roundToIconSize && m_units) {
+ if (m_icon.width() > m_icon.height()) {
+ newSize = QSizeF(roundedWidth, m_icon.height() * (roundedWidth / static_cast(m_icon.width())));
+ } else {
+ newSize = QSizeF(roundedWidth, roundedWidth);
+ }
+ } else {
+ const QSizeF iconPixSize(m_icon.width() / m_devicePixelRatio, m_icon.height() / m_devicePixelRatio);
+
+ const qreal w = widthValid() ? width() : iconPixSize.width();
+ const qreal widthScale = w / iconPixSize.width();
+ const qreal h = heightValid() ? height() : iconPixSize.height();
+ const qreal heightScale = h / iconPixSize.height();
+
+ if (widthScale <= heightScale) {
+ newSize = QSizeF(w, widthScale * iconPixSize.height());
+ } else if (heightScale < widthScale) {
+ newSize = QSizeF(heightScale * iconPixSize.width(), h);
+ }
+ }
+ }
+ if (newSize != m_paintedSize) {
+ m_paintedSize = newSize;
+ m_textureChanged = true;
+ update();
+ Q_EMIT paintedAreaChanged();
+ }
+}
+
+bool Icon::isAnimated() const
+{
+ return m_animated;
+}
+
+void Icon::setAnimated(bool animated)
+{
+ if (m_animated == animated) {
+ return;
+ }
+
+ m_animated = animated;
+ Q_EMIT animatedChanged();
+}
+
+bool Icon::roundToIconSize() const
+{
+ return m_roundToIconSize;
+}
+
+void Icon::setRoundToIconSize(bool roundToIconSize)
+{
+ if (m_roundToIconSize == roundToIconSize) {
+ return;
+ }
+
+ const QSizeF oldPaintedSize = m_paintedSize;
+
+ m_roundToIconSize = roundToIconSize;
+ Q_EMIT roundToIconSizeChanged();
+
+ updatePaintedGeometry();
+ if (oldPaintedSize != m_paintedSize) {
+ Q_EMIT paintedAreaChanged();
+ m_textureChanged = true;
+ update();
+ }
+}
+
+void Icon::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& value)
+{
+ if (change == QQuickItem::ItemDevicePixelRatioHasChanged) {
+ m_blockNextAnimation = true;
+ if (window()) {
+ m_devicePixelRatio = window()->effectiveDevicePixelRatio();
+ }
+ polish();
+ } else if (change == QQuickItem::ItemSceneChange) {
+ if (m_window) {
+ disconnect(m_window.data(), &QWindow::visibleChanged, this, &Icon::windowVisibleChanged);
+ }
+ m_window = value.window;
+ if (m_window) {
+ connect(m_window.data(), &QWindow::visibleChanged, this, &Icon::windowVisibleChanged);
+ m_devicePixelRatio = m_window->effectiveDevicePixelRatio();
+ }
+ } else if (change == ItemVisibleHasChanged && value.boolValue) {
+ m_blockNextAnimation = true;
+ }
+ QQuickItem::itemChange(change, value);
+}
+
+void Icon::valueChanged(const QVariant& value)
+{
+ m_animValue = value.toReal();
+ update();
+}
+
+void Icon::windowVisibleChanged(bool visible)
+{
+ if (visible) {
+ m_blockNextAnimation = true;
+ }
+}
+
diff --git a/launcher/qml/org/prismlauncher/primitives/icon.h b/launcher/qml/org/prismlauncher/primitives/icon.h
new file mode 100644
index 0000000000..996eaf2b51
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/icon.h
@@ -0,0 +1,306 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright © 2011 Marco Martin
+ * Copyright © 2014 Aleix Pol Gonzalez
+ * Copyright © 2020 Carson Black
+ *
+ * Licensed under LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include
+
+class QNetworkReply;
+class QQuickWindow;
+class QPropertyAnimation;
+
+namespace PrismLauncher {
+namespace Platform {
+class PlatformTheme;
+class Units;
+} // namespace Platform
+} // namespace PrismLauncher
+
+/**
+ * Class for rendering an icon in UI.
+ */
+class Icon : public QQuickItem {
+ Q_OBJECT
+ QML_ELEMENT
+
+ /**
+ * The source of this icon. An `Icon` can pull from:
+ *
+ * * The icon theme:
+ * @include icon/IconThemeSource.qml
+ * * The filesystem:
+ * @include icon/FilesystemSource.qml
+ * * Remote URIs:
+ * @include icon/InternetSource.qml
+ * * Custom providers:
+ * @include icon/CustomSource.qml
+ * * Your application's bundled resources:
+ * @include icon/ResourceSource.qml
+ *
+ * @note See https://doc.qt.io/qt-5/qtquickcontrols2-icons.html for how to
+ * bundle icon themes in your application to refer to them by name instead of
+ * by resource URL.
+ *
+ * @note Use `fallback` to provide a fallback theme name for icons.
+ *
+ * @note Cuttlefish is a KDE application that lets you view all the icons that
+ * you can use for your application. It offers a number of useful features such
+ * as previews of their appearance across different installed themes, previews
+ * at different sizes, and more. You might find it a useful tool when deciding
+ * on which icons to use in your application.
+ */
+ Q_PROPERTY(QVariant source READ source WRITE setSource NOTIFY sourceChanged FINAL)
+
+ /**
+ * The name of a fallback icon to load from the icon theme when the `source`
+ * cannot be found. The default fallback icon is `"unknown"`.
+ *
+ * @include icon/Fallback.qml
+ *
+ * @note This will only be loaded if source is unavailable (e.g. it doesn't exist, or network issues have prevented loading).
+ */
+ Q_PROPERTY(QString fallback READ fallback WRITE setFallback NOTIFY fallbackChanged FINAL)
+
+ /**
+ * The name of an icon from the icon theme to show while the icon set in `source` is
+ * being loaded. This is primarily relevant for remote sources, or those using slow-
+ * loading image providers. The default temporary icon is `"image-x-icon"`
+ *
+ * @note This will only be loaded if the source is a type which can be so long-loading
+ * that a temporary image makes sense (e.g. a remote image, or from an ImageProvider
+ * of the type QQmlImageProviderBase::ImageResponse)
+ *
+ * @since 5.15
+ */
+ Q_PROPERTY(QString placeholder READ placeholder WRITE setPlaceholder NOTIFY placeholderChanged FINAL)
+
+ /**
+ * Whether this icon will use the QIcon::Active mode when drawing the icon,
+ * resulting in a graphical effect being applied to the icon to indicate that
+ * it is currently active.
+ *
+ * This is typically used to indicate when an item is being hovered or pressed.
+ *
+ * @image html icon/active.png
+ *
+ * The color differences under the default KDE color palette, Breeze. Note
+ * that a dull highlight background is typically displayed behind active icons and
+ * it is recommended to add one if you are creating a custom component.
+ */
+ Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged FINAL)
+
+ /**
+ * Whether this icon's `source` is valid and it is being used.
+ */
+ Q_PROPERTY(bool valid READ valid NOTIFY validChanged FINAL)
+
+ /**
+ * Whether this icon will use the QIcon::Selected mode when drawing the icon,
+ * resulting in a graphical effect being applied to the icon to indicate that
+ * it is currently selected.
+ *
+ * This is typically used to indicate when a list item is currently selected.
+ *
+ * @image html icon/selected.png
+ *
+ * The color differences under the default KDE color palette, Breeze. Note
+ * that a blue background is typically displayed behind selected elements.
+ */
+ Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged FINAL)
+
+ /**
+ * Whether this icon will be treated as a mask. When an icon is being used
+ * as a mask, all non-transparent colors are replaced with the color provided in the Icon's
+ * @link Icon::color color @endlink property.
+ *
+ * @see color
+ */
+ Q_PROPERTY(bool isMask READ isMask WRITE setIsMask NOTIFY isMaskChanged FINAL)
+
+ /**
+ * The color to use when drawing this icon when `isMask` is enabled.
+ * If this property is not set or is `Qt::transparent`, the icon will use
+ * the text or the selected text color, depending on if `selected` is set to
+ * true.
+ */
+ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL)
+
+ /**
+ * Whether the icon is correctly loaded, is asynchronously loading or there was an error.
+ * Note that image loading will not be initiated until the item is shown, so if the Icon is not visible,
+ * it can only have Null or Loading states.
+ * @since 5.15
+ */
+ Q_PROPERTY(Icon::Status status READ status NOTIFY statusChanged FINAL)
+
+ /**
+ * The width of the painted area measured in pixels. This will be smaller than or
+ * equal to the width of the area taken up by the Item itself. This can be 0.
+ *
+ * @since 5.15
+ */
+ Q_PROPERTY(qreal paintedWidth READ paintedWidth NOTIFY paintedAreaChanged FINAL)
+
+ /**
+ * The height of the painted area measured in pixels. This will be smaller than or
+ * equal to the height of the area taken up by the Item itself. This can be 0.
+ *
+ * @since 5.15
+ */
+ Q_PROPERTY(qreal paintedHeight READ paintedHeight NOTIFY paintedAreaChanged FINAL)
+
+ /**
+ * If set, icon will blend when the source is changed
+ */
+ Q_PROPERTY(bool animated READ isAnimated WRITE setAnimated NOTIFY animatedChanged FINAL)
+
+ /**
+ * If set, icon will round the painted size to defined icon sizes. Default is true.
+ */
+ Q_PROPERTY(bool roundToIconSize READ roundToIconSize WRITE setRoundToIconSize NOTIFY roundToIconSizeChanged FINAL)
+
+ public:
+ enum Status {
+ Null = 0, /// No icon has been set
+ Ready, /// The icon loaded correctly
+ Loading, // The icon is being loaded, but not ready yet
+ Error, /// There was an error while loading the icon, for instance a non existent themed name, or an invalid url
+ };
+ Q_ENUM(Status)
+
+ Icon(QQuickItem* parent = nullptr);
+ ~Icon() override;
+
+ void componentComplete() override;
+
+ void setSource(const QVariant& source);
+ QVariant source() const;
+
+ void setActive(bool active = true);
+ bool active() const;
+
+ bool valid() const;
+
+ void setSelected(bool selected = true);
+ bool selected() const;
+
+ void setIsMask(bool mask);
+ bool isMask() const;
+
+ void setColor(const QColor& color);
+ QColor color() const;
+
+ QString fallback() const;
+ void setFallback(const QString& fallback);
+
+ QString placeholder() const;
+ void setPlaceholder(const QString& placeholder);
+
+ Status status() const;
+
+ qreal paintedWidth() const;
+ qreal paintedHeight() const;
+
+ bool isAnimated() const;
+ void setAnimated(bool animated);
+
+ bool roundToIconSize() const;
+ void setRoundToIconSize(bool roundToIconSize);
+
+ QSGNode* updatePaintNode(QSGNode* node, UpdatePaintNodeData* data) override;
+
+ Q_SIGNALS:
+ void sourceChanged();
+ void activeChanged();
+ void validChanged();
+ void selectedChanged();
+ void isMaskChanged();
+ void colorChanged();
+ void fallbackChanged(const QString& fallback);
+ void placeholderChanged(const QString& placeholder);
+ void statusChanged();
+ void paintedAreaChanged();
+ void animatedChanged();
+ void roundToIconSizeChanged();
+
+ protected:
+ void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override;
+ QImage findIcon(const QSize& size);
+ void handleFinished(QNetworkReply* reply);
+ void handleRedirect(QNetworkReply* reply);
+ QIcon::Mode iconMode() const;
+ bool guessMonochrome(const QImage& img);
+ void setStatus(Status status);
+ void updatePolish() override;
+ void updatePaintedGeometry();
+ void updateIsMaskHeuristic(const QString& iconSource);
+ void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& value) override;
+
+ private:
+ void valueChanged(const QVariant& value);
+ void windowVisibleChanged(bool visible);
+ QSGNode* createSubtree(qreal initialOpacity);
+ void updateSubtree(QSGNode* node, qreal opacity);
+ QSize iconSizeHint() const;
+ inline QImage iconPixmap(const QIcon& icon) const;
+ QIcon loadFromTheme(const QString& iconName) const;
+
+ PrismLauncher::Platform::Units* m_units = nullptr;
+ QPointer m_networkReply;
+ QHash m_monochromeHeuristics;
+ QVariant m_source;
+ qreal m_devicePixelRatio = 1.0;
+ Status m_status = Null;
+ bool m_textureChanged = false;
+ bool m_sizeChanged = false;
+ bool m_active;
+ bool m_selected;
+ bool m_isMask;
+ bool m_isMaskHeuristic = false;
+ QImage m_loadedImage;
+ QColor m_color = Qt::transparent;
+ QString m_fallback = QStringLiteral("unknown");
+ QString m_placeholder = QStringLiteral("image-png");
+ QSizeF m_paintedSize;
+
+ QImage m_oldIcon;
+ QImage m_icon;
+
+ // animation on image change
+ QPropertyAnimation* m_animation = nullptr;
+ qreal m_animValue = 1.0;
+ bool m_animated = false;
+ bool m_roundToIconSize = true;
+ bool m_blockNextAnimation = false;
+ QPointer m_window;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.cpp
new file mode 100644
index 0000000000..5c021b1775
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.cpp
@@ -0,0 +1,61 @@
+/*
+ * SPDX-FileCopyrightText: 2011 Marco Martin
+ * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez
+ * SPDX-FileCopyrightText: 2020 Carson Black
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "managedtexturenode.h"
+
+ManagedTextureNode::ManagedTextureNode()
+{
+}
+
+void ManagedTextureNode::setTexture(std::shared_ptr texture)
+{
+ m_texture = texture;
+ QSGSimpleTextureNode::setTexture(texture.get());
+}
+
+ImageTexturesCache::ImageTexturesCache()
+ : d(new ImageTexturesCachePrivate)
+{
+}
+
+ImageTexturesCache::~ImageTexturesCache()
+{
+}
+
+std::shared_ptr ImageTexturesCache::loadTexture(QQuickWindow *window, const QImage &image, QQuickWindow::CreateTextureOptions options)
+{
+ qint64 id = image.cacheKey();
+ std::shared_ptr texture = d->cache.value(id).value(window).lock();
+
+ if (!texture) {
+ auto cleanAndDelete = [this, window, id](QSGTexture *texture) {
+ QHash> &textures = (d->cache)[id];
+ textures.remove(window);
+ if (textures.isEmpty()) {
+ d->cache.remove(id);
+ }
+ delete texture;
+ };
+ texture = std::shared_ptr(window->createTextureFromImage(image, options), cleanAndDelete);
+ (d->cache)[id][window] = texture;
+ }
+
+ // if we have a cache in an atlas but our request cannot use an atlassed texture
+ // create a new texture and use that
+ // don't use removedFromAtlas() as that requires keeping a reference to the non atlased version
+ if (!(options & QQuickWindow::TextureCanUseAtlas) && texture->isAtlasTexture()) {
+ texture = std::shared_ptr(window->createTextureFromImage(image, options));
+ }
+
+ return texture;
+}
+
+std::shared_ptr ImageTexturesCache::loadTexture(QQuickWindow *window, const QImage &image)
+{
+ return loadTexture(window, image, {});
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.h
new file mode 100644
index 0000000000..b65a498ca2
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/managedtexturenode.h
@@ -0,0 +1,52 @@
+/*
+ * SPDX-FileCopyrightText: 2011 Marco Martin
+ * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez
+ * SPDX-FileCopyrightText: 2020 Carson Black
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+#include
+#include
+#include
+#include
+#include
+
+class ManagedTextureNode : public QSGSimpleTextureNode
+{
+ Q_DISABLE_COPY(ManagedTextureNode)
+public:
+ ManagedTextureNode();
+
+ void setTexture(std::shared_ptr texture);
+
+private:
+ std::shared_ptr m_texture;
+};
+
+typedef QHash>> TexturesCache;
+
+struct ImageTexturesCachePrivate {
+ TexturesCache cache;
+};
+
+class ImageTexturesCache
+{
+public:
+ ImageTexturesCache();
+ ~ImageTexturesCache();
+
+ /**
+ * @returns the texture for a given @p window and @p image.
+ *
+ * If an @p image id is the same as one already provided before, we won't create
+ * a new texture and return a shared pointer to the existing texture.
+ */
+ std::shared_ptr loadTexture(QQuickWindow *window, const QImage &image, QQuickWindow::CreateTextureOptions options);
+
+ std::shared_ptr loadTexture(QQuickWindow *window, const QImage &image);
+
+private:
+ std::unique_ptr d;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.cpp
new file mode 100644
index 0000000000..46981af5f0
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.cpp
@@ -0,0 +1,57 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "paintedrectangleitem.h"
+
+#include
+#include
+
+PaintedRectangleItem::PaintedRectangleItem(QQuickItem *parent)
+ : QQuickPaintedItem(parent)
+{
+}
+
+void PaintedRectangleItem::setColor(const QColor &color)
+{
+ m_color = color;
+ update();
+}
+
+void PaintedRectangleItem::setRadius(qreal radius)
+{
+ m_radius = radius;
+ update();
+}
+
+void PaintedRectangleItem::setBorderColor(const QColor &color)
+{
+ m_borderColor = color;
+ update();
+}
+
+void PaintedRectangleItem::setBorderWidth(qreal width)
+{
+ m_borderWidth = width;
+ update();
+}
+
+void PaintedRectangleItem::paint(QPainter *painter)
+{
+ painter->setRenderHint(QPainter::Antialiasing, true);
+ painter->setPen(Qt::transparent);
+
+ auto radius = std::min(m_radius, std::min(width(), height()) / 2);
+ auto borderWidth = std::floor(m_borderWidth);
+
+ if (borderWidth > 0.0) {
+ painter->setBrush(m_borderColor);
+ painter->drawRoundedRect(0, 0, width(), height(), radius, radius);
+ }
+
+ painter->setBrush(m_color);
+ painter->drawRoundedRect(borderWidth, borderWidth, width() - borderWidth * 2, height() - borderWidth * 2, radius, radius);
+}
+
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.h
new file mode 100644
index 0000000000..8036682c92
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/paintedrectangleitem.h
@@ -0,0 +1,43 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#ifndef PAINTEDRECTANGLEITEM_H
+#define PAINTEDRECTANGLEITEM_H
+
+#include
+
+/**
+ * A rectangle with a border and rounded corners, rendered through QPainter.
+ *
+ * This is a helper used by ShadowedRectangle as fallback for when software
+ * rendering is used, which means our shaders cannot be used.
+ *
+ * Since we cannot actually use QSGPaintedNode, we need to do some trickery
+ * using QQuickPaintedItem as a child of ShadowedRectangle.
+ *
+ * \warning This item is **not** intended as a general purpose item.
+ */
+class PaintedRectangleItem : public QQuickPaintedItem
+{
+ Q_OBJECT
+public:
+ explicit PaintedRectangleItem(QQuickItem *parent = nullptr);
+
+ void setColor(const QColor &color);
+ void setRadius(qreal radius);
+ void setBorderColor(const QColor &color);
+ void setBorderWidth(qreal width);
+
+ void paint(QPainter *painter) override;
+
+private:
+ QColor m_color;
+ qreal m_radius = 0.0;
+ QColor m_borderColor;
+ qreal m_borderWidth = 0.0;
+};
+
+#endif // PAINTEDRECTANGLEITEM_H
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.cpp
new file mode 100644
index 0000000000..be41113cea
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.cpp
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedborderrectanglematerial.h"
+
+#include
+
+QSGMaterialType ShadowedBorderRectangleMaterial::staticType;
+
+ShadowedBorderRectangleMaterial::ShadowedBorderRectangleMaterial()
+{
+ setFlag(QSGMaterial::Blending, true);
+}
+
+QSGMaterialShader *ShadowedBorderRectangleMaterial::createShader(QSGRendererInterface::RenderMode) const
+{
+ return new ShadowedBorderRectangleShader{shaderType};
+}
+
+QSGMaterialType *ShadowedBorderRectangleMaterial::type() const
+{
+ return &staticType;
+}
+
+int ShadowedBorderRectangleMaterial::compare(const QSGMaterial *other) const
+{
+ auto material = static_cast(other);
+
+ auto result = ShadowedRectangleMaterial::compare(other);
+ /* clang-format off */
+ if (result == 0
+ && material->borderColor == borderColor
+ && qFuzzyCompare(material->borderWidth, borderWidth)) { /* clang-format on */
+ return 0;
+ }
+
+ return QSGMaterial::compare(other);
+}
+
+ShadowedBorderRectangleShader::ShadowedBorderRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType)
+ : ShadowedRectangleShader(shaderType)
+{
+ setShader(shaderType, QStringLiteral("shadowedborderrectangle"));
+}
+
+bool ShadowedBorderRectangleShader::updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
+{
+ bool changed = ShadowedRectangleShader::updateUniformData(state, newMaterial, oldMaterial);
+ QByteArray *buf = state.uniformData();
+ Q_ASSERT(buf->size() >= 160);
+
+ if (!oldMaterial || newMaterial->compare(oldMaterial) != 0) {
+ const auto material = static_cast(newMaterial);
+ memcpy(buf->data() + 136, &material->borderWidth, 8);
+ float c[4];
+ material->borderColor.getRgbF(&c[0], &c[1], &c[2], &c[3]);
+ memcpy(buf->data() + 144, c, 16);
+ changed = true;
+ }
+
+ return changed;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.h
new file mode 100644
index 0000000000..d7620ccc8f
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedborderrectanglematerial.h
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include "shadowedrectanglematerial.h"
+
+/**
+ * A material rendering a rectangle with a shadow and a border.
+ *
+ * This material uses a distance field shader to render a rectangle with a
+ * shadow below it, optionally with rounded corners and a border.
+ */
+class ShadowedBorderRectangleMaterial : public ShadowedRectangleMaterial
+{
+public:
+ ShadowedBorderRectangleMaterial();
+
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override;
+ QSGMaterialType *type() const override;
+ int compare(const QSGMaterial *other) const override;
+
+ float borderWidth = 0.0;
+ QColor borderColor = Qt::black;
+
+ static QSGMaterialType staticType;
+};
+
+class ShadowedBorderRectangleShader : public ShadowedRectangleShader
+{
+public:
+ ShadowedBorderRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType);
+
+ bool updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.cpp
new file mode 100644
index 0000000000..786631b08a
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.cpp
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedbordertexturematerial.h"
+
+#include
+
+QSGMaterialType ShadowedBorderTextureMaterial::staticType;
+
+ShadowedBorderTextureMaterial::ShadowedBorderTextureMaterial()
+ : ShadowedBorderRectangleMaterial()
+{
+ setFlag(QSGMaterial::Blending, true);
+}
+
+QSGMaterialShader *ShadowedBorderTextureMaterial::createShader(QSGRendererInterface::RenderMode) const
+{
+ return new ShadowedBorderTextureShader{shaderType};
+}
+
+QSGMaterialType *ShadowedBorderTextureMaterial::type() const
+{
+ return &staticType;
+}
+
+int ShadowedBorderTextureMaterial::compare(const QSGMaterial *other) const
+{
+ auto material = static_cast(other);
+
+ auto result = ShadowedBorderRectangleMaterial::compare(other);
+ if (result == 0) {
+ if (material->textureSource == textureSource) {
+ return 0;
+ } else {
+ return (material->textureSource < textureSource) ? 1 : -1;
+ }
+ }
+
+ return QSGMaterial::compare(other);
+}
+
+ShadowedBorderTextureShader::ShadowedBorderTextureShader(ShadowedRectangleMaterial::ShaderType shaderType)
+ : ShadowedBorderRectangleShader(shaderType)
+{
+ setShader(shaderType, QStringLiteral("shadowedbordertexture"));
+}
+
+void ShadowedBorderTextureShader::updateSampledImage(QSGMaterialShader::RenderState &state,
+ int binding,
+ QSGTexture **texture,
+ QSGMaterial *newMaterial,
+ QSGMaterial *oldMaterial)
+{
+ Q_UNUSED(state);
+ Q_UNUSED(oldMaterial);
+ if (binding == 1) {
+ *texture = static_cast(newMaterial)->textureSource;
+ }
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.h
new file mode 100644
index 0000000000..e6441748fb
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedbordertexturematerial.h
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+
+#include "shadowedborderrectanglematerial.h"
+
+class ShadowedBorderTextureMaterial : public ShadowedBorderRectangleMaterial
+{
+public:
+ ShadowedBorderTextureMaterial();
+
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override;
+ QSGMaterialType *type() const override;
+ int compare(const QSGMaterial *other) const override;
+
+ QSGTexture *textureSource = nullptr;
+
+ static QSGMaterialType staticType;
+};
+
+class ShadowedBorderTextureShader : public ShadowedBorderRectangleShader
+{
+public:
+ ShadowedBorderTextureShader(ShadowedRectangleMaterial::ShaderType shaderType);
+
+ void
+ updateSampledImage(QSGMaterialShader::RenderState &state, int binding, QSGTexture **texture, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.cpp
new file mode 100644
index 0000000000..d5beaa6692
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.cpp
@@ -0,0 +1,95 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedrectanglematerial.h"
+
+#include
+
+QSGMaterialType ShadowedRectangleMaterial::staticType;
+
+ShadowedRectangleMaterial::ShadowedRectangleMaterial()
+{
+ setFlag(QSGMaterial::Blending, true);
+}
+
+QSGMaterialShader *ShadowedRectangleMaterial::createShader(QSGRendererInterface::RenderMode) const
+{
+ return new ShadowedRectangleShader{shaderType};
+}
+
+QSGMaterialType *ShadowedRectangleMaterial::type() const
+{
+ return &staticType;
+}
+
+int ShadowedRectangleMaterial::compare(const QSGMaterial *other) const
+{
+ auto material = static_cast(other);
+ /* clang-format off */
+ if (material->color == color
+ && material->shadowColor == shadowColor
+ && material->offset == offset
+ && material->aspect == aspect
+ && qFuzzyCompare(material->size, size)
+ && qFuzzyCompare(material->radius, radius)) { /* clang-format on */
+ return 0;
+ }
+
+ return QSGMaterial::compare(other);
+}
+
+ShadowedRectangleShader::ShadowedRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType)
+{
+ setShader(shaderType, QStringLiteral("shadowedrectangle"));
+}
+
+bool ShadowedRectangleShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
+{
+ bool changed = false;
+ QByteArray *buf = state.uniformData();
+ Q_ASSERT(buf->size() >= 160);
+
+ if (state.isMatrixDirty()) {
+ const QMatrix4x4 m = state.combinedMatrix();
+ memcpy(buf->data(), m.constData(), 64);
+ changed = true;
+ }
+
+ if (state.isOpacityDirty()) {
+ const float opacity = state.opacity();
+ memcpy(buf->data() + 72, &opacity, 4);
+ changed = true;
+ }
+
+ if (!oldMaterial || newMaterial->compare(oldMaterial) != 0) {
+ const auto material = static_cast(newMaterial);
+ memcpy(buf->data() + 64, &material->aspect, 8);
+ memcpy(buf->data() + 76, &material->size, 4);
+ memcpy(buf->data() + 80, &material->radius, 16);
+ float c[4];
+ material->color.getRgbF(&c[0], &c[1], &c[2], &c[3]);
+ memcpy(buf->data() + 96, c, 16);
+ material->shadowColor.getRgbF(&c[0], &c[1], &c[2], &c[3]);
+ memcpy(buf->data() + 112, c, 16);
+ memcpy(buf->data() + 128, &material->offset, 8);
+ changed = true;
+ }
+
+ return changed;
+}
+
+void ShadowedRectangleShader::setShader(ShadowedRectangleMaterial::ShaderType shaderType, const QString &shader)
+{
+ const auto shaderRoot = QStringLiteral(":/qt/qml/org/kde/kirigami/primitives/shaders/");
+
+ setShaderFileName(QSGMaterialShader::VertexStage, shaderRoot + QStringLiteral("shadowedrectangle.vert.qsb"));
+
+ auto shaderFile = shader;
+ if (shaderType == ShadowedRectangleMaterial::ShaderType::LowPower) {
+ shaderFile += QStringLiteral("_lowpower");
+ }
+ setShaderFileName(QSGMaterialShader::FragmentStage, shaderRoot + shaderFile + QStringLiteral(".frag.qsb"));
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.h
new file mode 100644
index 0000000000..bafcae8d9a
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglematerial.h
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+/**
+ * A material rendering a rectangle with a shadow.
+ *
+ * This material uses a distance field shader to render a rectangle with a
+ * shadow below it, optionally with rounded corners.
+ */
+class ShadowedRectangleMaterial : public QSGMaterial
+{
+public:
+ enum class ShaderType {
+ Standard,
+ LowPower,
+ };
+
+ ShadowedRectangleMaterial();
+
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override;
+ QSGMaterialType *type() const override;
+ int compare(const QSGMaterial *other) const override;
+
+ QVector2D aspect = QVector2D{1.0, 1.0};
+ float size = 0.0;
+ QVector4D radius = QVector4D{0.0, 0.0, 0.0, 0.0};
+ QColor color = Qt::white;
+ QColor shadowColor = Qt::black;
+ QVector2D offset;
+ ShaderType shaderType = ShaderType::Standard;
+
+ static QSGMaterialType staticType;
+};
+
+class ShadowedRectangleShader : public QSGMaterialShader
+{
+public:
+ ShadowedRectangleShader(ShadowedRectangleMaterial::ShaderType shaderType);
+
+ bool updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+
+protected:
+ void setShader(ShadowedRectangleMaterial::ShaderType shaderType, const QString &shader);
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.cpp
new file mode 100644
index 0000000000..ee1e58ec9b
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.cpp
@@ -0,0 +1,207 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedrectanglenode.h"
+#include "shadowedborderrectanglematerial.h"
+
+QColor premultiply(const QColor &color)
+{
+ return QColor::fromRgbF(color.redF() * color.alphaF(), //
+ color.greenF() * color.alphaF(),
+ color.blueF() * color.alphaF(),
+ color.alphaF());
+}
+
+ShadowedRectangleNode::ShadowedRectangleNode()
+{
+ m_geometry = new QSGGeometry{QSGGeometry::defaultAttributes_TexturedPoint2D(), 4};
+ setGeometry(m_geometry);
+
+ setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial);
+}
+
+void ShadowedRectangleNode::setBorderEnabled(bool enabled)
+{
+ // We can achieve more performant shaders by splitting the two into separate
+ // shaders. This requires separating the materials as well. So when
+ // borderWidth is increased to something where the border should be visible,
+ // switch to the with-border material. Otherwise use the no-border version.
+
+ if (enabled) {
+ if (!m_material || m_material->type() == borderlessMaterialType()) {
+ auto newMaterial = createBorderMaterial();
+ newMaterial->shaderType = m_shaderType;
+ setMaterial(newMaterial);
+ m_material = newMaterial;
+ m_rect = QRectF{};
+ markDirty(QSGNode::DirtyMaterial);
+ }
+ } else {
+ if (!m_material || m_material->type() == borderMaterialType()) {
+ auto newMaterial = createBorderlessMaterial();
+ newMaterial->shaderType = m_shaderType;
+ setMaterial(newMaterial);
+ m_material = newMaterial;
+ m_rect = QRectF{};
+ markDirty(QSGNode::DirtyMaterial);
+ }
+ }
+}
+
+void ShadowedRectangleNode::setRect(const QRectF &rect)
+{
+ if (rect == m_rect) {
+ return;
+ }
+
+ m_rect = rect;
+
+ QVector2D newAspect{1.0, 1.0};
+ if (m_rect.width() >= m_rect.height()) {
+ newAspect.setX(m_rect.width() / m_rect.height());
+ } else {
+ newAspect.setY(m_rect.height() / m_rect.width());
+ }
+
+ if (m_material->aspect != newAspect) {
+ m_material->aspect = newAspect;
+ markDirty(QSGNode::DirtyMaterial);
+ m_aspect = newAspect;
+ }
+}
+
+void ShadowedRectangleNode::setSize(qreal size)
+{
+ auto minDimension = std::min(m_rect.width(), m_rect.height());
+ float uniformSize = (size / minDimension) * 2.0;
+
+ if (!qFuzzyCompare(m_material->size, uniformSize)) {
+ m_material->size = uniformSize;
+ markDirty(QSGNode::DirtyMaterial);
+ m_size = size;
+ }
+}
+
+void ShadowedRectangleNode::setRadius(const QVector4D &radius)
+{
+ float minDimension = std::min(m_rect.width(), m_rect.height());
+ auto uniformRadius = QVector4D{std::min(radius.x() * 2.0f / minDimension, 1.0f),
+ std::min(radius.y() * 2.0f / minDimension, 1.0f),
+ std::min(radius.z() * 2.0f / minDimension, 1.0f),
+ std::min(radius.w() * 2.0f / minDimension, 1.0f)};
+
+ if (m_material->radius != uniformRadius) {
+ m_material->radius = uniformRadius;
+ markDirty(QSGNode::DirtyMaterial);
+ m_radius = radius;
+ }
+}
+
+void ShadowedRectangleNode::setColor(const QColor &color)
+{
+ auto premultiplied = premultiply(color);
+ if (m_material->color != premultiplied) {
+ m_material->color = premultiplied;
+ markDirty(QSGNode::DirtyMaterial);
+ }
+}
+
+void ShadowedRectangleNode::setShadowColor(const QColor &color)
+{
+ auto premultiplied = premultiply(color);
+ if (m_material->shadowColor != premultiplied) {
+ m_material->shadowColor = premultiplied;
+ markDirty(QSGNode::DirtyMaterial);
+ }
+}
+
+void ShadowedRectangleNode::setOffset(const QVector2D &offset)
+{
+ auto minDimension = std::min(m_rect.width(), m_rect.height());
+ auto uniformOffset = offset / minDimension;
+
+ if (m_material->offset != uniformOffset) {
+ m_material->offset = uniformOffset;
+ markDirty(QSGNode::DirtyMaterial);
+ m_offset = offset;
+ }
+}
+
+void ShadowedRectangleNode::setBorderWidth(qreal width)
+{
+ if (m_material->type() != borderMaterialType()) {
+ return;
+ }
+
+ auto minDimension = std::min(m_rect.width(), m_rect.height());
+ float uniformBorderWidth = width / minDimension;
+
+ auto borderMaterial = static_cast(m_material);
+ if (!qFuzzyCompare(borderMaterial->borderWidth, uniformBorderWidth)) {
+ borderMaterial->borderWidth = uniformBorderWidth;
+ markDirty(QSGNode::DirtyMaterial);
+ m_borderWidth = width;
+ }
+}
+
+void ShadowedRectangleNode::setBorderColor(const QColor &color)
+{
+ if (m_material->type() != borderMaterialType()) {
+ return;
+ }
+
+ auto borderMaterial = static_cast(m_material);
+ auto premultiplied = premultiply(color);
+ if (borderMaterial->borderColor != premultiplied) {
+ borderMaterial->borderColor = premultiplied;
+ markDirty(QSGNode::DirtyMaterial);
+ }
+}
+
+void ShadowedRectangleNode::setShaderType(ShadowedRectangleMaterial::ShaderType type)
+{
+ m_shaderType = type;
+}
+
+void ShadowedRectangleNode::updateGeometry()
+{
+ auto rect = m_rect;
+ if (m_shaderType == ShadowedRectangleMaterial::ShaderType::Standard) {
+ rect = rect.adjusted(-m_size * m_aspect.x(), //
+ -m_size * m_aspect.y(),
+ m_size * m_aspect.x(),
+ m_size * m_aspect.y());
+
+ auto offsetLength = m_offset.length();
+ rect = rect.adjusted(-offsetLength * m_aspect.x(), //
+ -offsetLength * m_aspect.y(),
+ offsetLength * m_aspect.x(),
+ offsetLength * m_aspect.y());
+ }
+
+ QSGGeometry::updateTexturedRectGeometry(m_geometry, rect, QRectF{0.0, 0.0, 1.0, 1.0});
+ markDirty(QSGNode::DirtyGeometry);
+}
+
+ShadowedRectangleMaterial *ShadowedRectangleNode::createBorderlessMaterial()
+{
+ return new ShadowedRectangleMaterial{};
+}
+
+ShadowedBorderRectangleMaterial *ShadowedRectangleNode::createBorderMaterial()
+{
+ return new ShadowedBorderRectangleMaterial{};
+}
+
+QSGMaterialType *ShadowedRectangleNode::borderlessMaterialType()
+{
+ return &ShadowedRectangleMaterial::staticType;
+}
+
+QSGMaterialType *ShadowedRectangleNode::borderMaterialType()
+{
+ return &ShadowedBorderRectangleMaterial::staticType;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.h
new file mode 100644
index 0000000000..755f56f7f1
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedrectanglenode.h
@@ -0,0 +1,79 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "shadowedrectanglematerial.h"
+
+struct QSGMaterialType;
+class ShadowedBorderRectangleMaterial;
+
+/**
+ * Scene graph node for a shadowed rectangle.
+ *
+ * This node will set up the geometry and materials for a shadowed rectangle,
+ * optionally with rounded corners.
+ *
+ * \note You must call updateGeometry() after setting properties of this node,
+ * otherwise the node's state will not correctly reflect all the properties.
+ *
+ * \sa ShadowedRectangle
+ */
+class ShadowedRectangleNode : public QSGGeometryNode
+{
+public:
+ ShadowedRectangleNode();
+
+ /**
+ * Set whether to draw a border.
+ *
+ * Note that this will switch between a material with or without border.
+ * This means this needs to be called before any other setters.
+ */
+ void setBorderEnabled(bool enabled);
+
+ void setRect(const QRectF &rect);
+ void setSize(qreal size);
+ void setRadius(const QVector4D &radius);
+ void setColor(const QColor &color);
+ void setShadowColor(const QColor &color);
+ void setOffset(const QVector2D &offset);
+ void setBorderWidth(qreal width);
+ void setBorderColor(const QColor &color);
+ void setShaderType(ShadowedRectangleMaterial::ShaderType type);
+
+ /**
+ * Update the geometry for this node.
+ *
+ * This is done as an explicit step to avoid the geometry being recreated
+ * multiple times while updating properties.
+ */
+ void updateGeometry();
+
+protected:
+ virtual ShadowedRectangleMaterial *createBorderlessMaterial();
+ virtual ShadowedBorderRectangleMaterial *createBorderMaterial();
+ virtual QSGMaterialType *borderMaterialType();
+ virtual QSGMaterialType *borderlessMaterialType();
+
+ QSGGeometry *m_geometry;
+ ShadowedRectangleMaterial *m_material = nullptr;
+ ShadowedRectangleMaterial::ShaderType m_shaderType = ShadowedRectangleMaterial::ShaderType::Standard;
+
+private:
+ QRectF m_rect;
+ qreal m_size = 0.0;
+ QVector4D m_radius = QVector4D{0.0, 0.0, 0.0, 0.0};
+ QVector2D m_offset = QVector2D{0.0, 0.0};
+ QVector2D m_aspect = QVector2D{1.0, 1.0};
+ qreal m_borderWidth = 0.0;
+ QColor m_borderColor;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.cpp
new file mode 100644
index 0000000000..8cd82d2457
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.cpp
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedtexturematerial.h"
+
+#include
+
+QSGMaterialType ShadowedTextureMaterial::staticType;
+
+ShadowedTextureMaterial::ShadowedTextureMaterial()
+ : ShadowedRectangleMaterial()
+{
+ setFlag(QSGMaterial::Blending, true);
+}
+
+QSGMaterialShader *ShadowedTextureMaterial::createShader(QSGRendererInterface::RenderMode) const
+{
+ return new ShadowedTextureShader{shaderType};
+}
+
+QSGMaterialType *ShadowedTextureMaterial::type() const
+{
+ return &staticType;
+}
+
+int ShadowedTextureMaterial::compare(const QSGMaterial *other) const
+{
+ auto material = static_cast(other);
+
+ auto result = ShadowedRectangleMaterial::compare(other);
+ if (result == 0) {
+ if (material->textureSource == textureSource) {
+ return 0;
+ } else {
+ return (material->textureSource < textureSource) ? 1 : -1;
+ }
+ }
+
+ return QSGMaterial::compare(other);
+}
+
+ShadowedTextureShader::ShadowedTextureShader(ShadowedRectangleMaterial::ShaderType shaderType)
+ : ShadowedRectangleShader(shaderType)
+{
+ setShader(shaderType, QStringLiteral("shadowedtexture"));
+}
+
+void ShadowedTextureShader::updateSampledImage(QSGMaterialShader::RenderState &state,
+ int binding,
+ QSGTexture **texture,
+ QSGMaterial *newMaterial,
+ QSGMaterial *oldMaterial)
+{
+ Q_UNUSED(state);
+ Q_UNUSED(oldMaterial);
+ if (binding == 1) {
+ *texture = static_cast(newMaterial)->textureSource;
+ }
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.h
new file mode 100644
index 0000000000..00799b2889
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturematerial.h
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+
+#include "shadowedrectanglematerial.h"
+
+/**
+ * A material rendering a rectangle with a shadow.
+ *
+ * This material uses a distance field shader to render a rectangle with a
+ * shadow below it, optionally with rounded corners.
+ */
+class ShadowedTextureMaterial : public ShadowedRectangleMaterial
+{
+public:
+ ShadowedTextureMaterial();
+
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override;
+ QSGMaterialType *type() const override;
+ int compare(const QSGMaterial *other) const override;
+
+ QSGTexture *textureSource = nullptr;
+
+ static QSGMaterialType staticType;
+};
+
+class ShadowedTextureShader : public ShadowedRectangleShader
+{
+public:
+ ShadowedTextureShader(ShadowedRectangleMaterial::ShaderType shaderType);
+
+ void
+ updateSampledImage(QSGMaterialShader::RenderState &state, int binding, QSGTexture **texture, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.cpp b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.cpp
new file mode 100644
index 0000000000..0b22a4d91b
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.cpp
@@ -0,0 +1,86 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedtexturenode.h"
+
+#include "shadowedbordertexturematerial.h"
+
+template
+inline void preprocessTexture(QSGMaterial *material, QSGTextureProvider *provider)
+{
+ auto m = static_cast(material);
+ // Since we handle texture coordinates differently in the shader, we
+ // need to remove the texture from the atlas for now.
+ if (provider->texture()->isAtlasTexture()) {
+ // Blegh, I have no idea why "removedFromAtlas" doesn't just return
+ // the texture when it's not an atlas.
+ m->textureSource = provider->texture()->removedFromAtlas();
+ } else {
+ m->textureSource = provider->texture();
+ }
+ if (QSGDynamicTexture *dynamic_texture = qobject_cast(m->textureSource)) {
+ dynamic_texture->updateTexture();
+ }
+}
+
+ShadowedTextureNode::ShadowedTextureNode()
+ : ShadowedRectangleNode()
+{
+ setFlag(QSGNode::UsePreprocess);
+}
+
+ShadowedTextureNode::~ShadowedTextureNode()
+{
+ QObject::disconnect(m_textureChangeConnectionHandle);
+}
+
+void ShadowedTextureNode::setTextureSource(QSGTextureProvider *source)
+{
+ if (m_textureSource == source) {
+ return;
+ }
+
+ if (m_textureSource) {
+ m_textureSource->disconnect();
+ }
+
+ m_textureSource = source;
+ m_textureChangeConnectionHandle = QObject::connect(m_textureSource.data(), &QSGTextureProvider::textureChanged, [this] {
+ markDirty(QSGNode::DirtyMaterial);
+ });
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void ShadowedTextureNode::preprocess()
+{
+ if (m_textureSource && m_material && m_textureSource->texture()) {
+ if (m_material->type() == borderlessMaterialType()) {
+ preprocessTexture(m_material, m_textureSource);
+ } else {
+ preprocessTexture(m_material, m_textureSource);
+ }
+ }
+}
+
+ShadowedRectangleMaterial *ShadowedTextureNode::createBorderlessMaterial()
+{
+ return new ShadowedTextureMaterial{};
+}
+
+ShadowedBorderRectangleMaterial *ShadowedTextureNode::createBorderMaterial()
+{
+ return new ShadowedBorderTextureMaterial{};
+}
+
+QSGMaterialType *ShadowedTextureNode::borderlessMaterialType()
+{
+ return &ShadowedTextureMaterial::staticType;
+}
+
+QSGMaterialType *ShadowedTextureNode::borderMaterialType()
+{
+ return &ShadowedBorderTextureMaterial::staticType;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.h b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.h
new file mode 100644
index 0000000000..d0b0efcbcc
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/scenegraph/shadowedtexturenode.h
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+#include
+
+#include "shadowedrectanglenode.h"
+#include "shadowedtexturematerial.h"
+
+/**
+ * Scene graph node for a shadowed texture source.
+ *
+ * This node will set up the geometry and materials for a shadowed rectangle,
+ * optionally with rounded corners, using a supplied texture source as the color
+ * for the rectangle.
+ *
+ * \note You must call updateGeometry() after setting properties of this node,
+ * otherwise the node's state will not correctly reflect all the properties.
+ *
+ * \sa ShadowedTexture
+ */
+class ShadowedTextureNode : public ShadowedRectangleNode
+{
+public:
+ ShadowedTextureNode();
+ ~ShadowedTextureNode();
+
+ void setTextureSource(QSGTextureProvider *source);
+ void preprocess() override;
+
+private:
+ ShadowedRectangleMaterial *createBorderlessMaterial() override;
+ ShadowedBorderRectangleMaterial *createBorderMaterial() override;
+ QSGMaterialType *borderlessMaterialType() override;
+ QSGMaterialType *borderMaterialType() override;
+
+ QPointer m_textureSource;
+ QMetaObject::Connection m_textureChangeConnectionHandle;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/sdf.glsl b/launcher/qml/org/prismlauncher/primitives/shaders/sdf.glsl
new file mode 100644
index 0000000000..69402c5c31
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/sdf.glsl
@@ -0,0 +1,240 @@
+// SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+// SPDX-FileCopyrightText: 2017 Inigo Quilez
+//
+// SPDX-License-Identifier: MIT
+//
+// This file is based on
+// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
+
+//if not GLES
+// include "desktop_header.glsl"
+//else
+// include "es_header.glsl"
+
+// A maximum point count to be used for sdf_polygon input arrays.
+// Unfortunately even function inputs require a fixed size at declaration time
+// for arrays, unless we were to use OpenGL 4.5.
+// Since the polygon is most likely to be defined in a uniform, this should be
+// at least less than MAX_FRAGMENT_UNIFORM_COMPONENTS / 2 (since we need vec2).
+#define SDF_POLYGON_MAX_POINT_COUNT 400
+
+/*********************************
+ Shapes
+*********************************/
+
+// Distance field for a circle.
+//
+// \param point A point on the distance field.
+// \param radius The radius of the circle.
+//
+// \return The signed distance from point to the circle. If negative, point is
+// inside the circle.
+lowp float sdf_circle(in lowp vec2 point, in lowp float radius)
+{
+ return length(point) - radius;
+}
+
+// Distance field for a triangle.
+//
+// \param point A point on the distance field.
+// \param p0 The first vertex of the triangle.
+// \param p0 The second vertex of the triangle.
+// \param p0 The third vertex of the triangle.
+//
+// \note The ordering of the three vertices does not matter.
+//
+// \return The signed distance from point to triangle. If negative, point is
+// inside the triangle.
+lowp float sdf_triangle(in lowp vec2 point, in lowp vec2 p0, in lowp vec2 p1, in lowp vec2 p2)
+{
+ lowp vec2 e0 = p1 - p0;
+ lowp vec2 e1 = p2 - p1;
+ lowp vec2 e2 = p0 - p2;
+
+ lowp vec2 v0 = point - p0;
+ lowp vec2 v1 = point - p1;
+ lowp vec2 v2 = point - p2;
+
+ lowp vec2 pq0 = v0 - e0 * clamp( dot(v0, e0) / dot(e0, e0), 0.0, 1.0 );
+ lowp vec2 pq1 = v1 - e1 * clamp( dot(v1, e1) / dot(e1, e1), 0.0, 1.0 );
+ lowp vec2 pq2 = v2 - e2 * clamp( dot(v2, e2) / dot(e2, e2), 0.0, 1.0 );
+
+ lowp float s = sign( e0.x*e2.y - e0.y*e2.x );
+ lowp vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
+ vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
+ vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
+
+ return -sqrt(d.x)*sign(d.y);
+}
+
+// Distance field for a rectangle.
+//
+// \param point A point on the distance field.
+// \param rect A vec2 with the size of the rectangle.
+//
+// \return The signed distance from point to rectangle. If negative, point is
+// inside the rectangle.
+lowp float sdf_rectangle(in lowp vec2 point, in lowp vec2 rect)
+{
+ lowp vec2 d = abs(point) - rect;
+ return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
+}
+
+// Distance field for a rectangle with rounded corners.
+//
+// \param point The point to calculate the distance of.
+// \param rect The rectangle to calculate the distance of.
+// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left.
+//
+// \return The signed distance from point to rectangle. If negative, point is
+// inside the rectangle.
+lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec4 radius)
+{
+ radius.xy = (point.x > 0.0) ? radius.xy : radius.zw;
+ radius.x = (point.y > 0.0) ? radius.x : radius.y;
+ lowp vec2 d = abs(point) - rect + radius.x;
+ return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x;
+}
+
+/*********************
+ Operators
+*********************/
+
+// Convert a distance field to an annular (hollow) distance field.
+//
+// \param sdf The result of an sdf shape to convert.
+// \param thickness The thickness of the resulting shape.
+//
+// \return The value of sdf modified to an annular shape.
+lowp float sdf_annular(in lowp float sdf, in lowp float thickness)
+{
+ return abs(sdf) - thickness;
+}
+
+// Union two sdf shapes together.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return The union of sdf1 and sdf2, that is, the distance to both sdf1 and
+// sdf2.
+lowp float sdf_union(in lowp float sdf1, in lowp float sdf2)
+{
+ return min(sdf1, sdf2);
+}
+
+// Subtract two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return sdf1 with sdf2 subtracted from it.
+lowp float sdf_subtract(in lowp float sdf1, in lowp float sdf2)
+{
+ return max(sdf1, -sdf2);
+}
+
+// Intersect two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return The intersection between sdf1 and sdf2, that is, the area where both
+// sdf1 and sdf2 provide the same distance value.
+lowp float sdf_intersect(in lowp float sdf1, in lowp float sdf2)
+{
+ return max(sdf1, sdf2);
+}
+
+// Smoothly intersect two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+// \param smoothing The amount of smoothing to apply.
+//
+// \return A smoothed version of the intersect operation.
+lowp float sdf_intersect_smooth(in lowp float sdf1, in lowp float sdf2, in lowp float smoothing)
+{
+ lowp float h = clamp(0.5 - 0.5 * (sdf1 - sdf2) / smoothing, 0.0, 1.0);
+ return mix(sdf1, sdf2, h) + smoothing * h * (1.0 - h);
+}
+
+// Round an sdf shape.
+//
+// \param sdf The sdf shape to round.
+// \param amount The amount of rounding to apply.
+//
+// \return The rounded shape of sdf.
+// Note that rounding happens by basically selecting an isoline of sdf,
+// therefore, the resulting shape may be larger than the input shape.
+lowp float sdf_round(in lowp float sdf, in lowp float amount)
+{
+ return sdf - amount;
+}
+
+// Convert an sdf shape to an outline of its shape.
+//
+// \param sdf The sdf shape to turn into an outline.
+//
+// \return The outline of sdf.
+lowp float sdf_outline(in lowp float sdf)
+{
+ return abs(sdf);
+}
+
+/********************
+ Convenience
+********************/
+
+// A constant to represent a "null" value of an sdf.
+//
+// Since 0 is a point exactly on the outline of an sdf shape, and negative
+// values are inside the shape, this uses a very large positive constant to
+// indicate a value that is really far away from the actual sdf shape.
+const lowp float sdf_null = 99999.0;
+
+// A constant for a default level of smoothing when rendering an sdf.
+//
+// This
+const lowp float sdf_default_smoothing = 0.625;
+
+// Render an sdf shape alpha-blended onto an existing color.
+//
+// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a
+// blending amount and a smoothing amount.
+//
+// \param alpha The alpha to use for blending.
+// \param smoothing The amount of smoothing to apply to the sdf.
+//
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float alpha, in lowp float smoothing)
+{
+ lowp float g = fwidth(sdf);
+ return mix(sourceColor, sdfColor, alpha * (1.0 - smoothstep(-smoothing * g, smoothing * g, sdf)));
+}
+
+// Render an sdf shape.
+//
+// This will render the sdf shape on top of whatever source color is input,
+// making sure to apply smoothing if desired.
+//
+// \param sdf The sdf shape to render.
+// \param sourceColor The source color to render on top of.
+// \param sdfColor The color to use for rendering the sdf shape.
+//
+// \return sourceColor with the sdf shape rendered on top.
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor)
+{
+ return sdf_render(sdf, sourceColor, sdfColor, 1.0, sdf_default_smoothing);
+}
+
+// Render an sdf shape.
+//
+// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a
+// smoothing amount.
+//
+// \param smoothing The amount of smoothing to apply to the sdf.
+//
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float smoothing)
+{
+ return sdf_render(sdf, sourceColor, sdfColor, 1.0, smoothing);
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/sdf_lowpower.glsl b/launcher/qml/org/prismlauncher/primitives/shaders/sdf_lowpower.glsl
new file mode 100644
index 0000000000..8cdc3648ce
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/sdf_lowpower.glsl
@@ -0,0 +1,240 @@
+// SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+// SPDX-FileCopyrightText: 2017 Inigo Quilez
+//
+// SPDX-License-Identifier: MIT
+//
+// This file is based on
+// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
+
+//if not GLES
+// include "desktop_header.glsl"
+//else
+// include "es_header.glsl"
+
+// A maximum point count to be used for sdf_polygon input arrays.
+// Unfortunately even function inputs require a fixed size at declaration time
+// for arrays, unless we were to use OpenGL 4.5.
+// Since the polygon is most likely to be defined in a uniform, this should be
+// at least less than MAX_FRAGMENT_UNIFORM_COMPONENTS / 2 (since we need vec2).
+#define SDF_POLYGON_MAX_POINT_COUNT 400
+
+/*********************************
+ Shapes
+*********************************/
+
+// Distance field for a circle.
+//
+// \param point A point on the distance field.
+// \param radius The radius of the circle.
+//
+// \return The signed distance from point to the circle. If negative, point is
+// inside the circle.
+lowp float sdf_circle(in lowp vec2 point, in lowp float radius)
+{
+ return length(point) - radius;
+}
+
+// Distance field for a triangle.
+//
+// \param point A point on the distance field.
+// \param p0 The first vertex of the triangle.
+// \param p0 The second vertex of the triangle.
+// \param p0 The third vertex of the triangle.
+//
+// \note The ordering of the three vertices does not matter.
+//
+// \return The signed distance from point to triangle. If negative, point is
+// inside the triangle.
+lowp float sdf_triangle(in lowp vec2 point, in lowp vec2 p0, in lowp vec2 p1, in lowp vec2 p2)
+{
+ lowp vec2 e0 = p1 - p0;
+ lowp vec2 e1 = p2 - p1;
+ lowp vec2 e2 = p0 - p2;
+
+ lowp vec2 v0 = point - p0;
+ lowp vec2 v1 = point - p1;
+ lowp vec2 v2 = point - p2;
+
+ lowp vec2 pq0 = v0 - e0 * clamp( dot(v0, e0) / dot(e0, e0), 0.0, 1.0 );
+ lowp vec2 pq1 = v1 - e1 * clamp( dot(v1, e1) / dot(e1, e1), 0.0, 1.0 );
+ lowp vec2 pq2 = v2 - e2 * clamp( dot(v2, e2) / dot(e2, e2), 0.0, 1.0 );
+
+ lowp float s = sign( e0.x*e2.y - e0.y*e2.x );
+ lowp vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
+ vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
+ vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
+
+ return -sqrt(d.x)*sign(d.y);
+}
+
+// Distance field for a rectangle.
+//
+// \param point A point on the distance field.
+// \param rect A vec2 with the size of the rectangle.
+//
+// \return The signed distance from point to rectangle. If negative, point is
+// inside the rectangle.
+lowp float sdf_rectangle(in lowp vec2 point, in lowp vec2 rect)
+{
+ lowp vec2 d = abs(point) - rect;
+ return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
+}
+
+// Distance field for a rectangle with rounded corners.
+//
+// \param point The point to calculate the distance of.
+// \param rect The rectangle to calculate the distance of.
+// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left.
+//
+// \return The signed distance from point to rectangle. If negative, point is
+// inside the rectangle.
+lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec4 radius)
+{
+ radius.xy = (point.x > 0.0) ? radius.xy : radius.zw;
+ radius.x = (point.y > 0.0) ? radius.x : radius.y;
+ lowp vec2 d = abs(point) - rect + radius.x;
+ return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x;
+}
+
+/*********************
+ Operators
+*********************/
+
+// Convert a distance field to an annular (hollow) distance field.
+//
+// \param sdf The result of an sdf shape to convert.
+// \param thickness The thickness of the resulting shape.
+//
+// \return The value of sdf modified to an annular shape.
+lowp float sdf_annular(in lowp float sdf, in lowp float thickness)
+{
+ return abs(sdf) - thickness;
+}
+
+// Union two sdf shapes together.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return The union of sdf1 and sdf2, that is, the distance to both sdf1 and
+// sdf2.
+lowp float sdf_union(in lowp float sdf1, in lowp float sdf2)
+{
+ return min(sdf1, sdf2);
+}
+
+// Subtract two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return sdf1 with sdf2 subtracted from it.
+lowp float sdf_subtract(in lowp float sdf1, in lowp float sdf2)
+{
+ return max(sdf1, -sdf2);
+}
+
+// Intersect two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return The intersection between sdf1 and sdf2, that is, the area where both
+// sdf1 and sdf2 provide the same distance value.
+lowp float sdf_intersect(in lowp float sdf1, in lowp float sdf2)
+{
+ return max(sdf1, sdf2);
+}
+
+// Smoothly intersect two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+// \param smoothing The amount of smoothing to apply.
+//
+// \return A smoothed version of the intersect operation.
+lowp float sdf_intersect_smooth(in lowp float sdf1, in lowp float sdf2, in lowp float smoothing)
+{
+ lowp float h = clamp(0.5 - 0.5 * (sdf1 - sdf2) / smoothing, 0.0, 1.0);
+ return mix(sdf1, sdf2, h) + smoothing * h * (1.0 - h);
+}
+
+// Round an sdf shape.
+//
+// \param sdf The sdf shape to round.
+// \param amount The amount of rounding to apply.
+//
+// \return The rounded shape of sdf.
+// Note that rounding happens by basically selecting an isoline of sdf,
+// therefore, the resulting shape may be larger than the input shape.
+lowp float sdf_round(in lowp float sdf, in lowp float amount)
+{
+ return sdf - amount;
+}
+
+// Convert an sdf shape to an outline of its shape.
+//
+// \param sdf The sdf shape to turn into an outline.
+//
+// \return The outline of sdf.
+lowp float sdf_outline(in lowp float sdf)
+{
+ return abs(sdf);
+}
+
+/********************
+ Convenience
+********************/
+
+// A constant to represent a "null" value of an sdf.
+//
+// Since 0 is a point exactly on the outline of an sdf shape, and negative
+// values are inside the shape, this uses a very large positive constant to
+// indicate a value that is really far away from the actual sdf shape.
+const lowp float sdf_null = 99999.0;
+
+// A constant for a default level of smoothing when rendering an sdf.
+//
+// This
+const lowp float sdf_default_smoothing = 0.625;
+
+// Render an sdf shape alpha-blended onto an existing color.
+//
+// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a
+// blending amount and a smoothing amount.
+//
+// \param alpha The alpha to use for blending.
+// \param smoothing The amount of smoothing to apply to the sdf.
+//
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float alpha, in lowp float smoothing)
+{
+ lowp float g = smoothing * fwidth(sdf);
+ return mix(sourceColor, sdfColor, alpha * (1.0 - clamp(sdf / g, 0.0, 1.0)));
+}
+
+// Render an sdf shape.
+//
+// This will render the sdf shape on top of whatever source color is input,
+// making sure to apply smoothing if desired.
+//
+// \param sdf The sdf shape to render.
+// \param sourceColor The source color to render on top of.
+// \param sdfColor The color to use for rendering the sdf shape.
+//
+// \return sourceColor with the sdf shape rendered on top.
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor)
+{
+ return sdf_render(sdf, sourceColor, sdfColor, 1.0, sdf_default_smoothing);
+}
+
+// Render an sdf shape.
+//
+// This is an overload of sdf_render(float, vec4, vec4) that allows specifying a
+// smoothing amount.
+//
+// \param smoothing The amount of smoothing to apply to the sdf.
+//
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float smoothing)
+{
+ return sdf_render(sdf, sourceColor, sdfColor, 1.0, smoothing);
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle.frag
new file mode 100644
index 0000000000..5bb51be545
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle.frag
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf.glsl"
+// See sdf.glsl for the SDF related functions.
+
+// This shader renders a rectangle with rounded corners and a shadow below it.
+// In addition it renders a border around it.
+
+#include "uniforms.glsl"
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+const lowp float minimum_shadow_radius = 0.05;
+
+void main()
+{
+ // Scaling factor that is the inverse of the amount of scaling applied to the geometry.
+ lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0);
+
+ // Correction factor to round the corners of a larger shadow.
+ // We want to account for size in regards to shadow radius, so that a larger shadow is
+ // more rounded, but only if we are not already rounding the corners due to corner radius.
+ lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius));
+ lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor;
+
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the shadow's distance field.
+ lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale);
+ // Render it, interpolating the color over the distance.
+ col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow));
+
+ // Scale corrected corner radius
+ lowp vec4 corner_radius = ubuf.radius * inverse_scale;
+
+ // Calculate the outer rectangle distance field and render it.
+ lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, corner_radius);
+
+ col = sdf_render(outer_rect, col, ubuf.borderColor);
+
+ // The inner rectangle distance field is the outer reduced by twice the border size.
+ lowp float inner_rect = outer_rect + (ubuf.borderWidth * inverse_scale) * 2.0;
+
+ // Finally, render the inner rectangle.
+ col = sdf_render(inner_rect, col, ubuf.color);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle_lowpower.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle_lowpower.frag
new file mode 100644
index 0000000000..abf1c155d3
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedborderrectangle_lowpower.frag
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf_lowpower.glsl"
+// See sdf.glsl for the SDF related functions.
+
+// This is a version of shadowedborderrectangle.frag for extremely low powered
+// hardware (PinePhone). It does not draw a shadow and also eliminates alpha
+// blending.
+
+#include "uniforms.glsl"
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+void main()
+{
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the outer rectangle distance field and render it.
+ lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius);
+
+ col = sdf_render(outer_rect, col, ubuf.borderColor);
+
+ // The inner distance field is the outer reduced by border width.
+ lowp float inner_rect = outer_rect + ubuf.borderWidth * 2.0;
+
+ // Render it.
+ col = sdf_render(inner_rect, col, ubuf.color);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture.frag
new file mode 100644
index 0000000000..da1a19cf20
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture.frag
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf.glsl"
+// See sdf.glsl for the SDF related functions.
+
+// This shader renders a rectangle with rounded corners and a shadow below it.
+// In addition it renders a border around it.
+
+#include "uniforms.glsl"
+layout(binding = 1) uniform sampler2D textureSource;
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+const lowp float minimum_shadow_radius = 0.05;
+
+void main()
+{
+ // Scaling factor that is the inverse of the amount of scaling applied to the geometry.
+ lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0);
+
+ // Correction factor to round the corners of a larger shadow.
+ // We want to account for size in regards to shadow radius, so that a larger shadow is
+ // more rounded, but only if we are not already rounding the corners due to corner radius.
+ lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius));
+ lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor;
+
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the shadow's distance field.
+ lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale);
+ // Render it, interpolating the color over the distance.
+ col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow));
+
+ // Scale corrected corner radius
+ lowp vec4 corner_radius = ubuf.radius * inverse_scale;
+
+ // Calculate the outer rectangle distance field and render it.
+ lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, corner_radius);
+
+ col = sdf_render(outer_rect, col, ubuf.borderColor);
+
+ // The inner rectangle distance field is the outer reduced by twice the border width.
+ lowp float inner_rect = outer_rect + (ubuf.borderWidth * inverse_scale) * 2.0;
+
+ // Render the inner rectangle.
+ col = sdf_render(inner_rect, col, ubuf.color);
+
+ // Sample the texture, then blend it on top of the background color.
+ lowp vec2 texture_uv = ((uv / ubuf.aspect) + (1.0 * inverse_scale)) / (2.0 * inverse_scale);
+ lowp vec4 texture_color = texture(textureSource, texture_uv);
+ col = sdf_render(inner_rect, col, texture_color, texture_color.a, sdf_default_smoothing);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture_lowpower.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture_lowpower.frag
new file mode 100644
index 0000000000..ca79d7be9a
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedbordertexture_lowpower.frag
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf_lowpower.glsl"
+// See sdf.glsl for the SDF related functions.
+
+// This shader renders a rectangle with rounded corners and a shadow below it.
+// In addition it renders a border around it.
+
+#include "uniforms.glsl"
+layout(binding = 1) uniform sampler2D textureSource;
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+const lowp float minimum_shadow_radius = 0.05;
+
+void main()
+{
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the outer rectangle distance field.
+ lowp float outer_rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius);
+
+ // Render it
+ col = sdf_render(outer_rect, col, ubuf.borderColor);
+
+ // Inner rectangle distance field equals outer reduced by twice the border width
+ lowp float inner_rect = outer_rect + ubuf.borderWidth * 2.0;
+
+ // Render it so we have a background for the image.
+ col = sdf_render(inner_rect, col, ubuf.color);
+
+ // Sample the texture, then render it, blending with the background color.
+ lowp vec2 texture_uv = ((uv / ubuf.aspect) + 1.0) / 2.0;
+ lowp vec4 texture_color = texture(textureSource, texture_uv);
+ col = sdf_render(inner_rect, col, texture_color, texture_color.a, sdf_default_smoothing);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.frag
new file mode 100644
index 0000000000..6705d454a9
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.frag
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf.glsl"
+// See sdf.glsl for the SDF related functions.
+
+// This shader renders a rectangle with rounded corners and a shadow below it.
+
+#include "uniforms.glsl"
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+const lowp float minimum_shadow_radius = 0.05;
+
+void main()
+{
+ // Scaling factor that is the inverse of the amount of scaling applied to the geometry.
+ lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0);
+
+ // Correction factor to round the corners of a larger shadow.
+ // We want to account for size in regards to shadow radius, so that a larger shadow is
+ // more rounded, but only if we are not already rounding the corners due to corner radius.
+ lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius));
+ lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor;
+
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the shadow's distance field.
+ lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale);
+ // Render it, interpolating the color over the distance.
+ col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow));
+
+ // Calculate the main rectangle distance field and render it.
+ lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, ubuf.radius * inverse_scale);
+
+ col = sdf_render(rect, col, ubuf.color);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.vert b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.vert
new file mode 100644
index 0000000000..312ed1b365
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle.vert
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "uniforms.glsl"
+
+layout(location = 0) in highp vec4 in_vertex;
+layout(location = 1) in mediump vec2 in_uv;
+
+layout(location = 0) out mediump vec2 uv;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+void main() {
+ uv = (-1.0 + 2.0 * in_uv) * ubuf.aspect;
+ gl_Position = ubuf.matrix * in_vertex;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle_lowpower.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle_lowpower.frag
new file mode 100644
index 0000000000..8a886911c1
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedrectangle_lowpower.frag
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+// See sdf.glsl for the SDF related functions.
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf_lowpower.glsl"
+
+// This is a version of shadowedrectangle.frag meant for very low power hardware
+// (PinePhone). It does not render a shadow and does not do alpha blending.
+
+#include "uniforms.glsl"
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+void main()
+{
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the main rectangle distance field.
+ lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius);
+
+ // Render it.
+ col = sdf_render(rect, col, ubuf.color);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture.frag
new file mode 100644
index 0000000000..508e3f9899
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture.frag
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf.glsl"
+// See sdf.glsl for the SDF related functions.
+
+// This shader renders a texture on top of a rectangle with rounded corners and
+// a shadow below it.
+
+#include "uniforms.glsl"
+layout(binding = 1) uniform sampler2D textureSource;
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+const lowp float minimum_shadow_radius = 0.05;
+
+void main()
+{
+ // Scaling factor that is the inverse of the amount of scaling applied to the geometry.
+ lowp float inverse_scale = 1.0 / (1.0 + ubuf.size + length(ubuf.offset) * 2.0);
+
+ // Correction factor to round the corners of a larger shadow.
+ // We want to account for size in regards to shadow radius, so that a larger shadow is
+ // more rounded, but only if we are not already rounding the corners due to corner radius.
+ lowp vec4 size_factor = 0.5 * (minimum_shadow_radius / max(ubuf.radius, minimum_shadow_radius));
+ lowp vec4 shadow_radius = ubuf.radius + ubuf.size * size_factor;
+
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the shadow's distance field.
+ lowp float shadow = sdf_rounded_rectangle(uv - ubuf.offset * 2.0 * inverse_scale, ubuf.aspect * inverse_scale, shadow_radius * inverse_scale);
+ // Render it, interpolating the color over the distance.
+ col = mix(col, ubuf.shadowColor * sign(ubuf.size), 1.0 - smoothstep(-ubuf.size * 0.5, ubuf.size * 0.5, shadow));
+
+ // Calculate the main rectangle distance field and render it.
+ lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect * inverse_scale, ubuf.radius * inverse_scale);
+
+ col = sdf_render(rect, col, ubuf.color);
+
+ // Sample the texture, then blend it on top of the background color.
+ lowp vec2 texture_uv = ((uv / ubuf.aspect) + (1.0 * inverse_scale)) / (2.0 * inverse_scale);
+ lowp vec4 texture_color = texture(textureSource, texture_uv);
+ col = sdf_render(rect, col, texture_color, texture_color.a, sdf_default_smoothing);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture_lowpower.frag b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture_lowpower.frag
new file mode 100644
index 0000000000..e2b95c47f5
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/shadowedtexture_lowpower.frag
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf_lowpower.glsl"
+// See sdf.glsl for the SDF related functions.
+
+// This shader renders a texture on top of a rectangle with rounded corners and
+// a shadow below it.
+
+#include "uniforms.glsl"
+layout(binding = 1) uniform sampler2D textureSource;
+
+layout(location = 0) in lowp vec2 uv;
+layout(location = 0) out lowp vec4 out_color;
+
+void main()
+{
+ lowp vec4 col = vec4(0.0);
+
+ // Calculate the main rectangle distance field.
+ lowp float rect = sdf_rounded_rectangle(uv, ubuf.aspect, ubuf.radius);
+
+ // Render it, so we have a background for the image.
+ col = sdf_render(rect, col, ubuf.color);
+
+ // Sample the texture, then render it, blending it with the background.
+ lowp vec2 texture_uv = ((uv / ubuf.aspect) + 1.0) / 2.0;
+ lowp vec4 texture_color = texture(textureSource, texture_uv);
+ col = sdf_render(rect, col, texture_color, texture_color.a, sdf_default_smoothing);
+
+ out_color = col * ubuf.opacity;
+}
diff --git a/launcher/qml/org/prismlauncher/primitives/shaders/uniforms.glsl b/launcher/qml/org/prismlauncher/primitives/shaders/uniforms.glsl
new file mode 100644
index 0000000000..0df5fb66dc
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shaders/uniforms.glsl
@@ -0,0 +1,20 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+layout(std140, binding = 0) uniform buf {
+ highp mat4 matrix; // offset 0
+ lowp vec2 aspect; // offset 64
+
+ lowp float opacity; // offset 72
+ lowp float size; // offset 76
+ lowp vec4 radius; // offset 80
+ lowp vec4 color; // offset 96
+ lowp vec4 shadowColor; // offset 112
+ lowp vec2 offset; // offset 128
+
+ lowp float borderWidth; // offset 136
+ lowp vec4 borderColor; // offset 144
+} ubuf; // size 160
diff --git a/launcher/qml/org/prismlauncher/primitives/shadowedrectangle.cpp b/launcher/qml/org/prismlauncher/primitives/shadowedrectangle.cpp
new file mode 100644
index 0000000000..9845dab050
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shadowedrectangle.cpp
@@ -0,0 +1,365 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedrectangle.h"
+
+#include
+#include
+#include
+
+#include "scenegraph/paintedrectangleitem.h"
+#include "scenegraph/shadowedrectanglenode.h"
+
+BorderGroup::BorderGroup(QObject *parent)
+ : QObject(parent)
+{
+}
+
+qreal BorderGroup::width() const
+{
+ return m_width;
+}
+
+void BorderGroup::setWidth(qreal newWidth)
+{
+ if (newWidth == m_width) {
+ return;
+ }
+
+ m_width = newWidth;
+ Q_EMIT changed();
+}
+
+QColor BorderGroup::color() const
+{
+ return m_color;
+}
+
+void BorderGroup::setColor(const QColor &newColor)
+{
+ if (newColor == m_color) {
+ return;
+ }
+
+ m_color = newColor;
+ Q_EMIT changed();
+}
+
+ShadowGroup::ShadowGroup(QObject *parent)
+ : QObject(parent)
+{
+}
+
+qreal ShadowGroup::size() const
+{
+ return m_size;
+}
+
+void ShadowGroup::setSize(qreal newSize)
+{
+ if (newSize == m_size) {
+ return;
+ }
+
+ m_size = newSize;
+ Q_EMIT changed();
+}
+
+qreal ShadowGroup::xOffset() const
+{
+ return m_xOffset;
+}
+
+void ShadowGroup::setXOffset(qreal newXOffset)
+{
+ if (newXOffset == m_xOffset) {
+ return;
+ }
+
+ m_xOffset = newXOffset;
+ Q_EMIT changed();
+}
+
+qreal ShadowGroup::yOffset() const
+{
+ return m_yOffset;
+}
+
+void ShadowGroup::setYOffset(qreal newYOffset)
+{
+ if (newYOffset == m_yOffset) {
+ return;
+ }
+
+ m_yOffset = newYOffset;
+ Q_EMIT changed();
+}
+
+QColor ShadowGroup::color() const
+{
+ return m_color;
+}
+
+void ShadowGroup::setColor(const QColor &newColor)
+{
+ if (newColor == m_color) {
+ return;
+ }
+
+ m_color = newColor;
+ Q_EMIT changed();
+}
+
+CornersGroup::CornersGroup(QObject *parent)
+ : QObject(parent)
+{
+}
+
+qreal CornersGroup::topLeft() const
+{
+ return m_topLeft;
+}
+
+void CornersGroup::setTopLeft(qreal newTopLeft)
+{
+ if (newTopLeft == m_topLeft) {
+ return;
+ }
+
+ m_topLeft = newTopLeft;
+ Q_EMIT changed();
+}
+
+qreal CornersGroup::topRight() const
+{
+ return m_topRight;
+}
+
+void CornersGroup::setTopRight(qreal newTopRight)
+{
+ if (newTopRight == m_topRight) {
+ return;
+ }
+
+ m_topRight = newTopRight;
+ Q_EMIT changed();
+}
+
+qreal CornersGroup::bottomLeft() const
+{
+ return m_bottomLeft;
+}
+
+void CornersGroup::setBottomLeft(qreal newBottomLeft)
+{
+ if (newBottomLeft == m_bottomLeft) {
+ return;
+ }
+
+ m_bottomLeft = newBottomLeft;
+ Q_EMIT changed();
+}
+
+qreal CornersGroup::bottomRight() const
+{
+ return m_bottomRight;
+}
+
+void CornersGroup::setBottomRight(qreal newBottomRight)
+{
+ if (newBottomRight == m_bottomRight) {
+ return;
+ }
+
+ m_bottomRight = newBottomRight;
+ Q_EMIT changed();
+}
+
+QVector4D CornersGroup::toVector4D(float all) const
+{
+ return QVector4D{m_bottomRight < 0.0 ? all : m_bottomRight,
+ m_topRight < 0.0 ? all : m_topRight,
+ m_bottomLeft < 0.0 ? all : m_bottomLeft,
+ m_topLeft < 0.0 ? all : m_topLeft};
+}
+
+ShadowedRectangle::ShadowedRectangle(QQuickItem *parentItem)
+ : QQuickItem(parentItem)
+ , m_border(std::make_unique())
+ , m_shadow(std::make_unique())
+ , m_corners(std::make_unique())
+{
+ setFlag(QQuickItem::ItemHasContents, true);
+
+ connect(m_border.get(), &BorderGroup::changed, this, &ShadowedRectangle::update);
+ connect(m_shadow.get(), &ShadowGroup::changed, this, &ShadowedRectangle::update);
+ connect(m_corners.get(), &CornersGroup::changed, this, &ShadowedRectangle::update);
+}
+
+ShadowedRectangle::~ShadowedRectangle()
+{
+}
+
+BorderGroup *ShadowedRectangle::border() const
+{
+ return m_border.get();
+}
+
+ShadowGroup *ShadowedRectangle::shadow() const
+{
+ return m_shadow.get();
+}
+
+CornersGroup *ShadowedRectangle::corners() const
+{
+ return m_corners.get();
+}
+
+qreal ShadowedRectangle::radius() const
+{
+ return m_radius;
+}
+
+void ShadowedRectangle::setRadius(qreal newRadius)
+{
+ if (newRadius == m_radius) {
+ return;
+ }
+
+ m_radius = newRadius;
+ if (!isSoftwareRendering()) {
+ update();
+ }
+ Q_EMIT radiusChanged();
+}
+
+QColor ShadowedRectangle::color() const
+{
+ return m_color;
+}
+
+void ShadowedRectangle::setColor(const QColor &newColor)
+{
+ if (newColor == m_color) {
+ return;
+ }
+
+ m_color = newColor;
+ if (!isSoftwareRendering()) {
+ update();
+ }
+ Q_EMIT colorChanged();
+}
+
+ShadowedRectangle::RenderType ShadowedRectangle::renderType() const
+{
+ return m_renderType;
+}
+
+void ShadowedRectangle::setRenderType(RenderType renderType)
+{
+ if (renderType == m_renderType) {
+ return;
+ }
+ m_renderType = renderType;
+ update();
+ Q_EMIT renderTypeChanged();
+}
+
+void ShadowedRectangle::componentComplete()
+{
+ QQuickItem::componentComplete();
+
+ checkSoftwareItem();
+}
+
+bool ShadowedRectangle::isSoftwareRendering() const
+{
+ return (window() && window()->rendererInterface()->graphicsApi() == QSGRendererInterface::Software) || m_renderType == RenderType::Software;
+}
+
+PaintedRectangleItem *ShadowedRectangle::softwareItem() const
+{
+ return m_softwareItem;
+}
+
+void ShadowedRectangle::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
+{
+ if (change == QQuickItem::ItemSceneChange && value.window) {
+ checkSoftwareItem();
+ // TODO: only conditionally emit?
+ Q_EMIT softwareRenderingChanged();
+ }
+
+ QQuickItem::itemChange(change, value);
+}
+
+QSGNode *ShadowedRectangle::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data)
+{
+ Q_UNUSED(data);
+
+ if (boundingRect().isEmpty()) {
+ delete node;
+ return nullptr;
+ }
+
+ auto shadowNode = static_cast(node);
+
+ if (!shadowNode) {
+ shadowNode = new ShadowedRectangleNode{};
+
+ // Cache lowPower state so we only execute the full check once.
+ static bool lowPower = QByteArrayList{"1", "true"}.contains(qgetenv("KIRIGAMI_LOWPOWER_HARDWARE").toLower());
+ if (m_renderType == RenderType::LowQuality || (m_renderType == RenderType::Auto && lowPower)) {
+ shadowNode->setShaderType(ShadowedRectangleMaterial::ShaderType::LowPower);
+ }
+ }
+
+ shadowNode->setBorderEnabled(m_border->isEnabled());
+ shadowNode->setRect(boundingRect());
+ shadowNode->setSize(m_shadow->size());
+ shadowNode->setRadius(m_corners->toVector4D(m_radius));
+ shadowNode->setOffset(QVector2D{float(m_shadow->xOffset()), float(m_shadow->yOffset())});
+ shadowNode->setColor(m_color);
+ shadowNode->setShadowColor(m_shadow->color());
+ shadowNode->setBorderWidth(m_border->width());
+ shadowNode->setBorderColor(m_border->color());
+ shadowNode->updateGeometry();
+ return shadowNode;
+}
+
+void ShadowedRectangle::checkSoftwareItem()
+{
+ if (!m_softwareItem && isSoftwareRendering()) {
+ m_softwareItem = new PaintedRectangleItem{this};
+ // The software item is added as a "normal" child item, this means it
+ // will be part of the normal item sort order. Since there is no way to
+ // control the ordering of children, just make sure to have a very low Z
+ // value for the child, to force it to be the lowest item.
+ m_softwareItem->setZ(-99.0);
+
+ auto updateItem = [this]() {
+ auto borderWidth = m_border->width();
+ auto rect = boundingRect();
+ m_softwareItem->setSize(rect.size());
+ m_softwareItem->setColor(m_color);
+ m_softwareItem->setRadius(m_radius);
+ m_softwareItem->setBorderWidth(borderWidth);
+ m_softwareItem->setBorderColor(m_border->color());
+ };
+
+ updateItem();
+
+ connect(this, &ShadowedRectangle::widthChanged, m_softwareItem, updateItem);
+ connect(this, &ShadowedRectangle::heightChanged, m_softwareItem, updateItem);
+ connect(this, &ShadowedRectangle::colorChanged, m_softwareItem, updateItem);
+ connect(this, &ShadowedRectangle::radiusChanged, m_softwareItem, updateItem);
+ connect(m_border.get(), &BorderGroup::changed, m_softwareItem, updateItem);
+ setFlag(QQuickItem::ItemHasContents, false);
+ }
+}
+
+#include "moc_shadowedrectangle.cpp"
diff --git a/launcher/qml/org/prismlauncher/primitives/shadowedrectangle.h b/launcher/qml/org/prismlauncher/primitives/shadowedrectangle.h
new file mode 100644
index 0000000000..ed1d8813ea
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shadowedrectangle.h
@@ -0,0 +1,370 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+#include
+
+#include
+
+class PaintedRectangleItem;
+
+/**
+ * @brief Grouped property for rectangle border.
+ */
+class BorderGroup : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+ /**
+ * @brief This property holds the border's width in pixels.
+ *
+ * default: ``0``px
+ */
+ Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY changed FINAL)
+ /**
+ * @brief This property holds the border's color.
+ *
+ * Full RGBA colors are supported.
+ *
+ * default: ``Qt::black``
+ */
+ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed FINAL)
+
+public:
+ explicit BorderGroup(QObject *parent = nullptr);
+
+ qreal width() const;
+ void setWidth(qreal newWidth);
+
+ QColor color() const;
+ void setColor(const QColor &newColor);
+
+ Q_SIGNAL void changed();
+
+ inline bool isEnabled() const
+ {
+ return !qFuzzyIsNull(m_width);
+ }
+
+private:
+ qreal m_width = 0.0;
+ QColor m_color = Qt::black;
+};
+
+/**
+ * @brief Grouped property for the rectangle's shadow.
+ */
+class ShadowGroup : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+ /**
+ * @brief This property holds the shadow's approximate size in pixels.
+ * @note The actual shadow size can be less than this value due to falloff.
+ *
+ * default: ``0``px
+ */
+ Q_PROPERTY(qreal size READ size WRITE setSize NOTIFY changed FINAL)
+ /**
+ * @brief This property holds the shadow's offset in pixels on the X axis.
+ *
+ * default: ``0``px
+ */
+ Q_PROPERTY(qreal xOffset READ xOffset WRITE setXOffset NOTIFY changed FINAL)
+ /**
+ * @brief This property holds the shadow's offset in pixels on the Y axis.
+ *
+ * default: ``0``px
+ */
+ Q_PROPERTY(qreal yOffset READ yOffset WRITE setYOffset NOTIFY changed FINAL)
+ /**
+ * @brief This property holds the shadow's color.
+ *
+ * Full RGBA colors are supported.
+ *
+ * default: ``Qt::black``
+ */
+ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed FINAL)
+
+public:
+ explicit ShadowGroup(QObject *parent = nullptr);
+
+ qreal size() const;
+ void setSize(qreal newSize);
+
+ qreal xOffset() const;
+ void setXOffset(qreal newXOffset);
+
+ qreal yOffset() const;
+ void setYOffset(qreal newYOffset);
+
+ QColor color() const;
+ void setColor(const QColor &newShadowColor);
+
+ Q_SIGNAL void changed();
+
+private:
+ qreal m_size = 0.0;
+ qreal m_xOffset = 0.0;
+ qreal m_yOffset = 0.0;
+ QColor m_color = Qt::black;
+};
+
+/**
+ * @brief Grouped property for corner radius.
+ */
+class CornersGroup : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+ /**
+ * @brief This property holds the top-left corner's radius in pixels.
+ *
+ * Setting this to ``-1`` indicates that the value should be ignored.
+ *
+ * default: ``-1``px
+ */
+ Q_PROPERTY(qreal topLeftRadius READ topLeft WRITE setTopLeft NOTIFY changed FINAL)
+
+ /**
+ * @brief This property holds the top-right corner's radius in pixels.
+ *
+ * Setting this to ``-1`` indicates that the value should be ignored.
+ *
+ * default: ``-1``px
+ */
+ Q_PROPERTY(qreal topRightRadius READ topRight WRITE setTopRight NOTIFY changed FINAL)
+
+ /**
+ * @brief This property holds the bottom-left corner's radius in pixels.
+ *
+ * Setting this to ``-1`` indicates that the value should be ignored.
+ *
+ * default: ``-1``px
+ */
+ Q_PROPERTY(qreal bottomLeftRadius READ bottomLeft WRITE setBottomLeft NOTIFY changed FINAL)
+
+ /**
+ * @brief This property holds the bottom-right corner's radius in pixels.
+ *
+ * Setting this to ``-1`` indicates that the value should be ignored.
+ *
+ * default: ``-1``px
+ */
+ Q_PROPERTY(qreal bottomRightRadius READ bottomRight WRITE setBottomRight NOTIFY changed FINAL)
+
+public:
+ explicit CornersGroup(QObject *parent = nullptr);
+
+ qreal topLeft() const;
+ void setTopLeft(qreal newTopLeft);
+
+ qreal topRight() const;
+ void setTopRight(qreal newTopRight);
+
+ qreal bottomLeft() const;
+ void setBottomLeft(qreal newBottomLeft);
+
+ qreal bottomRight() const;
+ void setBottomRight(qreal newBottomRight);
+
+ Q_SIGNAL void changed();
+
+ QVector4D toVector4D(float all) const;
+
+private:
+ float m_topLeft = -1.0;
+ float m_topRight = -1.0;
+ float m_bottomLeft = -1.0;
+ float m_bottomRight = -1.0;
+};
+
+/**
+ * @brief A rectangle with a shadow behind it.
+ *
+ * This item will render a rectangle, with a shadow below it. The rendering is done
+ * using distance fields, which provide greatly improved performance. The shadow is
+ * rendered outside of the item's bounds, so the item's width and height are the
+ * rectangle's width and height.
+ *
+ * @since 5.69
+ * @since 2.12
+ */
+class ShadowedRectangle : public QQuickItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+ /**
+ * @brief This property holds the radii of the rectangle's corners.
+ *
+ * This is the amount of rounding to apply to all of the rectangle's
+ * corners, in pixels. Each corner can have a different radius.
+ *
+ * default: ``0``
+ *
+ * @see corners
+ */
+ Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged FINAL)
+
+ /**
+ * @brief This property holds the rectangle's color.
+ *
+ * Full RGBA colors are supported.
+ *
+ * default: ``Qt::white``
+ */
+ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL)
+
+ /**
+ * @brief This property holds the border's grouped property.
+ *
+ * Example usage:
+ * @code
+ * Kirigami.ShadowedRectangle {
+ * border.width: 2
+ * border.color: Kirigami.Theme.textColor
+ * }
+ * @endcode
+ * @see BorderGroup
+ */
+ Q_PROPERTY(BorderGroup *border READ border CONSTANT FINAL)
+
+ /**
+ * @brief This property holds the shadow's grouped property.
+ *
+ * Example usage:
+ * @code
+ * Kirigami.ShadowedRectangle {
+ * shadow.size: 20
+ * shadow.xOffset: 5
+ * shadow.yOffset: 5
+ * }
+ * @endcode
+ *
+ * @see ShadowGroup
+ */
+ Q_PROPERTY(ShadowGroup *shadow READ shadow CONSTANT FINAL)
+
+ /**
+ * @brief This property holds the corners grouped property
+ *
+ * Note that the values from this group override \property radius for the
+ * corner they affect.
+ *
+ * Example usage:
+ * @code
+ * Kirigami.ShadowedRectangle {
+ * corners.topLeftRadius: 4
+ * corners.topRightRadius: 5
+ * corners.bottomLeftRadius: 2
+ * corners.bottomRightRadius: 10
+ * @endcode
+ *
+ * @see CornersGroup
+ */
+ Q_PROPERTY(CornersGroup *corners READ corners CONSTANT FINAL)
+
+ /**
+ * @brief This property holds the rectangle's render mode.
+ *
+ * default: ``RenderType::Auto``
+ *
+ * @see RenderType
+ */
+ Q_PROPERTY(RenderType renderType READ renderType WRITE setRenderType NOTIFY renderTypeChanged FINAL)
+
+ /**
+ * @brief This property tells whether software rendering is being used.
+ *
+ * default: ``false``
+ */
+ Q_PROPERTY(bool softwareRendering READ isSoftwareRendering NOTIFY softwareRenderingChanged FINAL)
+
+public:
+ ShadowedRectangle(QQuickItem *parent = nullptr);
+ ~ShadowedRectangle() override;
+
+ /**
+ * @brief Available rendering types for ShadowedRectangle.
+ */
+ enum RenderType {
+ /**
+ * @brief Automatically determine the optimal rendering type.
+ *
+ * This will use the highest rendering quality possible, falling back to
+ * lower quality if the hardware doesn't support it. It will use software
+ * rendering if the QtQuick scene graph is set to use software rendering.
+ */
+ Auto,
+
+ /**
+ * @brief Use the highest rendering quality possible, even if the hardware might
+ * not be able to handle it normally.
+ */
+ HighQuality,
+
+ /**
+ * @brief Use the lowest rendering quality, even if the hardware could handle
+ * higher quality rendering.
+ *
+ * This might result in certain effects being omitted, like shadows.
+ */
+ LowQuality,
+
+ /**
+ * @brief Always use software rendering for this rectangle.
+ *
+ * Software rendering is intended as a fallback when the QtQuick scene
+ * graph is configured to use software rendering. It will result in
+ * a number of missing features, like shadows and multiple corner radii.
+ */
+ Software
+ };
+ Q_ENUM(RenderType)
+
+ BorderGroup *border() const;
+ ShadowGroup *shadow() const;
+ CornersGroup *corners() const;
+
+ qreal radius() const;
+ void setRadius(qreal newRadius);
+ Q_SIGNAL void radiusChanged();
+
+ QColor color() const;
+ void setColor(const QColor &newColor);
+ Q_SIGNAL void colorChanged();
+
+ RenderType renderType() const;
+ void setRenderType(RenderType renderType);
+ Q_SIGNAL void renderTypeChanged();
+
+ void componentComplete() override;
+
+ bool isSoftwareRendering() const;
+
+Q_SIGNALS:
+ void softwareRenderingChanged();
+
+protected:
+ PaintedRectangleItem *softwareItem() const;
+ void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override;
+ QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) override;
+
+private:
+ void checkSoftwareItem();
+ const std::unique_ptr m_border;
+ const std::unique_ptr m_shadow;
+ const std::unique_ptr m_corners;
+ qreal m_radius = 0.0;
+ QColor m_color = Qt::white;
+ RenderType m_renderType = RenderType::Auto;
+ PaintedRectangleItem *m_softwareItem = nullptr;
+};
diff --git a/launcher/qml/org/prismlauncher/primitives/shadowedtexture.cpp b/launcher/qml/org/prismlauncher/primitives/shadowedtexture.cpp
new file mode 100644
index 0000000000..6dded8f416
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shadowedtexture.cpp
@@ -0,0 +1,89 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "shadowedtexture.h"
+
+#include
+#include
+#include
+
+#include "scenegraph/shadowedtexturenode.h"
+
+ShadowedTexture::ShadowedTexture(QQuickItem *parentItem)
+ : ShadowedRectangle(parentItem)
+{
+}
+
+ShadowedTexture::~ShadowedTexture()
+{
+}
+
+QQuickItem *ShadowedTexture::source() const
+{
+ return m_source;
+}
+
+void ShadowedTexture::setSource(QQuickItem *newSource)
+{
+ if (newSource == m_source) {
+ return;
+ }
+
+ m_source = newSource;
+ m_sourceChanged = true;
+ if (m_source && !m_source->parentItem()) {
+ m_source->setParentItem(this);
+ }
+
+ if (!isSoftwareRendering()) {
+ update();
+ }
+ Q_EMIT sourceChanged();
+}
+
+QSGNode *ShadowedTexture::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data)
+{
+ Q_UNUSED(data)
+
+ if (boundingRect().isEmpty()) {
+ delete node;
+ return nullptr;
+ }
+
+ auto shadowNode = static_cast(node);
+
+ if (!shadowNode || m_sourceChanged) {
+ m_sourceChanged = false;
+ delete shadowNode;
+ if (m_source) {
+ shadowNode = new ShadowedTextureNode{};
+ } else {
+ shadowNode = new ShadowedRectangleNode{};
+ }
+
+ if (qEnvironmentVariableIsSet("KIRIGAMI_LOWPOWER_HARDWARE")) {
+ shadowNode->setShaderType(ShadowedRectangleMaterial::ShaderType::LowPower);
+ }
+ }
+
+ shadowNode->setBorderEnabled(border()->isEnabled());
+ shadowNode->setRect(boundingRect());
+ shadowNode->setSize(shadow()->size());
+ shadowNode->setRadius(corners()->toVector4D(radius()));
+ shadowNode->setOffset(QVector2D{float(shadow()->xOffset()), float(shadow()->yOffset())});
+ shadowNode->setColor(color());
+ shadowNode->setShadowColor(shadow()->color());
+ shadowNode->setBorderWidth(border()->width());
+ shadowNode->setBorderColor(border()->color());
+
+ if (m_source) {
+ static_cast(shadowNode)->setTextureSource(m_source->textureProvider());
+ }
+
+ shadowNode->updateGeometry();
+ return shadowNode;
+}
+
diff --git a/launcher/qml/org/prismlauncher/primitives/shadowedtexture.h b/launcher/qml/org/prismlauncher/primitives/shadowedtexture.h
new file mode 100644
index 0000000000..39798042f2
--- /dev/null
+++ b/launcher/qml/org/prismlauncher/primitives/shadowedtexture.h
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include "shadowedrectangle.h"
+
+/**
+ * A rectangle with a shadow, using a QQuickItem as texture.
+ *
+ * This item will render a source item, with a shadow below it. The rendering is done
+ * using distance fields, which provide greatly improved performance. The shadow is
+ * rendered outside of the item's bounds, so the item's width and height are the
+ * rectangle's width and height.
+ *
+ * @since 5.69 / 2.12
+ */
+class ShadowedTexture : public ShadowedRectangle
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+ /**
+ * This property holds the source item that will get rendered with the
+ * shadow.
+ */
+ Q_PROPERTY(QQuickItem *source READ source WRITE setSource NOTIFY sourceChanged FINAL)
+
+public:
+ ShadowedTexture(QQuickItem *parent = nullptr);
+ ~ShadowedTexture() override;
+
+ QQuickItem *source() const;
+ void setSource(QQuickItem *newSource);
+ Q_SIGNAL void sourceChanged();
+
+protected:
+ QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) override;
+
+private:
+ QQuickItem *m_source = nullptr;
+ bool m_sourceChanged = false;
+};