Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
566b71d386 | ||
|
|
c5608dc9b9 | ||
|
|
78a9577209 | ||
|
|
90184bff58 | ||
|
|
abdde2450d | ||
|
|
fd05b3d4c4 | ||
|
|
067291f478 | ||
|
|
395d1dad3d | ||
|
|
118b8f482a | ||
|
|
b58b4fdc9c | ||
|
|
a56afd7fac | ||
|
|
b0cb43f75c | ||
|
|
e655ec5bc7 | ||
|
|
dc0f222ace | ||
|
|
de19fd6ac4 | ||
|
|
9edbcbd610 | ||
|
|
cd44d27b74 | ||
|
|
0d0faecb1c |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,6 +6,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
GuiPrivate
|
||||
Quick
|
||||
CanvasPainter
|
||||
CanvasPainterTools
|
||||
QuickPrivate
|
||||
QuickLayouts
|
||||
QuickLayoutsPrivate
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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] = {};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user