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 ZShell.Internal
import qs.Components
import qs.Services
Item {
id: root
readonly property alias content: contentLoader.item
required property ScreenState screenState
property rect holeRect
property real holeRadius: 0
Loader {
id: contentLoader
@@ -18,6 +22,9 @@ Item {
anchors.fill: parent
sourceComponent: StrokeCanvas {
anchors.fill: parent
hole: root.holeRect
holeRadius: root.holeRadius
penColor: root.screenState.penColor
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();
}
}
}
+121 -198
View File
@@ -1,25 +1,33 @@
import Quickshell
import QtQuick
import ZShell.Config
import ZShell.Internal
import qs.Components
import qs.Helpers
import qs.Modules.Bar.Popouts
Item {
CustomMouseArea {
id: root
required property Drawing drawing
required property DrawingInput input
required property Item bar
required property real borderThickness
property bool dashboardShortcutActive
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 dashboardShortcutActive
property point dragStart
property bool osdShortcutActive
property bool setInitialPoint: false
property bool singleGestureTriggered: false
property bool utilitiesShortcutActive
required property ScreenState screenState
function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -56,124 +64,116 @@ Item {
return x >= panelX && x <= panelX + panel.width;
}
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent
hoverEnabled: true
DragHandler {
id: singleHandler
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
dragThreshold: 0
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;
}
onContainsMouseChanged: {
if (!containsMouse) {
if (!osdShortcutActive) {
screenState.osd = false;
panels.osd.hovered = false;
}
if (root.geometry.insetTop(root.borderThickness) > pressY && (Config.dashboard.showOnHover || !inDashboard)) {
if (dragY > 20) {
root.screenState.settings = true;
root.singleGestureTriggered = true;
} else if (dragY < -20) {
root.screenState.settings = false;
root.singleGestureTriggered = true;
}
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
popouts.hasCurrent = false;
}
if (pressY > root.screen.height - root.geometry.insetBottom(root.borderThickness) && pressX < root.screen.width / 5 && dragY < -50)
root.screenState.clipboard = true;
if (Config.bar.autoHide || bar.fullscreen)
bar.isHovered = false;
if (!Config.dock.hoverToReveal && pressY > root.screen.height - root.geometry.insetTop(root.borderThickness) && pressX > root.screen.width / 5 && !root.screenState.launcher)
if (dragY < -10) {
root.screenState.dock = true;
root.singleGestureTriggered = true;
}
if (pressX > root.screen.width - Config.bar.border && pressY < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) {
root.screenState.sidebar = true;
root.singleGestureTriggered = true;
}
if (pressX >= root.screen.width - Config.bar.border && pressY > (root.screen.height / 2) && dragX < -20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
}
if (pressX <= Config.bar.border && dragX > 20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
}
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;
PointHandler {
id: drawingHandler
if (screenState.isDrawing && !inLeftPanel(panels.drawing, x, y)) {
input.z = 2;
panels.drawing.collapse();
}
property bool setInitialPoint: false
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
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)
if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true;
onActiveChanged: {
if (!active) {
setInitialPoint = false;
root.drawing.content.endStroke();
} else {
root.panels.drawing.collapse();
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;
}
}
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) {
root.drawing.content.clear();
return;
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;
}
if (x === 0 && y === 0 && origX === 0 && origY === 0)
return;
} else {
const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!setInitialPoint) {
setInitialPoint = true;
root.drawing.content.beginStroke(origX, origY);
return;
if (!osdShortcutActive) {
screenState.osd = showOsd;
panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
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 {
@@ -184,98 +184,20 @@ Item {
running: false
onTriggered: {
if (hoverHandler.hovered)
if (root.containsMouse)
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 {
function onDashboardChanged() {
function onDashboardChanged(): void {
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) {
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.sidebar = false;
root.screenState.resources = false;
@@ -285,12 +207,15 @@ Item {
}
}
function onIsDrawingChanged() {
if (!root.screenState.isDrawing)
root.drawing.clear();
function onIsDrawingChanged(): void {
if (root.screenState.isDrawing) {
root.drawing.content.showHover();
} else {
root.drawing.content.clear();
}
}
function onLauncherChanged() {
function onLauncherChanged(): void {
if (!root.screenState.launcher) {
root.dashboardShortcutActive = false;
root.osdShortcutActive = false;
@@ -305,16 +230,13 @@ Item {
}
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.settings = false;
}
}
function onOsdChanged() {
function onOsdChanged(): void {
if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
@@ -325,7 +247,7 @@ Item {
}
}
function onResourcesChanged() {
function onResourcesChanged(): void {
if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false;
@@ -335,7 +257,7 @@ Item {
}
}
function onSettingsChanged() {
function onSettingsChanged(): void {
if (root.screenState.settings) {
root.screenState.resources = false;
root.screenState.dashboard = false;
@@ -345,9 +267,10 @@ Item {
}
}
function onSidebarChanged() {
function onSidebarChanged(): void {
if (root.screenState.sidebar) {
root.screenState.dashboard = false;
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;
}
-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
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
x: ZUtils.snap((parent.width - implicitWidth) / 2, dashboardWrapper)
clip: offsetScale > 0
implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: dashboard.implicitWidth
@@ -194,8 +196,11 @@ Item {
Settings.Wrapper {
id: settings
anchors.centerIn: parent
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
x: ZUtils.snap((parent.width - implicitWidth) / 2, settings)
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
screen: root.screen
screenState: root.screenState
+24 -2
View File
@@ -56,7 +56,7 @@ CustomWindow {
property color surfaceColor: Colors.tPalette.m3surface
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
color: "transparent"
mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
@@ -347,12 +347,31 @@ CustomWindow {
Drawing {
id: drawing
readonly property int extraWidth: drawingBg.radius * 2
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
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
anchors.fill: parent
@@ -365,6 +384,8 @@ CustomWindow {
popouts: panels.popouts
screen: root.screen
screenState: root.screenState
input: lazyLoader.item
z: 1
Panels {
id: panels
@@ -442,6 +463,7 @@ CustomWindow {
parent: root.contentItem
screen: root.screen
screenState: root.screenState
z: 4
}
}
+2 -1
View File
@@ -8,6 +8,7 @@ import ZShell.Components
import qs.Components
import qs.Modules.Bar.Components
import qs.Modules.Bar.Popouts
import ZShell
SnapLayout {
id: root
@@ -23,7 +24,7 @@ SnapLayout {
function axisCenterOf(item: Item): real {
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 {
+4 -4
View File
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import qs.Components
import ZShell.Config
import ZShell
Item {
id: root
@@ -36,7 +37,7 @@ Item {
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0)
return off + diff;
return Math.floor(Math.max(off, 0));
return ZUtils.snap(Math.max(off, 0), root);
}
y: {
if (horizontal)
@@ -46,12 +47,11 @@ Item {
const diff = parent.height - Math.floor(off + content.nonAnimHeight);
if (diff < 0)
return off + diff;
return Math.max(off, 0);
return ZUtils.snap(Math.max(off, 0), root);
}
Behavior on offsetScale {
Anim {
}
Anim {}
}
Behavior on x {
enabled: !root.horizontal || root.offsetScale < 1
+3 -2
View File
@@ -2,6 +2,7 @@ import Quickshell
import QtQuick
import qs.Components
import ZShell.Config
import ZShell
Item {
id: root
@@ -26,8 +27,8 @@ Item {
detachedMode = "";
}
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
implicitHeight: ZUtils.snap(nonAnimHeight, root)
implicitWidth: ZUtils.snap(nonAnimWidth, root)
Behavior on implicitHeight {
enabled: root.offsetScale < 1
+4 -4
View File
@@ -17,7 +17,7 @@ Item {
implicitWidth: btn.implicitWidth * 0.9
implicitHeight: btn.implicitHeight * 0.9
BlobGroup {
BlobGroup2 {
id: blobGroup
color: Colors.palette.m3surfaceContainerHighest
@@ -29,7 +29,7 @@ Item {
}
}
BlobRect {
BlobRect2 {
id: btnRect
anchors.fill: parent
@@ -42,7 +42,7 @@ Item {
}
}
BlobRect {
BlobRect2 {
id: rect
anchors.right: parent.right
@@ -143,8 +143,8 @@ Item {
]
SnapLayout {
orientation: Qt.Vertical
id: layout
orientation: Qt.Vertical
anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall
+3 -17
View File
@@ -53,24 +53,10 @@ Item {
lock: root.lock
}
SnapLayout {
NotifDock {
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 {
lock: root.lock
}
}
Layout.fillHeight: true
lock: root.lock
}
}
}
+4 -11
View File
@@ -23,7 +23,6 @@ CustomRect {
topRightRadius: Tokens.rounding.largeIncreased
SnapLayout {
orientation: Qt.Vertical
anchors.fill: parent
anchors.margins: Tokens.padding.large
@@ -52,16 +51,10 @@ CustomRect {
duration: Tokens.anim.durations.extraLarge
}
}
sourceComponent: SnapLayout {
orientation: Qt.Vertical
spacing: Tokens.spacing.largeIncreased
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
sourceComponent: CustomText {
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
}
+1 -1
View File
@@ -9,7 +9,6 @@ import ZShell.Config
import qs.Services
SnapLayout {
orientation: Qt.Vertical
id: root
property bool animateScroll: false
@@ -69,6 +68,7 @@ orientation: Qt.Vertical
}
spacing: Tokens.spacing.largeIncreased
orientation: Qt.Vertical
Component.onCompleted: applySearchAnchor()
+2 -1
View File
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import ZShell.Config
import qs.Components
import qs.Services
@@ -10,7 +11,7 @@ CustomText {
Layout.bottomMargin: Tokens.spacing.extraSmall
Layout.fillWidth: true
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
elide: Text.ElideRight
font: Tokens.font.label.medium
+6 -2
View File
@@ -18,10 +18,12 @@ PageBase {
title: qsTr("All apps")
SnapLayout {
orientation: Qt.Vertical
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
orientation: Qt.Vertical
width: root.cappedWidth
Repeater {
@@ -54,6 +56,7 @@ orientation: Qt.Vertical
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
IconImage {
@@ -63,8 +66,9 @@ orientation: Qt.Vertical
}
SnapLayout {
orientation: Qt.Vertical
Layout.fillWidth: true
orientation: Qt.Vertical
spacing: 0
CustomText {
+1
View File
@@ -6,6 +6,7 @@ find_package(Qt6 REQUIRED COMPONENTS
GuiPrivate
Quick
CanvasPainter
CanvasPainterTools
QuickPrivate
QuickLayouts
QuickLayoutsPrivate
+39 -2
View File
@@ -2,7 +2,17 @@
#include "blobinvertedrect.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() {
for (auto* shape : std::as_const(m_shapes))
@@ -12,6 +22,7 @@ BlobGroup::~BlobGroup() {
}
void BlobGroup::setSmoothing(qreal s) {
if (s < 0.5) s = 0.5;
if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s;
emit smoothingChanged();
@@ -73,7 +84,6 @@ void BlobGroup::markShapeDirty(BlobShape* source) {
source->polish();
source->update();
// Use cached padded rects to find spatial neighbors
const float pad = static_cast<float>(m_smoothing) * 2.0f;
const QRectF srcRect(
static_cast<double>(source->m_cachedPaddedX - pad),
@@ -103,6 +113,33 @@ void BlobGroup::markShapeDirty(BlobShape* source) {
void BlobGroup::ensurePhysicsUpdated() {
if (m_physicsUpdated) return;
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))
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 <qobject.h>
#include <qqmlengine.h>
#include <qtimer.h>
class BlobShape;
class BlobInvertedRect;
@@ -51,16 +52,21 @@ class BlobGroup : public QObject {
void markShapeDirty(BlobShape* source);
void ensurePhysicsUpdated();
void requestPhysics();
signals:
void smoothingChanged();
void colorChanged();
void cornerFillChanged();
private:
void pumpPhysics();
qreal m_smoothing = 32.0;
QColor m_color{0x44, 0x88, 0xff};
bool m_cornerFill = true;
QList<BlobShape*> m_shapes;
BlobInvertedRect* m_invertedRect = nullptr;
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);
}
namespace {
constexpr int k_pumpMs = 16;
} // namespace
BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents);
m_physicsPump.setInterval(k_pumpMs);
m_physicsPump.setTimerType(Qt::CoarseTimer);
connect(&m_physicsPump, &QTimer::timeout, this, &BlobGroup2::pumpPhysics);
}
BlobGroup2::~BlobGroup2() {
@@ -41,6 +48,7 @@ BlobGroup2::~BlobGroup2() {
}
void BlobGroup2::setSmoothing(qreal s) {
if (s < 0.5) s = 0.5;
if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s;
emit smoothingChanged();
@@ -101,17 +109,11 @@ void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) {
void BlobGroup2::markDirty() {
m_physicsUpdated = 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();
update();
}
void BlobGroup2::updatePolish() {
// The group is the only thing that draws, so it also drives the shapes'
// physics.
ensureDataBuilt();
}
@@ -122,32 +124,53 @@ void BlobGroup2::ensurePhysicsUpdated() {
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() {
if (m_dataBuilt) return;
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_data.rects.clear();
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);
// 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 +
static_cast<double>(m_shadowRadius);
// Group order, no pruning by proximity: the shader sees this list for every pixel. A
// 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.
const bool dropAtRest = m_invertedRect != nullptr;
QVector<BlobShape2*> entryShapes;
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 (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects)
continue;
@@ -194,7 +217,6 @@ void BlobGroup2::ensureDataBuilt() {
const int count = static_cast<int>(m_data.rects.size());
// Exclusion gates only the smooth blend.
for (int i = 0; i < count; ++i) {
int mask = 0;
BlobShape2* const si = entryShapes[i];
@@ -214,10 +236,11 @@ void BlobGroup2::ensureDataBuilt() {
BlobShape2* const si = entryShapes[i];
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 cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh;
const float dcx = ri.cx + ri.offsetX, dcy = ri.cy + ri.offsetY;
const float cTrX = dcx + ri.hw, cTrY = dcy - ri.hh;
const float cBrX = dcx + ri.hw, cBrY = dcy + ri.hh;
const float cBlX = dcx - ri.hw, cBlY = dcy + ri.hh;
const float cTlX = dcx - ri.hw, cTlY = dcy - ri.hh;
if (cornerFill) {
for (int j = 0; j < count; ++j) {
@@ -227,37 +250,41 @@ void BlobGroup2::ensureDataBuilt() {
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
continue;
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,
cornerFillFactor(
cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh),
cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
fBr = std::min(
fBr,
cornerFillFactor(
cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh),
cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
fBl = std::min(
fBl,
cornerFillFactor(
cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh),
cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
fTl = std::min(
fTl,
cornerFillFactor(
cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh),
cpuSdBox(cTlX, cTlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
}
}
ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
ri.radius[0] = ri.radius[0] > 0.0f ? std::max(ri.radius[0] * fTr, minR)
: 0.0f;
ri.radius[1] = ri.radius[1] > 0.0f ? std::max(ri.radius[1] * fBr, 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;
auto* inv = static_cast<BlobShape2*>(m_invertedRect);
@@ -297,6 +324,8 @@ void BlobGroup2::ensureDataBuilt() {
m_data.invertedInner[2] = innerHW;
m_data.invertedInner[3] = innerHH;
inv->outerCornerRadii(m_data.invertedOuterRadii);
m_bounds = m_bounds.united(QRectF(
static_cast<double>(outerCX) - static_cast<double>(outerHW) - 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_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_color = m_color;
material->m_hasInverted = m_data.hasInverted ? 1 : 0;
@@ -324,6 +352,10 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_invertedInner,
data().invertedInner,
sizeof(data().invertedInner));
memcpy(
material->m_invertedOuterRadii,
data().invertedOuterRadii,
sizeof(data().invertedOuterRadii));
material->m_shadowRadius = static_cast<float>(m_shadowRadius);
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*) {
// Data is built during polish. This only covers a render pass that happens
// without one.
ensureDataBuilt();
if (m_data.rects.isEmpty() && !m_data.hasInverted) {
@@ -364,10 +394,6 @@ QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
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();
const QPointF p0 = mapFromScene(m_bounds.topLeft());
const QPointF p1 = mapFromScene(m_bounds.bottomRight());
+6 -14
View File
@@ -9,22 +9,13 @@
#include <qquickitem.h>
#include <qrect.h>
#include <qsgnode.h>
#include <qtimer.h>
#include <qvector.h>
class BlobShape2;
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 {
// 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;
QVector<BlobRectData> rects;
@@ -32,6 +23,7 @@ struct BlobGroupData {
float invertedRadius = 0;
float invertedOuter[4] = {};
float invertedInner[4] = {};
float invertedOuterRadii[4] = {};
};
class BlobGroup2 : public QQuickItem {
@@ -91,10 +83,10 @@ class BlobGroup2 : public QQuickItem {
[[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 requestPhysics();
signals:
void smoothingChanged();
void colorChanged();
@@ -110,6 +102,7 @@ class BlobGroup2 : public QQuickItem {
void ensurePhysicsUpdated();
void ensureDataBuilt();
void fillMaterial(BlobMaterial* material);
void pumpPhysics();
qreal m_smoothing = 32.0;
QColor m_color{0x44, 0x88, 0xff};
@@ -120,9 +113,8 @@ class BlobGroup2 : public QQuickItem {
BlobInvertedRect2* m_invertedRect = nullptr;
bool m_physicsUpdated = false;
bool m_dataBuilt = false;
QTimer m_physicsPump;
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;
};
+8 -14
View File
@@ -11,28 +11,24 @@
BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {}
static void setFrameIndices(quint16* idx) {
// Top strip: 0-1-4, 1-5-4
idx[0] = 0;
idx[1] = 1;
idx[2] = 4;
idx[3] = 1;
idx[4] = 5;
idx[5] = 4;
// Right strip: 1-2-5, 2-6-5
idx[6] = 1;
idx[7] = 2;
idx[8] = 5;
idx[9] = 2;
idx[10] = 6;
idx[11] = 5;
// Bottom strip: 2-3-6, 3-7-6
idx[12] = 2;
idx[13] = 3;
idx[14] = 6;
idx[15] = 3;
idx[16] = 7;
idx[17] = 6;
// Left strip: 3-0-7, 0-4-7
idx[18] = 3;
idx[19] = 0;
idx[20] = 7;
@@ -50,17 +46,20 @@ QSGNode* BlobInvertedRect::updatePaintNode(
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 holeLeft = static_cast<float>(m_borderLeft) + inset;
const float holeTop = static_cast<float>(m_borderTop) + inset;
const float holeRight = static_cast<float>(width() - m_borderRight) - 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);
}
auto* node = static_cast<QSGGeometryNode*>(oldNode);
@@ -87,7 +86,6 @@ QSGNode* BlobInvertedRect::updatePaintNode(
node->setFlag(QSGNode::OwnsMaterial);
}
// Outer bounds (local coords)
const float x0 = static_cast<float>(m_localPaddedRect.x());
const float y0 = static_cast<float>(m_localPaddedRect.y());
const float x1 = x0 + static_cast<float>(m_localPaddedRect.width());
@@ -95,15 +93,12 @@ QSGNode* BlobInvertedRect::updatePaintNode(
const float w = x1 - x0;
const float h = y1 - y0;
// Update vertex positions and texture coordinates
auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
// Outer corners
v[0].set(x0, y0, 0.0f, 0.0f);
v[1].set(x1, y0, 1.0f, 0.0f);
v[2].set(x1, y1, 1.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[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h);
v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h);
@@ -111,7 +106,6 @@ QSGNode* BlobInvertedRect::updatePaintNode(
node->markDirty(QSGNode::DirtyGeometry);
// Update material uniforms
auto* material = static_cast<BlobMaterial*>(node->material());
material->m_paddedX = m_cachedPaddedX;
material->m_paddedY = m_cachedPaddedY;
@@ -218,7 +212,7 @@ void BlobInvertedRect::outerCornerRadii(float out[4]) const {
const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
out[0] = std::min(
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
: base,
: base,
maxR);
out[1] = std::min(
m_outerBottomRightRadius >= 0
+6 -3
View File
@@ -59,12 +59,15 @@ class BlobInvertedRect : public BlobShape {
void setOuterRadius(qreal v);
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect).
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; }
[[nodiscard]] qreal outerTopLeftRadius() const {
return m_outerTopLeftRadius;
}
void setOuterTopLeftRadius(qreal v);
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; }
[[nodiscard]] qreal outerTopRightRadius() const {
return m_outerTopRightRadius;
}
void setOuterTopRightRadius(qreal v);
@@ -1,6 +1,8 @@
#include "blobinvertedrect2.hpp"
#include "blobgroup2.hpp"
#include <algorithm>
BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {}
BlobInvertedRect2::~BlobInvertedRect2() {
@@ -35,6 +37,64 @@ void BlobInvertedRect2::setBorderBottom(qreal v) {
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() {
if (m_group) m_group->setInvertedRect(this);
}
@@ -19,6 +19,21 @@ class BlobInvertedRect2 : public BlobShape2 {
Q_PROPERTY(
qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY
borderBottomChanged)
Q_PROPERTY(
qreal outerRadius READ outerRadius WRITE setOuterRadius NOTIFY
outerRadiusChanged)
Q_PROPERTY(
qreal outerTopLeftRadius READ outerTopLeftRadius WRITE
setOuterTopLeftRadius NOTIFY outerTopLeftRadiusChanged)
Q_PROPERTY(
qreal outerTopRightRadius READ outerTopRightRadius WRITE
setOuterTopRightRadius NOTIFY outerTopRightRadiusChanged)
Q_PROPERTY(
qreal outerBottomLeftRadius READ outerBottomLeftRadius WRITE
setOuterBottomLeftRadius NOTIFY outerBottomLeftRadiusChanged)
Q_PROPERTY(
qreal outerBottomRightRadius READ outerBottomRightRadius WRITE
setOuterBottomRightRadius NOTIFY outerBottomRightRadiusChanged)
public:
explicit BlobInvertedRect2(QQuickItem* parent = nullptr);
@@ -40,13 +55,48 @@ class BlobInvertedRect2 : public BlobShape2 {
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:
void borderLeftChanged();
void borderRightChanged();
void borderTopChanged();
void borderBottomChanged();
void outerRadiusChanged();
void outerTopLeftRadiusChanged();
void outerTopRightRadiusChanged();
void outerBottomLeftRadiusChanged();
void outerBottomRightRadiusChanged();
protected:
void outerCornerRadii(float out[4]) const override;
void registerWithGroup() override;
void unregisterFromGroup() override;
@@ -55,4 +105,10 @@ class BlobInvertedRect2 : public BlobShape2 {
qreal m_borderRight = 0;
qreal m_borderTop = 0;
qreal m_borderBottom = 0;
qreal m_outerRadius = 0;
qreal m_outerTopLeftRadius = -1;
qreal m_outerTopRightRadius = -1;
qreal m_outerBottomLeftRadius = -1;
qreal m_outerBottomRightRadius = -1;
};
-15
View File
@@ -44,22 +44,17 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + 64, &opacity, 4);
}
// Padded rect (offset 68)
memcpy(buf->data() + 68, &mat->m_paddedX, 4);
memcpy(buf->data() + 72, &mat->m_paddedY, 4);
memcpy(buf->data() + 76, &mat->m_paddedW, 4);
memcpy(buf->data() + 80, &mat->m_paddedH, 4);
// Smooth factor (offset 84)
memcpy(buf->data() + 84, &mat->m_smoothFactor, 4);
// Rect count (offset 88)
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);
// Color as vec4 (offset 96, 16 bytes)
const float color[4] = {
mat->m_color.redF(),
mat->m_color.greenF(),
@@ -68,26 +63,19 @@ bool BlobMaterialShader::updateUniformData(
};
memcpy(buf->data() + 96, color, 16);
// Has inverted (offset 112)
memcpy(buf->data() + 112, &mat->m_hasInverted, 4);
// Inverted radius (offset 116)
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);
// Inverted inner (offset 144, 16 bytes)
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);
for (int i = 0; i < count; ++i) {
const auto& r = mat->m_rects[i];
const int base = 160 + i * 80;
// Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt)
float maskAsFloat = NAN;
memcpy(&maskAsFloat, &r.excludeMask, sizeof(float));
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);
}
// 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() + 1456, mat->m_shadowColor, 16);
// Outer edge corner radii, after shadowColor, at 1472.
memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16);
return true;
-10
View File
@@ -8,14 +8,9 @@ struct BlobRectData {
float cx = 0, cy = 0, hw = 0, hh = 0;
float offsetX = 0, offsetY = 0;
float minEig = 1.0f;
// Inverse of 2x2 deformation matrix, column-major for GLSL
float invDeform[4] = {1, 0, 0, 1};
// Screen-space AABB half-extents of the deformed rect
float screenHalfX = 0, screenHalfY = 0;
// Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU
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;
};
@@ -32,19 +27,14 @@ class BlobMaterial : public QSGMaterial {
float m_paddedH = 0;
float m_smoothFactor = 32.0f;
int m_rectCount = 0;
// -2 means no owner test: the single group draw writes every pixel it covers.
int m_myIndex = -2;
QColor m_color{0x44, 0x88, 0xff};
int m_hasInverted = 0;
float m_invertedRadius = 0;
float m_invertedOuter[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_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f};
// Outer-edge corner radii (tr, br, bl, tl). Matches the uniform block, which
// puts this after shadowColor.
float m_invertedOuterRadii[4] = {};
BlobRectData m_rects[16] = {};
};
+123 -81
View File
@@ -7,19 +7,94 @@
namespace {
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() {
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() {
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;
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) {
const float t = std::clamp(
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);
if (t >= 1.0f) {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_deformMatrix = QMatrix4x4();
m_retiring = false;
m_physicsActive = false;
} else {
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
m_dm01 = m_retire01 - m_retire01 * s;
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
m_deformMatrix = QMatrix4x4(
m_dm00,
m_dm01,
0,
0,
m_dm01,
m_dm11,
0,
0,
0,
0,
1,
0,
0,
0,
0,
1);
finishAtIdentity();
return;
}
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
m_dm01 = m_retire01 - m_retire01 * s;
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
m_deformMatrix = QMatrix4x4(
m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
emit rawDeformMatrixChanged();
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));
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) {
m_prevScenePos = scenePos;
m_elapsed.start();
m_hasPrevPos = true;
m_stillSamples = k_settleStillSamples;
return;
}
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f;
if (dt > 0.1f || dt < 0.001f) {
m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
const qint64 evalMs = m_elapsed.elapsed();
if (evalMs < k_minEvalMs) {
return;
}
m_elapsed.restart();
const float velX =
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
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;
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;
}
const float velX = dx / dt;
const float velY = dy / dt;
const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) {
if (speed > 5.0f)
if (speed > k_moveSpeed)
m_retiring = false;
else
return;
}
if (!m_physicsActive) {
if (speed < 5.0f) return;
if (speed < k_moveSpeed) return;
m_physicsActive = true;
requestPhysics();
}
const float kStretchFactor = static_cast<float>(m_deformScale);
@@ -131,7 +186,7 @@ void BlobRect::updatePhysics() {
float target01 = 0.0f;
float target11 = 1.0f;
if (speed > 5.0f) {
if (speed > k_moveSpeed) {
const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch;
@@ -165,7 +220,7 @@ void BlobRect::updatePhysics() {
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
checkAtRest(speed);
trySettle(speed);
}
void BlobRect::setTopLeftRadius(qreal r) {
@@ -340,16 +395,3 @@ void BlobRect::excludeCornersRemoveLast(QQmlListProperty<BlobRect>* prop) {
if (self->m_group) self->m_group->markDirty();
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:
void updatePolish() override;
void updatePhysics() override;
bool needsFrame() const override;
void resetDeformMatrix() override;
private:
void checkAtRest(float speed);
void trySettle(float speed);
void beginRetire();
void finishAtIdentity();
void requestPhysics();
QPointF m_prevScenePos;
QElapsedTimer m_elapsed;
bool m_physicsActive = false;
bool m_hasPrevPos = false;
int m_stillSamples = 0;
bool m_retiring = false;
QElapsedTimer m_retireElapsed;
+109 -50
View File
@@ -6,14 +6,59 @@
namespace {
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
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {}
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {
m_stillSamples = k_settleStillSamples;
}
BlobRect2::~BlobRect2() {
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() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
@@ -23,11 +68,12 @@ void BlobRect2::finishAtIdentity() {
m_physicsActive = false;
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
if (m_group) m_group->markDirty();
updateMoving();
}
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;
m_retire00 = m_dm00;
@@ -36,10 +82,26 @@ void BlobRect2::beginRetire() {
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
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() {
// Shapes are data only, so the group drives physics here.
if (m_retiring) {
const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
@@ -63,12 +125,16 @@ void BlobRect2::updatePhysics() {
if (!m_physicsActive) return;
}
// No "not active" guard here: an inactive shape still has to be evaluated so it
// can see the panel start moving and become active. The frame loop is only kept
// alive while active, and it is requested before anything can return early —
// requesting it at the end lets a shape stop being evaluated with the matrix
// still off identity, which is what left the panel contents blurry.
if (m_physicsActive && m_group) m_group->markDirty();
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_physicsActive) requestPhysics();
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
@@ -76,34 +142,54 @@ void BlobRect2::updatePhysics() {
m_prevScenePos = scenePos;
m_elapsed.start();
m_hasPrevPos = true;
m_stillSamples = k_settleStillSamples;
updateMoving();
return;
}
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f;
if (dt > 0.1f || dt < 0.001f) {
m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
const qint64 evalMs = m_elapsed.elapsed();
if (evalMs < k_minEvalMs) {
return;
}
m_elapsed.restart();
const float velX =
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
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;
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;
}
const float velX = dx / dt;
const float velY = dy / dt;
const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) {
if (speed > 5.0f)
if (speed > k_moveSpeed)
m_retiring = false;
else
return;
}
if (!m_physicsActive) {
if (speed < 5.0f) return;
if (speed < k_moveSpeed) return;
m_physicsActive = true;
requestPhysics();
}
const float kStretchFactor = static_cast<float>(m_deformScale);
@@ -113,7 +199,7 @@ void BlobRect2::updatePhysics() {
float target01 = 0.0f;
float target11 = 1.0f;
if (speed > 5.0f) {
if (speed > k_moveSpeed) {
const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch;
@@ -147,22 +233,8 @@ void BlobRect2::updatePhysics() {
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
// Start the retire as soon as the matrix is close to identity, as in v1. Waiting
// for every term to fall below kEpsilon keeps the matrix just off identity for
// 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);
trySettle(speed);
updateMoving();
}
void BlobRect2::setTopLeftRadius(qreal r) {
@@ -338,16 +410,3 @@ void BlobRect2::excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop) {
if (self->m_group) self->m_group->markDirty();
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(
bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY
atRestOverrideChanged)
Q_PROPERTY(bool moving READ isMoving NOTIFY movingChanged)
Q_PROPERTY(
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
topLeftRadiusChanged)
@@ -78,18 +79,13 @@ class BlobRect2 : public BlobShape2 {
if (m_atRestOverride == o) return;
m_atRestOverride = o;
emit atRestOverrideChanged();
updateMoving();
if (m_group) m_group->markDirty();
}
bool isExcluded(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
// 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;
}
bool isMoving() const override;
void cornerRadii(float out[4]) const override;
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
@@ -117,6 +113,7 @@ class BlobRect2 : public BlobShape2 {
void excludeChanged();
void excludeCornersChanged();
void atRestOverrideChanged();
void movingChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomLeftRadiusChanged();
@@ -124,20 +121,24 @@ class BlobRect2 : public BlobShape2 {
protected:
void updatePhysics() override;
bool needsFrame() const override;
void resetDeformMatrix() override;
private:
void checkAtRest(float speed);
void trySettle(float speed);
void beginRetire();
void finishAtIdentity();
void requestPhysics();
void updateMoving();
// Physics state
QPointF m_prevScenePos;
QElapsedTimer m_elapsed;
bool m_physicsActive = false;
bool m_hasPrevPos = false;
// Set from QML. Keeps the shape in the group's data while it is at rest; when it is
// false the spring state decides on its own.
int m_stillSamples = 0;
bool m_movingEmitted = false;
bool m_atRestOverride = false;
bool m_retiring = false;
@@ -146,13 +147,10 @@ class BlobRect2 : public BlobShape2 {
float m_retire01 = 0.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_dm01 = 0.0f;
float m_dm11 = 1.0f;
// Spring velocities for each component
float m_dmVel00 = 0.0f;
float m_dmVel01 = 0.0f;
float m_dmVel11 = 0.0f;
+40 -15
View File
@@ -8,6 +8,10 @@
#include <algorithm>
#include <cmath>
namespace {
constexpr int kMaxRects = 16;
} // namespace
static float deformPadding(const QMatrix4x4& dm, float hw, float hh) {
const float dm00 = dm(0, 0), dm01 = dm(0, 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) {
if (m_group == g) return;
if (m_group && isComponentComplete()) unregisterFromGroup();
if (m_group) resetDeformMatrix();
m_group = g;
if (m_group && isComponentComplete()) registerWithGroup();
emit groupChanged();
@@ -87,6 +92,8 @@ void BlobShape::geometryChange(
std::abs(m_pendingDh) > syncThreshold) {
m_pendingDx = 0;
m_pendingDy = 0;
m_pendingDw = 0;
m_pendingDh = 0;
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 {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto r = std::min(static_cast<float>(m_radius), maxR);
@@ -187,6 +200,12 @@ void BlobShape::updatePolish() {
}
if (include) {
if (other == this && m_cachedRects.size() >= kMaxRects) {
m_cachedRects.removeLast();
rectShapes.removeLast();
}
if (m_cachedRects.size() >= kMaxRects) continue;
if (other == this)
m_cachedMyIndex = static_cast<int>(m_cachedRects.size());
@@ -240,8 +259,7 @@ void BlobShape::updatePolish() {
m_cachedInvertedRadius = 0;
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
memset(
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
memset(m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
auto* inv = m_group->invertedRect();
if (inv) {
@@ -308,10 +326,11 @@ void BlobShape::updatePolish() {
BlobShape* const si = rectShapes[i];
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 cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh;
const float dcx = ri.cx + ri.offsetX, dcy = ri.cy + ri.offsetY;
const float cTrX = dcx + ri.hw, cTrY = dcy - ri.hh;
const float cBrX = dcx + ri.hw, cBrY = dcy + 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) {
if (j == i) continue;
@@ -319,10 +338,12 @@ void BlobShape::updatePolish() {
BlobShape* const sj = rectShapes[j];
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) continue;
const auto& rj = m_cachedRects[j];
const float sdTr = cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh);
const float sdBr = cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh);
const float sdBl = cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh);
const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh);
const float jcx = rj.cx + rj.offsetX;
const float jcy = rj.cy + rj.offsetY;
const float sdTr = cpuSdBox(cTrX, cTrY, jcx, jcy, 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));
fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor));
fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor));
@@ -360,10 +381,14 @@ void BlobShape::updatePolish() {
-cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh)));
}
ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
ri.radius[0] = ri.radius[0] > 0.0f ? std::max(ri.radius[0] * fTr, minR)
: 0.0f;
ri.radius[1] = ri.radius[1] > 0.0f ? std::max(ri.radius[1] * fBr, 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));
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;
for (int i = 0; i < count; ++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;
// Outer-edge corner radii. Only the inverted rect draws an outer frame edge,
// so the base has no outer rounding and returns zeros. Subclasses that expose
// outer corner properties override this (order: tr, br, bl, tl).
virtual void outerCornerRadii(float out[4]) const {
out[0] = 0;
out[1] = 0;
@@ -73,16 +70,19 @@ class BlobShape : public QQuickItem {
virtual void updatePhysics() {}
virtual bool needsFrame() const { return false; }
virtual void registerWithGroup();
virtual void unregisterFromGroup();
void updateCenteredDeformMatrix();
virtual void resetDeformMatrix();
BlobGroup* m_group = nullptr;
qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default
QMatrix4x4 m_centeredDeformMatrix;
// Cached data from updatePolish
float m_cachedPaddedX = 0;
float m_cachedPaddedY = 0;
float m_cachedPaddedW = 0;
+7 -1
View File
@@ -9,6 +9,7 @@ BlobShape2::BlobShape2(QQuickItem* parent) : QQuickItem(parent) {}
void BlobShape2::setGroup(BlobGroup2* g) {
if (m_group == g) return;
if (m_group && isComponentComplete()) unregisterFromGroup();
if (m_group) resetDeformMatrix();
m_group = g;
if (m_group && isComponentComplete()) registerWithGroup();
emit groupChanged();
@@ -32,7 +33,6 @@ void BlobShape2::geometryChange(
QQuickItem::geometryChange(newGeometry, oldGeometry);
updateCenteredDeformMatrix();
// One draw covers the whole group, so any shape change is a group change.
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 {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto r = std::min(static_cast<float>(m_radius), maxR);
+11 -9
View File
@@ -7,12 +7,6 @@
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 {
Q_OBJECT
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; }
// 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 isCornerExcluded(const BlobShape2* /*other*/) const {
@@ -66,13 +58,23 @@ class BlobShape2 : public QQuickItem {
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 bool needsFrame() const { return false; }
virtual void registerWithGroup();
virtual void unregisterFromGroup();
void updateCenteredDeformMatrix();
virtual void resetDeformMatrix();
BlobGroup2* m_group = nullptr;
qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default
+35 -94
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) {
// r = (topRight, bottomRight, bottomLeft, topLeft)
p -= center;
r.xy = (p.x > 0.0) ? r.xy : r.wz;
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) {
// Cubic smooth min (C2 continuous — no curvature kinks at blend boundary)
float h = max(k - abs(a - b), 0.0) / k;
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) {
// 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 blend = h * h * h * k * (1.0 / 6.0);
blend *= smoothstep(0.0, k * 0.5, -a);
@@ -67,10 +62,6 @@ float smaxSharpA(float a, float b, float k) {
void main() {
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];
int owner = -2;
float minDist = 1e10;
@@ -84,12 +75,8 @@ void main() {
vec4 radii =
rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
// Offset center for asymmetric deformation
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);
if (abs(pixel.x - center.x) > extent.x ||
abs(pixel.y - center.y) > extent.y) {
@@ -97,19 +84,13 @@ void main() {
continue;
}
// Apply pre-computed inverse deformation to the evaluation point
mat2 invDeform = mat2(invDm.xy, invDm.zw);
vec2 transformedPixel = center + invDeform * (pixel - center);
// Use pre-computed effective per-corner radii
float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii);
// Use pre-computed minimum eigenvalue for SDF correction
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) {
vec2 screenHalf = sh.xy;
@@ -122,26 +103,21 @@ void main() {
float distX1 =
(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),
smoothstep(0.0, smoothFactor, distY1));
float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0),
smoothstep(0.0, smoothFactor, distX1));
// Smooth axis weights: gradient-based at corners, face-based inside.
vec2 q = abs(pixel - center) - screenHalf;
vec2 qp = max(q, vec2(0.0));
float cornerLen = length(qp);
// Gradient direction in corner region (smooth 90-degree rotation)
float gradX = qp.x / 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 faceX = 1.0 - faceY;
// Blend: gradient in corner region, face-based inside
float t = smoothstep(0.0, 2.0, cornerLen);
float xWeight = mix(faceX, gradX, t);
float yWeight = mix(faceY, gradY, t);
@@ -158,15 +134,11 @@ void main() {
}
}
// Phase 2: hard-min baseline over all rects.
float mergedSdf = 1e10;
for (int i = 0; i < rectCount; 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++) {
if (dArr[i] >= 1e9)
continue;
@@ -176,7 +148,6 @@ void main() {
continue;
if ((excludeMask & (1 << j)) != 0)
continue;
// smin only deviates from min within smoothFactor
if (abs(dArr[i] - dArr[j]) >= smoothFactor)
continue;
mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor));
@@ -184,16 +155,12 @@ void main() {
}
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 =
sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw,
invertedOuterRadii) - 1.0;
float dInner =
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 innerBot = invertedInner.y + invertedInner.w;
float innerLeft = invertedInner.x - invertedInner.z;
@@ -203,62 +170,51 @@ void main() {
float outerLeft = invertedOuter.x - invertedOuter.z;
float outerRight = invertedOuter.x + invertedOuter.z;
float topZone =
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
float leftZone =
1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
float rightZone =
smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
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;
float preOff = smoothFactor * (1.0 / 6.0);
float s = smoothFactor * 2.0;
// Screen-space center (with offset) and pre-computed AABB half-extents
vec2 ctr = rect.xy + sinkProps.yz;
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;
// Delay sink to absorb smin blend depth (cubic smin max = k/6)
float preOff = smoothFactor * (1.0 / 6.0);
vec2 ctr = rect.xy + sinkProps.yz;
// Top border: track rect's BOTTOM edge, only within border thickness
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
innerTop - outerTop);
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);
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);
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);
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);
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 =
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
float leftZone =
1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
float rightZone =
smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
float s = smoothFactor * 2.0;
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
botPen * smoothstep(s, 0.0, hLat) * botZone),
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
rightPen * smoothstep(s, 0.0, vLat) * rightZone));
sinkValue = max(sinkValue, sink);
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
botPen * smoothstep(s, 0.0, hLat) * botZone),
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
rightPen * smoothstep(s, 0.0, vLat) * rightZone));
sinkValue = max(sinkValue, sink);
}
}
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 dFrame = dOuter >= negInner ? dOuter
: 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);
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;
// 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)
discard;
// Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad
// boundary cannot make a wide edge.
float fw = min(fwidth(mergedSdf), 1.0);
float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
float materialAlpha = clamp(0.5 - mergedSdf / pxWidth, 0.0, 1.0);
// 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;
if (shadowRadius > 0.0 && myIndex == -2) {
float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0);
shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a;
}
// Material over shadow, premultiplied.
float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha);
vec3 rgb = color.rgb * materialAlpha
+ shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha);
Binary file not shown.
+138 -46
View File
@@ -4,6 +4,7 @@
#include <QQuickWindow>
#include <QScopeGuard>
#include <QtGui/private/qfont_p.h>
#include <algorithm>
#include <cmath>
Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts")
@@ -54,49 +55,13 @@ SnapGridLayoutItem::SnapGridLayoutItem(
int column,
int rowSpan,
int columnSpan,
Qt::Alignment alignment,
SnapLayout* layout)
: QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment)
, m_layout(layout) {}
Qt::Alignment alignment)
: QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment) {}
void SnapGridLayoutItem::setGeometry(const QRectF& rect) {
QQuickLayoutAttached* info = attachedLayoutObject(m_item, false);
QRectF placed = info ? rect.marginsRemoved(info->effectiveQMargins())
: rect;
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);
}
engineRect = info ? rect.marginsRemoved(info->effectiveQMargins()) : rect;
placed = true;
}
void SnapLayoutPrivate::mirrorChange() {
@@ -119,6 +84,7 @@ SnapLayout::~SnapLayout() {
void SnapLayout::componentComplete() {
QQuickLayout::componentComplete();
applySpacing();
invalidate();
ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints);
@@ -128,8 +94,12 @@ void SnapLayout::componentComplete() {
void SnapLayout::itemChange(ItemChange change, const ItemChangeData& 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();
}
}
QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
@@ -169,16 +139,33 @@ Qt::LayoutDirection SnapLayout::effectiveLayoutDirection() const {
qreal SnapLayout::spacing() const {
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) {
Q_D(SnapLayout);
#pragma GCC diagnostic push
#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
d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical);
d->spacingRequest = space;
applySpacing();
invalidate();
emit spacingChanged();
}
@@ -242,6 +229,14 @@ void SnapLayout::updateLayoutItems() {
Q_D(SnapLayout);
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();
insertLayoutItems();
}
@@ -286,8 +281,8 @@ void SnapLayout::insertLayoutItems() {
int gridRow = 0;
int gridColumn = index;
if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn);
QGridLayoutItem* layoutItem = new SnapGridLayoutItem(
child, gridRow, gridColumn, 1, 1, alignment, this);
QGridLayoutItem* layoutItem =
new SnapGridLayoutItem(child, gridRow, gridColumn, 1, 1, alignment);
if (hStretch >= 0) {
if (!fillWidth)
@@ -315,6 +310,7 @@ void SnapLayout::removeGridItem(QGridLayoutItem* gridItem) {
void SnapLayout::itemDestroyed(QQuickItem* item) {
if (!isReady()) return;
Q_D(SnapLayout);
d->childSizes.remove(item);
if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) {
removeGridItem(gridItem);
delete gridItem;
@@ -381,6 +377,98 @@ void SnapLayout::clearAncestorListeners() {
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) {
Q_D(SnapLayout);
if (!isReady() || !size.isValid()) return;
@@ -408,7 +496,11 @@ void SnapLayout::rearrange(const QSizeF& 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);
arrangeChildren();
d->rearranging = false;
for (auto childItem : std::as_const(d->invalidateAfterRearrange))
+33 -5
View File
@@ -1,6 +1,8 @@
#pragma once
#include <QHash>
#include <QPointer>
#include <QtMath>
#include <QtGui/private/qabstractlayoutstyleinfo_p.h>
#include <QtGui/private/qgridlayoutengine_p.h>
#include <QtQuickLayouts/private/qquickgridlayoutengine_p.h>
@@ -20,6 +22,20 @@ class SnapLayoutStyleInfo : public QAbstractLayoutStyleInfo {
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 {
public:
SnapGridLayoutItem(
@@ -28,13 +44,12 @@ class SnapGridLayoutItem : public QQuickGridLayoutItem {
int column,
int rowSpan,
int columnSpan,
Qt::Alignment alignment,
SnapLayout* layout);
Qt::Alignment alignment);
void setGeometry(const QRectF& rect) override;
private:
SnapLayout* m_layout;
QRectF engineRect;
bool placed = false;
};
class SnapLayout : public QQuickLayout {
@@ -101,6 +116,10 @@ class SnapLayout : public QQuickLayout {
void insertLayoutItems();
void removeGridItem(QGridLayoutItem* gridItem);
void arrangeChildren();
void applySpacing();
void updateAncestorListeners();
void clearAncestorListeners();
};
@@ -113,10 +132,11 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate {
void mirrorChange() override;
QQuickGridLayoutEngine engine;
SnapGridLayoutEngine engine;
SnapLayoutStyleInfo* styleInfo = nullptr;
Qt::Orientation orientation = Qt::Horizontal;
Qt::LayoutDirection layoutDirection = Qt::LeftToRight;
qreal spacingRequest = qQNaN();
bool rearranging = false;
int recurRearrangeCounter = 0;
QList<QQuickItem*> invalidateAfterRearrange;
@@ -124,6 +144,14 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate {
QVector<QPointer<QQuickItem>> ancestorListeners;
qint64 originResidualX = -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
+6
View File
@@ -29,3 +29,9 @@ qml_module(ZShell-internal
PkgConfig::GL
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);
}
+39 -12
View File
@@ -71,11 +71,23 @@ static QCanvasPath buildStrokePath(
void StrokeCanvasItem::undoLastGroup() {
if (m_strokes.isEmpty()) return;
const int lastGroup = m_strokes.last().groupId;
QVector<Stroke> group;
for (int i = static_cast<int>(m_strokes.size()) - 1;
i >= 0 && m_strokes[i].groupId == lastGroup;
--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();
}
@@ -106,19 +118,14 @@ void StrokeCanvasItem::setHoverPoint(const QPointF& point) {
emit hoverPointChanged();
}
void StrokeCanvasItem::showHover(qreal x, qreal y) {
const QPointF newPoint{x, y};
const bool pointChanged = (m_hoverPoint != newPoint);
const bool visibleChanged = !m_hoverVisible;
void StrokeCanvasItem::showHover() {
if (m_hoverVisible) return;
if (pointChanged || visibleChanged) {
m_hoverVisible = true;
m_hoverPoint = newPoint;
if (pointChanged) emit hoverPointChanged();
if (visibleChanged) emit hoverVisibleChanged();
update();
}
m_hoverVisible = true;
update();
emit hoverVisibleChanged();
}
void StrokeCanvasItem::hideHover() {
@@ -139,6 +146,24 @@ void StrokeCanvasItem::setPenWidth(qreal width) {
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) {
m_isDrawing = true;
m_currentStroke.points.clear();
@@ -199,6 +224,7 @@ void StrokeCanvasItem::endStroke() {
m_currentStroke.points.clear();
m_strokes.append(m_currentStroke);
m_currentStroke = {};
m_redoStack.clear();
update();
}
@@ -206,6 +232,7 @@ void StrokeCanvasItem::endStroke() {
void StrokeCanvasItem::clear() {
m_strokes.clear();
m_currentStroke = {};
m_redoStack.clear();
update();
}
+16 -1
View File
@@ -3,6 +3,7 @@
#include <QCanvasPainterItem>
#include <QColor>
#include <QPointF>
#include <QRectF>
#include <QVector>
#include <qcolor.h>
#include <qcontainerfwd.h>
@@ -27,6 +28,10 @@ class StrokeCanvasItem : public QCanvasPainterItem {
hoverPointChanged)
Q_PROPERTY(
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:
explicit StrokeCanvasItem(QQuickItem* parent = nullptr);
@@ -37,15 +42,20 @@ class StrokeCanvasItem : public QCanvasPainterItem {
void setHoverVisible(bool visible);
void setHoverPoint(const QPointF& point);
Q_INVOKABLE void showHover(qreal x, qreal y);
Q_INVOKABLE void showHover();
Q_INVOKABLE void hideHover();
Q_INVOKABLE void undoLastGroup();
Q_INVOKABLE void redo();
[[nodiscard]] QColor penColor() const { return m_penColor; }
[[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 setPenWidth(qreal width);
void setHole(const QRectF& hole);
void setHoleRadius(qreal radius);
Q_INVOKABLE void clear();
@@ -56,6 +66,8 @@ class StrokeCanvasItem : public QCanvasPainterItem {
signals:
void penColorChanged();
void penWidthChanged();
void holeChanged();
void holeRadiusChanged();
void hoverVisibleChanged();
void hoverPointChanged();
@@ -72,9 +84,12 @@ class StrokeCanvasItem : public QCanvasPainterItem {
qreal m_penWidth = 4.0;
bool m_isDrawing = false;
QRectF m_hole;
qreal m_holeRadius = 0.0;
int m_nextGroupId = 0;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
QVector<QVector<Stroke>> m_redoStack;
};
}; // namespace ZShell::internal
+104 -11
View File
@@ -1,11 +1,98 @@
#include "strokecanvasrenderer.hpp"
#include "strokecanvasitem.hpp"
#include <QCanvasPainter>
#include <QQuickWindow>
#include <QVector4D>
#include <qcolor.h>
#include <QSet>
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,
const QVector<QPointF>& points,
const QColor& color,
@@ -13,8 +100,10 @@ static void drawStroke(
bool polyline) {
if (points.isEmpty()) return;
painter->setStrokeStyle(color);
painter->setFillStyle(color);
QCanvasCustomBrush& brush = brushFor(color);
painter->setStrokeStyle(brush);
painter->setFillStyle(brush);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -65,7 +154,7 @@ static void drawStroke(
painter->stroke();
}
static void drawHoverCursor(
void StrokeCanvasRenderer::drawHoverCursor(
QCanvasPainter* painter,
const QPointF& point,
qreal penWidth,
@@ -74,7 +163,7 @@ static void drawHoverCursor(
const qreal radius = penWidth * 0.5;
if (isDrawing) {
painter->setFillStyle(penColor);
painter->setFillStyle(brushFor(penColor));
painter->beginPath();
painter->circle(point, radius);
painter->fill();
@@ -85,7 +174,7 @@ static void drawHoverCursor(
const bool useDashes = penWidth > 10.0;
auto drawOutline = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color);
painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -117,7 +206,7 @@ static void drawHoverCursor(
};
auto drawCrosshair = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color);
painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -148,8 +237,11 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
m_penColor = canvas->m_penColor;
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;
for (const auto& stroke : canvas->m_strokes)
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;
for (const auto& stroke : m_strokes)
rendererGroupIds.insert(stroke.groupId);
@@ -180,6 +271,8 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
}
void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
updateBrushHole();
for (int id : m_pendingGroupRemovals)
painter->removePathGroup(id);
m_pendingGroupRemovals.clear();
@@ -187,8 +280,8 @@ void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
painter->clearRect(0, 0, width(), height());
for (const auto& stroke : m_strokes) {
painter->setStrokeStyle(stroke.color);
painter->setFillStyle(stroke.color);
painter->setStrokeStyle(brushFor(stroke.color));
painter->setFillStyle(brushFor(stroke.color));
painter->setLineWidth(stroke.width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -1,26 +1,58 @@
#pragma once
#include <QCanvasCustomBrush>
#include <QCanvasPainterItemRenderer>
#include <QHash>
#include <QRectF>
#include <QVector4D>
#include <qcontainerfwd.h>
#include "stroke.hpp"
namespace ZShell::internal {
struct BrushEntry {
QCanvasCustomBrush brush;
qint64 holeToken = -1; // hole generation this brush was built with
};
class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer {
public:
void synchronizeData(QCanvasPainterItem* item) override;
void paint(QCanvasPainter* painter) override;
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;
qreal m_penWidth = 4.0;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
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;
Stroke m_currentStroke;
QVector<int> m_pendingGroupRemovals;
QHash<QRgb, BrushEntry> m_brushes;
};
}; // namespace ZShell::internal
+9 -7
View File
@@ -545,7 +545,7 @@ def generate(
except (PermissionError, OSError, BlockingIOError):
pass
def smart_mode(image_path: Path) -> str:
def smart_mode(image_path: Path | io.BytesIO) -> str:
is_dark = ""
with Image.open(image_path) as img:
@@ -739,7 +739,7 @@ def generate(
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)
pixel_len = image.width * image.height
image_data = image.getdata()
@@ -837,11 +837,13 @@ def generate(
image_path = image_path or Path(WALL_PATH)
blob = scheme_thumb_from_db(str(DB_PATH), str(image_path))
if blob is not None:
open_thumb = lambda: io.BytesIO(blob)
else:
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
open_thumb = lambda: thumb_path
def open_thumb():
if blob is not None:
return io.BytesIO(blob)
else:
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
return thumb_path
seed = seed_from_image(open_thumb())
name = "dynamic"