Author SHA1 Message Date
zach 566b71d386 feat: drawing popout now clips strokes again + add redo + add undo and redo keybinds
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Python / verify (pull_request) Failing after 1m41s
Rust / build (pull_request) Successful in 1m15s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m6s
2026-10-30 14:16:19 +01:00
zach c5608dc9b9 fix: blobs now allow to be drawn in device pixels to stay sharp
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 25s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Failing after 1m25s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 59s
2026-10-30 01:52:33 +01:00
zach 78a9577209 Merge branch 'main' into feat/device-pixel-alignment
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 26s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m29s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 50s
2026-10-29 20:11:33 +01:00
zach 90184bff58 Merge pull request 'feat: video wallpapers' (#163) from feat/video-wallpaper into main
Reviewed-on: #163
2026-10-29 19:55:05 +01:00
zach abdde2450d fix: don't use lambda expression
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m27s
Rust / fmt (pull_request) Successful in 31s
Rust / clippy (pull_request) Successful in 58s
2026-10-29 19:50:02 +01:00
zach fd05b3d4c4 Merge branch 'main' into feat/video-wallpaper
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 37s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 1m5s
2026-10-29 19:03:10 +01:00
zach 067291f478 fix: lockscreen notifdock
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 14s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 35s
Rust / clippy (pull_request) Successful in 53s
2026-10-29 18:43:50 +01:00
zach 395d1dad3d fix: remove duped property 2026-10-29 17:51:20 +01:00
zach 118b8f482a fix: fix sub-pixel layout arrangement for normalized resolutions
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 31s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 54s
2026-10-27 23:00:26 +01:00
zach b58b4fdc9c fix: remove more uses of cliprect and finally fix workspace's layer texture
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 33s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 32s
Rust / clippy (pull_request) Successful in 57s
2026-10-27 15:15:57 +01:00
zach a56afd7fac fix: don't link eglDestroyImageKHR
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 45s
2026-10-27 02:30:55 +01:00
zach b0cb43f75c feat: add vaapi path for video wallpapers on generic gpus
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 32s
Rust / build (pull_request) Successful in 52s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 51s
Rust / clippy (pull_request) Successful in 1m2s
2026-10-27 02:15:16 +01:00
zach e655ec5bc7 feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Python / verify (pull_request) Failing after 1m29s
Rust / build (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m4s
2026-10-26 14:47:47 +01:00
zach dc0f222ace fix: don't store dbPath as const
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 53s
2026-10-26 03:13:33 +01:00
zach de19fd6ac4 fix: restore code removed by incorrect replacement command
C++ / fmt (pull_request) Failing after 5s
C++ / clang-tidy (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 6s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m19s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 1m0s
2026-10-26 03:03:20 +01:00
zach 9edbcbd610 feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
2026-10-26 02:51:28 +01:00
zach cd44d27b74 chore: suppress more warnings
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 59s
Python / verify (pull_request) Failing after 1m31s
Rust / fmt (pull_request) Successful in 33s
Rust / clippy (pull_request) Successful in 52s
2026-10-25 02:10:50 +02:00
zach 0d0faecb1c feat: initial video wallpaper implementation 2026-10-25 02:10:50 +02:00
45 changed files with 1277 additions and 1009 deletions
+7
View File
@@ -4,12 +4,16 @@ import Quickshell
import QtQuick import QtQuick
import ZShell.Internal import ZShell.Internal
import qs.Components import qs.Components
import qs.Services
Item { Item {
id: root id: root
readonly property alias content: contentLoader.item readonly property alias content: contentLoader.item
required property ScreenState screenState required property ScreenState screenState
property rect holeRect
property real holeRadius: 0
Loader { Loader {
id: contentLoader id: contentLoader
@@ -18,6 +22,9 @@ Item {
anchors.fill: parent anchors.fill: parent
sourceComponent: StrokeCanvas { sourceComponent: StrokeCanvas {
anchors.fill: parent
hole: root.holeRect
holeRadius: root.holeRadius
penColor: root.screenState.penColor penColor: root.screenState.penColor
penWidth: root.screenState.penWidth penWidth: root.screenState.penWidth
} }
+95
View File
@@ -0,0 +1,95 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
CustomMouseArea {
id: root
required property real borderThickness
required property Drawing drawing
required property Panels panels
required property ScreenState screenState
required property EdgeGeometry geometry
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + geometry.insetTop(borderThickness);
return y >= panelY && y <= panelY + panel.height;
}
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.BlankCursor
enabled: z > 0
hoverEnabled: true
focus: root.screenState.isDrawing
z: 2
Binding {
target: root.drawing.content
property: "hoverPoint"
value: Qt.point(root.mouseX, root.mouseY)
}
Keys.onPressed: e => {
const mod = e.modifiers;
const key = e.key;
if ((key == Qt.Key_Z) && (mod & Qt.ControlModifier) && (mod & Qt.ShiftModifier)) {
root.drawing.content.redo();
e.accepted = true;
} else if ((key == Qt.Key_Z) && (mod & Qt.ControlModifier)) {
root.drawing.content.undoLastGroup();
e.accepted = true;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (!drawing.content.hoverVisible)
drawing.content.showHover();
if (event.buttons & Qt.LeftButton) {
drawing.content.appendPoint(x, y);
return;
}
if (!(event.buttons & Qt.LeftButton) && inLeftPanel(panels.drawing, x, y)) {
z = -2;
panels.drawing.expand();
drawing.content.hideHover();
}
}
onPressed: event => {
const x = event.x;
const y = event.y;
panels.drawing.collapse();
if (event.buttons & Qt.LeftButton) {
panels.drawing.collapse();
drawing.content.beginStroke(x, y);
return;
}
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
}
onReleased: event => {
const x = event.x;
const y = event.y;
if (event.button & Qt.LeftButton) {
drawing.content.endStroke();
drawing.content.showHover();
}
}
}
+117 -194
View File
@@ -1,25 +1,33 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
import ZShell.Internal
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
Item { CustomMouseArea {
id: root id: root
required property Drawing drawing
required property DrawingInput input
required property Item bar required property Item bar
required property real borderThickness required property real borderThickness
property bool dashboardShortcutActive
required property Drawing drawing
required property EdgeGeometry geometry required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels required property Panels panels
required property Wrapper popouts required property Wrapper popouts
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
property bool dashboardShortcutActive
property point dragStart
property bool osdShortcutActive
property bool setInitialPoint: false
property bool singleGestureTriggered: false property bool singleGestureTriggered: false
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
required property ScreenState screenState
function inBottomPanel(panel: Item, x: real, y: real): bool { function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -56,124 +64,116 @@ Item {
return x >= panelX && x <= panelX + panel.width; return x >= panelX && x <= panelX + panel.width;
} }
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
DragHandler { onContainsMouseChanged: {
id: singleHandler if (!containsMouse) {
if (!osdShortcutActive) {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined screenState.osd = false;
dragThreshold: 0 panels.osd.hovered = false;
enabled: !root.screenState.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1
minimumPointCount: 1
target: null
onActiveChanged: {
if (!active)
root.singleGestureTriggered = false;
}
onCentroidChanged: {
const x = centroid.position.x;
const y = centroid.position.y;
const pressX = centroid.pressPosition.x;
const pressY = centroid.pressPosition.y;
const dragX = x - pressX;
const dragY = y - pressY;
if (pressY >= root.screen.height - Config.bar.border && pressX > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold)
root.screenState.launcher = true;
if (root.singleGestureTriggered)
return;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, pressX, pressY);
if (inDashboard && root.withinPanelWidth(root.panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold) {
root.screenState.dashboard = true;
root.singleGestureTriggered = true;
} else if (dragY < -Config.dashboard.dragThreshold) {
root.screenState.dashboard = false;
root.singleGestureTriggered = true;
}
} }
if (root.geometry.insetTop(root.borderThickness) > pressY && (Config.dashboard.showOnHover || !inDashboard)) { if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
popouts.hasCurrent = false;
}
if (Config.bar.autoHide || bar.fullscreen)
bar.isHovered = false;
if (screenState.dashboard && Config.dashboard.showOnHover)
screenState.dashboard = false;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
if (screenState.isDrawing && !inLeftPanel(panels.drawing, x, y)) {
input.z = 2;
panels.drawing.collapse();
}
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true;
const entryAt = bar.entryAt(geometry.axisPos(x, y));
const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
if (!dashboardShortcutActive) {
screenState.dashboard = showDashboard;
} else if (showDashboard) {
dashboardShortcutActive = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && withinPanelWidth(panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold)
screenState.dashboard = true;
else if (dragY < -Config.dashboard.dragThreshold)
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) { if (dragY > 20) {
root.screenState.settings = true; screenState.settings = true;
root.singleGestureTriggered = true;
} else if (dragY < -20) { } else if (dragY < -20) {
root.screenState.settings = false; screenState.settings = false;
root.singleGestureTriggered = true;
} }
} }
if (pressY > root.screen.height - root.geometry.insetBottom(root.borderThickness) && pressX < root.screen.width / 5 && dragY < -50) if (pressed && dragStart.y > screen.height - geometry.insetBottom(borderThickness) && dragStart.x < screen.width / 5 && dragY < -50)
root.screenState.clipboard = true; screenState.clipboard = true;
if (!Config.dock.hoverToReveal && pressY > root.screen.height - root.geometry.insetTop(root.borderThickness) && pressX > root.screen.width / 5 && !root.screenState.launcher) if (pressed && !Config.dock.hoverToReveal && dragStart.y > screen.height - geometry.insetTop(borderThickness) && dragStart.x > screen.width / 5 && !screenState.launcher)
if (dragY < -10) { if (dragY < -10)
root.screenState.dock = true; screenState.dock = true;
root.singleGestureTriggered = true;
}
if (pressX > root.screen.width - Config.bar.border && pressY < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) { if (pressed && dragStart.x > screen.width - Config.bar.border && dragStart.y < (screen.height / 2) && dragX < -Config.sidebar.dragThreshold)
root.screenState.sidebar = true; screenState.sidebar = true;
root.singleGestureTriggered = true;
}
if (pressX >= root.screen.width - Config.bar.border && pressY > (root.screen.height / 2) && dragX < -20) { if (pressed && dragStart.x >= screen.width - Config.bar.border && dragStart.y > (screen.height / 2) && dragX < -20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`); Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
}
if (pressX <= Config.bar.border && dragX > 20) { if (pressed && dragStart.x <= Config.bar.border && dragX > 20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`); Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
if (!screenState.bar && Config.bar.autoHide && geometry.barContains(x, y, true))
bar.isHovered = true;
if (panels.sidebar.offsetScale === 1) {
const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
} }
}
}
PointHandler {
id: drawingHandler
property bool setInitialPoint: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.screenState.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded)
onActiveChanged: {
if (!active) {
setInitialPoint = false;
root.drawing.content.endStroke();
} else { } else {
root.panels.drawing.collapse(); const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
} const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
}
onPointChanged: {
if (!active)
return;
const x = point.position.x;
const y = point.position.y;
const origX = point.pressPosition.x;
const origY = point.pressPosition.y;
if (point.pressedButtons & Qt.RightButton) { if (!osdShortcutActive) {
root.drawing.content.clear(); screenState.osd = showOsd;
return; panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
} }
if (x === 0 && y === 0 && origX === 0 && origY === 0)
return;
if (!setInitialPoint) {
setInitialPoint = true;
root.drawing.content.beginStroke(origX, origY);
return;
} }
root.drawing.content.appendPoint(x, y); if (Config.dock.enable && Config.dock.hoverToReveal && !screenState.dock && !screenState.launcher && inBottomPanel(panels.dock, x, y))
screenState.dock = true;
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
} }
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
} }
Timer { Timer {
@@ -184,98 +184,20 @@ Item {
running: false running: false
onTriggered: { onTriggered: {
if (hoverHandler.hovered) if (root.containsMouse)
root.bar.isHovered = true; root.bar.isHovered = true;
} }
} }
HoverHandler {
id: hoverHandler
cursorShape: root.screenState.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
onHoveredChanged: {
if (!hovered) {
if (!root.osdShortcutActive) {
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
if (!root.popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
root.popouts.hasCurrent = false;
}
if (Config.bar.autoHide || root.bar.fullscreen)
root.bar.isHovered = false;
if (root.screenState.dashboard && Config.dashboard.showOnHover)
root.screenState.dashboard = false;
}
}
onPointChanged: {
const x = point.position.x;
const y = point.position.y;
if (root.screenState.isDrawing) {
if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) {
root.panels.drawing.expand();
root.drawing.content.hideHover();
return;
}
root.drawing.content.showHover(x, y);
return;
}
if (!root.screenState.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true))
root.bar.isHovered = true;
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (root.panels.sidebar.offsetScale === 1) {
const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y);
if (showOsd) {
root.osdShortcutActive = false;
root.panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < root.width - root.panels.sidebar.width * (1 - root.panels.sidebar.offsetScale);
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
if (!root.osdShortcutActive) {
root.screenState.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
root.osdShortcutActive = false;
root.panels.osd.hovered = true;
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !root.screenState.dock && !root.screenState.launcher && root.inBottomPanel(root.panels.dock, x, y))
root.screenState.dock = true;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, x, y);
if (Config.dashboard.showOnHover)
root.screenState.dashboard = inDashboard;
if (root.geometry.barContains(x, y))
root.bar.checkPopout(root.geometry.axisPos(x, y));
}
}
Connections { Connections {
function onDashboardChanged() { function onDashboardChanged(): void {
if (root.screenState.dashboard) { if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); const inDashboardArea = root.inTopPanel(root.panels.dashboardWrapper, root.mouseX, root.mouseY);
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x)
root.screenState.launcher = false;
root.screenState.launcher = false;
root.screenState.settings = false; root.screenState.settings = false;
root.screenState.sidebar = false; root.screenState.sidebar = false;
root.screenState.resources = false; root.screenState.resources = false;
@@ -285,12 +207,15 @@ Item {
} }
} }
function onIsDrawingChanged() { function onIsDrawingChanged(): void {
if (!root.screenState.isDrawing) if (root.screenState.isDrawing) {
root.drawing.clear(); root.drawing.content.showHover();
} else {
root.drawing.content.clear();
}
} }
function onLauncherChanged() { function onLauncherChanged(): void {
if (!root.screenState.launcher) { if (!root.screenState.launcher) {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
root.osdShortcutActive = false; root.osdShortcutActive = false;
@@ -305,16 +230,13 @@ Item {
} }
if (root.screenState.launcher) { if (root.screenState.launcher) {
if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) {
root.screenState.dashboard = false; root.screenState.dashboard = false;
}
root.screenState.dock = false; root.screenState.dock = false;
root.screenState.settings = false; root.screenState.settings = false;
} }
} }
function onOsdChanged() { function onOsdChanged(): void {
if (root.screenState.osd) { if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
@@ -325,7 +247,7 @@ Item {
} }
} }
function onResourcesChanged() { function onResourcesChanged(): void {
if (root.screenState.resources) { if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates")) if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
@@ -335,7 +257,7 @@ Item {
} }
} }
function onSettingsChanged() { function onSettingsChanged(): void {
if (root.screenState.settings) { if (root.screenState.settings) {
root.screenState.resources = false; root.screenState.resources = false;
root.screenState.dashboard = false; root.screenState.dashboard = false;
@@ -345,8 +267,9 @@ Item {
} }
} }
function onSidebarChanged() { function onSidebarChanged(): void {
if (root.screenState.sidebar) { if (root.screenState.sidebar) {
if (root.panels.dashboardWrapper.x + root.panels.dashboardWrapper.width > root.panels.sidebar.x)
root.screenState.dashboard = false; root.screenState.dashboard = false;
root.screenState.settings = false; root.screenState.settings = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
-315
View File
@@ -1,315 +0,0 @@
import Quickshell
import QtQuick
import ZShell.Config
import ZShell.Internal
import qs.Components
import qs.Helpers
import qs.Modules.Bar.Popouts
CustomMouseArea {
id: root
required property Item bar
required property real borderThickness
property bool dashboardShortcutActive
property point dragStart
required property Drawing drawing
required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels
required property Wrapper popouts
required property ShellScreen screen
required property ScreenState screenState
property bool setInitialPoint: false
property bool singleGestureTriggered: false
property bool utilitiesShortcutActive
function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y > root.height - (panelHeight + geometry.insetBottom(borderThickness)) && withinPanelWidth(panel, x, y);
}
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inPopoutArea(x: real, y: real): bool {
const panel = panels.popoutsWrapper;
if (!geometry.horizontal)
return inLeftPanel(panel, x, y);
return withinPanelWidth(panel, x, y) && withinPanelHeight(panel, x, y);
}
function inRightPanel(panel: Item, x: real, y: real): bool {
return x > panel.x + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inTopPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y < geometry.insetTop(borderThickness) + panelHeight && withinPanelWidth(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + geometry.insetTop(borderThickness);
return y >= panelY && y <= panelY + panel.height;
}
function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = panel.x + geometry.insetLeft(borderThickness);
return x >= panelX && x <= panelX + panel.width;
}
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: {
if (!containsMouse) {
if (!osdShortcutActive) {
screenState.osd = false;
panels.osd.hovered = false;
}
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
popouts.hasCurrent = false;
}
if (Config.bar.autoHide || bar.fullscreen)
bar.isHovered = false;
if (screenState.dashboard && Config.dashboard.showOnHover)
screenState.dashboard = false;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
if (screenState.isDrawing) {
if (pressed) {
drawing.content.appendPoint(x, y);
} else {
if (inLeftPanel(panels.drawing, x, y)) {
if (!(pressedButtons & Qt.LeftButton)) {
panels.drawing.expand();
drawing.content.hideHover();
Cursor.clearOverride();
}
} else {
Cursor.setOverride(Qt.BlankCursor);
drawing.content.showHover(x, y);
}
}
if (drawing.content.hoverVisible)
drawing.content.showHover(x, y);
return;
}
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true;
const entryAt = bar.entryAt(geometry.axisPos(x, y));
const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
if (!dashboardShortcutActive) {
screenState.dashboard = showDashboard;
} else if (showDashboard) {
dashboardShortcutActive = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && withinPanelWidth(panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold)
screenState.dashboard = true;
else if (dragY < -Config.dashboard.dragThreshold)
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) {
screenState.settings = true;
} else if (dragY < -20) {
screenState.settings = false;
}
}
if (pressed && dragStart.y > screen.height - geometry.insetBottom(borderThickness) && dragStart.x < screen.width / 5 && dragY < -50)
screenState.clipboard = true;
if (pressed && !Config.dock.hoverToReveal && dragStart.y > screen.height - geometry.insetTop(borderThickness) && dragStart.x > screen.width / 5 && !screenState.launcher)
if (dragY < -10)
screenState.dock = true;
if (pressed && dragStart.x > screen.width - Config.bar.border && dragStart.y < (screen.height / 2) && dragX < -Config.sidebar.dragThreshold)
screenState.sidebar = true;
if (pressed && dragStart.x >= screen.width - Config.bar.border && dragStart.y > (screen.height / 2) && dragX < -20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
if (pressed && dragStart.x <= Config.bar.border && dragX > 20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
if (!screenState.bar && Config.bar.autoHide && geometry.barContains(x, y, true))
bar.isHovered = true;
if (panels.sidebar.offsetScale === 1) {
const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!osdShortcutActive) {
screenState.osd = showOsd;
panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !screenState.dock && !screenState.launcher && inBottomPanel(panels.dock, x, y))
screenState.dock = true;
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
}
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
if (screenState.isDrawing) {
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.beginStroke(event.x, event.y);
}
}
onPressedChanged: {
if (!pressed) {
if (screenState.isDrawing) {
setInitialPoint = false;
drawing.content.endStroke();
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.showHover(mouseX, mouseY);
}
} else {
if (screenState.isDrawing) {
panels.drawing.collapse();
}
}
}
Timer {
id: unhideTimer
interval: Config.bar.revealDelay
repeat: false
running: false
onTriggered: {
if (root.containsMouse)
root.bar.isHovered = true;
}
}
Connections {
function onDashboardChanged(): void {
if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboardWrapper, root.mouseX, root.mouseY);
if (!inDashboardArea) {
root.dashboardShortcutActive = true;
}
root.screenState.launcher = false;
root.screenState.settings = false;
root.screenState.sidebar = false;
root.screenState.resources = false;
root.popouts.hasCurrent = false;
} else {
root.dashboardShortcutActive = false;
}
}
function onIsDrawingChanged(): void {
if (root.screenState.isDrawing) {
Cursor.setOverride(Qt.BlankCursor);
} else {
Cursor.clearOverride();
}
}
function onLauncherChanged(): void {
if (!root.screenState.launcher) {
root.dashboardShortcutActive = false;
root.osdShortcutActive = false;
root.utilitiesShortcutActive = false;
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
}
if (root.screenState.launcher) {
root.screenState.dashboard = false;
root.screenState.dock = false;
root.screenState.settings = false;
}
}
function onOsdChanged(): void {
if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.osdShortcutActive = true;
}
} else {
root.osdShortcutActive = false;
}
}
function onResourcesChanged(): void {
if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false;
root.screenState.settings = false;
root.screenState.dashboard = false;
}
}
function onSettingsChanged(): void {
if (root.screenState.settings) {
root.screenState.resources = false;
root.screenState.dashboard = false;
root.screenState.sidebar = false;
root.panels.popouts.hasCurrent = false;
root.screenState.launcher = false;
}
}
function onSidebarChanged(): void {
if (root.screenState.sidebar) {
if (root.panels.dashboardWrapper.x + root.panels.dashboardWrapper.width > root.panels.sidebar.x)
root.screenState.dashboard = false;
root.screenState.settings = false;
root.popouts.hasCurrent = false;
}
}
target: root.screenState
}
}
+8 -3
View File
@@ -154,8 +154,10 @@ Item {
property real offsetScale: dashboard.shouldBeActive ? 0 : 1 property real offsetScale: dashboard.shouldBeActive ? 0 : 1
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
x: ZUtils.snap((parent.width - implicitWidth) / 2, dashboardWrapper)
clip: offsetScale > 0 clip: offsetScale > 0
implicitHeight: dashboard.implicitHeight * (1 - offsetScale) implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: dashboard.implicitWidth implicitWidth: dashboard.implicitWidth
@@ -194,8 +196,11 @@ Item {
Settings.Wrapper { Settings.Wrapper {
id: settings id: settings
anchors.centerIn: parent x: ZUtils.snap((parent.width - implicitWidth) / 2, settings)
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale y: ZUtils.snap((parent.height - implicitHeight) / 2 - (implicitHeight + 5 + (parent.height - implicitHeight) / 2) * offsetScale, settings)
// anchors.centerIn: parent
// anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
panels: root panels: root
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
+24 -2
View File
@@ -56,7 +56,7 @@ CustomWindow {
property color surfaceColor: Colors.tPalette.m3surface property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: screenState.screenshot ? WlrKeyboardFocus.Exclusive : screenState.launcher || screenState.settings || screenState.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: screenState.screenshot ? WlrKeyboardFocus.Exclusive : screenState.launcher || screenState.settings || screenState.sidebar || screenState.isDrawing ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent" color: "transparent"
mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions) mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
@@ -347,12 +347,31 @@ CustomWindow {
Drawing { Drawing {
id: drawing id: drawing
readonly property int extraWidth: drawingBg.radius * 2
anchors.fill: parent anchors.fill: parent
holeRect: Qt.rect(root.borderThickness + panels.drawing.x - extraWidth, panels.drawing.y + bar.implicitHeight, panels.drawing.implicitWidth + extraWidth, panels.drawing.implicitHeight)
holeRadius: drawingBg.radius
screenState: root.screenState screenState: root.screenState
z: 2 z: 2
} }
Interactions2 { LazyLoader {
id: lazyLoader
activeAsync: root.screenState.isDrawing
component: DrawingInput {
borderThickness: root.borderLayoutThickness
drawing: drawing
panels: panels
screenState: root.screenState
geometry: geometry
parent: root.contentItem
}
}
Interactions {
id: interactions id: interactions
anchors.fill: parent anchors.fill: parent
@@ -365,6 +384,8 @@ CustomWindow {
popouts: panels.popouts popouts: panels.popouts
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
input: lazyLoader.item
z: 1
Panels { Panels {
id: panels id: panels
@@ -442,6 +463,7 @@ CustomWindow {
parent: root.contentItem parent: root.contentItem
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
z: 4
} }
} }
+2 -1
View File
@@ -8,6 +8,7 @@ import ZShell.Components
import qs.Components import qs.Components
import qs.Modules.Bar.Components import qs.Modules.Bar.Components
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
import ZShell
SnapLayout { SnapLayout {
id: root id: root
@@ -23,7 +24,7 @@ SnapLayout {
function axisCenterOf(item: Item): real { function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2); const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
return horizontal ? Math.floor(c.x) : Math.floor(c.y); return horizontal ? ZUtils.snap(c.x, item) : ZUtils.snap(c.y, item);
} }
function checkPopout(pos: real): void { function checkPopout(pos: real): void {
+4 -4
View File
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import ZShell
Item { Item {
id: root id: root
@@ -36,7 +37,7 @@ Item {
const diff = parent.width - Math.floor(off + content.nonAnimWidth); const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return Math.floor(Math.max(off, 0)); return ZUtils.snap(Math.max(off, 0), root);
} }
y: { y: {
if (horizontal) if (horizontal)
@@ -46,12 +47,11 @@ Item {
const diff = parent.height - Math.floor(off + content.nonAnimHeight); const diff = parent.height - Math.floor(off + content.nonAnimHeight);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return Math.max(off, 0); return ZUtils.snap(Math.max(off, 0), root);
} }
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {}
}
} }
Behavior on x { Behavior on x {
enabled: !root.horizontal || root.offsetScale < 1 enabled: !root.horizontal || root.offsetScale < 1
+3 -2
View File
@@ -2,6 +2,7 @@ import Quickshell
import QtQuick import QtQuick
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import ZShell
Item { Item {
id: root id: root
@@ -26,8 +27,8 @@ Item {
detachedMode = ""; detachedMode = "";
} }
implicitHeight: nonAnimHeight implicitHeight: ZUtils.snap(nonAnimHeight, root)
implicitWidth: nonAnimWidth implicitWidth: ZUtils.snap(nonAnimWidth, root)
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.offsetScale < 1 enabled: root.offsetScale < 1
+4 -4
View File
@@ -17,7 +17,7 @@ Item {
implicitWidth: btn.implicitWidth * 0.9 implicitWidth: btn.implicitWidth * 0.9
implicitHeight: btn.implicitHeight * 0.9 implicitHeight: btn.implicitHeight * 0.9
BlobGroup { BlobGroup2 {
id: blobGroup id: blobGroup
color: Colors.palette.m3surfaceContainerHighest color: Colors.palette.m3surfaceContainerHighest
@@ -29,7 +29,7 @@ Item {
} }
} }
BlobRect { BlobRect2 {
id: btnRect id: btnRect
anchors.fill: parent anchors.fill: parent
@@ -42,7 +42,7 @@ Item {
} }
} }
BlobRect { BlobRect2 {
id: rect id: rect
anchors.right: parent.right anchors.right: parent.right
@@ -143,8 +143,8 @@ Item {
] ]
SnapLayout { SnapLayout {
orientation: Qt.Vertical
id: layout id: layout
orientation: Qt.Vertical
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
+2 -16
View File
@@ -53,24 +53,10 @@ Item {
lock: root.lock lock: root.lock
} }
SnapLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.medium
orientation: Qt.Vertical
CustomRect {
Layout.fillHeight: true
Layout.fillWidth: true
bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
NotifDock { NotifDock {
Layout.fillWidth: true
Layout.fillHeight: true
lock: root.lock lock: root.lock
} }
} }
} }
}
}
+1 -8
View File
@@ -23,7 +23,6 @@ CustomRect {
topRightRadius: Tokens.rounding.largeIncreased topRightRadius: Tokens.rounding.largeIncreased
SnapLayout { SnapLayout {
orientation: Qt.Vertical
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
@@ -52,18 +51,12 @@ CustomRect {
duration: Tokens.anim.durations.extraLarge duration: Tokens.anim.durations.extraLarge
} }
} }
sourceComponent: SnapLayout { sourceComponent: CustomText {
orientation: Qt.Vertical
spacing: Tokens.spacing.largeIncreased
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build() font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications") text: qsTr("No Notifications")
} }
} }
}
CustomListView { CustomListView {
anchors.fill: parent anchors.fill: parent
+1 -1
View File
@@ -9,7 +9,6 @@ import ZShell.Config
import qs.Services import qs.Services
SnapLayout { SnapLayout {
orientation: Qt.Vertical
id: root id: root
property bool animateScroll: false property bool animateScroll: false
@@ -69,6 +68,7 @@ orientation: Qt.Vertical
} }
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
orientation: Qt.Vertical
Component.onCompleted: applySearchAnchor() Component.onCompleted: applySearchAnchor()
+2 -1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -10,7 +11,7 @@ CustomText {
Layout.bottomMargin: Tokens.spacing.extraSmall Layout.bottomMargin: Tokens.spacing.extraSmall
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: Tokens.padding.extraSmall Layout.leftMargin: Tokens.padding.extraSmall
Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as ColumnLayout).spacing ?? 0) Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as SnapLayout).spacing ?? 0)
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight elide: Text.ElideRight
font: Tokens.font.label.medium font: Tokens.font.label.medium
+6 -2
View File
@@ -18,10 +18,12 @@ PageBase {
title: qsTr("All apps") title: qsTr("All apps")
SnapLayout { SnapLayout {
orientation: Qt.Vertical
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2 spacing: Tokens.spacing.extraSmall / 2
orientation: Qt.Vertical
width: root.cappedWidth width: root.cappedWidth
Repeater { Repeater {
@@ -54,6 +56,7 @@ orientation: Qt.Vertical
anchors.leftMargin: Tokens.padding.largeIncreased anchors.leftMargin: Tokens.padding.largeIncreased
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.largeIncreased anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
IconImage { IconImage {
@@ -63,8 +66,9 @@ orientation: Qt.Vertical
} }
SnapLayout { SnapLayout {
orientation: Qt.Vertical
Layout.fillWidth: true Layout.fillWidth: true
orientation: Qt.Vertical
spacing: 0 spacing: 0
CustomText { CustomText {
+1
View File
@@ -6,6 +6,7 @@ find_package(Qt6 REQUIRED COMPONENTS
GuiPrivate GuiPrivate
Quick Quick
CanvasPainter CanvasPainter
CanvasPainterTools
QuickPrivate QuickPrivate
QuickLayouts QuickLayouts
QuickLayoutsPrivate QuickLayoutsPrivate
+39 -2
View File
@@ -2,7 +2,17 @@
#include "blobinvertedrect.hpp" #include "blobinvertedrect.hpp"
#include "blobshape.hpp" #include "blobshape.hpp"
BlobGroup::BlobGroup(QObject* parent) : QObject(parent) {} #include <algorithm>
namespace {
constexpr int k_pumpMs = 16;
} // namespace
BlobGroup::BlobGroup(QObject* parent) : QObject(parent) {
m_physicsPump.setInterval(k_pumpMs);
m_physicsPump.setTimerType(Qt::CoarseTimer);
connect(&m_physicsPump, &QTimer::timeout, this, &BlobGroup::pumpPhysics);
}
BlobGroup::~BlobGroup() { BlobGroup::~BlobGroup() {
for (auto* shape : std::as_const(m_shapes)) for (auto* shape : std::as_const(m_shapes))
@@ -12,6 +22,7 @@ BlobGroup::~BlobGroup() {
} }
void BlobGroup::setSmoothing(qreal s) { void BlobGroup::setSmoothing(qreal s) {
if (s < 0.5) s = 0.5;
if (qFuzzyCompare(m_smoothing, s)) return; if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s; m_smoothing = s;
emit smoothingChanged(); emit smoothingChanged();
@@ -73,7 +84,6 @@ void BlobGroup::markShapeDirty(BlobShape* source) {
source->polish(); source->polish();
source->update(); source->update();
// Use cached padded rects to find spatial neighbors
const float pad = static_cast<float>(m_smoothing) * 2.0f; const float pad = static_cast<float>(m_smoothing) * 2.0f;
const QRectF srcRect( const QRectF srcRect(
static_cast<double>(source->m_cachedPaddedX - pad), static_cast<double>(source->m_cachedPaddedX - pad),
@@ -103,6 +113,33 @@ void BlobGroup::markShapeDirty(BlobShape* source) {
void BlobGroup::ensurePhysicsUpdated() { void BlobGroup::ensurePhysicsUpdated() {
if (m_physicsUpdated) return; if (m_physicsUpdated) return;
m_physicsUpdated = true; m_physicsUpdated = true;
for (auto* shape : std::as_const(m_shapes)) {
shape->updatePhysics();
if (shape->needsFrame()) {
shape->polish();
shape->update();
}
}
}
void BlobGroup::requestPhysics() {
if (!m_physicsPump.isActive()) m_physicsPump.start();
}
void BlobGroup::pumpPhysics() {
for (auto* shape : std::as_const(m_shapes)) for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics(); shape->updatePhysics();
const bool anyMoving = std::any_of(
m_shapes.cbegin(), m_shapes.cend(), [](const BlobShape* shape) {
return shape->needsFrame();
});
if (!anyMoving) {
m_physicsPump.stop();
return;
}
markDirty();
m_physicsUpdated = true;
} }
+6
View File
@@ -4,6 +4,7 @@
#include <qlist.h> #include <qlist.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qtimer.h>
class BlobShape; class BlobShape;
class BlobInvertedRect; class BlobInvertedRect;
@@ -51,16 +52,21 @@ class BlobGroup : public QObject {
void markShapeDirty(BlobShape* source); void markShapeDirty(BlobShape* source);
void ensurePhysicsUpdated(); void ensurePhysicsUpdated();
void requestPhysics();
signals: signals:
void smoothingChanged(); void smoothingChanged();
void colorChanged(); void colorChanged();
void cornerFillChanged(); void cornerFillChanged();
private: private:
void pumpPhysics();
qreal m_smoothing = 32.0; qreal m_smoothing = 32.0;
QColor m_color{0x44, 0x88, 0xff}; QColor m_color{0x44, 0x88, 0xff};
bool m_cornerFill = true; bool m_cornerFill = true;
QList<BlobShape*> m_shapes; QList<BlobShape*> m_shapes;
BlobInvertedRect* m_invertedRect = nullptr; BlobInvertedRect* m_invertedRect = nullptr;
bool m_physicsUpdated = false; bool m_physicsUpdated = false;
QTimer m_physicsPump;
}; };
+65 -39
View File
@@ -29,8 +29,15 @@ static float cornerFillFactor(float sd, float smoothFactor) {
return std::max(outside, inside); return std::max(outside, inside);
} }
namespace {
constexpr int k_pumpMs = 16;
} // namespace
BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) { BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents); setFlag(ItemHasContents);
m_physicsPump.setInterval(k_pumpMs);
m_physicsPump.setTimerType(Qt::CoarseTimer);
connect(&m_physicsPump, &QTimer::timeout, this, &BlobGroup2::pumpPhysics);
} }
BlobGroup2::~BlobGroup2() { BlobGroup2::~BlobGroup2() {
@@ -41,6 +48,7 @@ BlobGroup2::~BlobGroup2() {
} }
void BlobGroup2::setSmoothing(qreal s) { void BlobGroup2::setSmoothing(qreal s) {
if (s < 0.5) s = 0.5;
if (qFuzzyCompare(m_smoothing, s)) return; if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s; m_smoothing = s;
emit smoothingChanged(); emit smoothingChanged();
@@ -101,17 +109,11 @@ void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) {
void BlobGroup2::markDirty() { void BlobGroup2::markDirty() {
m_physicsUpdated = false; m_physicsUpdated = false;
m_dataBuilt = false; m_dataBuilt = false;
// Physics runs during polish, before the render pass. The panel content is
// transformed by deformMatrix from QML, so the matrix has to be final for this
// frame; building the data in updatePaintNode would let the transform lag one
// frame behind the material.
polish(); polish();
update(); update();
} }
void BlobGroup2::updatePolish() { void BlobGroup2::updatePolish() {
// The group is the only thing that draws, so it also drives the shapes'
// physics.
ensureDataBuilt(); ensureDataBuilt();
} }
@@ -122,32 +124,53 @@ void BlobGroup2::ensurePhysicsUpdated() {
shape->updatePhysics(); shape->updatePhysics();
} }
void BlobGroup2::requestPhysics() {
if (!m_physicsPump.isActive()) m_physicsPump.start();
}
void BlobGroup2::pumpPhysics() {
for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics();
m_physicsUpdated = true;
const bool anyMoving = std::any_of(
m_shapes.cbegin(), m_shapes.cend(), [](const BlobShape2* shape) {
return shape->needsFrame();
});
if (!anyMoving) {
m_physicsPump.stop();
return;
}
m_dataBuilt = false;
polish();
update();
}
void BlobGroup2::ensureDataBuilt() { void BlobGroup2::ensureDataBuilt() {
if (m_dataBuilt) return; if (m_dataBuilt) return;
ensurePhysicsUpdated(); ensurePhysicsUpdated();
// Physics can request the next frame through markDirty(); the data is
// refreshed here anyway, so the flag is set after it runs.
m_dataBuilt = true; m_dataBuilt = true;
m_data.rects.clear(); m_data.rects.clear();
m_bounds = QRectF(); m_bounds = QRectF();
m_data.hasInverted = false;
m_data.invertedRadius = 0;
memset(m_data.invertedOuter, 0, sizeof(m_data.invertedOuter));
memset(m_data.invertedInner, 0, sizeof(m_data.invertedInner));
memset(m_data.invertedOuterRadii, 0, sizeof(m_data.invertedOuterRadii));
const float smoothFactor = static_cast<float>(m_smoothing); const float smoothFactor = static_cast<float>(m_smoothing);
// The same reach the shader uses to decide whether a rect can contribute, plus
// the shadow, so every pixel the shader can give a value to is inside the node.
const double extent = static_cast<double>(smoothFactor) * 1.5 + const double extent = static_cast<double>(smoothFactor) * 1.5 +
static_cast<double>(m_shadowRadius); static_cast<double>(m_shadowRadius);
// Group order, no pruning by proximity: the shader sees this list for every pixel. A const bool dropAtRest = m_invertedRect != nullptr;
// shape that is not pushing is not in the list at all: shapes here are data for the
// group draw, so leaving a panel at rest out means it cannot define any part of the
// edge. A closing panel is still in the list while its curve runs out, even with no
// size left, which is what keeps the bounce.
// entryShapes maps a shared entry back to the shape it came from.
QVector<BlobShape2*> entryShapes; QVector<BlobShape2*> entryShapes;
for (auto* shape : std::as_const(m_shapes)) { for (auto* shape : std::as_const(m_shapes)) {
if (!shape->isMoving()) continue; if (dropAtRest && !shape->isMoving()) continue;
if (shape->width() <= 0 || shape->height() <= 0) continue; if (shape->width() <= 0 || shape->height() <= 0) continue;
if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects) if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects)
continue; continue;
@@ -194,7 +217,6 @@ void BlobGroup2::ensureDataBuilt() {
const int count = static_cast<int>(m_data.rects.size()); const int count = static_cast<int>(m_data.rects.size());
// Exclusion gates only the smooth blend.
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
int mask = 0; int mask = 0;
BlobShape2* const si = entryShapes[i]; BlobShape2* const si = entryShapes[i];
@@ -214,10 +236,11 @@ void BlobGroup2::ensureDataBuilt() {
BlobShape2* const si = entryShapes[i]; BlobShape2* const si = entryShapes[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; const float dcx = ri.cx + ri.offsetX, dcy = ri.cy + ri.offsetY;
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; const float cTrX = dcx + ri.hw, cTrY = dcy - ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; const float cBrX = dcx + ri.hw, cBrY = dcy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; const float cBlX = dcx - ri.hw, cBlY = dcy + ri.hh;
const float cTlX = dcx - ri.hw, cTlY = dcy - ri.hh;
if (cornerFill) { if (cornerFill) {
for (int j = 0; j < count; ++j) { for (int j = 0; j < count; ++j) {
@@ -227,37 +250,41 @@ void BlobGroup2::ensureDataBuilt() {
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
continue; continue;
const auto& rj = m_data.rects[j]; const auto& rj = m_data.rects[j];
const float jcx = rj.cx + rj.offsetX;
const float jcy = rj.cy + rj.offsetY;
fTr = std::min( fTr = std::min(
fTr, fTr,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
fBr = std::min( fBr = std::min(
fBr, fBr,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
fBl = std::min( fBl = std::min(
fBl, fBl,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
fTl = std::min( fTl = std::min(
fTl, fTl,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cTlX, cTlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
} }
} }
ri.radius[0] = std::max(ri.radius[0] * fTr, minR); ri.radius[0] = ri.radius[0] > 0.0f ? std::max(ri.radius[0] * fTr, minR)
ri.radius[1] = std::max(ri.radius[1] * fBr, minR); : 0.0f;
ri.radius[2] = std::max(ri.radius[2] * fBl, minR); ri.radius[1] = ri.radius[1] > 0.0f ? std::max(ri.radius[1] * fBr, minR)
ri.radius[3] = std::max(ri.radius[3] * fTl, minR); : 0.0f;
ri.radius[2] = ri.radius[2] > 0.0f ? std::max(ri.radius[2] * fBl, minR)
: 0.0f;
ri.radius[3] = ri.radius[3] > 0.0f ? std::max(ri.radius[3] * fTl, minR)
: 0.0f;
} }
// Inverted rect. The frame term is evaluated for every pixel in the bounds,
// so the group flag cannot be gated per shape.
if (!m_invertedRect) return; if (!m_invertedRect) return;
auto* inv = static_cast<BlobShape2*>(m_invertedRect); auto* inv = static_cast<BlobShape2*>(m_invertedRect);
@@ -297,6 +324,8 @@ void BlobGroup2::ensureDataBuilt() {
m_data.invertedInner[2] = innerHW; m_data.invertedInner[2] = innerHW;
m_data.invertedInner[3] = innerHH; m_data.invertedInner[3] = innerHH;
inv->outerCornerRadii(m_data.invertedOuterRadii);
m_bounds = m_bounds.united(QRectF( m_bounds = m_bounds.united(QRectF(
static_cast<double>(outerCX) - static_cast<double>(outerHW) - extent, static_cast<double>(outerCX) - static_cast<double>(outerHW) - extent,
static_cast<double>(outerCY) - static_cast<double>(outerHH) - extent, static_cast<double>(outerCY) - static_cast<double>(outerHH) - extent,
@@ -311,7 +340,6 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_paddedH = static_cast<float>(m_bounds.height()); material->m_paddedH = static_cast<float>(m_bounds.height());
material->m_smoothFactor = static_cast<float>(m_smoothing); material->m_smoothFactor = static_cast<float>(m_smoothing);
// The single group draw has no owner, so the shader skips the ownership test.
material->m_myIndex = -2; material->m_myIndex = -2;
material->m_color = m_color; material->m_color = m_color;
material->m_hasInverted = m_data.hasInverted ? 1 : 0; material->m_hasInverted = m_data.hasInverted ? 1 : 0;
@@ -324,6 +352,10 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_invertedInner, material->m_invertedInner,
data().invertedInner, data().invertedInner,
sizeof(data().invertedInner)); sizeof(data().invertedInner));
memcpy(
material->m_invertedOuterRadii,
data().invertedOuterRadii,
sizeof(data().invertedOuterRadii));
material->m_shadowRadius = static_cast<float>(m_shadowRadius); material->m_shadowRadius = static_cast<float>(m_shadowRadius);
material->m_shadowColor[0] = static_cast<float>(m_shadowColor.redF()); material->m_shadowColor[0] = static_cast<float>(m_shadowColor.redF());
@@ -339,8 +371,6 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) {
} }
QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
// Data is built during polish. This only covers a render pass that happens
// without one.
ensureDataBuilt(); ensureDataBuilt();
if (m_data.rects.isEmpty() && !m_data.hasInverted) { if (m_data.rects.isEmpty() && !m_data.hasInverted) {
@@ -364,10 +394,6 @@ QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
node->setFlag(QSGNode::OwnsMaterial); node->setFlag(QSGNode::OwnsMaterial);
} }
// The shader evaluates the field at the scene position a vertex maps to, so
// the group item itself must not be transformed. The vertex positions are the
// bounds in this item's frame; the material carries the same rect in scene
// coordinates.
auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
const QPointF p0 = mapFromScene(m_bounds.topLeft()); const QPointF p0 = mapFromScene(m_bounds.topLeft());
const QPointF p1 = mapFromScene(m_bounds.bottomRight()); const QPointF p1 = mapFromScene(m_bounds.bottomRight());
+6 -14
View File
@@ -9,22 +9,13 @@
#include <qquickitem.h> #include <qquickitem.h>
#include <qrect.h> #include <qrect.h>
#include <qsgnode.h> #include <qsgnode.h>
#include <qtimer.h>
#include <qvector.h> #include <qvector.h>
class BlobShape2; class BlobShape2;
class BlobInvertedRect2; class BlobInvertedRect2;
// One shared valuation for a whole group, drawn by one node.
//
// The shapes here are data only: they say where the material should be. The group
// draws a single quad over everything the shader can evaluate, so a pixel cannot
// be evaluated by one shape and missed by another, and no second draw can
// disagree with the first. That is what makes overlapping shapes read as one
// surface - one value, one writer, one alpha per pixel.
struct BlobGroupData { struct BlobGroupData {
// The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit
// mask. The shared list is capped at 16, in group order, so the cap is the
// same list for every pixel.
static constexpr int kMaxRects = 16; static constexpr int kMaxRects = 16;
QVector<BlobRectData> rects; QVector<BlobRectData> rects;
@@ -32,6 +23,7 @@ struct BlobGroupData {
float invertedRadius = 0; float invertedRadius = 0;
float invertedOuter[4] = {}; float invertedOuter[4] = {};
float invertedInner[4] = {}; float invertedInner[4] = {};
float invertedOuterRadii[4] = {};
}; };
class BlobGroup2 : public QQuickItem { class BlobGroup2 : public QQuickItem {
@@ -91,10 +83,10 @@ class BlobGroup2 : public QQuickItem {
[[nodiscard]] const BlobGroupData& data() const { return m_data; } [[nodiscard]] const BlobGroupData& data() const { return m_data; }
// One draw per group, so a change to any shape changes the value for every
// pixel. Refresh the whole group.
void markDirty(); void markDirty();
void requestPhysics();
signals: signals:
void smoothingChanged(); void smoothingChanged();
void colorChanged(); void colorChanged();
@@ -110,6 +102,7 @@ class BlobGroup2 : public QQuickItem {
void ensurePhysicsUpdated(); void ensurePhysicsUpdated();
void ensureDataBuilt(); void ensureDataBuilt();
void fillMaterial(BlobMaterial* material); void fillMaterial(BlobMaterial* material);
void pumpPhysics();
qreal m_smoothing = 32.0; qreal m_smoothing = 32.0;
QColor m_color{0x44, 0x88, 0xff}; QColor m_color{0x44, 0x88, 0xff};
@@ -120,9 +113,8 @@ class BlobGroup2 : public QQuickItem {
BlobInvertedRect2* m_invertedRect = nullptr; BlobInvertedRect2* m_invertedRect = nullptr;
bool m_physicsUpdated = false; bool m_physicsUpdated = false;
bool m_dataBuilt = false; bool m_dataBuilt = false;
QTimer m_physicsPump;
BlobGroupData m_data; BlobGroupData m_data;
// Scene-space union of what the shader can evaluate for any shape. The node
// covers exactly this, so gaps are not possible.
QRectF m_bounds; QRectF m_bounds;
}; };
+7 -13
View File
@@ -11,28 +11,24 @@
BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {} BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {}
static void setFrameIndices(quint16* idx) { static void setFrameIndices(quint16* idx) {
// Top strip: 0-1-4, 1-5-4
idx[0] = 0; idx[0] = 0;
idx[1] = 1; idx[1] = 1;
idx[2] = 4; idx[2] = 4;
idx[3] = 1; idx[3] = 1;
idx[4] = 5; idx[4] = 5;
idx[5] = 4; idx[5] = 4;
// Right strip: 1-2-5, 2-6-5
idx[6] = 1; idx[6] = 1;
idx[7] = 2; idx[7] = 2;
idx[8] = 5; idx[8] = 5;
idx[9] = 2; idx[9] = 2;
idx[10] = 6; idx[10] = 6;
idx[11] = 5; idx[11] = 5;
// Bottom strip: 2-3-6, 3-7-6
idx[12] = 2; idx[12] = 2;
idx[13] = 3; idx[13] = 3;
idx[14] = 6; idx[14] = 6;
idx[15] = 3; idx[15] = 3;
idx[16] = 7; idx[16] = 7;
idx[17] = 6; idx[17] = 6;
// Left strip: 3-0-7, 0-4-7
idx[18] = 3; idx[18] = 3;
idx[19] = 0; idx[19] = 0;
idx[20] = 7; idx[20] = 7;
@@ -50,17 +46,20 @@ QSGNode* BlobInvertedRect::updatePaintNode(
const float pad = static_cast<float>(m_group->smoothing()); const float pad = static_cast<float>(m_group->smoothing());
// Compute inner hole boundary in local coords
// Inset past the inner border edge by 2x smoothing to cover the blend zone
const float inset = pad * 2.0f; const float inset = pad * 2.0f;
const float holeLeft = static_cast<float>(m_borderLeft) + inset; const float holeLeft = static_cast<float>(m_borderLeft) + inset;
const float holeTop = static_cast<float>(m_borderTop) + inset; const float holeTop = static_cast<float>(m_borderTop) + inset;
const float holeRight = static_cast<float>(width() - m_borderRight) - inset; const float holeRight = static_cast<float>(width() - m_borderRight) - inset;
const float holeBot = static_cast<float>(height() - m_borderBottom) - inset; const float holeBot = static_cast<float>(height() - m_borderBottom) - inset;
// If the hole is too small or invalid, fall back to full quad if (holeLeft >= holeRight || holeTop >= holeBot) {
if (holeLeft >= holeRight || holeTop >= holeBot) auto* oldGeoNode = static_cast<QSGGeometryNode*>(oldNode);
if (oldNode && oldGeoNode->geometry()->vertexCount() != 4) {
delete oldNode;
oldNode = nullptr;
}
return BlobShape::updatePaintNode(oldNode, nullptr); return BlobShape::updatePaintNode(oldNode, nullptr);
}
auto* node = static_cast<QSGGeometryNode*>(oldNode); auto* node = static_cast<QSGGeometryNode*>(oldNode);
@@ -87,7 +86,6 @@ QSGNode* BlobInvertedRect::updatePaintNode(
node->setFlag(QSGNode::OwnsMaterial); node->setFlag(QSGNode::OwnsMaterial);
} }
// Outer bounds (local coords)
const float x0 = static_cast<float>(m_localPaddedRect.x()); const float x0 = static_cast<float>(m_localPaddedRect.x());
const float y0 = static_cast<float>(m_localPaddedRect.y()); const float y0 = static_cast<float>(m_localPaddedRect.y());
const float x1 = x0 + static_cast<float>(m_localPaddedRect.width()); const float x1 = x0 + static_cast<float>(m_localPaddedRect.width());
@@ -95,15 +93,12 @@ QSGNode* BlobInvertedRect::updatePaintNode(
const float w = x1 - x0; const float w = x1 - x0;
const float h = y1 - y0; const float h = y1 - y0;
// Update vertex positions and texture coordinates
auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
// Outer corners
v[0].set(x0, y0, 0.0f, 0.0f); v[0].set(x0, y0, 0.0f, 0.0f);
v[1].set(x1, y0, 1.0f, 0.0f); v[1].set(x1, y0, 1.0f, 0.0f);
v[2].set(x1, y1, 1.0f, 1.0f); v[2].set(x1, y1, 1.0f, 1.0f);
v[3].set(x0, y1, 0.0f, 1.0f); v[3].set(x0, y1, 0.0f, 1.0f);
// Inner corners (hole)
v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h);
v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h);
v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h);
@@ -111,7 +106,6 @@ QSGNode* BlobInvertedRect::updatePaintNode(
node->markDirty(QSGNode::DirtyGeometry); node->markDirty(QSGNode::DirtyGeometry);
// Update material uniforms
auto* material = static_cast<BlobMaterial*>(node->material()); auto* material = static_cast<BlobMaterial*>(node->material());
material->m_paddedX = m_cachedPaddedX; material->m_paddedX = m_cachedPaddedX;
material->m_paddedY = m_cachedPaddedY; material->m_paddedY = m_cachedPaddedY;
+6 -3
View File
@@ -59,12 +59,15 @@ class BlobInvertedRect : public BlobShape {
void setOuterRadius(qreal v); void setOuterRadius(qreal v);
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect). [[nodiscard]] qreal outerTopLeftRadius() const {
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; } return m_outerTopLeftRadius;
}
void setOuterTopLeftRadius(qreal v); void setOuterTopLeftRadius(qreal v);
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; } [[nodiscard]] qreal outerTopRightRadius() const {
return m_outerTopRightRadius;
}
void setOuterTopRightRadius(qreal v); void setOuterTopRightRadius(qreal v);
@@ -1,6 +1,8 @@
#include "blobinvertedrect2.hpp" #include "blobinvertedrect2.hpp"
#include "blobgroup2.hpp" #include "blobgroup2.hpp"
#include <algorithm>
BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {} BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {}
BlobInvertedRect2::~BlobInvertedRect2() { BlobInvertedRect2::~BlobInvertedRect2() {
@@ -35,6 +37,64 @@ void BlobInvertedRect2::setBorderBottom(qreal v) {
if (m_group) m_group->markDirty(); if (m_group) m_group->markDirty();
} }
void BlobInvertedRect2::setOuterRadius(qreal v) {
if (qFuzzyCompare(m_outerRadius, v)) return;
m_outerRadius = v;
emit outerRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterTopLeftRadius(qreal v) {
if (qFuzzyCompare(m_outerTopLeftRadius, v)) return;
m_outerTopLeftRadius = v;
emit outerTopLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterTopRightRadius(qreal v) {
if (qFuzzyCompare(m_outerTopRightRadius, v)) return;
m_outerTopRightRadius = v;
emit outerTopRightRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterBottomLeftRadius(qreal v) {
if (qFuzzyCompare(m_outerBottomLeftRadius, v)) return;
m_outerBottomLeftRadius = v;
emit outerBottomLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterBottomRightRadius(qreal v) {
if (qFuzzyCompare(m_outerBottomRightRadius, v)) return;
m_outerBottomRightRadius = v;
emit outerBottomRightRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::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 BlobInvertedRect2::registerWithGroup() { void BlobInvertedRect2::registerWithGroup() {
if (m_group) m_group->setInvertedRect(this); if (m_group) m_group->setInvertedRect(this);
} }
@@ -19,6 +19,21 @@ class BlobInvertedRect2 : public BlobShape2 {
Q_PROPERTY( Q_PROPERTY(
qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY
borderBottomChanged) 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: public:
explicit BlobInvertedRect2(QQuickItem* parent = nullptr); explicit BlobInvertedRect2(QQuickItem* parent = nullptr);
@@ -40,13 +55,48 @@ class BlobInvertedRect2 : public BlobShape2 {
void setBorderBottom(qreal v); void setBorderBottom(qreal v);
[[nodiscard]] qreal outerRadius() const { return m_outerRadius; }
void setOuterRadius(qreal v);
[[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: signals:
void borderLeftChanged(); void borderLeftChanged();
void borderRightChanged(); void borderRightChanged();
void borderTopChanged(); void borderTopChanged();
void borderBottomChanged(); void borderBottomChanged();
void outerRadiusChanged();
void outerTopLeftRadiusChanged();
void outerTopRightRadiusChanged();
void outerBottomLeftRadiusChanged();
void outerBottomRightRadiusChanged();
protected: protected:
void outerCornerRadii(float out[4]) const override;
void registerWithGroup() override; void registerWithGroup() override;
void unregisterFromGroup() override; void unregisterFromGroup() override;
@@ -55,4 +105,10 @@ class BlobInvertedRect2 : public BlobShape2 {
qreal m_borderRight = 0; qreal m_borderRight = 0;
qreal m_borderTop = 0; qreal m_borderTop = 0;
qreal m_borderBottom = 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;
}; };
-15
View File
@@ -44,22 +44,17 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + 64, &opacity, 4); memcpy(buf->data() + 64, &opacity, 4);
} }
// Padded rect (offset 68)
memcpy(buf->data() + 68, &mat->m_paddedX, 4); memcpy(buf->data() + 68, &mat->m_paddedX, 4);
memcpy(buf->data() + 72, &mat->m_paddedY, 4); memcpy(buf->data() + 72, &mat->m_paddedY, 4);
memcpy(buf->data() + 76, &mat->m_paddedW, 4); memcpy(buf->data() + 76, &mat->m_paddedW, 4);
memcpy(buf->data() + 80, &mat->m_paddedH, 4); memcpy(buf->data() + 80, &mat->m_paddedH, 4);
// Smooth factor (offset 84)
memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); memcpy(buf->data() + 84, &mat->m_smoothFactor, 4);
// Rect count (offset 88)
memcpy(buf->data() + 88, &mat->m_rectCount, 4); memcpy(buf->data() + 88, &mat->m_rectCount, 4);
// My index (offset 92). -2 skips the ownership test in the shader.
memcpy(buf->data() + 92, &mat->m_myIndex, 4); memcpy(buf->data() + 92, &mat->m_myIndex, 4);
// Color as vec4 (offset 96, 16 bytes)
const float color[4] = { const float color[4] = {
mat->m_color.redF(), mat->m_color.redF(),
mat->m_color.greenF(), mat->m_color.greenF(),
@@ -68,26 +63,19 @@ bool BlobMaterialShader::updateUniformData(
}; };
memcpy(buf->data() + 96, color, 16); memcpy(buf->data() + 96, color, 16);
// Has inverted (offset 112)
memcpy(buf->data() + 112, &mat->m_hasInverted, 4); memcpy(buf->data() + 112, &mat->m_hasInverted, 4);
// Inverted radius (offset 116)
memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); memcpy(buf->data() + 116, &mat->m_invertedRadius, 4);
// Padding at 120-127 (skip)
// Inverted outer (offset 128, 16 bytes)
memcpy(buf->data() + 128, mat->m_invertedOuter, 16); memcpy(buf->data() + 128, mat->m_invertedOuter, 16);
// Inverted inner (offset 144, 16 bytes)
memcpy(buf->data() + 144, mat->m_invertedInner, 16); memcpy(buf->data() + 144, mat->m_invertedInner, 16);
// Rect data (offset 160, each rect = 5 vec4s = 80 bytes)
const int count = qMin(mat->m_rectCount, 16); const int count = qMin(mat->m_rectCount, 16);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
const auto& r = mat->m_rects[i]; const auto& r = mat->m_rects[i];
const int base = 160 + i * 80; const int base = 160 + i * 80;
// Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt)
float maskAsFloat = NAN; float maskAsFloat = NAN;
memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); memcpy(&maskAsFloat, &r.excludeMask, sizeof(float));
const float d0[4] = {r.cx, r.cy, r.hw, r.hh}; const float d0[4] = {r.cx, r.cy, r.hw, r.hh};
@@ -100,12 +88,9 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + base + 64, r.radius, 16); memcpy(buf->data() + base + 64, r.radius, 16);
} }
// Shadow: after rectData, which ends at 1440. shadowColor is 16-byte aligned,
// so it lands at 1456.
memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4); memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4);
memcpy(buf->data() + 1456, mat->m_shadowColor, 16); memcpy(buf->data() + 1456, mat->m_shadowColor, 16);
// Outer edge corner radii, after shadowColor, at 1472.
memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16); memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16);
return true; return true;
-10
View File
@@ -8,14 +8,9 @@ struct BlobRectData {
float cx = 0, cy = 0, hw = 0, hh = 0; float cx = 0, cy = 0, hw = 0, hh = 0;
float offsetX = 0, offsetY = 0; float offsetX = 0, offsetY = 0;
float minEig = 1.0f; float minEig = 1.0f;
// Inverse of 2x2 deformation matrix, column-major for GLSL
float invDeform[4] = {1, 0, 0, 1}; float invDeform[4] = {1, 0, 0, 1};
// Screen-space AABB half-extents of the deformed rect
float screenHalfX = 0, screenHalfY = 0; float screenHalfX = 0, screenHalfY = 0;
// Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU
float radius[4] = {0, 0, 0, 0}; float radius[4] = {0, 0, 0, 0};
// Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect.
// Used by the shader to skip smin between excluded pairs.
int excludeMask = 0; int excludeMask = 0;
}; };
@@ -32,19 +27,14 @@ class BlobMaterial : public QSGMaterial {
float m_paddedH = 0; float m_paddedH = 0;
float m_smoothFactor = 32.0f; float m_smoothFactor = 32.0f;
int m_rectCount = 0; int m_rectCount = 0;
// -2 means no owner test: the single group draw writes every pixel it covers.
int m_myIndex = -2; int m_myIndex = -2;
QColor m_color{0x44, 0x88, 0xff}; QColor m_color{0x44, 0x88, 0xff};
int m_hasInverted = 0; int m_hasInverted = 0;
float m_invertedRadius = 0; float m_invertedRadius = 0;
float m_invertedOuter[4] = {}; float m_invertedOuter[4] = {};
float m_invertedInner[4] = {}; float m_invertedInner[4] = {};
// Phase 2: shadow around the material edges. Matches the uniform block, which
// puts these after rectData.
float m_shadowRadius = 0.0f; float m_shadowRadius = 0.0f;
float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f}; 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] = {}; float m_invertedOuterRadii[4] = {};
BlobRectData m_rects[16] = {}; BlobRectData m_rects[16] = {};
}; };
+121 -79
View File
@@ -7,19 +7,94 @@
namespace { namespace {
constexpr float k_retireMs = 120.0f; constexpr float k_retireMs = 120.0f;
} constexpr qint64 k_minEvalMs = 10;
constexpr float k_staleSampleSec = 0.1f;
constexpr float k_staleMovePx = 2.0f;
constexpr float k_moveSpeed = 5.0f;
constexpr float k_settleDelta = 0.004f;
constexpr float k_settleVel = 0.05f;
constexpr float k_identityEps = 0.0005f;
constexpr float k_stillPx = 0.5f;
constexpr int k_settleStillSamples = 3;
} // namespace
BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {} BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {
m_stillSamples = k_settleStillSamples;
}
BlobRect::~BlobRect() { BlobRect::~BlobRect() {
if (m_group) m_group->removeShape(this); if (m_group) m_group->removeShape(this);
} }
bool BlobRect::needsFrame() const {
return m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples ||
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f) >
k_identityEps;
}
void BlobRect::resetDeformMatrix() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retiring = false;
m_physicsActive = false;
BlobShape::resetDeformMatrix();
}
void BlobRect::requestPhysics() {
if (m_group) m_group->requestPhysics();
}
void BlobRect::updatePolish() { void BlobRect::updatePolish() {
BlobShape::updatePolish(); BlobShape::updatePolish();
if (m_physicsActive || m_retiring) requestPhysics();
}
void BlobRect::finishAtIdentity() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_deformMatrix = QMatrix4x4();
m_retiring = false;
m_physicsActive = false;
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
if (m_group) m_group->markDirty();
}
void BlobRect::beginRetire() {
if (m_retiring) return;
m_retire00 = m_dm00;
m_retire01 = m_dm01;
m_retire11 = m_dm11;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
m_retiring = true;
requestPhysics();
}
void BlobRect::trySettle(float speed) {
if (m_retiring) return;
if (!m_physicsActive) return; if (!m_physicsActive) return;
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (speed < k_moveSpeed && totalDelta < k_settleDelta &&
totalVel < k_settleVel && m_stillSamples >= k_settleStillSamples) {
beginRetire();
}
}
void BlobRect::updatePhysics() {
if (m_retiring) { if (m_retiring) {
const float t = std::clamp( const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs, static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
@@ -28,100 +103,80 @@ void BlobRect::updatePolish() {
const float s = t * t * (3.0f - 2.0f * t); const float s = t * t * (3.0f - 2.0f * t);
if (t >= 1.0f) { if (t >= 1.0f) {
m_dm00 = 1.0f; finishAtIdentity();
m_dm01 = 0.0f; return;
m_dm11 = 1.0f; }
m_deformMatrix = QMatrix4x4();
m_retiring = false;
m_physicsActive = false;
} else {
m_dm00 = m_retire00 + (1.0f - m_retire00) * s; m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
m_dm01 = m_retire01 - m_retire01 * s; m_dm01 = m_retire01 - m_retire01 * s;
m_dm11 = m_retire11 + (1.0f - m_retire11) * s; m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
m_deformMatrix = QMatrix4x4( m_deformMatrix = QMatrix4x4(
m_dm00, m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
m_dm01,
0,
0,
m_dm01,
m_dm11,
0,
0,
0,
0,
1,
0,
0,
0,
0,
1);
}
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
if (!m_physicsActive) return;
} else {
const float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (totalDelta < 0.004f && totalVel < 0.05f) beginRetire();
} }
QMetaObject::invokeMethod(
this,
[this]() {
if (m_physicsActive && m_group) m_group->markDirty();
},
Qt::QueuedConnection);
}
void BlobRect::beginRetire() {
m_retire00 = m_dm00;
m_retire01 = m_dm01;
m_retire11 = m_dm11;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
m_retiring = true;
}
void BlobRect::updatePhysics() {
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
if (!m_physicsActive && !m_retiring) {
const float stuck = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f);
if (stuck > k_identityEps) {
beginRetire();
return;
}
}
if (!m_hasPrevPos) { if (!m_hasPrevPos) {
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
m_elapsed.start(); m_elapsed.start();
m_hasPrevPos = true; m_hasPrevPos = true;
m_stillSamples = k_settleStillSamples;
return; return;
} }
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f; const qint64 evalMs = m_elapsed.elapsed();
if (dt > 0.1f || dt < 0.001f) { if (evalMs < k_minEvalMs) {
return;
}
m_elapsed.restart();
const float dx = static_cast<float>(scenePos.x() - m_prevScenePos.x());
const float dy = static_cast<float>(scenePos.y() - m_prevScenePos.y());
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
if (std::abs(dx) + std::abs(dy) > k_stillPx)
m_stillSamples = 0;
else
m_stillSamples += 1;
const float dt = static_cast<float>(evalMs) / 1000.0f;
if (dt > k_staleSampleSec) {
if (std::abs(dx) + std::abs(dy) > k_staleMovePx) {
m_physicsActive = true;
requestPhysics();
} else {
trySettle(0.0f);
}
return; return;
} }
const float velX = const float velX = dx / dt;
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt; const float velY = dy / dt;
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
m_prevScenePos = scenePos;
const float speed = std::sqrt(velX * velX + velY * velY); const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) { if (m_retiring) {
if (speed > 5.0f) if (speed > k_moveSpeed)
m_retiring = false; m_retiring = false;
else else
return; return;
} }
if (!m_physicsActive) { if (!m_physicsActive) {
if (speed < 5.0f) return; if (speed < k_moveSpeed) return;
m_physicsActive = true; m_physicsActive = true;
requestPhysics();
} }
const float kStretchFactor = static_cast<float>(m_deformScale); const float kStretchFactor = static_cast<float>(m_deformScale);
@@ -131,7 +186,7 @@ void BlobRect::updatePhysics() {
float target01 = 0.0f; float target01 = 0.0f;
float target11 = 1.0f; float target11 = 1.0f;
if (speed > 5.0f) { if (speed > k_moveSpeed) {
const float targetStretch = const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch); 1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch; const float targetCompress = 1.0f / targetStretch;
@@ -165,7 +220,7 @@ void BlobRect::updatePhysics() {
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
checkAtRest(speed); trySettle(speed);
} }
void BlobRect::setTopLeftRadius(qreal r) { void BlobRect::setTopLeftRadius(qreal r) {
@@ -340,16 +395,3 @@ void BlobRect::excludeCornersRemoveLast(QQmlListProperty<BlobRect>* prop) {
if (self->m_group) self->m_group->markDirty(); if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged(); emit self->excludeCornersChanged();
} }
void BlobRect::checkAtRest(float speed) {
if (m_retiring) return;
constexpr float kEpsilon = 0.002f;
const bool atRest =
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon &&
speed < 5.0f;
if (atRest) beginRetire();
}
+6 -1
View File
@@ -105,15 +105,20 @@ class BlobRect : public BlobShape {
protected: protected:
void updatePolish() override; void updatePolish() override;
void updatePhysics() override; void updatePhysics() override;
bool needsFrame() const override;
void resetDeformMatrix() override;
private: private:
void checkAtRest(float speed); void trySettle(float speed);
void beginRetire(); void beginRetire();
void finishAtIdentity();
void requestPhysics();
QPointF m_prevScenePos; QPointF m_prevScenePos;
QElapsedTimer m_elapsed; QElapsedTimer m_elapsed;
bool m_physicsActive = false; bool m_physicsActive = false;
bool m_hasPrevPos = false; bool m_hasPrevPos = false;
int m_stillSamples = 0;
bool m_retiring = false; bool m_retiring = false;
QElapsedTimer m_retireElapsed; QElapsedTimer m_retireElapsed;
+109 -50
View File
@@ -6,14 +6,59 @@
namespace { namespace {
constexpr float k_retireMs = 120.0f; constexpr float k_retireMs = 120.0f;
constexpr qint64 k_minEvalMs = 10;
constexpr float k_staleSampleSec = 0.1f;
constexpr float k_staleMovePx = 2.0f;
constexpr float k_moveSpeed = 5.0f;
constexpr float k_settleDelta = 0.004f;
constexpr float k_settleVel = 0.05f;
constexpr float k_identityEps = 0.0005f;
constexpr float k_stillPx = 0.5f;
constexpr int k_settleStillSamples = 3;
} // namespace } // namespace
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {} BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {
m_stillSamples = k_settleStillSamples;
}
BlobRect2::~BlobRect2() { BlobRect2::~BlobRect2() {
if (m_group) m_group->removeShape(this); if (m_group) m_group->removeShape(this);
} }
bool BlobRect2::needsFrame() const {
return m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples ||
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f) >
k_identityEps;
}
bool BlobRect2::isMoving() const {
return m_atRestOverride || m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples;
}
void BlobRect2::updateMoving() {
const bool moving = isMoving();
if (moving == m_movingEmitted) return;
m_movingEmitted = moving;
emit movingChanged();
}
void BlobRect2::resetDeformMatrix() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retiring = false;
m_physicsActive = false;
BlobShape2::resetDeformMatrix();
}
void BlobRect2::requestPhysics() {
if (m_group) m_group->requestPhysics();
}
void BlobRect2::finishAtIdentity() { void BlobRect2::finishAtIdentity() {
m_dm00 = 1.0f; m_dm00 = 1.0f;
m_dm01 = 0.0f; m_dm01 = 0.0f;
@@ -23,11 +68,12 @@ void BlobRect2::finishAtIdentity() {
m_physicsActive = false; m_physicsActive = false;
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
if (m_group) m_group->markDirty();
updateMoving();
} }
void BlobRect2::beginRetire() { void BlobRect2::beginRetire() {
// The settle check would otherwise re-enter this every frame and restart the
// curve timer, so the curve never reaches t = 1.
if (m_retiring) return; if (m_retiring) return;
m_retire00 = m_dm00; m_retire00 = m_dm00;
@@ -36,10 +82,26 @@ void BlobRect2::beginRetire() {
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start(); m_retireElapsed.start();
m_retiring = true; m_retiring = true;
requestPhysics();
updateMoving();
}
void BlobRect2::trySettle(float speed) {
if (m_retiring) return;
if (!m_physicsActive) return;
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (speed < k_moveSpeed && totalDelta < k_settleDelta &&
totalVel < k_settleVel && m_stillSamples >= k_settleStillSamples) {
beginRetire();
}
} }
void BlobRect2::updatePhysics() { void BlobRect2::updatePhysics() {
// Shapes are data only, so the group drives physics here.
if (m_retiring) { if (m_retiring) {
const float t = std::clamp( const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs, static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
@@ -63,12 +125,16 @@ void BlobRect2::updatePhysics() {
if (!m_physicsActive) return; if (!m_physicsActive) return;
} }
// No "not active" guard here: an inactive shape still has to be evaluated so it if (!m_physicsActive && !m_retiring) {
// can see the panel start moving and become active. The frame loop is only kept const float stuck = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
// alive while active, and it is requested before anything can return early — std::abs(m_dm11 - 1.0f);
// requesting it at the end lets a shape stop being evaluated with the matrix if (stuck > k_identityEps) {
// still off identity, which is what left the panel contents blurry. beginRetire();
if (m_physicsActive && m_group) m_group->markDirty(); return;
}
}
if (m_physicsActive) requestPhysics();
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
@@ -76,34 +142,54 @@ void BlobRect2::updatePhysics() {
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
m_elapsed.start(); m_elapsed.start();
m_hasPrevPos = true; m_hasPrevPos = true;
m_stillSamples = k_settleStillSamples;
updateMoving();
return; return;
} }
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f; const qint64 evalMs = m_elapsed.elapsed();
if (dt > 0.1f || dt < 0.001f) { if (evalMs < k_minEvalMs) {
return;
}
m_elapsed.restart();
const float dx = static_cast<float>(scenePos.x() - m_prevScenePos.x());
const float dy = static_cast<float>(scenePos.y() - m_prevScenePos.y());
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
if (std::abs(dx) + std::abs(dy) > k_stillPx)
m_stillSamples = 0;
else
m_stillSamples += 1;
const float dt = static_cast<float>(evalMs) / 1000.0f;
if (dt > k_staleSampleSec) {
if (std::abs(dx) + std::abs(dy) > k_staleMovePx) {
m_physicsActive = true;
requestPhysics();
updateMoving();
} else {
trySettle(0.0f);
}
return; return;
} }
const float velX = const float velX = dx / dt;
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt; const float velY = dy / dt;
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
m_prevScenePos = scenePos;
const float speed = std::sqrt(velX * velX + velY * velY); const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) { if (m_retiring) {
if (speed > 5.0f) if (speed > k_moveSpeed)
m_retiring = false; m_retiring = false;
else else
return; return;
} }
if (!m_physicsActive) { if (!m_physicsActive) {
if (speed < 5.0f) return; if (speed < k_moveSpeed) return;
m_physicsActive = true; m_physicsActive = true;
requestPhysics();
} }
const float kStretchFactor = static_cast<float>(m_deformScale); const float kStretchFactor = static_cast<float>(m_deformScale);
@@ -113,7 +199,7 @@ void BlobRect2::updatePhysics() {
float target01 = 0.0f; float target01 = 0.0f;
float target11 = 1.0f; float target11 = 1.0f;
if (speed > 5.0f) { if (speed > k_moveSpeed) {
const float targetStretch = const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch); 1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch; const float targetCompress = 1.0f / targetStretch;
@@ -147,22 +233,8 @@ void BlobRect2::updatePhysics() {
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
// Start the retire as soon as the matrix is close to identity, as in v1. Waiting trySettle(speed);
// for every term to fall below kEpsilon keeps the matrix just off identity for updateMoving();
// longer, and the panel content is transformed by it, which reads as blur on
// text.
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
// The residual is already inside the rest epsilon, so the eased curve would only
// be aborted or restarted every frame by the panel creeping. Snap instead.
if (totalDelta < 0.004f && totalVel < 0.05f) {
finishAtIdentity();
return;
}
checkAtRest(speed);
} }
void BlobRect2::setTopLeftRadius(qreal r) { void BlobRect2::setTopLeftRadius(qreal r) {
@@ -338,16 +410,3 @@ void BlobRect2::excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop) {
if (self->m_group) self->m_group->markDirty(); if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged(); emit self->excludeCornersChanged();
} }
void BlobRect2::checkAtRest(float speed) {
if (m_retiring) return;
constexpr float kEpsilon = 0.002f;
const bool atRest =
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon &&
speed < 5.0f;
if (atRest) beginRetire();
}
+12 -14
View File
@@ -25,6 +25,7 @@ class BlobRect2 : public BlobShape2 {
Q_PROPERTY( Q_PROPERTY(
bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY
atRestOverrideChanged) atRestOverrideChanged)
Q_PROPERTY(bool moving READ isMoving NOTIFY movingChanged)
Q_PROPERTY( Q_PROPERTY(
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
topLeftRadiusChanged) topLeftRadiusChanged)
@@ -78,18 +79,13 @@ class BlobRect2 : public BlobShape2 {
if (m_atRestOverride == o) return; if (m_atRestOverride == o) return;
m_atRestOverride = o; m_atRestOverride = o;
emit atRestOverrideChanged(); emit atRestOverrideChanged();
updateMoving();
if (m_group) m_group->markDirty(); if (m_group) m_group->markDirty();
} }
bool isExcluded(const BlobShape2* other) const override; bool isExcluded(const BlobShape2* other) const override;
bool isCornerExcluded(const BlobShape2* other) const override; bool isCornerExcluded(const BlobShape2* other) const override;
// The shape that is pushing is the one that can shape the bar edge. A shape that is bool isMoving() const override;
// retiring still counts, because the retire curve is still moving the surface.
// atRestOverride keeps the shape in the group's data even at rest; when it is false
// the actual spring state decides, so it is not an override of the state itself.
bool isMoving() const override {
return m_atRestOverride || m_physicsActive || m_retiring;
}
void cornerRadii(float out[4]) const override; void cornerRadii(float out[4]) const override;
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; } [[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
@@ -117,6 +113,7 @@ class BlobRect2 : public BlobShape2 {
void excludeChanged(); void excludeChanged();
void excludeCornersChanged(); void excludeCornersChanged();
void atRestOverrideChanged(); void atRestOverrideChanged();
void movingChanged();
void topLeftRadiusChanged(); void topLeftRadiusChanged();
void topRightRadiusChanged(); void topRightRadiusChanged();
void bottomLeftRadiusChanged(); void bottomLeftRadiusChanged();
@@ -124,20 +121,24 @@ class BlobRect2 : public BlobShape2 {
protected: protected:
void updatePhysics() override; void updatePhysics() override;
bool needsFrame() const override;
void resetDeformMatrix() override;
private: private:
void checkAtRest(float speed); void trySettle(float speed);
void beginRetire(); void beginRetire();
void finishAtIdentity(); void finishAtIdentity();
void requestPhysics();
void updateMoving();
// Physics state
QPointF m_prevScenePos; QPointF m_prevScenePos;
QElapsedTimer m_elapsed; QElapsedTimer m_elapsed;
bool m_physicsActive = false; bool m_physicsActive = false;
bool m_hasPrevPos = false; bool m_hasPrevPos = false;
// Set from QML. Keeps the shape in the group's data while it is at rest; when it is int m_stillSamples = 0;
// false the spring state decides on its own. bool m_movingEmitted = false;
bool m_atRestOverride = false; bool m_atRestOverride = false;
bool m_retiring = false; bool m_retiring = false;
@@ -146,13 +147,10 @@ class BlobRect2 : public BlobShape2 {
float m_retire01 = 0.0f; float m_retire01 = 0.0f;
float m_retire11 = 1.0f; float m_retire11 = 1.0f;
// Symmetric 2x2 deformation matrix components (3 independent: m00, m01,
// m11) Rest state is identity: m00=1, m01=0, m11=1
float m_dm00 = 1.0f; float m_dm00 = 1.0f;
float m_dm01 = 0.0f; float m_dm01 = 0.0f;
float m_dm11 = 1.0f; float m_dm11 = 1.0f;
// Spring velocities for each component
float m_dmVel00 = 0.0f; float m_dmVel00 = 0.0f;
float m_dmVel01 = 0.0f; float m_dmVel01 = 0.0f;
float m_dmVel11 = 0.0f; float m_dmVel11 = 0.0f;
+40 -15
View File
@@ -8,6 +8,10 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
namespace {
constexpr int kMaxRects = 16;
} // namespace
static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { static float deformPadding(const QMatrix4x4& dm, float hw, float hh) {
const float dm00 = dm(0, 0), dm01 = dm(0, 1); const float dm00 = dm(0, 0), dm01 = dm(0, 1);
const float dm10 = dm(1, 0), dm11 = dm(1, 1); const float dm10 = dm(1, 0), dm11 = dm(1, 1);
@@ -45,6 +49,7 @@ BlobShape::BlobShape(QQuickItem* parent) : QQuickItem(parent) {
void BlobShape::setGroup(BlobGroup* g) { void BlobShape::setGroup(BlobGroup* g) {
if (m_group == g) return; if (m_group == g) return;
if (m_group && isComponentComplete()) unregisterFromGroup(); if (m_group && isComponentComplete()) unregisterFromGroup();
if (m_group) resetDeformMatrix();
m_group = g; m_group = g;
if (m_group && isComponentComplete()) registerWithGroup(); if (m_group && isComponentComplete()) registerWithGroup();
emit groupChanged(); emit groupChanged();
@@ -87,6 +92,8 @@ void BlobShape::geometryChange(
std::abs(m_pendingDh) > syncThreshold) { std::abs(m_pendingDh) > syncThreshold) {
m_pendingDx = 0; m_pendingDx = 0;
m_pendingDy = 0; m_pendingDy = 0;
m_pendingDw = 0;
m_pendingDh = 0;
m_group->markShapeDirty(this); m_group->markShapeDirty(this);
} }
} }
@@ -105,6 +112,12 @@ void BlobShape::updateCenteredDeformMatrix() {
} }
} }
void BlobShape::resetDeformMatrix() {
m_deformMatrix = QMatrix4x4();
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
}
void BlobShape::cornerRadii(float out[4]) const { void BlobShape::cornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f; const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto r = std::min(static_cast<float>(m_radius), maxR); const auto r = std::min(static_cast<float>(m_radius), maxR);
@@ -187,6 +200,12 @@ void BlobShape::updatePolish() {
} }
if (include) { if (include) {
if (other == this && m_cachedRects.size() >= kMaxRects) {
m_cachedRects.removeLast();
rectShapes.removeLast();
}
if (m_cachedRects.size() >= kMaxRects) continue;
if (other == this) if (other == this)
m_cachedMyIndex = static_cast<int>(m_cachedRects.size()); m_cachedMyIndex = static_cast<int>(m_cachedRects.size());
@@ -240,8 +259,7 @@ void BlobShape::updatePolish() {
m_cachedInvertedRadius = 0; m_cachedInvertedRadius = 0;
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
memset( memset(m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
auto* inv = m_group->invertedRect(); auto* inv = m_group->invertedRect();
if (inv) { if (inv) {
@@ -308,10 +326,11 @@ void BlobShape::updatePolish() {
BlobShape* const si = rectShapes[i]; BlobShape* const si = rectShapes[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; const float dcx = ri.cx + ri.offsetX, dcy = ri.cy + ri.offsetY;
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; const float cTrX = dcx + ri.hw, cTrY = dcy - ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; const float cBrX = dcx + ri.hw, cBrY = dcy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; const float cBlX = dcx - ri.hw, cBlY = dcy + ri.hh;
const float cTlX = dcx - ri.hw, cTlY = dcy - ri.hh;
for (qsizetype j = 0; cornerFill && j < rectCount; ++j) { for (qsizetype j = 0; cornerFill && j < rectCount; ++j) {
if (j == i) continue; if (j == i) continue;
@@ -319,10 +338,12 @@ void BlobShape::updatePolish() {
BlobShape* const sj = rectShapes[j]; BlobShape* const sj = rectShapes[j];
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) continue; if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) continue;
const auto& rj = m_cachedRects[j]; const auto& rj = m_cachedRects[j];
const float sdTr = cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh); const float jcx = rj.cx + rj.offsetX;
const float sdBr = cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh); const float jcy = rj.cy + rj.offsetY;
const float sdBl = cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh); const float sdTr = cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh);
const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh); const float sdBr = cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh);
const float sdBl = cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh);
const float sdTl = cpuSdBox(cTlX, cTlY, jcx, jcy, rj.hw, rj.hh);
fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor)); fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor));
fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor)); fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor));
fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor)); fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor));
@@ -360,10 +381,14 @@ void BlobShape::updatePolish() {
-cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh)));
} }
ri.radius[0] = std::max(ri.radius[0] * fTr, minR); ri.radius[0] = ri.radius[0] > 0.0f ? std::max(ri.radius[0] * fTr, minR)
ri.radius[1] = std::max(ri.radius[1] * fBr, minR); : 0.0f;
ri.radius[2] = std::max(ri.radius[2] * fBl, minR); ri.radius[1] = ri.radius[1] > 0.0f ? std::max(ri.radius[1] * fBr, minR)
ri.radius[3] = std::max(ri.radius[3] * fTl, minR); : 0.0f;
ri.radius[2] = ri.radius[2] > 0.0f ? std::max(ri.radius[2] * fBl, minR)
: 0.0f;
ri.radius[3] = ri.radius[3] > 0.0f ? std::max(ri.radius[3] * fTl, minR)
: 0.0f;
} }
} }
@@ -428,7 +453,7 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
sizeof(m_cachedInvertedOuterRadii)); sizeof(m_cachedInvertedOuterRadii));
const int count = const int count =
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16))); static_cast<int>(qMin(m_cachedRects.size(), qsizetype(kMaxRects)));
material->m_rectCount = count; material->m_rectCount = count;
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
material->m_rects[i] = m_cachedRects[i]; material->m_rects[i] = m_cachedRects[i];
+4 -4
View File
@@ -61,9 +61,6 @@ class BlobShape : public QQuickItem {
virtual void cornerRadii(float out[4]) const; 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 { virtual void outerCornerRadii(float out[4]) const {
out[0] = 0; out[0] = 0;
out[1] = 0; out[1] = 0;
@@ -73,16 +70,19 @@ class BlobShape : public QQuickItem {
virtual void updatePhysics() {} virtual void updatePhysics() {}
virtual bool needsFrame() const { return false; }
virtual void registerWithGroup(); virtual void registerWithGroup();
virtual void unregisterFromGroup(); virtual void unregisterFromGroup();
void updateCenteredDeformMatrix(); void updateCenteredDeformMatrix();
virtual void resetDeformMatrix();
BlobGroup* m_group = nullptr; BlobGroup* m_group = nullptr;
qreal m_radius = 0; qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default QMatrix4x4 m_deformMatrix; // identity by default
QMatrix4x4 m_centeredDeformMatrix; QMatrix4x4 m_centeredDeformMatrix;
// Cached data from updatePolish
float m_cachedPaddedX = 0; float m_cachedPaddedX = 0;
float m_cachedPaddedY = 0; float m_cachedPaddedY = 0;
float m_cachedPaddedW = 0; float m_cachedPaddedW = 0;
+7 -1
View File
@@ -9,6 +9,7 @@ BlobShape2::BlobShape2(QQuickItem* parent) : QQuickItem(parent) {}
void BlobShape2::setGroup(BlobGroup2* g) { void BlobShape2::setGroup(BlobGroup2* g) {
if (m_group == g) return; if (m_group == g) return;
if (m_group && isComponentComplete()) unregisterFromGroup(); if (m_group && isComponentComplete()) unregisterFromGroup();
if (m_group) resetDeformMatrix();
m_group = g; m_group = g;
if (m_group && isComponentComplete()) registerWithGroup(); if (m_group && isComponentComplete()) registerWithGroup();
emit groupChanged(); emit groupChanged();
@@ -32,7 +33,6 @@ void BlobShape2::geometryChange(
QQuickItem::geometryChange(newGeometry, oldGeometry); QQuickItem::geometryChange(newGeometry, oldGeometry);
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
// One draw covers the whole group, so any shape change is a group change.
if (m_group) m_group->markDirty(); if (m_group) m_group->markDirty();
} }
@@ -49,6 +49,12 @@ void BlobShape2::updateCenteredDeformMatrix() {
} }
} }
void BlobShape2::resetDeformMatrix() {
m_deformMatrix = QMatrix4x4();
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
}
void BlobShape2::cornerRadii(float out[4]) const { void BlobShape2::cornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f; const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto r = std::min(static_cast<float>(m_radius), maxR); const auto r = std::min(static_cast<float>(m_radius), maxR);
+11 -9
View File
@@ -7,12 +7,6 @@
class BlobGroup2; class BlobGroup2;
// Data-only shape.
//
// The group draws a single node for the whole group, so a shape here contributes
// only what the shader needs: position, radii, exclusion, physics. A shape does
// not create a render node, so no pixel can be evaluated by one shape and missed
// by another.
class BlobShape2 : public QQuickItem { class BlobShape2 : public QQuickItem {
Q_OBJECT Q_OBJECT
Q_PROPERTY(BlobGroup2* group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(BlobGroup2* group READ group WRITE setGroup NOTIFY groupChanged)
@@ -56,8 +50,6 @@ class BlobShape2 : public QQuickItem {
virtual bool isExcluded(const BlobShape2*) const { return false; } virtual bool isExcluded(const BlobShape2*) const { return false; }
// Whether this shape is part of the group's data. Only a pushing shape is, so a shape
// at rest has no part in the draw. BlobRect2 also honours the QML atRestOverride.
virtual bool isMoving() const { return false; } virtual bool isMoving() const { return false; }
virtual bool isCornerExcluded(const BlobShape2* /*other*/) const { virtual bool isCornerExcluded(const BlobShape2* /*other*/) const {
@@ -66,13 +58,23 @@ class BlobShape2 : public QQuickItem {
virtual void cornerRadii(float out[4]) const; virtual void cornerRadii(float out[4]) const;
// Called by the group when it rebuilds the shared data, once per frame. virtual void outerCornerRadii(float out[4]) const {
out[0] = 0;
out[1] = 0;
out[2] = 0;
out[3] = 0;
}
virtual void updatePhysics() {} virtual void updatePhysics() {}
virtual bool needsFrame() const { return false; }
virtual void registerWithGroup(); virtual void registerWithGroup();
virtual void unregisterFromGroup(); virtual void unregisterFromGroup();
void updateCenteredDeformMatrix(); void updateCenteredDeformMatrix();
virtual void resetDeformMatrix();
BlobGroup2* m_group = nullptr; BlobGroup2* m_group = nullptr;
qreal m_radius = 0; qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default QMatrix4x4 m_deformMatrix; // identity by default
+30 -89
View File
@@ -30,7 +30,6 @@ float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
} }
float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) {
// r = (topRight, bottomRight, bottomLeft, topLeft)
p -= center; p -= center;
r.xy = (p.x > 0.0) ? r.xy : r.wz; r.xy = (p.x > 0.0) ? r.xy : r.wz;
r.x = (p.y > 0.0) ? r.y : r.x; r.x = (p.y > 0.0) ? r.y : r.x;
@@ -44,7 +43,6 @@ float sdBox(vec2 p, vec2 center, vec2 halfSize) {
} }
float smin(float a, float b, float k) { float smin(float a, float b, float k) {
// Cubic smooth min (C2 continuous — no curvature kinks at blend boundary)
float h = max(k - abs(a - b), 0.0) / k; float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * h * k * (1.0 / 6.0); return min(a, b) - h * h * h * k * (1.0 / 6.0);
} }
@@ -55,9 +53,6 @@ float smax(float a, float b, float k) {
} }
float smaxSharpA(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 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 h = max(k - abs(a - b), 0.0) / k;
float blend = h * h * h * k * (1.0 / 6.0); float blend = h * h * h * k * (1.0 / 6.0);
blend *= smoothstep(0.0, k * 0.5, -a); blend *= smoothstep(0.0, k * 0.5, -a);
@@ -67,10 +62,6 @@ float smaxSharpA(float a, float b, float k) {
void main() { void main() {
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
// Phase 1: compute per-rect SDF, and track which rect owns the pixel for the
// per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner:
// myIndex stays -2, so the ownership test is skipped and this pass writes every
// pixel the quad covers. Excluded pairs still need the pairwise pass below.
float dArr[16]; float dArr[16];
int owner = -2; int owner = -2;
float minDist = 1e10; float minDist = 1e10;
@@ -84,12 +75,8 @@ void main() {
vec4 radii = vec4 radii =
rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
// Offset center for asymmetric deformation
vec2 center = rect.xy + props.yz; vec2 center = rect.xy + props.yz;
// AABB early-out: skip rects far from this pixel. The reach has to include the
// shadow, otherwise a rect that is only inside the shadow range would be
// skipped and its distance could not be used.
vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius); vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius);
if (abs(pixel.x - center.x) > extent.x || if (abs(pixel.x - center.x) > extent.x ||
abs(pixel.y - center.y) > extent.y) { abs(pixel.y - center.y) > extent.y) {
@@ -97,19 +84,13 @@ void main() {
continue; continue;
} }
// Apply pre-computed inverse deformation to the evaluation point
mat2 invDeform = mat2(invDm.xy, invDm.zw); mat2 invDeform = mat2(invDm.xy, invDm.zw);
vec2 transformedPixel = center + invDeform * (pixel - center); vec2 transformedPixel = center + invDeform * (pixel - center);
// Use pre-computed effective per-corner radii
float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii);
// Use pre-computed minimum eigenvalue for SDF correction
d *= max(props.w, 0.01); d *= max(props.w, 0.01);
// Scale SDF on the axis facing a nearby border to narrow the smin blend
// zone in that direction only, without reducing k (which would cause sharp
// edges).
if (hasInverted != 0) { if (hasInverted != 0) {
vec2 screenHalf = sh.xy; vec2 screenHalf = sh.xy;
@@ -122,26 +103,21 @@ void main() {
float distX1 = float distX1 =
(invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x);
// 0 = far from border, 1 = at border (max compression)
float yProx = 1.0 - min(smoothstep(0.0, smoothFactor, distY0), float yProx = 1.0 - min(smoothstep(0.0, smoothFactor, distY0),
smoothstep(0.0, smoothFactor, distY1)); smoothstep(0.0, smoothFactor, distY1));
float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0), float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0),
smoothstep(0.0, smoothFactor, distX1)); smoothstep(0.0, smoothFactor, distX1));
// Smooth axis weights: gradient-based at corners, face-based inside.
vec2 q = abs(pixel - center) - screenHalf; vec2 q = abs(pixel - center) - screenHalf;
vec2 qp = max(q, vec2(0.0)); vec2 qp = max(q, vec2(0.0));
float cornerLen = length(qp); float cornerLen = length(qp);
// Gradient direction in corner region (smooth 90-degree rotation)
float gradX = qp.x / max(cornerLen, 0.001); float gradX = qp.x / max(cornerLen, 0.001);
float gradY = qp.y / max(cornerLen, 0.001); float gradY = qp.y / max(cornerLen, 0.001);
// Smooth face weights for inside/edge (no hard branch)
float faceY = smoothstep(-4.0, 4.0, q.y - q.x); float faceY = smoothstep(-4.0, 4.0, q.y - q.x);
float faceX = 1.0 - faceY; float faceX = 1.0 - faceY;
// Blend: gradient in corner region, face-based inside
float t = smoothstep(0.0, 2.0, cornerLen); float t = smoothstep(0.0, 2.0, cornerLen);
float xWeight = mix(faceX, gradX, t); float xWeight = mix(faceX, gradX, t);
float yWeight = mix(faceY, gradY, t); float yWeight = mix(faceY, gradY, t);
@@ -158,15 +134,11 @@ void main() {
} }
} }
// Phase 2: hard-min baseline over all rects.
float mergedSdf = 1e10; float mergedSdf = 1e10;
for (int i = 0; i < rectCount; i++) { for (int i = 0; i < rectCount; i++) {
mergedSdf = min(mergedSdf, dArr[i]); mergedSdf = min(mergedSdf, dArr[i]);
} }
// Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin
// <= min, so taking the min over all non-excluded pair smins gives the
// smoothly-merged SDF.
for (int i = 0; i < rectCount; i++) { for (int i = 0; i < rectCount; i++) {
if (dArr[i] >= 1e9) if (dArr[i] >= 1e9)
continue; continue;
@@ -176,7 +148,6 @@ void main() {
continue; continue;
if ((excludeMask & (1 << j)) != 0) if ((excludeMask & (1 << j)) != 0)
continue; continue;
// smin only deviates from min within smoothFactor
if (abs(dArr[i] - dArr[j]) >= smoothFactor) if (abs(dArr[i] - dArr[j]) >= smoothFactor)
continue; continue;
mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor));
@@ -184,16 +155,12 @@ void main() {
} }
if (hasInverted != 0) { if (hasInverted != 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 = float dOuter =
sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw, sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw,
invertedOuterRadii) - 1.0; invertedOuterRadii) - 1.0;
float dInner = float dInner =
sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
// Border sinks: track the opposite rect edge, clamped to border thickness
float innerTop = invertedInner.y - invertedInner.w; float innerTop = invertedInner.y - invertedInner.w;
float innerBot = invertedInner.y + invertedInner.w; float innerBot = invertedInner.y + invertedInner.w;
float innerLeft = invertedInner.x - invertedInner.z; float innerLeft = invertedInner.x - invertedInner.z;
@@ -203,40 +170,6 @@ void main() {
float outerLeft = invertedOuter.x - invertedOuter.z; float outerLeft = invertedOuter.x - invertedOuter.z;
float outerRight = invertedOuter.x + invertedOuter.z; float outerRight = invertedOuter.x + invertedOuter.z;
float sinkValue = 0.0;
for (int i = 0; i < rectCount; i++) {
vec4 rect = rectData[i * 5];
vec4 sinkProps = rectData[i * 5 + 1];
vec2 sinkSh = rectData[i * 5 + 3].xy;
// Screen-space center (with offset) and pre-computed AABB half-extents
vec2 ctr = rect.xy + sinkProps.yz;
// Delay sink to absorb smin blend depth (cubic smin max = k/6)
float preOff = smoothFactor * (1.0 / 6.0);
// Top border: track rect's BOTTOM edge, only within border thickness
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
innerTop - outerTop);
// Bottom border: track rect's TOP edge
float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0,
outerBot - innerBot);
// Left border: track rect's RIGHT edge
float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0,
innerLeft - outerLeft);
// Right border: track rect's LEFT edge
float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0,
outerRight - innerRight);
// Lateral distance from pixel to rect's extent along each edge
float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0);
float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
// Perpendicular proximity: full strength in border, fade inside inner
// area
float topZone = float topZone =
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
@@ -245,20 +178,43 @@ void main() {
float rightZone = float rightZone =
smoothstep(innerRight - smoothFactor, innerRight, pixel.x); smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
float sinkValue = 0.0;
float preOff = smoothFactor * (1.0 / 6.0);
float s = smoothFactor * 2.0; float s = smoothFactor * 2.0;
if (topZone > 0.0 || botZone > 0.0 || leftZone > 0.0 || rightZone > 0.0) {
for (int i = 0; i < rectCount; i++) {
vec4 rect = rectData[i * 5];
vec4 sinkProps = rectData[i * 5 + 1];
vec2 sinkSh = rectData[i * 5 + 3].xy;
vec2 ctr = rect.xy + sinkProps.yz;
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
innerTop - outerTop);
float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0,
outerBot - innerBot);
float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0,
innerLeft - outerLeft);
float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0,
outerRight - innerRight);
float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0);
float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone, float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
botPen * smoothstep(s, 0.0, hLat) * botZone), botPen * smoothstep(s, 0.0, hLat) * botZone),
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
rightPen * smoothstep(s, 0.0, vLat) * rightZone)); rightPen * smoothstep(s, 0.0, vLat) * rightZone));
sinkValue = max(sinkValue, sink); sinkValue = max(sinkValue, sink);
} }
}
dInner -= sinkValue; dInner -= sinkValue;
// 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 negInner = -dInner;
float dFrame = dOuter >= negInner ? dOuter float dFrame = dOuter >= negInner ? dOuter
: smaxSharpA(dOuter, negInner, smoothFactor); : smaxSharpA(dOuter, negInner, smoothFactor);
@@ -269,37 +225,22 @@ void main() {
} }
} }
// A pixel this far from every rect cannot have a value, so it is not written.
// The one-unit margin keeps the outermost pixels of the quad out of the AA: their
// derivatives are undefined (the quad neighbour is outside the primitive), which
// is what drew a line along the coverage edge.
float reach = max(smoothFactor, shadowRadius); float reach = max(smoothFactor, shadowRadius);
if (mergedSdf > reach - 1.0) float pxWidth = max(length(vec2(dFdx(mergedSdf), dFdy(mergedSdf))), 1e-5);
if (mergedSdf > reach - max(2.0 * pxWidth, 1.0))
discard; discard;
// Only the owner writes a pixel, which is what makes the per-shape path look
// like one surface: qt_Opacity is applied once per pixel instead of once per
// overlapping draw. The single group draw has no owner (myIndex == -2), so the
// test is skipped and the value below is the only value a pixel can have.
if (myIndex >= -1 && owner != myIndex) if (myIndex >= -1 && owner != myIndex)
discard; discard;
// Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad float materialAlpha = clamp(0.5 - mergedSdf / pxWidth, 0.0, 1.0);
// boundary cannot make a wide edge.
float fw = min(fwidth(mergedSdf), 1.0);
float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
// Phase 2: the shadow, as a distance falloff from the merged surface. The
// quadratic reaches zero with zero slope at its outer edge, so that edge needs no
// AA of its own. Only the single group draw writes a shadow: in the per-shape
// path overlapping draws would darken the same area twice.
float shadowAlpha = 0.0; float shadowAlpha = 0.0;
if (shadowRadius > 0.0 && myIndex == -2) { if (shadowRadius > 0.0 && myIndex == -2) {
float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0); float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0);
shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a; shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a;
} }
// Material over shadow, premultiplied.
float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha); float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha);
vec3 rgb = color.rgb * materialAlpha vec3 rgb = color.rgb * materialAlpha
+ shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha); + shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha);
Binary file not shown.
+138 -46
View File
@@ -4,6 +4,7 @@
#include <QQuickWindow> #include <QQuickWindow>
#include <QScopeGuard> #include <QScopeGuard>
#include <QtGui/private/qfont_p.h> #include <QtGui/private/qfont_p.h>
#include <algorithm>
#include <cmath> #include <cmath>
Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts") Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts")
@@ -54,49 +55,13 @@ SnapGridLayoutItem::SnapGridLayoutItem(
int column, int column,
int rowSpan, int rowSpan,
int columnSpan, int columnSpan,
Qt::Alignment alignment, Qt::Alignment alignment)
SnapLayout* layout) : QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment) {}
: QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment)
, m_layout(layout) {}
void SnapGridLayoutItem::setGeometry(const QRectF& rect) { void SnapGridLayoutItem::setGeometry(const QRectF& rect) {
QQuickLayoutAttached* info = attachedLayoutObject(m_item, false); QQuickLayoutAttached* info = attachedLayoutObject(m_item, false);
QRectF placed = info ? rect.marginsRemoved(info->effectiveQMargins()) engineRect = info ? rect.marginsRemoved(info->effectiveQMargins()) : rect;
: rect; placed = true;
const QQuickWindow* win = m_item->window();
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
if (dpr > 0.0) {
const QPointF origin = absoluteOrigin(m_layout);
const qreal left = (origin.x() + placed.x()) * dpr;
const qreal top = (origin.y() + placed.y()) * dpr;
const qreal right = (origin.x() + placed.right()) * dpr;
const qreal bottom = (origin.y() + placed.bottom()) * dpr;
const qint64 nearX = std::llround(left);
const qint64 nearY = std::llround(top);
const qint64 farX = std::llround(right);
const qint64 farY = std::llround(bottom);
placed = QRectF(
qreal(nearX) / dpr - origin.x(),
qreal(nearY) / dpr - origin.y(),
qreal(farX - nearX) / dpr,
qreal(farY - nearY) / dpr);
}
const QSizeF oldSize(m_item->width(), m_item->height());
const QSizeF newSize = placed.size();
m_item->setPosition(placed.topLeft());
if (newSize == oldSize) {
if (QQuickLayout* lay = qobject_cast<QQuickLayout*>(m_item)) {
if (lay->invalidatedArrangement()) lay->rearrange(newSize);
}
} else {
m_item->setSize(newSize);
}
} }
void SnapLayoutPrivate::mirrorChange() { void SnapLayoutPrivate::mirrorChange() {
@@ -119,6 +84,7 @@ SnapLayout::~SnapLayout() {
void SnapLayout::componentComplete() { void SnapLayout::componentComplete() {
QQuickLayout::componentComplete(); QQuickLayout::componentComplete();
applySpacing();
invalidate(); invalidate();
ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints); ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints);
@@ -128,9 +94,13 @@ void SnapLayout::componentComplete() {
void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) { void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) {
QQuickLayout::itemChange(change, value); QQuickLayout::itemChange(change, value);
if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) if (change == ItemChildRemovedChange && value.item)
d_func()->childSizes.remove(value.item);
if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) {
applySpacing();
invalidate(); invalidate();
} }
}
QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const { QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
Q_D(const SnapLayout); Q_D(const SnapLayout);
@@ -169,16 +139,33 @@ Qt::LayoutDirection SnapLayout::effectiveLayoutDirection() const {
qreal SnapLayout::spacing() const { qreal SnapLayout::spacing() const {
Q_D(const SnapLayout); Q_D(const SnapLayout);
return d->engine.spacing(d->orientation, d->styleInfo); if (!std::isnan(d->spacingRequest)) return d->spacingRequest;
return d->styleInfo->spacing(d->orientation);
}
void SnapLayout::applySpacing() {
Q_D(SnapLayout);
if (!isReady()) return;
const QQuickWindow* win = window();
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
qreal space = d->spacingRequest;
if (std::isnan(space)) space = d->styleInfo->spacing(d->orientation);
if (dpr > 0.0) space = qreal(std::llround(space * dpr)) / dpr;
d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical);
} }
void SnapLayout::setSpacing(qreal space) { void SnapLayout::setSpacing(qreal space) {
Q_D(SnapLayout); Q_D(SnapLayout);
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wfloat-equal"
if (std::isnan(space) || spacing() == space) return; if (std::isnan(space) || d->spacingRequest == space) return;
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical); d->spacingRequest = space;
applySpacing();
invalidate(); invalidate();
emit spacingChanged(); emit spacingChanged();
} }
@@ -242,6 +229,14 @@ void SnapLayout::updateLayoutItems() {
Q_D(SnapLayout); Q_D(SnapLayout);
if (!isReady()) return; if (!isReady()) return;
const auto items = childItems();
for (auto entry = d->childSizes.begin(); entry != d->childSizes.end();) {
if (items.contains(entry.key()))
++entry;
else
entry = d->childSizes.erase(entry);
}
d->engine.deleteItems(); d->engine.deleteItems();
insertLayoutItems(); insertLayoutItems();
} }
@@ -286,8 +281,8 @@ void SnapLayout::insertLayoutItems() {
int gridRow = 0; int gridRow = 0;
int gridColumn = index; int gridColumn = index;
if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn); if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn);
QGridLayoutItem* layoutItem = new SnapGridLayoutItem( QGridLayoutItem* layoutItem =
child, gridRow, gridColumn, 1, 1, alignment, this); new SnapGridLayoutItem(child, gridRow, gridColumn, 1, 1, alignment);
if (hStretch >= 0) { if (hStretch >= 0) {
if (!fillWidth) if (!fillWidth)
@@ -315,6 +310,7 @@ void SnapLayout::removeGridItem(QGridLayoutItem* gridItem) {
void SnapLayout::itemDestroyed(QQuickItem* item) { void SnapLayout::itemDestroyed(QQuickItem* item) {
if (!isReady()) return; if (!isReady()) return;
Q_D(SnapLayout); Q_D(SnapLayout);
d->childSizes.remove(item);
if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) { if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) {
removeGridItem(gridItem); removeGridItem(gridItem);
delete gridItem; delete gridItem;
@@ -381,6 +377,98 @@ void SnapLayout::clearAncestorListeners() {
d->ancestorListeners.clear(); d->ancestorListeners.clear();
} }
void SnapLayout::arrangeChildren() {
Q_D(SnapLayout);
const QQuickWindow* win = window();
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
if (dpr <= 0.0) return;
const bool horiz = d->orientation == Qt::Horizontal;
const QPointF origin = absoluteOrigin(this);
const qint64 originXDev = std::llround(origin.x() * dpr);
const qint64 originYDev = std::llround(origin.y() * dpr);
qreal prevEndExact = 0.0;
qint64 prevNearDev = 0;
qint64 prevSizeDev = 0;
bool started = false;
for (int i = 0; i < d->engine.itemCount(); ++i) {
SnapGridLayoutItem* gridItem =
static_cast<SnapGridLayoutItem*>(d->engine.itemAt(i));
if (!gridItem->placed) continue;
QQuickItem* child = gridItem->layoutItem();
const QRectF& cell = gridItem->engineRect;
const qreal mainExact = (horiz ? cell.x() : cell.y()) * dpr;
const qreal mainSizeExact =
(horiz ? cell.width() : cell.height()) * dpr;
const qreal crossExact = (horiz ? cell.y() : cell.x()) * dpr;
const qint64 mainSizeDev = std::llround(mainSizeExact);
const qint64 crossSizeDev =
std::llround((horiz ? cell.height() : cell.width()) * dpr);
const qint64 engineWidth = horiz ? mainSizeDev : crossSizeDev;
const qint64 engineHeight = horiz ? crossSizeDev : mainSizeDev;
qint64 mainNear = 0;
if (!started) {
mainNear = std::llround(mainExact);
started = true;
} else {
mainNear = prevNearDev + prevSizeDev +
std::llround(mainExact - prevEndExact);
}
const qint64 mainFarDev = std::llround(mainExact + mainSizeExact);
const auto stored = d->childSizes.constFind(child);
qint64 width = engineWidth;
qint64 height = engineHeight;
if (horiz)
width = std::max(mainFarDev - mainNear, qint64(0));
else
height = std::max(mainFarDev - mainNear, qint64(0));
if (stored != d->childSizes.constEnd() &&
stored.value().engineWidth == engineWidth &&
stored.value().engineHeight == engineHeight &&
std::llround(child->width() * dpr) == stored.value().width &&
std::llround(child->height() * dpr) == stored.value().height) {
width = stored.value().width;
height = stored.value().height;
} else {
d->childSizes[child] = SnapLayoutPrivate::ChildSize{
engineWidth, engineHeight, width, height};
}
prevEndExact = mainExact + mainSizeExact;
prevNearDev = mainNear;
prevSizeDev = horiz ? width : height;
const qint64 crossNear = std::llround(crossExact);
const qint64 nearX = originXDev + (horiz ? mainNear : crossNear);
const qint64 nearY = originYDev + (horiz ? crossNear : mainNear);
const QRectF snapped(
qreal(nearX) / dpr - origin.x(),
qreal(nearY) / dpr - origin.y(),
qreal(width) / dpr,
qreal(height) / dpr);
const QSizeF oldSize(child->width(), child->height());
const QSizeF newSize = snapped.size();
child->setPosition(snapped.topLeft());
if (newSize == oldSize) {
if (QQuickLayout* lay = qobject_cast<QQuickLayout*>(child)) {
if (lay->invalidatedArrangement()) lay->rearrange(newSize);
}
} else {
child->setSize(newSize);
}
}
}
void SnapLayout::rearrange(const QSizeF& size) { void SnapLayout::rearrange(const QSizeF& size) {
Q_D(SnapLayout); Q_D(SnapLayout);
if (!isReady() || !size.isValid()) return; if (!isReady() || !size.isValid()) return;
@@ -408,7 +496,11 @@ void SnapLayout::rearrange(const QSizeF& size) {
} }
QQuickLayout::rearrange(size); QQuickLayout::rearrange(size);
for (int i = 0; i < d->engine.itemCount(); ++i)
static_cast<SnapGridLayoutItem*>(d->engine.itemAt(i))->placed = false;
d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo); d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo);
arrangeChildren();
d->rearranging = false; d->rearranging = false;
for (auto childItem : std::as_const(d->invalidateAfterRearrange)) for (auto childItem : std::as_const(d->invalidateAfterRearrange))
+33 -5
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <QHash>
#include <QPointer> #include <QPointer>
#include <QtMath>
#include <QtGui/private/qabstractlayoutstyleinfo_p.h> #include <QtGui/private/qabstractlayoutstyleinfo_p.h>
#include <QtGui/private/qgridlayoutengine_p.h> #include <QtGui/private/qgridlayoutengine_p.h>
#include <QtQuickLayouts/private/qquickgridlayoutengine_p.h> #include <QtQuickLayouts/private/qquickgridlayoutengine_p.h>
@@ -20,6 +22,20 @@ class SnapLayoutStyleInfo : public QAbstractLayoutStyleInfo {
bool hasChangedCore() const override; bool hasChangedCore() const override;
}; };
class SnapGridLayoutEngine : public QGridLayoutEngine {
public:
SnapGridLayoutEngine() : QGridLayoutEngine(Qt::AlignVCenter, false) {}
QQuickGridLayoutItem* findLayoutItem(QQuickItem* item) const {
for (int i = int(q_items.size()) - 1; i >= 0; --i) {
QQuickGridLayoutItem* layoutItem =
static_cast<QQuickGridLayoutItem*>(q_items.at(i));
if (layoutItem->layoutItem() == item) return layoutItem;
}
return nullptr;
}
};
class SnapGridLayoutItem : public QQuickGridLayoutItem { class SnapGridLayoutItem : public QQuickGridLayoutItem {
public: public:
SnapGridLayoutItem( SnapGridLayoutItem(
@@ -28,13 +44,12 @@ class SnapGridLayoutItem : public QQuickGridLayoutItem {
int column, int column,
int rowSpan, int rowSpan,
int columnSpan, int columnSpan,
Qt::Alignment alignment, Qt::Alignment alignment);
SnapLayout* layout);
void setGeometry(const QRectF& rect) override; void setGeometry(const QRectF& rect) override;
private: QRectF engineRect;
SnapLayout* m_layout; bool placed = false;
}; };
class SnapLayout : public QQuickLayout { class SnapLayout : public QQuickLayout {
@@ -101,6 +116,10 @@ class SnapLayout : public QQuickLayout {
void insertLayoutItems(); void insertLayoutItems();
void removeGridItem(QGridLayoutItem* gridItem); void removeGridItem(QGridLayoutItem* gridItem);
void arrangeChildren();
void applySpacing();
void updateAncestorListeners(); void updateAncestorListeners();
void clearAncestorListeners(); void clearAncestorListeners();
}; };
@@ -113,10 +132,11 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate {
void mirrorChange() override; void mirrorChange() override;
QQuickGridLayoutEngine engine; SnapGridLayoutEngine engine;
SnapLayoutStyleInfo* styleInfo = nullptr; SnapLayoutStyleInfo* styleInfo = nullptr;
Qt::Orientation orientation = Qt::Horizontal; Qt::Orientation orientation = Qt::Horizontal;
Qt::LayoutDirection layoutDirection = Qt::LeftToRight; Qt::LayoutDirection layoutDirection = Qt::LeftToRight;
qreal spacingRequest = qQNaN();
bool rearranging = false; bool rearranging = false;
int recurRearrangeCounter = 0; int recurRearrangeCounter = 0;
QList<QQuickItem*> invalidateAfterRearrange; QList<QQuickItem*> invalidateAfterRearrange;
@@ -124,6 +144,14 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate {
QVector<QPointer<QQuickItem>> ancestorListeners; QVector<QPointer<QQuickItem>> ancestorListeners;
qint64 originResidualX = -1; qint64 originResidualX = -1;
qint64 originResidualY = -1; qint64 originResidualY = -1;
struct ChildSize {
qint64 engineWidth = 0;
qint64 engineHeight = 0;
qint64 width = 0;
qint64 height = 0;
};
QHash<QQuickItem*, ChildSize> childSizes;
}; };
} // namespace ZShell::components } // namespace ZShell::components
+6
View File
@@ -29,3 +29,9 @@ qml_module(ZShell-internal
PkgConfig::GL PkgConfig::GL
ZShell-config ZShell-config
) )
qt_add_custom_brush_shaders(ZShell-internal "internal_brush_shaders"
PREFIX "/"
FILES
shaders/strokeholemask.frag
)
@@ -0,0 +1,22 @@
#version 440
QC_INCLUDE "customfrag.glsl"
float sdRoundRect(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + vec2(r);
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
}
void main() {
float keep = 1.0;
if (data4.x > 0.5) {
vec2 p = fragCoord - data1.xy;
float feather = max(data3.y, 0.0001);
keep = smoothstep(-feather, feather, sdRoundRect(p, data1.zw, data3.x));
}
float a = keep * data2.w * globalAlpha * antialiasingAlpha();
fragColor = vec4(data2.rgb * a, a);
}
+37 -10
View File
@@ -71,11 +71,23 @@ static QCanvasPath buildStrokePath(
void StrokeCanvasItem::undoLastGroup() { void StrokeCanvasItem::undoLastGroup() {
if (m_strokes.isEmpty()) return; if (m_strokes.isEmpty()) return;
const int lastGroup = m_strokes.last().groupId; const int lastGroup = m_strokes.last().groupId;
QVector<Stroke> group;
for (int i = static_cast<int>(m_strokes.size()) - 1; for (int i = static_cast<int>(m_strokes.size()) - 1;
i >= 0 && m_strokes[i].groupId == lastGroup; i >= 0 && m_strokes[i].groupId == lastGroup;
--i) --i)
m_strokes.remove(i); group.prepend(m_strokes.takeAt(i));
m_redoStack.append(group);
update();
}
void StrokeCanvasItem::redo() {
if (m_redoStack.isEmpty()) return;
m_strokes.append(m_redoStack.takeLast());
update(); update();
} }
@@ -106,19 +118,14 @@ void StrokeCanvasItem::setHoverPoint(const QPointF& point) {
emit hoverPointChanged(); emit hoverPointChanged();
} }
void StrokeCanvasItem::showHover(qreal x, qreal y) { void StrokeCanvasItem::showHover() {
const QPointF newPoint{x, y}; if (m_hoverVisible) return;
const bool pointChanged = (m_hoverPoint != newPoint);
const bool visibleChanged = !m_hoverVisible;
if (pointChanged || visibleChanged) {
m_hoverVisible = true; m_hoverVisible = true;
m_hoverPoint = newPoint;
if (pointChanged) emit hoverPointChanged();
if (visibleChanged) emit hoverVisibleChanged();
update(); update();
}
emit hoverVisibleChanged();
} }
void StrokeCanvasItem::hideHover() { void StrokeCanvasItem::hideHover() {
@@ -139,6 +146,24 @@ void StrokeCanvasItem::setPenWidth(qreal width) {
emit penWidthChanged(); emit penWidthChanged();
} }
void StrokeCanvasItem::setHole(const QRectF& hole) {
if (m_hole == hole) return;
m_hole = hole;
update();
emit holeChanged();
}
void StrokeCanvasItem::setHoleRadius(qreal radius) {
if (qFuzzyCompare(m_holeRadius, radius)) return;
m_holeRadius = radius;
update();
emit holeRadiusChanged();
}
void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) { void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) {
m_isDrawing = true; m_isDrawing = true;
m_currentStroke.points.clear(); m_currentStroke.points.clear();
@@ -199,6 +224,7 @@ void StrokeCanvasItem::endStroke() {
m_currentStroke.points.clear(); m_currentStroke.points.clear();
m_strokes.append(m_currentStroke); m_strokes.append(m_currentStroke);
m_currentStroke = {}; m_currentStroke = {};
m_redoStack.clear();
update(); update();
} }
@@ -206,6 +232,7 @@ void StrokeCanvasItem::endStroke() {
void StrokeCanvasItem::clear() { void StrokeCanvasItem::clear() {
m_strokes.clear(); m_strokes.clear();
m_currentStroke = {}; m_currentStroke = {};
m_redoStack.clear();
update(); update();
} }
+16 -1
View File
@@ -3,6 +3,7 @@
#include <QCanvasPainterItem> #include <QCanvasPainterItem>
#include <QColor> #include <QColor>
#include <QPointF> #include <QPointF>
#include <QRectF>
#include <QVector> #include <QVector>
#include <qcolor.h> #include <qcolor.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
@@ -27,6 +28,10 @@ class StrokeCanvasItem : public QCanvasPainterItem {
hoverPointChanged) hoverPointChanged)
Q_PROPERTY( Q_PROPERTY(
qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged) qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
Q_PROPERTY(QRectF hole READ hole WRITE setHole NOTIFY holeChanged)
Q_PROPERTY(
qreal holeRadius READ holeRadius WRITE setHoleRadius NOTIFY
holeRadiusChanged)
public: public:
explicit StrokeCanvasItem(QQuickItem* parent = nullptr); explicit StrokeCanvasItem(QQuickItem* parent = nullptr);
@@ -37,15 +42,20 @@ class StrokeCanvasItem : public QCanvasPainterItem {
void setHoverVisible(bool visible); void setHoverVisible(bool visible);
void setHoverPoint(const QPointF& point); void setHoverPoint(const QPointF& point);
Q_INVOKABLE void showHover(qreal x, qreal y); Q_INVOKABLE void showHover();
Q_INVOKABLE void hideHover(); Q_INVOKABLE void hideHover();
Q_INVOKABLE void undoLastGroup(); Q_INVOKABLE void undoLastGroup();
Q_INVOKABLE void redo();
[[nodiscard]] QColor penColor() const { return m_penColor; } [[nodiscard]] QColor penColor() const { return m_penColor; }
[[nodiscard]] qreal penWidth() const { return m_penWidth; } [[nodiscard]] qreal penWidth() const { return m_penWidth; }
[[nodiscard]] QRectF hole() const { return m_hole; }
[[nodiscard]] qreal holeRadius() const { return m_holeRadius; }
void setPenColor(const QColor& color); void setPenColor(const QColor& color);
void setPenWidth(qreal width); void setPenWidth(qreal width);
void setHole(const QRectF& hole);
void setHoleRadius(qreal radius);
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
@@ -56,6 +66,8 @@ class StrokeCanvasItem : public QCanvasPainterItem {
signals: signals:
void penColorChanged(); void penColorChanged();
void penWidthChanged(); void penWidthChanged();
void holeChanged();
void holeRadiusChanged();
void hoverVisibleChanged(); void hoverVisibleChanged();
void hoverPointChanged(); void hoverPointChanged();
@@ -72,9 +84,12 @@ class StrokeCanvasItem : public QCanvasPainterItem {
qreal m_penWidth = 4.0; qreal m_penWidth = 4.0;
bool m_isDrawing = false; bool m_isDrawing = false;
QRectF m_hole;
qreal m_holeRadius = 0.0;
int m_nextGroupId = 0; int m_nextGroupId = 0;
QVector<Stroke> m_strokes; QVector<Stroke> m_strokes;
Stroke m_currentStroke; Stroke m_currentStroke;
QVector<QVector<Stroke>> m_redoStack;
}; };
}; // namespace ZShell::internal }; // namespace ZShell::internal
+104 -11
View File
@@ -1,11 +1,98 @@
#include "strokecanvasrenderer.hpp" #include "strokecanvasrenderer.hpp"
#include "strokecanvasitem.hpp" #include "strokecanvasitem.hpp"
#include <QCanvasPainter>
#include <QQuickWindow>
#include <QVector4D>
#include <qcolor.h> #include <qcolor.h>
#include <QSet> #include <QSet>
namespace ZShell::internal { namespace ZShell::internal {
static void drawStroke( using Qt::StringLiterals::operator""_s;
static const QString kStrokeHoleShader = u":/shaders/strokeholemask.frag.qsb"_s;
namespace {
struct HoleUniforms {
QVector4D data1; // center.xy, halfSize.xy
QVector4D data3; // x: radius, y: feather in logical px
QVector4D data4; // x: hole enabled
};
static HoleUniforms holeUniforms(
const QRectF& hole, qreal radius, qreal devicePixelRatio) {
const bool active = hole.isValid() && hole.width() > 0 && hole.height() > 0;
if (!active) return {QVector4D(), QVector4D(), QVector4D()};
const QPointF center = hole.center();
const QPointF half(hole.width() * 0.5, hole.height() * 0.5);
const qreal maxRadius = qMin(half.x(), half.y());
return {
QVector4D(
static_cast<float>(center.x()),
static_cast<float>(center.y()),
static_cast<float>(half.x()),
static_cast<float>(half.y())),
QVector4D(
static_cast<float>(qMin(radius, maxRadius)),
static_cast<float>(1.0 / devicePixelRatio),
0.0f,
0.0f),
QVector4D(1.0, 0.0, 0.0, 0.0),
};
}
static void applyHole(QCanvasCustomBrush& brush, const HoleUniforms& u) {
brush.setData1(u.data1);
brush.setData3(u.data3);
brush.setData4(u.data4);
}
}; // namespace
void StrokeCanvasRenderer::updateBrushHole() {
const HoleUniforms uniforms =
holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio);
if (uniforms.data1 == m_lastHoleData1 &&
uniforms.data3 == m_lastHoleData3 && uniforms.data4 == m_lastHoleData4)
return;
m_lastHoleData1 = uniforms.data1;
m_lastHoleData3 = uniforms.data3;
m_lastHoleData4 = uniforms.data4;
++m_holeToken;
for (auto& entry : m_brushes) {
if (entry.holeToken == m_holeToken) continue;
applyHole(entry.brush, uniforms);
entry.holeToken = m_holeToken;
}
}
QCanvasCustomBrush& StrokeCanvasRenderer::brushFor(const QColor& color) {
const QRgb key = color.rgba();
if (auto it = m_brushes.find(key); it != m_brushes.end()) return it->brush;
const HoleUniforms uniforms =
holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio);
BrushEntry entry;
entry.brush.setFragmentShader(kStrokeHoleShader);
applyHole(entry.brush, uniforms);
entry.brush.setData2(
QVector4D(color.redF(), color.greenF(), color.blueF(), color.alphaF()));
entry.holeToken = m_holeToken;
return m_brushes.insert(key, entry)->brush;
}
void StrokeCanvasRenderer::drawStroke(
QCanvasPainter* painter, QCanvasPainter* painter,
const QVector<QPointF>& points, const QVector<QPointF>& points,
const QColor& color, const QColor& color,
@@ -13,8 +100,10 @@ static void drawStroke(
bool polyline) { bool polyline) {
if (points.isEmpty()) return; if (points.isEmpty()) return;
painter->setStrokeStyle(color); QCanvasCustomBrush& brush = brushFor(color);
painter->setFillStyle(color);
painter->setStrokeStyle(brush);
painter->setFillStyle(brush);
painter->setLineWidth(width); painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round); painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -65,7 +154,7 @@ static void drawStroke(
painter->stroke(); painter->stroke();
} }
static void drawHoverCursor( void StrokeCanvasRenderer::drawHoverCursor(
QCanvasPainter* painter, QCanvasPainter* painter,
const QPointF& point, const QPointF& point,
qreal penWidth, qreal penWidth,
@@ -74,7 +163,7 @@ static void drawHoverCursor(
const qreal radius = penWidth * 0.5; const qreal radius = penWidth * 0.5;
if (isDrawing) { if (isDrawing) {
painter->setFillStyle(penColor); painter->setFillStyle(brushFor(penColor));
painter->beginPath(); painter->beginPath();
painter->circle(point, radius); painter->circle(point, radius);
painter->fill(); painter->fill();
@@ -85,7 +174,7 @@ static void drawHoverCursor(
const bool useDashes = penWidth > 10.0; const bool useDashes = penWidth > 10.0;
auto drawOutline = [&](const QColor& color, qreal width) { auto drawOutline = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color); painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width); painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -117,7 +206,7 @@ static void drawHoverCursor(
}; };
auto drawCrosshair = [&](const QColor& color, qreal width) { auto drawCrosshair = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color); painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width); painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -148,8 +237,11 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
m_penColor = canvas->m_penColor; m_penColor = canvas->m_penColor;
m_penWidth = canvas->m_penWidth; m_penWidth = canvas->m_penWidth;
m_hole = canvas->m_hole;
m_holeRadius = canvas->m_holeRadius;
m_devicePixelRatio =
canvas->window() ? canvas->window()->effectiveDevicePixelRatio() : 1.0;
// Detect removed strokes: present in renderer but no longer in canvas
QSet<int> canvasGroupIds; QSet<int> canvasGroupIds;
for (const auto& stroke : canvas->m_strokes) for (const auto& stroke : canvas->m_strokes)
canvasGroupIds.insert(stroke.groupId); canvasGroupIds.insert(stroke.groupId);
@@ -162,7 +254,6 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
} }
} }
// Detect added strokes: present in canvas but not yet in renderer
QSet<int> rendererGroupIds; QSet<int> rendererGroupIds;
for (const auto& stroke : m_strokes) for (const auto& stroke : m_strokes)
rendererGroupIds.insert(stroke.groupId); rendererGroupIds.insert(stroke.groupId);
@@ -180,6 +271,8 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
} }
void StrokeCanvasRenderer::paint(QCanvasPainter* painter) { void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
updateBrushHole();
for (int id : m_pendingGroupRemovals) for (int id : m_pendingGroupRemovals)
painter->removePathGroup(id); painter->removePathGroup(id);
m_pendingGroupRemovals.clear(); m_pendingGroupRemovals.clear();
@@ -187,8 +280,8 @@ void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
painter->clearRect(0, 0, width(), height()); painter->clearRect(0, 0, width(), height());
for (const auto& stroke : m_strokes) { for (const auto& stroke : m_strokes) {
painter->setStrokeStyle(stroke.color); painter->setStrokeStyle(brushFor(stroke.color));
painter->setFillStyle(stroke.color); painter->setFillStyle(brushFor(stroke.color));
painter->setLineWidth(stroke.width); painter->setLineWidth(stroke.width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round); painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -1,26 +1,58 @@
#pragma once #pragma once
#include <QCanvasCustomBrush>
#include <QCanvasPainterItemRenderer> #include <QCanvasPainterItemRenderer>
#include <QHash>
#include <QRectF>
#include <QVector4D>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include "stroke.hpp" #include "stroke.hpp"
namespace ZShell::internal { namespace ZShell::internal {
struct BrushEntry {
QCanvasCustomBrush brush;
qint64 holeToken = -1; // hole generation this brush was built with
};
class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer { class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer {
public: public:
void synchronizeData(QCanvasPainterItem* item) override; void synchronizeData(QCanvasPainterItem* item) override;
void paint(QCanvasPainter* painter) override; void paint(QCanvasPainter* painter) override;
private: private:
QCanvasCustomBrush& brushFor(const QColor& color);
void updateBrushHole();
void drawStroke(
QCanvasPainter* painter,
const QVector<QPointF>& points,
const QColor& color,
qreal width,
bool polyline);
void drawHoverCursor(
QCanvasPainter* painter,
const QPointF& point,
qreal penWidth,
QColor penColor,
bool isDrawing);
QColor m_penColor; QColor m_penColor;
qreal m_penWidth = 4.0; qreal m_penWidth = 4.0;
bool m_hoverVisible = false; bool m_hoverVisible = false;
QPointF m_hoverPoint; QPointF m_hoverPoint;
bool m_isDrawing = false; bool m_isDrawing = false;
QRectF m_hole;
qreal m_holeRadius = 0.0;
qreal m_devicePixelRatio = 1.0;
QVector4D m_lastHoleData1;
QVector4D m_lastHoleData3;
QVector4D m_lastHoleData4;
qint64 m_holeToken = 0;
QVector<Stroke> m_strokes; QVector<Stroke> m_strokes;
Stroke m_currentStroke; Stroke m_currentStroke;
QVector<int> m_pendingGroupRemovals; QVector<int> m_pendingGroupRemovals;
QHash<QRgb, BrushEntry> m_brushes;
}; };
}; // namespace ZShell::internal }; // namespace ZShell::internal
+6 -4
View File
@@ -545,7 +545,7 @@ def generate(
except (PermissionError, OSError, BlockingIOError): except (PermissionError, OSError, BlockingIOError):
pass pass
def smart_mode(image_path: Path) -> str: def smart_mode(image_path: Path | io.BytesIO) -> str:
is_dark = "" is_dark = ""
with Image.open(image_path) as img: with Image.open(image_path) as img:
@@ -739,7 +739,7 @@ def generate(
return rendered_outputs return rendered_outputs
def seed_from_image(image_path: Path) -> Hct: def seed_from_image(image_path: Path | io.BytesIO) -> Hct:
image = Image.open(image_path) image = Image.open(image_path)
pixel_len = image.width * image.height pixel_len = image.width * image.height
image_data = image.getdata() image_data = image.getdata()
@@ -837,11 +837,13 @@ def generate(
image_path = image_path or Path(WALL_PATH) image_path = image_path or Path(WALL_PATH)
blob = scheme_thumb_from_db(str(DB_PATH), str(image_path)) blob = scheme_thumb_from_db(str(DB_PATH), str(image_path))
def open_thumb():
if blob is not None: if blob is not None:
open_thumb = lambda: io.BytesIO(blob) return io.BytesIO(blob)
else: else:
thumb_path = generate_thumbnail(image_path, THUMB_DIR) thumb_path = generate_thumbnail(image_path, THUMB_DIR)
open_thumb = lambda: thumb_path return thumb_path
seed = seed_from_image(open_thumb()) seed = seed_from_image(open_thumb())
name = "dynamic" name = "dynamic"