From 566b71d386f87c5003de4a8bdd84e182a4ad525a Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 30 Oct 2026 14:16:19 +0100 Subject: [PATCH] feat: drawing popout now clips strokes again + add redo + add undo and redo keybinds --- Drawers/Drawing.qml | 7 + Drawers/DrawingInput.qml | 95 ++++++ Drawers/Interactions.qml | 319 +++++++----------- Drawers/Interactions2.qml | 315 ----------------- Drawers/Windows.qml | 26 +- Plugins/CMakeLists.txt | 1 + Plugins/ZShell/Internal/CMakeLists.txt | 6 + .../Internal/shaders/strokeholemask.frag | 22 ++ Plugins/ZShell/Internal/strokecanvasitem.cpp | 51 ++- Plugins/ZShell/Internal/strokecanvasitem.hpp | 17 +- .../ZShell/Internal/strokecanvasrenderer.cpp | 115 ++++++- .../ZShell/Internal/strokecanvasrenderer.hpp | 32 ++ 12 files changed, 467 insertions(+), 539 deletions(-) create mode 100644 Drawers/DrawingInput.qml delete mode 100644 Drawers/Interactions2.qml create mode 100644 Plugins/ZShell/Internal/shaders/strokeholemask.frag diff --git a/Drawers/Drawing.qml b/Drawers/Drawing.qml index ac27301..523d9a1 100644 --- a/Drawers/Drawing.qml +++ b/Drawers/Drawing.qml @@ -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 } diff --git a/Drawers/DrawingInput.qml b/Drawers/DrawingInput.qml new file mode 100644 index 0000000..1e7ece6 --- /dev/null +++ b/Drawers/DrawingInput.qml @@ -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(); + } + } +} diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 4e0d6a5..a0a0c6d 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -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; } diff --git a/Drawers/Interactions2.qml b/Drawers/Interactions2.qml deleted file mode 100644 index e11bf02..0000000 --- a/Drawers/Interactions2.qml +++ /dev/null @@ -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 - } -} diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 97f5b7b..8704ccd 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -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 } } diff --git a/Plugins/CMakeLists.txt b/Plugins/CMakeLists.txt index df11413..2a14718 100644 --- a/Plugins/CMakeLists.txt +++ b/Plugins/CMakeLists.txt @@ -6,6 +6,7 @@ find_package(Qt6 REQUIRED COMPONENTS GuiPrivate Quick CanvasPainter + CanvasPainterTools QuickPrivate QuickLayouts QuickLayoutsPrivate diff --git a/Plugins/ZShell/Internal/CMakeLists.txt b/Plugins/ZShell/Internal/CMakeLists.txt index ff6b832..0bfcee2 100644 --- a/Plugins/ZShell/Internal/CMakeLists.txt +++ b/Plugins/ZShell/Internal/CMakeLists.txt @@ -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 +) diff --git a/Plugins/ZShell/Internal/shaders/strokeholemask.frag b/Plugins/ZShell/Internal/shaders/strokeholemask.frag new file mode 100644 index 0000000..8931102 --- /dev/null +++ b/Plugins/ZShell/Internal/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); +} diff --git a/Plugins/ZShell/Internal/strokecanvasitem.cpp b/Plugins/ZShell/Internal/strokecanvasitem.cpp index 50bc676..190a9a5 100644 --- a/Plugins/ZShell/Internal/strokecanvasitem.cpp +++ b/Plugins/ZShell/Internal/strokecanvasitem.cpp @@ -71,11 +71,23 @@ static QCanvasPath buildStrokePath( void StrokeCanvasItem::undoLastGroup() { if (m_strokes.isEmpty()) return; + const int lastGroup = m_strokes.last().groupId; + + QVector group; for (int i = static_cast(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(); } diff --git a/Plugins/ZShell/Internal/strokecanvasitem.hpp b/Plugins/ZShell/Internal/strokecanvasitem.hpp index 7f1e9a0..f727bda 100644 --- a/Plugins/ZShell/Internal/strokecanvasitem.hpp +++ b/Plugins/ZShell/Internal/strokecanvasitem.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -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 m_strokes; Stroke m_currentStroke; + QVector> m_redoStack; }; }; // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/strokecanvasrenderer.cpp b/Plugins/ZShell/Internal/strokecanvasrenderer.cpp index 11b2d08..7f729d4 100644 --- a/Plugins/ZShell/Internal/strokecanvasrenderer.cpp +++ b/Plugins/ZShell/Internal/strokecanvasrenderer.cpp @@ -1,11 +1,98 @@ #include "strokecanvasrenderer.hpp" #include "strokecanvasitem.hpp" +#include +#include +#include #include #include 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(center.x()), + static_cast(center.y()), + static_cast(half.x()), + static_cast(half.y())), + QVector4D( + static_cast(qMin(radius, maxRadius)), + static_cast(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& 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 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 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); diff --git a/Plugins/ZShell/Internal/strokecanvasrenderer.hpp b/Plugins/ZShell/Internal/strokecanvasrenderer.hpp index 2877b6d..e09bce3 100644 --- a/Plugins/ZShell/Internal/strokecanvasrenderer.hpp +++ b/Plugins/ZShell/Internal/strokecanvasrenderer.hpp @@ -1,26 +1,58 @@ #pragma once +#include #include +#include +#include +#include #include #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& 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 m_strokes; Stroke m_currentStroke; QVector m_pendingGroupRemovals; + QHash m_brushes; }; }; // namespace ZShell::internal