Compare commits
6
Commits
v0.5.0
..
39f99bb336
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39f99bb336
|
||
|
|
068319a4cb
|
||
|
|
bdb95af7a1
|
||
|
|
cbd4e6caa7 | ||
|
|
36cddc96c3 | ||
|
|
6781078467 |
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: apk add --no-cache docker-cli
|
||||
run: apk add --no-cache docker-cli docker-cli-buildx
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
||||
|
||||
@@ -20,28 +20,10 @@ jobs:
|
||||
-exec clang-format -i --style=file {} +
|
||||
git diff --exit-code && echo "clang-format: passed"
|
||||
|
||||
build:
|
||||
build & lint:
|
||||
runs-on: alpine
|
||||
container:
|
||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build
|
||||
run: ninja -C build
|
||||
|
||||
clang-tidy:
|
||||
runs-on: alpine
|
||||
container:
|
||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
fmt & lint:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
@@ -24,20 +24,6 @@ jobs:
|
||||
else
|
||||
echo "No JS/TS files found"
|
||||
fi
|
||||
|
||||
lint:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git
|
||||
|
||||
- name: ESLint
|
||||
run: |
|
||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
static:
|
||||
fmt & lint:
|
||||
runs-on: debian
|
||||
container: node:26-trixie-slim
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
. .venv/bin/activate
|
||||
ruff check .
|
||||
|
||||
verify:
|
||||
test & nuitka build:
|
||||
if: always()
|
||||
runs-on: debian
|
||||
container: node:26-trixie-slim
|
||||
@@ -64,4 +64,4 @@ jobs:
|
||||
- name: Nuitka module check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
nuitka --module --include-package=zshell cli/src/zshell/
|
||||
nuitka --module --include-package=zshell cli/src/zshell/
|
||||
|
||||
+21
-86
@@ -3,8 +3,9 @@ name: Rust
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
# jobs can be separated if we integrate tests
|
||||
jobs:
|
||||
build:
|
||||
build, clippy & fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
env:
|
||||
@@ -34,7 +35,9 @@ jobs:
|
||||
apk add --no-cache \
|
||||
git \
|
||||
cargo \
|
||||
rust
|
||||
rust \
|
||||
rust-clippy \
|
||||
rustfmt
|
||||
|
||||
- name: Cargo check
|
||||
run: |
|
||||
@@ -50,90 +53,6 @@ jobs:
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
env:
|
||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache cargo packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-cargo-packages
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry
|
||||
.cargo/git
|
||||
target
|
||||
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
rust-${{ runner.os }}-build-
|
||||
rust-
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
cargo \
|
||||
rust \
|
||||
rustfmt
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo fmt --manifest-path "$manifest" --check && \
|
||||
echo "$manifest: formatting OK" || \
|
||||
{ echo "$manifest: needs formatting"; status=1; }
|
||||
done
|
||||
exit $status
|
||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||
echo "Rust files found but no Cargo.toml"
|
||||
exit 1
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
|
||||
clippy:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
env:
|
||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache cargo packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-cargo-packages
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry
|
||||
.cargo/git
|
||||
target
|
||||
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
rust-${{ runner.os }}-build-
|
||||
rust-
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
cargo \
|
||||
rust \
|
||||
rust-clippy
|
||||
|
||||
- name: Clippy
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
@@ -150,3 +69,19 @@ jobs:
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
- name: Format check
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo fmt --manifest-path "$manifest" --check && \
|
||||
echo "$manifest: formatting OK" || \
|
||||
{ echo "$manifest: needs formatting"; status=1; }
|
||||
done
|
||||
exit $status
|
||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||
echo "Rust files found but no Cargo.toml"
|
||||
exit 1
|
||||
else
|
||||
echo "No Rust project found"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import ZShell.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real radius
|
||||
property real topLeftRadius: radius
|
||||
property real topRightRadius: radius
|
||||
property real bottomRightRadius: radius
|
||||
property real bottomLeftRadius: radius
|
||||
property color color: "transparent"
|
||||
default property alias contentData: contentItem.data
|
||||
|
||||
Item {
|
||||
id: contentItem
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
ShaderEffectSource {
|
||||
id: source
|
||||
|
||||
hideSource: true
|
||||
sourceItem: contentItem
|
||||
}
|
||||
|
||||
RoundedMask {
|
||||
anchors.fill: parent
|
||||
source: source
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
color: root.color
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ TextField {
|
||||
implicitHeight: contentHeight + topPadding + bottomPadding
|
||||
implicitWidth: contentWidth + leftPadding + rightPadding
|
||||
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
|
||||
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
|
||||
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.QtRendering
|
||||
selectedTextColor: color
|
||||
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Effects
|
||||
|
||||
CustomFlickable {
|
||||
@@ -9,6 +10,7 @@ CustomFlickable {
|
||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real fadeAmount: 0.1
|
||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
readonly property real dpr: Math.ceil((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
|
||||
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts
|
||||
@@ -28,6 +30,7 @@ CustomFlickable {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
// layer.textureSize: Qt.size(width * dpr, height * dpr)
|
||||
layer.effect: Mask {
|
||||
maskSource: mask
|
||||
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ import QtQuick
|
||||
import qs.Components
|
||||
import qs.Modules.Bar.Popouts as Popouts
|
||||
import qs.Modules.Notifications as Notifications
|
||||
import qs.Modules.Notifications.Sidebar as Sidebar
|
||||
import qs.Modules.Notifications.Sidebar.Utils as Utils
|
||||
import qs.Modules.Sidebar as Sidebar
|
||||
import qs.Modules.Sidebar.Utils as Utils
|
||||
import qs.Modules.Dashboard as Dashboard
|
||||
import qs.Modules.Osd as Osd
|
||||
import qs.Components.Toast as Toasts
|
||||
|
||||
@@ -23,7 +23,7 @@ Singleton {
|
||||
|
||||
function toggleNightLight(): void {
|
||||
service.manualToggle = true;
|
||||
service.toggle();
|
||||
service.toggle(true);
|
||||
}
|
||||
|
||||
NightlightManager {
|
||||
|
||||
@@ -19,7 +19,7 @@ Item {
|
||||
required property Wrapper popouts
|
||||
|
||||
function resolveIcon(app: string, icon: string): string {
|
||||
if (app === "chrome_status_icon_1") {
|
||||
if (app === "discord_status_icon_1" && Config.bar.tray.replaceIcons) {
|
||||
return Quickshell.iconPath("discord-tray");
|
||||
} else if (app === "AyuGramDesktop") {
|
||||
if (icon === Quickshell.iconPath("com.ayugram.desktop-attention-symbolic"))
|
||||
@@ -50,6 +50,8 @@ Item {
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
id: layer
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
anchors.fill: parent
|
||||
color: icon.layer.enabled && root.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
|
||||
@@ -33,8 +33,7 @@ Item {
|
||||
y: parent.y
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +100,7 @@ Item {
|
||||
value: Audio.volume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setVolume(value)
|
||||
@@ -123,7 +121,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 65 + Tokens.spacing.small * 2
|
||||
Layout.topMargin: root.topMargin
|
||||
@@ -136,17 +134,21 @@ Item {
|
||||
node: Audio.source
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: sourcePeakFill
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * sourcePeak.peak
|
||||
CustomRect {
|
||||
id: sourcePeakFill
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * sourcePeak.peak
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,8 +197,7 @@ Item {
|
||||
value: Audio.sourceVolume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setSourceVolume(value)
|
||||
@@ -230,7 +231,7 @@ Item {
|
||||
|
||||
model: Audio.streams.filter(s => s.isSink)
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: appBox
|
||||
|
||||
required property int index
|
||||
@@ -248,17 +249,21 @@ Item {
|
||||
node: appBox.modelData
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: peakFill
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * peak.peak
|
||||
CustomRect {
|
||||
id: peakFill
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * peak.peak
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,8 +321,7 @@ Item {
|
||||
value: appBox.modelData.audio.volume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setStreamVolume(appBox.modelData, value)
|
||||
|
||||
@@ -9,7 +9,7 @@ import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0
|
||||
@@ -73,6 +73,7 @@ CustomClippingRect {
|
||||
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
|
||||
implicitWidth: contentWidth
|
||||
spacing: Tokens.spacing.medium
|
||||
clip: true
|
||||
|
||||
delegate: CustomRect {
|
||||
id: update
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
|
||||
@@ -3,8 +3,9 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
|
||||
@@ -27,7 +28,7 @@ Item {
|
||||
for (let i = 0; i < count; i++)
|
||||
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
|
||||
|
||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
|
||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard)
|
||||
return 0;
|
||||
|
||||
if (screenState.osd) {
|
||||
@@ -36,25 +37,20 @@ Item {
|
||||
height = h;
|
||||
}
|
||||
|
||||
if (screenState.session) {
|
||||
const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2;
|
||||
if (height > h)
|
||||
height = h;
|
||||
}
|
||||
|
||||
return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - 1 * 2, height + padding * 2);
|
||||
}
|
||||
|
||||
implicitWidth: Config.notifs.sizes.width + padding * 2
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ClippingWrapperRectangle {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.large - root.padding
|
||||
clip: true
|
||||
// radius: Tokens.rounding.large - root.padding
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
|
||||
@@ -4,7 +4,7 @@ import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property color accent
|
||||
@@ -19,15 +19,20 @@ CustomClippingRect {
|
||||
implicitWidth: 450
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(root.accent, 0.05)
|
||||
implicitWidth: parent.width * root.usage
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(root.accent, 0.05)
|
||||
implicitWidth: parent.width * root.usage
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
VerticalFadeFlickable {
|
||||
Flickable {
|
||||
id: flickable
|
||||
|
||||
Layout.fillHeight: true
|
||||
@@ -158,7 +158,8 @@ ColumnLayout {
|
||||
|
||||
bottomMargin: Tokens.padding.extraLarge
|
||||
topMargin: Tokens.padding.large
|
||||
fadeAmount: 0.1
|
||||
clip: true
|
||||
// fadeAmount: 0.1
|
||||
|
||||
contentHeight: root.contentChild?.implicitHeight ?? 0
|
||||
contentItem.children: [root.contentChild]
|
||||
|
||||
@@ -6,7 +6,7 @@ import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color blobColor: Colors.tPalette.m3surfaceContainerLow
|
||||
@@ -21,7 +21,6 @@ CustomClippingRect {
|
||||
|
||||
implicitHeight: sState.screen.height * 0.7
|
||||
implicitWidth: implicitHeight * ratio
|
||||
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
||||
|
||||
Behavior on blobColor {
|
||||
CAnim {}
|
||||
@@ -46,6 +45,8 @@ CustomClippingRect {
|
||||
borderTop: Tokens.padding.small
|
||||
group: blobGroup
|
||||
opacity: root.blobColor.a
|
||||
// outerRadius: 10
|
||||
outerRadius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
}
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ PageBase {
|
||||
stepSize: 1
|
||||
subtext: qsTr("How long it takes for the gradual nightlight to finish (s)")
|
||||
text: qsTr("Fade duration")
|
||||
to: 60 * 5
|
||||
to: 60 * 20
|
||||
value: Config.display.nightlight.fadeDuration / 1000
|
||||
|
||||
onMoved: v => Config.display.nightlight.fadeDuration = v * 1000
|
||||
|
||||
@@ -2,8 +2,9 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -27,21 +28,17 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
sourceComponent: Content {
|
||||
sState.animatingContainer: content.opacity < 1
|
||||
sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode)
|
||||
sState.screen: root.screen
|
||||
}
|
||||
sourceComponent: Content {
|
||||
sState.animatingContainer: content.opacity < 1
|
||||
sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode)
|
||||
sState.screen: root.screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Modules.Sidebar.Chat.Content
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import qs.Effects
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
property bool expanded: false
|
||||
@@ -5,7 +5,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Modules.Sidebar.Chat.Content
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
|
||||
@@ -185,7 +185,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: fabMenuItem
|
||||
|
||||
required property var modelData
|
||||
@@ -197,6 +197,7 @@ Item {
|
||||
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
|
||||
color: Colors.palette.m3primaryContainer
|
||||
visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
|
||||
clip: true
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
+1
-1
@@ -6,7 +6,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Helpers
|
||||
import qs.Modules.Notifications.Sidebar
|
||||
import qs.Modules.Sidebar
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Modules.Sidebar.Chat
|
||||
|
||||
Item {
|
||||
id: root
|
||||
+3
-4
@@ -2,7 +2,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Modules.Sidebar.Chat
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
@@ -24,7 +24,7 @@ Item {
|
||||
|
||||
implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0)
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: bubble
|
||||
|
||||
property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
|
||||
@@ -37,8 +37,7 @@ Item {
|
||||
implicitWidth: layoutFrozen ? frozenWidth : layoutWidth
|
||||
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : markdown.implicitHeight + markdown.anchors.topMargin * 2
|
||||
anchors.right: root.isUser ? parent.right : undefined
|
||||
layer.enabled: false
|
||||
layer.smooth: true
|
||||
clip: true
|
||||
|
||||
transform: Scale {
|
||||
id: stretchScale
|
||||
+3
-2
@@ -6,7 +6,7 @@ import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Modules.Sidebar.Chat
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
@@ -223,12 +223,13 @@ MouseArea {
|
||||
height: width
|
||||
opacity: modelData ? 1 : 0
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.spacing.extraSmall
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
clip: true
|
||||
|
||||
FadeImage {
|
||||
id: attImage
|
||||
+1
-1
@@ -6,7 +6,7 @@ import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Llm
|
||||
import ZShell.Blobs
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Modules.Sidebar.Chat
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import ZShell.Blobs
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Modules.Sidebar.Chat.Content
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
@@ -5,7 +5,7 @@ import ZShell.Llm
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Modules.Sidebar.Chat
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
@@ -63,7 +63,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
Item {
|
||||
id: clipRect
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -71,8 +71,6 @@ Item {
|
||||
anchors.right: parent.right
|
||||
anchors.top: title.bottom
|
||||
anchors.topMargin: 10
|
||||
color: "transparent"
|
||||
radius: 6
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
@@ -3,7 +3,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Notifications.Sidebar.Utils.Cards
|
||||
import qs.Modules.Sidebar.Utils.Cards
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <qsggeometry.h>
|
||||
#include <qsgnode.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {}
|
||||
@@ -129,6 +130,10 @@ QSGNode* BlobInvertedRect::updatePaintNode(
|
||||
material->m_invertedInner,
|
||||
m_cachedInvertedInner,
|
||||
sizeof(m_cachedInvertedInner));
|
||||
memcpy(
|
||||
material->m_invertedOuterRadii,
|
||||
m_cachedInvertedOuterRadii,
|
||||
sizeof(m_cachedInvertedOuterRadii));
|
||||
|
||||
const int count =
|
||||
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
|
||||
@@ -173,6 +178,64 @@ void BlobInvertedRect::setBorderBottom(qreal v) {
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect::setOuterRadius(qreal v) {
|
||||
if (qFuzzyCompare(m_outerRadius, v)) return;
|
||||
m_outerRadius = v;
|
||||
emit outerRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect::setOuterTopLeftRadius(qreal v) {
|
||||
if (qFuzzyCompare(m_outerTopLeftRadius, v)) return;
|
||||
m_outerTopLeftRadius = v;
|
||||
emit outerTopLeftRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect::setOuterTopRightRadius(qreal v) {
|
||||
if (qFuzzyCompare(m_outerTopRightRadius, v)) return;
|
||||
m_outerTopRightRadius = v;
|
||||
emit outerTopRightRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect::setOuterBottomLeftRadius(qreal v) {
|
||||
if (qFuzzyCompare(m_outerBottomLeftRadius, v)) return;
|
||||
m_outerBottomLeftRadius = v;
|
||||
emit outerBottomLeftRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect::setOuterBottomRightRadius(qreal v) {
|
||||
if (qFuzzyCompare(m_outerBottomRightRadius, v)) return;
|
||||
m_outerBottomRightRadius = v;
|
||||
emit outerBottomRightRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect::outerCornerRadii(float out[4]) const {
|
||||
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
|
||||
const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
|
||||
out[0] = std::min(
|
||||
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
|
||||
: base,
|
||||
maxR);
|
||||
out[1] = std::min(
|
||||
m_outerBottomRightRadius >= 0
|
||||
? static_cast<float>(m_outerBottomRightRadius)
|
||||
: base,
|
||||
maxR);
|
||||
out[2] = std::min(
|
||||
m_outerBottomLeftRadius >= 0
|
||||
? static_cast<float>(m_outerBottomLeftRadius)
|
||||
: base,
|
||||
maxR);
|
||||
out[3] = std::min(
|
||||
m_outerTopLeftRadius >= 0 ? static_cast<float>(m_outerTopLeftRadius)
|
||||
: base,
|
||||
maxR);
|
||||
}
|
||||
|
||||
void BlobInvertedRect::registerWithGroup() {
|
||||
if (m_group) m_group->setInvertedRect(this);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,21 @@ class BlobInvertedRect : public BlobShape {
|
||||
Q_PROPERTY(
|
||||
qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY
|
||||
borderBottomChanged)
|
||||
Q_PROPERTY(
|
||||
qreal outerRadius READ outerRadius WRITE setOuterRadius NOTIFY
|
||||
outerRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal outerTopLeftRadius READ outerTopLeftRadius WRITE
|
||||
setOuterTopLeftRadius NOTIFY outerTopLeftRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal outerTopRightRadius READ outerTopRightRadius WRITE
|
||||
setOuterTopRightRadius NOTIFY outerTopRightRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal outerBottomLeftRadius READ outerBottomLeftRadius WRITE
|
||||
setOuterBottomLeftRadius NOTIFY outerBottomLeftRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal outerBottomRightRadius READ outerBottomRightRadius WRITE
|
||||
setOuterBottomRightRadius NOTIFY outerBottomRightRadiusChanged)
|
||||
|
||||
public:
|
||||
explicit BlobInvertedRect(QQuickItem* parent = nullptr);
|
||||
@@ -40,15 +55,47 @@ class BlobInvertedRect : public BlobShape {
|
||||
|
||||
void setBorderBottom(qreal v);
|
||||
|
||||
[[nodiscard]] qreal outerRadius() const { return m_outerRadius; }
|
||||
|
||||
void setOuterRadius(qreal v);
|
||||
|
||||
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
|
||||
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; }
|
||||
|
||||
void setOuterTopLeftRadius(qreal v);
|
||||
|
||||
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; }
|
||||
|
||||
void setOuterTopRightRadius(qreal v);
|
||||
|
||||
[[nodiscard]] qreal outerBottomLeftRadius() const {
|
||||
return m_outerBottomLeftRadius;
|
||||
}
|
||||
|
||||
void setOuterBottomLeftRadius(qreal v);
|
||||
|
||||
[[nodiscard]] qreal outerBottomRightRadius() const {
|
||||
return m_outerBottomRightRadius;
|
||||
}
|
||||
|
||||
void setOuterBottomRightRadius(qreal v);
|
||||
|
||||
signals:
|
||||
void borderLeftChanged();
|
||||
void borderRightChanged();
|
||||
void borderTopChanged();
|
||||
void borderBottomChanged();
|
||||
void outerRadiusChanged();
|
||||
void outerTopLeftRadiusChanged();
|
||||
void outerTopRightRadiusChanged();
|
||||
void outerBottomLeftRadiusChanged();
|
||||
void outerBottomRightRadiusChanged();
|
||||
|
||||
protected:
|
||||
[[nodiscard]] bool isInvertedRect() const override { return true; }
|
||||
|
||||
void outerCornerRadii(float out[4]) const override;
|
||||
|
||||
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
|
||||
|
||||
void registerWithGroup() override;
|
||||
@@ -59,4 +106,10 @@ class BlobInvertedRect : public BlobShape {
|
||||
qreal m_borderRight = 0;
|
||||
qreal m_borderTop = 0;
|
||||
qreal m_borderBottom = 0;
|
||||
|
||||
qreal m_outerRadius = 0;
|
||||
qreal m_outerTopLeftRadius = -1;
|
||||
qreal m_outerTopRightRadius = -1;
|
||||
qreal m_outerBottomLeftRadius = -1;
|
||||
qreal m_outerBottomRightRadius = -1;
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ bool BlobMaterialShader::updateUniformData(
|
||||
Q_UNUSED(oldMaterial);
|
||||
auto* mat = static_cast<BlobMaterial*>(newMaterial);
|
||||
QByteArray* buf = state.uniformData();
|
||||
Q_ASSERT(buf->size() >= 1472);
|
||||
Q_ASSERT(buf->size() >= 1488);
|
||||
|
||||
if (state.isMatrixDirty()) {
|
||||
const QMatrix4x4 m = state.combinedMatrix();
|
||||
@@ -105,5 +105,8 @@ bool BlobMaterialShader::updateUniformData(
|
||||
memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4);
|
||||
memcpy(buf->data() + 1456, mat->m_shadowColor, 16);
|
||||
|
||||
// Outer edge corner radii, after shadowColor, at 1472.
|
||||
memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ class BlobMaterial : public QSGMaterial {
|
||||
// puts these after rectData.
|
||||
float m_shadowRadius = 0.0f;
|
||||
float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f};
|
||||
// Outer-edge corner radii (tr, br, bl, tl). Matches the uniform block, which
|
||||
// puts this after shadowColor.
|
||||
float m_invertedOuterRadii[4] = {};
|
||||
BlobRectData m_rects[16] = {};
|
||||
};
|
||||
|
||||
|
||||
@@ -240,6 +240,8 @@ void BlobShape::updatePolish() {
|
||||
m_cachedInvertedRadius = 0;
|
||||
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
|
||||
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
|
||||
memset(
|
||||
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
|
||||
|
||||
auto* inv = m_group->invertedRect();
|
||||
if (inv) {
|
||||
@@ -290,6 +292,9 @@ void BlobShape::updatePolish() {
|
||||
m_cachedInvertedInner[1] = innerCY;
|
||||
m_cachedInvertedInner[2] = innerHW;
|
||||
m_cachedInvertedInner[3] = innerHH;
|
||||
|
||||
static_cast<BlobShape*>(inv)->outerCornerRadii(
|
||||
m_cachedInvertedOuterRadii);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +422,10 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
|
||||
material->m_invertedInner,
|
||||
m_cachedInvertedInner,
|
||||
sizeof(m_cachedInvertedInner));
|
||||
memcpy(
|
||||
material->m_invertedOuterRadii,
|
||||
m_cachedInvertedOuterRadii,
|
||||
sizeof(m_cachedInvertedOuterRadii));
|
||||
|
||||
const int count =
|
||||
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
|
||||
|
||||
@@ -61,6 +61,16 @@ class BlobShape : public QQuickItem {
|
||||
|
||||
virtual void cornerRadii(float out[4]) const;
|
||||
|
||||
// Outer-edge corner radii. Only the inverted rect draws an outer frame edge,
|
||||
// so the base has no outer rounding and returns zeros. Subclasses that expose
|
||||
// outer corner properties override this (order: tr, br, bl, tl).
|
||||
virtual void outerCornerRadii(float out[4]) const {
|
||||
out[0] = 0;
|
||||
out[1] = 0;
|
||||
out[2] = 0;
|
||||
out[3] = 0;
|
||||
}
|
||||
|
||||
virtual void updatePhysics() {}
|
||||
|
||||
virtual void registerWithGroup();
|
||||
@@ -88,4 +98,5 @@ class BlobShape : public QQuickItem {
|
||||
float m_cachedInvertedRadius = 0;
|
||||
float m_cachedInvertedOuter[4] = {};
|
||||
float m_cachedInvertedInner[4] = {};
|
||||
float m_cachedInvertedOuterRadii[4] = {};
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ layout(std140, binding = 0) uniform buf {
|
||||
vec4 rectData[80];
|
||||
float shadowRadius;
|
||||
vec4 shadowColor;
|
||||
vec4 invertedOuterRadii;
|
||||
};
|
||||
|
||||
float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
|
||||
@@ -55,7 +56,8 @@ float smax(float a, float b, float k) {
|
||||
|
||||
float smaxSharpA(float a, float b, float k) {
|
||||
// smax variant that keeps a's boundary sharp (no inward rounding at a = 0).
|
||||
// Used for the frame outer edge so it always fills to the edges.
|
||||
// Used for the frame inner side, so the outer edge keeps its own shape and
|
||||
// always fills to the edges.
|
||||
float h = max(k - abs(a - b), 0.0) / k;
|
||||
float blend = h * h * h * k * (1.0 / 6.0);
|
||||
blend *= smoothstep(0.0, k * 0.5, -a);
|
||||
@@ -182,7 +184,12 @@ void main() {
|
||||
}
|
||||
|
||||
if (hasInverted != 0) {
|
||||
float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0;
|
||||
// Outer edge rounds with its own per-corner radii. With all-zero radii
|
||||
// sdRoundedBox4 equals sdBox, so a frame that sets no outer radius is
|
||||
// unchanged. smaxSharpA keeps this boundary authoritative for the fill.
|
||||
float dOuter =
|
||||
sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw,
|
||||
invertedOuterRadii) - 1.0;
|
||||
float dInner =
|
||||
sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
|
||||
|
||||
@@ -248,7 +255,13 @@ void main() {
|
||||
|
||||
dInner -= sinkValue;
|
||||
|
||||
float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor);
|
||||
// dOuter is already a true circle (sdRoundedBox4). Keep it exact whenever the
|
||||
// outer arc is the active boundary; a smax term added there would pull the
|
||||
// arc's sides inward (curvature/tangent mismatch), so the corner would not be
|
||||
// a circle. Only blend on the inner side, where the inner boundary is active.
|
||||
float negInner = -dInner;
|
||||
float dFrame = dOuter >= negInner ? dOuter
|
||||
: smaxSharpA(dOuter, negInner, smoothFactor);
|
||||
|
||||
mergedSdf = smin(mergedSdf, dFrame, smoothFactor);
|
||||
if (dFrame < minDist) {
|
||||
|
||||
@@ -10,6 +10,8 @@ qml_module(ZShell-components
|
||||
sparklinecontent.hpp sparklinecontent.cpp
|
||||
sparklineitem.hpp sparklineitem.cpp
|
||||
sparklinesceneitem.hpp sparklinesceneitem.cpp
|
||||
roundedmask.hpp roundedmask.cpp
|
||||
roundedclip.hpp roundedclip.cpp
|
||||
wheelinverter.hpp
|
||||
LIBRARIES
|
||||
Qt::Quick
|
||||
@@ -17,3 +19,12 @@ qml_module(ZShell-components
|
||||
Qt::CanvasPainter
|
||||
ZShell-internal
|
||||
)
|
||||
|
||||
qt_add_shaders(ZShell-components "components_shaders"
|
||||
BATCHABLE OPTIMIZED NOHLSL NOMSL
|
||||
GLSL "300es,330"
|
||||
PREFIX "/"
|
||||
FILES
|
||||
shaders/roundedmask.frag
|
||||
shaders/roundedmask.vert
|
||||
)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "roundedclip.hpp"
|
||||
|
||||
#include <private/qquickclipnode_p.h>
|
||||
#include <private/qquickitem_p.h>
|
||||
|
||||
#include <qsggeometry.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
RoundedClip::RoundedClip(QQuickItem* parent) : QQuickItem(parent) {
|
||||
setFlag(ItemHasContents);
|
||||
setClip(true);
|
||||
}
|
||||
|
||||
bool RoundedClip::updateRadius(qreal& member, qreal radius) {
|
||||
if (qFuzzyCompare(member + 1.0, radius + 1.0)) return false;
|
||||
|
||||
member = radius;
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RoundedClip::geometryChange(
|
||||
const QRectF& newGeometry, const QRectF& oldGeometry) {
|
||||
QQuickItem::geometryChange(newGeometry, oldGeometry);
|
||||
if (newGeometry.size() != oldGeometry.size()) update();
|
||||
}
|
||||
|
||||
QSGNode* RoundedClip::updatePaintNode(
|
||||
QSGNode* oldNode, UpdatePaintNodeData* data) {
|
||||
Q_UNUSED(oldNode)
|
||||
Q_UNUSED(data)
|
||||
|
||||
QQuickItemPrivate* priv = QQuickItemPrivate::get(this);
|
||||
QQuickDefaultClipNode* clip = priv->clipNode();
|
||||
|
||||
if (!clip) return nullptr;
|
||||
|
||||
// Clip node geometry is in item-local coordinates. The clip node is a
|
||||
// child of the item's transform node, so the clip rect is
|
||||
// QRectF(0, 0, width, height), i.e. boundingRect()/clipRect().
|
||||
const QRectF r(0, 0, width(), height());
|
||||
|
||||
// Per-corner radii fall back to the shared `radius` shorthand.
|
||||
const qreal tl = m_topLeftRadius > 0 ? m_topLeftRadius : m_radius;
|
||||
const qreal tr = m_topRightRadius > 0 ? m_topRightRadius : m_radius;
|
||||
const qreal br = m_bottomRightRadius > 0 ? m_bottomRightRadius : m_radius;
|
||||
const qreal bl = m_bottomLeftRadius > 0 ? m_bottomLeftRadius : m_radius;
|
||||
|
||||
// Nothing rounded: plain rectangular clip.
|
||||
if (tl <= 0 && tr <= 0 && br <= 0 && bl <= 0) {
|
||||
clip->setRect(r);
|
||||
clip->update();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// All corners equal: let QQuickDefaultClipNode build its own known-correct
|
||||
// scanline geometry. This is the working path.
|
||||
if (qFuzzyCompare(tl, tr) && qFuzzyCompare(tr, br) &&
|
||||
qFuzzyCompare(br, bl)) {
|
||||
clip->setRect(r);
|
||||
clip->setRadius(tl);
|
||||
clip->update();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Per-corner radii. setRadius() supports only a single value, so we
|
||||
// must supply the geometry ourselves. Build it as a triangle fan from an
|
||||
// interior point so the polygon interior fills for the stencil clip.
|
||||
const qreal half = std::min(r.width() / 2, r.height() / 2);
|
||||
const qreal rtl = std::min(tl, half);
|
||||
const qreal rtr = std::min(tr, half);
|
||||
const qreal rbr = std::min(br, half);
|
||||
const qreal rbl = std::min(bl, half);
|
||||
|
||||
auto segments = [](qreal radius) {
|
||||
return qMin(30, int(std::ceil(radius)));
|
||||
};
|
||||
|
||||
const int s_tl = segments(rtl);
|
||||
const int s_tr = segments(rtr);
|
||||
const int s_br = segments(rbr);
|
||||
const int s_bl = segments(rbl);
|
||||
|
||||
clip->setRect(r);
|
||||
clip->setIsRectangular(false);
|
||||
|
||||
QSGGeometry* g = clip->geometry();
|
||||
g->setDrawingMode(QSGGeometry::DrawTriangleFan);
|
||||
|
||||
// One apex + one point per arc step + one point per zero-radius corner,
|
||||
// plus a closing apex to complete the fan.
|
||||
g->allocate(1 + s_tl + s_tr + s_br + s_bl + 5);
|
||||
|
||||
QSGGeometry::Point2D* vertices = g->vertexDataAsPoint2D();
|
||||
int vi = 0;
|
||||
|
||||
// Apex at an interior point.
|
||||
vertices[vi++] =
|
||||
QSGGeometry::Point2D(float(r.center().x()), float(r.center().y()));
|
||||
|
||||
// Each corner contributes its arc points (i = 0..segments) from its
|
||||
// edge-entry tangent to its edge-exit tangent. Consecutive corners are
|
||||
// joined by the implicit straight edges, forming the rounded rectangle.
|
||||
auto addArc =
|
||||
[&](int segs, qreal cx, qreal cy, qreal rad, qreal a0, qreal a1) {
|
||||
for (int i = 0; i <= segs; ++i) {
|
||||
const qreal angle = a0 + (a1 - a0) * qreal(i) / qreal(segs);
|
||||
vertices[vi++] = QSGGeometry::Point2D(
|
||||
float(cx + rad * std::cos(angle)),
|
||||
float(cy + rad * std::sin(angle)));
|
||||
}
|
||||
};
|
||||
|
||||
addArc(s_tl, rtl, rtl, rtl, M_PI, 3.0 * M_PI / 2.0);
|
||||
addArc(s_tr, r.width() - rtr, rtr, rtr, -M_PI / 2.0, 0.0);
|
||||
addArc(s_br, r.width() - rbr, r.height() - rbr, rbr, 0.0, M_PI / 2.0);
|
||||
addArc(s_bl, rbl, r.height() - rbl, rbl, M_PI / 2.0, M_PI);
|
||||
|
||||
// Close the fan back to the first perimeter vertex.
|
||||
vertices[vi] = vertices[1];
|
||||
|
||||
clip->markDirty(QSGNode::DirtyGeometry);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace ZShell::components
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <qqmlintegration.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qsgnode.h>
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
class RoundedClip : public QQuickItem {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
|
||||
topLeftRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY
|
||||
topRightRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal bottomRightRadius READ bottomRightRadius WRITE
|
||||
setBottomRightRadius NOTIFY bottomRightRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius
|
||||
NOTIFY bottomLeftRadiusChanged)
|
||||
|
||||
public:
|
||||
explicit RoundedClip(QQuickItem* parent = nullptr);
|
||||
|
||||
qreal radius() const { return m_radius; }
|
||||
|
||||
void setRadius(qreal radius) {
|
||||
if (updateRadius(m_radius, radius)) emit radiusChanged();
|
||||
}
|
||||
|
||||
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
|
||||
|
||||
void setTopLeftRadius(qreal radius) {
|
||||
if (updateRadius(m_topLeftRadius, radius)) emit topLeftRadiusChanged();
|
||||
}
|
||||
|
||||
[[nodiscard]] qreal topRightRadius() const { return m_topRightRadius; }
|
||||
|
||||
void setTopRightRadius(qreal radius) {
|
||||
if (updateRadius(m_topRightRadius, radius))
|
||||
emit topRightRadiusChanged();
|
||||
}
|
||||
|
||||
[[nodiscard]] qreal bottomRightRadius() const {
|
||||
return m_bottomRightRadius;
|
||||
}
|
||||
|
||||
void setBottomRightRadius(qreal radius) {
|
||||
if (updateRadius(m_bottomRightRadius, radius))
|
||||
emit bottomRightRadiusChanged();
|
||||
}
|
||||
|
||||
[[nodiscard]] qreal bottomLeftRadius() const { return m_bottomLeftRadius; }
|
||||
|
||||
void setBottomLeftRadius(qreal radius) {
|
||||
if (updateRadius(m_bottomLeftRadius, radius))
|
||||
emit bottomLeftRadiusChanged();
|
||||
}
|
||||
|
||||
signals:
|
||||
void radiusChanged();
|
||||
void topLeftRadiusChanged();
|
||||
void topRightRadiusChanged();
|
||||
void bottomRightRadiusChanged();
|
||||
void bottomLeftRadiusChanged();
|
||||
|
||||
protected:
|
||||
QSGNode* updatePaintNode(
|
||||
QSGNode* oldNode, UpdatePaintNodeData* data) override;
|
||||
void geometryChange(
|
||||
const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
||||
|
||||
private:
|
||||
bool updateRadius(qreal& member, qreal radius);
|
||||
|
||||
qreal m_radius = 0;
|
||||
qreal m_topLeftRadius = 0;
|
||||
qreal m_topRightRadius = 0;
|
||||
qreal m_bottomRightRadius = 0;
|
||||
qreal m_bottomLeftRadius = 0;
|
||||
};
|
||||
|
||||
} // namespace ZShell::components
|
||||
@@ -0,0 +1,268 @@
|
||||
#include "roundedmask.hpp"
|
||||
|
||||
#include <qimage.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qsggeometry.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
using Qt::StringLiterals::operator""_s;
|
||||
|
||||
RoundedMask::RoundedMask(QQuickItem* parent) : QQuickItem(parent) {
|
||||
setFlag(ItemHasContents);
|
||||
}
|
||||
|
||||
QQuickItem* RoundedMask::source() const {
|
||||
return m_source;
|
||||
}
|
||||
|
||||
void RoundedMask::setSource(QQuickItem* source) {
|
||||
if (m_source == source) return;
|
||||
|
||||
m_source = source;
|
||||
emit sourceChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
qreal RoundedMask::topLeftRadius() const {
|
||||
return m_topLeftRadius;
|
||||
}
|
||||
|
||||
void RoundedMask::setTopLeftRadius(qreal radius) {
|
||||
if (updateRadius(m_topLeftRadius, radius)) emit topLeftRadiusChanged();
|
||||
}
|
||||
|
||||
qreal RoundedMask::topRightRadius() const {
|
||||
return m_topRightRadius;
|
||||
}
|
||||
|
||||
void RoundedMask::setTopRightRadius(qreal radius) {
|
||||
if (updateRadius(m_topRightRadius, radius)) emit topRightRadiusChanged();
|
||||
}
|
||||
|
||||
qreal RoundedMask::bottomRightRadius() const {
|
||||
return m_bottomRightRadius;
|
||||
}
|
||||
|
||||
void RoundedMask::setBottomRightRadius(qreal radius) {
|
||||
if (updateRadius(m_bottomRightRadius, radius))
|
||||
emit bottomRightRadiusChanged();
|
||||
}
|
||||
|
||||
qreal RoundedMask::bottomLeftRadius() const {
|
||||
return m_bottomLeftRadius;
|
||||
}
|
||||
|
||||
void RoundedMask::setBottomLeftRadius(qreal radius) {
|
||||
if (updateRadius(m_bottomLeftRadius, radius))
|
||||
emit bottomLeftRadiusChanged();
|
||||
}
|
||||
|
||||
bool RoundedMask::updateRadius(qreal& member, qreal radius) {
|
||||
if (qFuzzyCompare(member + 1.0, radius + 1.0)) return false;
|
||||
|
||||
member = radius;
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
QColor RoundedMask::color() const {
|
||||
return m_color;
|
||||
}
|
||||
|
||||
void RoundedMask::setColor(const QColor& color) {
|
||||
if (m_color == color) return;
|
||||
|
||||
m_color = color;
|
||||
emit colorChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void RoundedMask::geometryChange(
|
||||
const QRectF& newGeometry, const QRectF& oldGeometry) {
|
||||
QQuickItem::geometryChange(newGeometry, oldGeometry);
|
||||
if (newGeometry.size() != oldGeometry.size()) update();
|
||||
}
|
||||
|
||||
void RoundedMask::releaseResources() {
|
||||
m_node = nullptr;
|
||||
}
|
||||
|
||||
void RoundedMask::invalidateSceneGraph() {
|
||||
m_node = nullptr;
|
||||
}
|
||||
|
||||
void RoundedMask::watchProvider(QSGTextureProvider* provider) {
|
||||
if (m_provider == provider) return;
|
||||
|
||||
if (m_provider) QObject::disconnect(m_provider, nullptr, this, nullptr);
|
||||
|
||||
m_provider = provider;
|
||||
if (!m_provider) return;
|
||||
|
||||
QObject::connect(
|
||||
m_provider,
|
||||
&QSGTextureProvider::textureChanged,
|
||||
this,
|
||||
&QQuickItem::update);
|
||||
// Emitted on the render thread by the provider's cleanup job; drop the texture before it is rendered again
|
||||
QObject::connect(
|
||||
m_provider,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
[this] {
|
||||
m_provider = nullptr;
|
||||
if (m_node) {
|
||||
static_cast<RoundedMaskMaterial*>(m_node->material())
|
||||
->m_texture = nullptr;
|
||||
m_node->markDirty(QSGNode::DirtyMaterial);
|
||||
}
|
||||
update();
|
||||
},
|
||||
Qt::DirectConnection);
|
||||
}
|
||||
|
||||
QSGNode* RoundedMask::updatePaintNode(
|
||||
QSGNode* oldNode, UpdatePaintNodeData* data) {
|
||||
Q_UNUSED(data);
|
||||
|
||||
auto* const provider = m_source && m_source->isTextureProvider()
|
||||
? m_source->textureProvider()
|
||||
: nullptr;
|
||||
watchProvider(provider);
|
||||
|
||||
auto* const texture = provider ? provider->texture() : nullptr;
|
||||
if (!texture || width() <= 0 || height() <= 0) {
|
||||
delete oldNode;
|
||||
m_node = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* node = static_cast<RoundedMaskNode*>(oldNode);
|
||||
if (!node) node = new RoundedMaskNode;
|
||||
m_node = node;
|
||||
|
||||
QSGGeometry::updateTexturedRectGeometry(
|
||||
node->geometry(), boundingRect(), QRectF(0, 0, 1, 1));
|
||||
node->markDirty(QSGNode::DirtyGeometry);
|
||||
|
||||
const auto dpr = static_cast<float>(window()->effectiveDevicePixelRatio());
|
||||
auto* const material = static_cast<RoundedMaskMaterial*>(node->material());
|
||||
if (!material->m_fallback) {
|
||||
QImage blank(1, 1, QImage::Format_RGBA8888_Premultiplied);
|
||||
blank.fill(Qt::transparent);
|
||||
material->m_fallback.reset(window()->createTextureFromImage(blank));
|
||||
}
|
||||
material->m_texture = texture;
|
||||
material->m_size[0] = static_cast<float>(width()) * dpr;
|
||||
material->m_size[1] = static_cast<float>(height()) * dpr;
|
||||
const auto maxRadius = static_cast<float>(std::min(width(), height()) / 2);
|
||||
const auto clampRadius = [&](qreal radius) {
|
||||
return std::clamp(static_cast<float>(radius), 0.0f, maxRadius) * dpr;
|
||||
};
|
||||
material->m_radii[0] = clampRadius(m_topLeftRadius);
|
||||
material->m_radii[1] = clampRadius(m_topRightRadius);
|
||||
material->m_radii[2] = clampRadius(m_bottomRightRadius);
|
||||
material->m_radii[3] = clampRadius(m_bottomLeftRadius);
|
||||
const auto alpha = m_color.alphaF();
|
||||
material->m_color[0] = m_color.redF() * alpha;
|
||||
material->m_color[1] = m_color.greenF() * alpha;
|
||||
material->m_color[2] = m_color.blueF() * alpha;
|
||||
material->m_color[3] = alpha;
|
||||
node->markDirty(QSGNode::DirtyMaterial);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
QSGMaterialType* RoundedMaskMaterial::type() const {
|
||||
static QSGMaterialType s_type;
|
||||
return &s_type;
|
||||
}
|
||||
|
||||
QSGMaterialShader* RoundedMaskMaterial::createShader(
|
||||
QSGRendererInterface::RenderMode mode) const {
|
||||
Q_UNUSED(mode);
|
||||
return new RoundedMaskMaterialShader;
|
||||
}
|
||||
|
||||
int RoundedMaskMaterial::compare(const QSGMaterial* other) const {
|
||||
const auto* const o = static_cast<const RoundedMaskMaterial*>(other);
|
||||
if (m_texture != o->m_texture) return m_texture < o->m_texture ? -1 : 1;
|
||||
|
||||
const auto fuzzyEqual = [](float a, float b) {
|
||||
return qFuzzyCompare(a + 1.0f, b + 1.0f);
|
||||
};
|
||||
if (!std::ranges::equal(m_size, o->m_size, fuzzyEqual) ||
|
||||
!std::ranges::equal(m_radii, o->m_radii, fuzzyEqual) ||
|
||||
!std::ranges::equal(m_color, o->m_color, fuzzyEqual))
|
||||
return this < other ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
RoundedMaskMaterialShader::RoundedMaskMaterialShader() {
|
||||
setShaderFileName(VertexStage, u":/shaders/roundedmask.vert.qsb"_s);
|
||||
setShaderFileName(FragmentStage, u":/shaders/roundedmask.frag.qsb"_s);
|
||||
}
|
||||
|
||||
bool RoundedMaskMaterialShader::updateUniformData(
|
||||
RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) {
|
||||
Q_UNUSED(oldMaterial);
|
||||
auto* const mat = static_cast<RoundedMaskMaterial*>(newMaterial);
|
||||
QByteArray* const buf = state.uniformData();
|
||||
Q_ASSERT(buf->size() >= 112);
|
||||
|
||||
if (state.isMatrixDirty()) {
|
||||
const QMatrix4x4 m = state.combinedMatrix();
|
||||
std::memcpy(buf->data(), m.constData(), 64);
|
||||
}
|
||||
if (state.isOpacityDirty()) {
|
||||
const float opacity = state.opacity();
|
||||
std::memcpy(buf->data() + 64, &opacity, 4);
|
||||
}
|
||||
|
||||
std::memcpy(buf->data() + 72, mat->m_size, 8);
|
||||
std::memcpy(buf->data() + 80, mat->m_radii, 16);
|
||||
std::memcpy(buf->data() + 96, mat->m_color, 16);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RoundedMaskMaterialShader::updateSampledImage(
|
||||
RenderState& state,
|
||||
int binding,
|
||||
QSGTexture** texture,
|
||||
QSGMaterial* newMaterial,
|
||||
QSGMaterial* oldMaterial) {
|
||||
Q_UNUSED(oldMaterial);
|
||||
if (binding != 1) return;
|
||||
|
||||
const auto* const mat =
|
||||
static_cast<const RoundedMaskMaterial*>(newMaterial);
|
||||
auto* const tex = mat->m_texture ? mat->m_texture : mat->m_fallback.data();
|
||||
tex->commitTextureOperations(state.rhi(), state.resourceUpdateBatch());
|
||||
*texture = tex;
|
||||
}
|
||||
|
||||
RoundedMaskNode::RoundedMaskNode() {
|
||||
auto* const geometry =
|
||||
new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
|
||||
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
|
||||
setGeometry(geometry);
|
||||
setFlag(OwnsGeometry);
|
||||
|
||||
auto* const material = new RoundedMaskMaterial;
|
||||
material->setFlag(QSGMaterial::Blending);
|
||||
setMaterial(material);
|
||||
setFlag(OwnsMaterial);
|
||||
setFlag(UsePreprocess);
|
||||
}
|
||||
|
||||
void RoundedMaskNode::preprocess() {
|
||||
auto* const texture = qobject_cast<QSGDynamicTexture*>(
|
||||
static_cast<RoundedMaskMaterial*>(material())->m_texture);
|
||||
if (texture && texture->updateTexture()) markDirty(DirtyMaterial);
|
||||
}
|
||||
|
||||
} // namespace ZShell::components
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qpointer.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qscopedpointer.h>
|
||||
#include <qsgmaterial.h>
|
||||
#include <qsgmaterialshader.h>
|
||||
#include <qsgnode.h>
|
||||
#include <qsgtexture.h>
|
||||
#include <qsgtextureprovider.h>
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
class RoundedMaskNode;
|
||||
|
||||
class RoundedMask : public QQuickItem {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(
|
||||
QQuickItem* source READ source WRITE setSource NOTIFY sourceChanged)
|
||||
Q_PROPERTY(
|
||||
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
|
||||
topLeftRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY
|
||||
topRightRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal bottomRightRadius READ bottomRightRadius WRITE
|
||||
setBottomRightRadius NOTIFY bottomRightRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius
|
||||
NOTIFY bottomLeftRadiusChanged)
|
||||
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
|
||||
|
||||
public:
|
||||
explicit RoundedMask(QQuickItem* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QQuickItem* source() const;
|
||||
void setSource(QQuickItem* source);
|
||||
|
||||
[[nodiscard]] qreal topLeftRadius() const;
|
||||
void setTopLeftRadius(qreal radius);
|
||||
|
||||
[[nodiscard]] qreal topRightRadius() const;
|
||||
void setTopRightRadius(qreal radius);
|
||||
|
||||
[[nodiscard]] qreal bottomRightRadius() const;
|
||||
void setBottomRightRadius(qreal radius);
|
||||
|
||||
[[nodiscard]] qreal bottomLeftRadius() const;
|
||||
void setBottomLeftRadius(qreal radius);
|
||||
|
||||
[[nodiscard]] QColor color() const;
|
||||
void setColor(const QColor& color);
|
||||
|
||||
signals:
|
||||
void sourceChanged();
|
||||
void topLeftRadiusChanged();
|
||||
void topRightRadiusChanged();
|
||||
void bottomRightRadiusChanged();
|
||||
void bottomLeftRadiusChanged();
|
||||
void colorChanged();
|
||||
|
||||
protected:
|
||||
QSGNode* updatePaintNode(
|
||||
QSGNode* oldNode, UpdatePaintNodeData* data) override;
|
||||
void geometryChange(
|
||||
const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
||||
void releaseResources() override;
|
||||
|
||||
private slots:
|
||||
void invalidateSceneGraph();
|
||||
|
||||
private:
|
||||
void watchProvider(QSGTextureProvider* provider);
|
||||
bool updateRadius(qreal& member, qreal radius);
|
||||
|
||||
QPointer<QQuickItem> m_source;
|
||||
qreal m_topLeftRadius = 0;
|
||||
qreal m_topRightRadius = 0;
|
||||
qreal m_bottomRightRadius = 0;
|
||||
qreal m_bottomLeftRadius = 0;
|
||||
QColor m_color = Qt::transparent;
|
||||
QSGTextureProvider* m_provider = nullptr;
|
||||
RoundedMaskNode* m_node = nullptr;
|
||||
};
|
||||
|
||||
class RoundedMaskMaterial : public QSGMaterial {
|
||||
public:
|
||||
[[nodiscard]] QSGMaterialType* type() const override;
|
||||
[[nodiscard]] QSGMaterialShader* createShader(
|
||||
QSGRendererInterface::RenderMode mode) const override;
|
||||
int compare(const QSGMaterial* other) const override;
|
||||
|
||||
QSGTexture* m_texture = nullptr;
|
||||
QScopedPointer<QSGTexture> m_fallback;
|
||||
float m_size[2] = {};
|
||||
float m_radii[4] = {};
|
||||
float m_color[4] = {};
|
||||
};
|
||||
|
||||
class RoundedMaskMaterialShader : public QSGMaterialShader {
|
||||
public:
|
||||
RoundedMaskMaterialShader();
|
||||
|
||||
bool updateUniformData(
|
||||
RenderState& state,
|
||||
QSGMaterial* newMaterial,
|
||||
QSGMaterial* oldMaterial) override;
|
||||
void updateSampledImage(
|
||||
RenderState& state,
|
||||
int binding,
|
||||
QSGTexture** texture,
|
||||
QSGMaterial* newMaterial,
|
||||
QSGMaterial* oldMaterial) override;
|
||||
};
|
||||
|
||||
class RoundedMaskNode : public QSGGeometryNode {
|
||||
public:
|
||||
RoundedMaskNode();
|
||||
|
||||
void preprocess() override;
|
||||
};
|
||||
|
||||
} // namespace ZShell::components
|
||||
@@ -0,0 +1,26 @@
|
||||
#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;
|
||||
vec2 size;
|
||||
vec4 radii;
|
||||
vec4 color;
|
||||
};
|
||||
|
||||
layout(binding = 1) uniform sampler2D source;
|
||||
|
||||
void main() {
|
||||
vec2 p = qt_TexCoord0 * size - size * 0.5;
|
||||
float radius = p.x < 0.0 ? (p.y < 0.0 ? radii.x : radii.w) : (p.y < 0.0 ? radii.y : radii.z);
|
||||
vec2 q = abs(p) - (size * 0.5 - radius);
|
||||
float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
|
||||
float mask = 1.0 - smoothstep(-0.5, 0.5, d);
|
||||
|
||||
vec4 content = texture(source, qt_TexCoord0);
|
||||
vec4 blended = content + color * (1.0 - content.a);
|
||||
fragColor = blended * mask * qt_Opacity;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#version 440
|
||||
|
||||
layout(location = 0) in vec4 qt_VertexPosition;
|
||||
layout(location = 1) in vec2 qt_VertexTexCoord;
|
||||
|
||||
layout(location = 0) out vec2 qt_TexCoord0;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
vec2 size;
|
||||
vec4 radii;
|
||||
vec4 color;
|
||||
};
|
||||
|
||||
void main() {
|
||||
gl_Position = qt_Matrix * qt_VertexPosition;
|
||||
qt_TexCoord0 = qt_VertexTexCoord;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ class Popouts : public settings::ObjectNode {
|
||||
class Tray : public settings::ObjectNode {
|
||||
CONFIG_NODE(Tray, settings::ObjectNode)
|
||||
|
||||
CONFIG_PROPERTY(bool, replaceIcons, false)
|
||||
CONFIG_PROPERTY(bool, recolorIcons, false)
|
||||
CONFIG_PROPERTY(bool, showOnHover, true)
|
||||
CONFIG_PROPERTY(int, trayIconSize, 24)
|
||||
|
||||
@@ -129,15 +129,15 @@ void NightlightManager::setFadeDuration(double duration) {
|
||||
emit fadeDurationChanged();
|
||||
}
|
||||
|
||||
void NightlightManager::toggle() {
|
||||
void NightlightManager::toggle(bool fast) {
|
||||
if (m_enabled) {
|
||||
end();
|
||||
end(fast ? 2 : m_fadeDuration);
|
||||
} else {
|
||||
start();
|
||||
start(fast ? 2 : m_fadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void NightlightManager::start() {
|
||||
void NightlightManager::start(double fade) {
|
||||
if (m_enabled && m_initialized) return;
|
||||
|
||||
m_initialized = true;
|
||||
@@ -149,9 +149,7 @@ void NightlightManager::start() {
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments(
|
||||
{"dispatch",
|
||||
QString("hl.plugin.zshell.nlSet(%1, %2)")
|
||||
.arg(m_temp)
|
||||
.arg(m_fadeDuration)});
|
||||
QString("hl.plugin.zshell.nlSet(%1, %2)").arg(m_temp).arg(fade)});
|
||||
m_process.startDetached();
|
||||
} else {
|
||||
m_process.setProgram("hyprctl");
|
||||
@@ -161,7 +159,7 @@ void NightlightManager::start() {
|
||||
}
|
||||
}
|
||||
|
||||
void NightlightManager::end() {
|
||||
void NightlightManager::end(double fade) {
|
||||
if (!m_enabled && m_initialized) return;
|
||||
|
||||
m_initialized = true;
|
||||
@@ -172,8 +170,7 @@ void NightlightManager::end() {
|
||||
if (m_useNativeNightlight) {
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments(
|
||||
{"dispatch",
|
||||
QString("hl.plugin.zshell.nlDisable(%1)").arg(m_fadeDuration)});
|
||||
{"dispatch", QString("hl.plugin.zshell.nlDisable(%1)").arg(fade)});
|
||||
m_process.startDetached();
|
||||
} else {
|
||||
m_process.setProgram("hyprctl");
|
||||
@@ -195,10 +192,14 @@ void NightlightManager::apply() {
|
||||
isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime);
|
||||
}
|
||||
|
||||
const auto activationTime = isDarkTime ? m_startTime : m_endTime;
|
||||
const auto elapsed = (currentMin - activationTime + 1440) % 1440;
|
||||
const bool pastActivation = elapsed > 20;
|
||||
|
||||
if (isDarkTime) {
|
||||
start();
|
||||
start(pastActivation ? 2 : m_fadeDuration);
|
||||
} else {
|
||||
end();
|
||||
end(pastActivation ? 2 : m_fadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class NightlightManager : public QObject {
|
||||
[[nodiscard]] int lastTemp() const;
|
||||
[[nodiscard]] int currentTemp() const;
|
||||
|
||||
Q_INVOKABLE void toggle();
|
||||
Q_INVOKABLE void toggle(bool fast);
|
||||
Q_INVOKABLE void apply();
|
||||
|
||||
void setStartTime(const int& time);
|
||||
@@ -105,8 +105,8 @@ class NightlightManager : public QObject {
|
||||
QByteArray m_socketBuffer;
|
||||
QTimer m_reconnectTimer;
|
||||
|
||||
void start();
|
||||
void end();
|
||||
void start(double fade);
|
||||
void end(double fade);
|
||||
[[nodiscard]] QString hyprlandDir() const;
|
||||
[[nodiscard]] QString eventSocketPath() const;
|
||||
[[nodiscard]] QString resolvePluginPath() const;
|
||||
|
||||
+25
-53
@@ -1,56 +1,28 @@
|
||||
FROM archlinux:latest
|
||||
|
||||
RUN pacman -Syyu --noconfirm
|
||||
RUN pacman -Syu --noconfirm \
|
||||
&& pacman -S --needed --noconfirm \
|
||||
base-devel cmake ninja clang lld nodejs tree-sitter \
|
||||
jkqtplotter cmark-gfm git wayland-protocols \
|
||||
qt6-base qt6-declarative qt6-shadertools qt6-5compat \
|
||||
qt6-canvaspainter qt6-wayland qt5-wayland\
|
||||
libqalculate pipewire aubio cava lm_sensors glib2 \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
# Build toolchain
|
||||
RUN pacman -S --noconfirm \
|
||||
base-devel \
|
||||
cmake \
|
||||
ninja \
|
||||
clang \
|
||||
lld \
|
||||
nodejs \
|
||||
tree-sitter \
|
||||
jkqtplotter \
|
||||
cmark-gfm \
|
||||
git
|
||||
|
||||
# Qt6
|
||||
RUN pacman -S --noconfirm \
|
||||
qt6-base \
|
||||
qt6-declarative \
|
||||
qt6-shadertools \
|
||||
qt6-5compat \
|
||||
qt6-canvaspainter
|
||||
|
||||
# Project-specific dependencies
|
||||
RUN pacman -S --noconfirm \
|
||||
libqalculate \
|
||||
pipewire \
|
||||
aubio \
|
||||
cava \
|
||||
lm_sensors \
|
||||
glib2
|
||||
|
||||
# AUR: libcava (provides development headers for cava)
|
||||
RUN pacman -Syu --noconfirm && \
|
||||
pacman -S --needed --noconfirm \
|
||||
sudo \
|
||||
meson \
|
||||
autoconf-archive \
|
||||
python-tqdm && \
|
||||
useradd -m builder && \
|
||||
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
|
||||
RUN for i in 1 2 3 4 5; do \
|
||||
rm -rf /tmp/libcava && \
|
||||
git clone --depth 1 https://aur.archlinux.org/libcava.git /tmp/libcava && \
|
||||
break || sleep 5; \
|
||||
done && \
|
||||
test -d /tmp/libcava/.git && \
|
||||
chown -R builder /tmp/libcava
|
||||
|
||||
RUN su builder -c "cd /tmp/libcava && makepkg -si --noconfirm" && \
|
||||
rm -rf /tmp/libcava && \
|
||||
userdel -r builder && \
|
||||
sed -i '/^builder/d' /etc/sudoers
|
||||
RUN pacman -Syu --noconfirm \
|
||||
&& pacman -S --needed --noconfirm \
|
||||
sudo meson autoconf-archive python-tqdm \
|
||||
&& useradd -m builder \
|
||||
&& printf '%s\n' 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder \
|
||||
&& chmod 0440 /etc/sudoers.d/builder \
|
||||
&& for i in 1 2 3 4 5; do \
|
||||
rm -rf /tmp/libcava; \
|
||||
if git clone --depth 1 https://aur.archlinux.org/libcava.git /tmp/libcava; then break; fi; \
|
||||
sleep 5; \
|
||||
done \
|
||||
&& test -d /tmp/libcava/.git \
|
||||
&& chown -R builder:builder /tmp/libcava \
|
||||
&& su builder -c "cd /tmp/libcava && makepkg -si --noconfirm" \
|
||||
&& rm -rf /tmp/libcava /etc/sudoers.d/builder \
|
||||
&& userdel -r builder \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
Reference in New Issue
Block a user