From fdc13023b7a2cf11a68756c673e6bf0f9d7a37fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 17:05:44 -0800 Subject: [PATCH] widgets: add ClippingRectangle --- BUILD.md | 1 + CMakeLists.txt | 2 +- default.nix | 3 ++ src/widgets/CMakeLists.txt | 20 +++++++- src/widgets/ClippingRectangle.qml | 82 +++++++++++++++++++++++++++++++ src/widgets/cliprect.cpp | 1 + src/widgets/cliprect.hpp | 20 ++++++++ src/widgets/module.md | 1 + src/widgets/shaders/cliprect.frag | 40 +++++++++++++++ 9 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 src/widgets/ClippingRectangle.qml create mode 100644 src/widgets/cliprect.cpp create mode 100644 src/widgets/cliprect.hpp create mode 100644 src/widgets/shaders/cliprect.frag diff --git a/BUILD.md b/BUILD.md index 8fa7884..2085a5b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -41,6 +41,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `cmake` - `qt6base` - `qt6declarative` +- `qtshadertools` (build-time only) - `pkg-config` - `cli11` diff --git a/CMakeLists.txt b/CMakeLists.txt index 23e6add..61cc296 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() -set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets) +set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) include(cmake/pch.cmake) diff --git a/default.nix b/default.nix index 298e561..83c5e54 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + spirv-tools, cli11, breakpad, jemalloc, @@ -45,6 +46,8 @@ nativeBuildInputs = with pkgs; [ cmake ninja + qt6.qtshadertools + spirv-tools qt6.wrapQtAppsHook pkg-config ] ++ (lib.optionals withWayland [ diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index def0aaf..5fbcc5b 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,10 +1,28 @@ -qt_add_library(quickshell-widgets STATIC) +qt_add_library(quickshell-widgets STATIC + cliprect.cpp +) qt_add_qml_module(quickshell-widgets URI Quickshell.Widgets VERSION 0.1 QML_FILES IconImage.qml + ClippingRectangle.qml +) + +qt6_add_shaders(quickshell-widgets "widgets-cliprect" + NOHLSL NOMSL BATCHABLE PRECOMPILE OPTIMIZED QUIET + PREFIX "/Quickshell/Widgets" + FILES shaders/cliprect.frag + OUTPUTS shaders/cliprect.frag.qsb +) + +qt6_add_shaders(quickshell-widgets "widgets-cliprect-ub" + NOHLSL NOMSL BATCHABLE PRECOMPILE OPTIMIZED QUIET + PREFIX "/Quickshell/Widgets" + FILES shaders/cliprect.frag + OUTPUTS shaders/cliprect-ub.frag.qsb + DEFINES CONTENT_UNDER_BORDER ) install_qml_module(quickshell-widgets) diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml new file mode 100644 index 0000000..ca8ae5a --- /dev/null +++ b/src/widgets/ClippingRectangle.qml @@ -0,0 +1,82 @@ +import QtQuick + +///! Rectangle capable of clipping content inside its border. +/// > [!WARNING] This type requires at least Qt 6.7. +/// +/// This is a specialized version of @@QtQuick.Rectangle that clips content +/// inside of its border, including rounded rectangles. It costs more than +/// @@QtQuick.Rectangle, so it should not be used unless you need to clip +/// items inside of it to the border. +Item { + id: root + + /// If content should be displayed underneath the border. + /// + /// Defaults to false, does nothing if the border is opaque. + property bool contentUnderBorder: false; + /// If the content item should be resized to fit inside the border. + /// + /// Defaults to `!contentUnderBorder`. Most useful when combined with + /// `anchors.fill: parent` on an item passed to the ClippingRectangle. + property bool contentInsideBorder: !root.contentUnderBorder; + /// If the rectangle should be antialiased. + /// + /// Defaults to true if any corner has a non-zero radius, otherwise false. + property /*bool*/alias antialiasing: rectangle.antialiasing; + /// The background color of the rectangle, which goes under its content. + property /*color*/alias color: shader.backgroundColor; + /// See @@QtQuick.Rectangle.border. + property clippingRectangleBorder border; + /// Radius of all corners. Defaults to 0. + property /*real*/alias radius: rectangle.radius + /// Radius of the top left corner. Defaults to @@radius. + property /*real*/alias topLeftRadius: rectangle.topLeftRadius + /// Radius of the top right corner. Defaults to @@radius. + property /*real*/alias topRightRadius: rectangle.topRightRadius + /// Radius of the bottom left corner. Defaults to @@radius. + property /*real*/alias bottomLeftRadius: rectangle.bottomLeftRadius + /// Radius of the bottom right corner. Defaults to @@radius. + property /*real*/alias borromRightRadius: rectangle.bottomRightRadius + + /// Visual children of the ClippingRectangle's @@contentItem. (`list`). + /// + /// See @@QtQuick.Item.children for details. + default property alias children: contentItem.children; + /// The item containing the rectangle's content. + /// There is usually no reason to use this directly. + readonly property alias contentItem: contentItem; + + Rectangle { + id: rectangle + anchors.fill: root + color: "#ffff0000" + border.color: "#ff00ff00" + border.pixelAligned: root.border.pixelAligned + border.width: root.border.width + layer.enabled: true + visible: false + } + + Item { + id: contentItemContainer + anchors.fill: root + layer.enabled: true + visible: false + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: root.contentInsideBorder ? root.border.width : 0 + } + } + + ShaderEffect { + id: shader + anchors.fill: root + fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` + property Rectangle rect: rectangle; + property color backgroundColor; + property color borderColor: root.border.color; + property Item content: contentItemContainer; + } +} diff --git a/src/widgets/cliprect.cpp b/src/widgets/cliprect.cpp new file mode 100644 index 0000000..a66147e --- /dev/null +++ b/src/widgets/cliprect.cpp @@ -0,0 +1 @@ +#include "cliprect.hpp" // NOLINT diff --git a/src/widgets/cliprect.hpp b/src/widgets/cliprect.hpp new file mode 100644 index 0000000..adc1381 --- /dev/null +++ b/src/widgets/cliprect.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ClippingRectangleBorder { + Q_GADGET; + Q_PROPERTY(QColor color MEMBER color); + Q_PROPERTY(bool pixelAligned MEMBER pixelAligned); + Q_PROPERTY(int width MEMBER width); + QML_VALUE_TYPE(clippingRectangleBorder); + +public: + QColor color = Qt::black; + bool pixelAligned = true; + int width = 0; +}; diff --git a/src/widgets/module.md b/src/widgets/module.md index 9a51894..c24bb87 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -2,5 +2,6 @@ name = "Quickshell.Widgets" description = "Bundled widgets" qml_files = [ "IconImage.qml", + "ClippingRectangle.qml", ] ----- diff --git a/src/widgets/shaders/cliprect.frag b/src/widgets/shaders/cliprect.frag new file mode 100644 index 0000000..f1c004a --- /dev/null +++ b/src/widgets/shaders/cliprect.frag @@ -0,0 +1,40 @@ +#version 440 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 backgroundColor; + vec4 borderColor; +}; + +layout(binding = 1) uniform sampler2D rect; +layout(binding = 2) uniform sampler2D content; + +vec4 overlay(vec4 base, vec4 overlay) { + if (overlay.a == 0.0) return base; + + float baseMul = 1.0 - overlay.a; + float newAlpha = overlay.a + base.a * baseMul; + vec3 rgb = (overlay.rgb * overlay.a + base.rgb * base.a * baseMul) / newAlpha; + return vec4(rgb, newAlpha); +} + +void main() { + vec4 contentColor = texture(content, qt_TexCoord0.xy); + vec4 rectColor = texture(rect, qt_TexCoord0.xy); + +#ifdef CONTENT_UNDER_BORDER + float contentAlpha = rectColor.a; +#else + float contentAlpha = rectColor.r; +#endif + + float borderAlpha = rectColor.g; + + vec4 innerColor = overlay(backgroundColor, contentColor) * contentAlpha; + vec4 borderColor = borderColor * borderAlpha; + + fragColor = (innerColor * (1.0 - borderColor.a) + borderColor) * qt_Opacity; +}