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; +};