From 1ee345f946a162cd5baf911cf0f7efe27c423d06 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 9 Mar 2026 12:47:09 +0100 Subject: [PATCH] drawing --- Components/ColorArcPicker.qml | 170 ++++++++++++++++++++++++++++++ Drawers/Backgrounds.qml | 8 +- Drawers/Bar.qml | 46 ++++++-- Drawers/Drawing.qml | 186 +++++++++++++++++++++++++++++++++ Drawers/DrawingInput.qml | 53 ++++++++++ Drawers/Interactions.qml | 117 +++------------------ Drawers/Panels.qml | 13 +++ Modules/Drawing/Background.qml | 66 ++++++++++++ Modules/Drawing/Content.qml | 76 ++++++++++++++ Modules/Drawing/Wrapper.qml | 133 +++++++++++++++++++++++ Modules/Shortcuts.qml | 9 ++ 11 files changed, 762 insertions(+), 115 deletions(-) create mode 100644 Components/ColorArcPicker.qml create mode 100644 Drawers/Drawing.qml create mode 100644 Drawers/DrawingInput.qml create mode 100644 Modules/Drawing/Background.qml create mode 100644 Modules/Drawing/Content.qml create mode 100644 Modules/Drawing/Wrapper.qml diff --git a/Components/ColorArcPicker.qml b/Components/ColorArcPicker.qml new file mode 100644 index 0000000..3e3a182 --- /dev/null +++ b/Components/ColorArcPicker.qml @@ -0,0 +1,170 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +Item { + id: root + + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property real currentHue: 0 + required property var drawing + property real handleSize: 30 + property real lastChromaticHue: 0 + property real ringThickness: 12 + readonly property int segmentCount: 180 + + function hueToAngle(hue) { + return arcStartAngle + arcSweep * hue; + } + + function normalizeAngle(angle) { + const tau = Math.PI * 2; + let a = angle % tau; + if (a < 0) + a += tau; + return a; + } + + function syncFromPenColor() { + if (!drawing) + return; + + const c = drawing.penColor; + + // QML color exposes HSL channels directly. + // If the current color is chromatic, move the handle to that hue. + // If it is achromatic (black/white/gray), keep the last useful hue. + if (c.hslSaturation > 0) { + currentHue = c.hslHue; + lastChromaticHue = c.hslHue; + } else { + currentHue = lastChromaticHue; + } + + canvas.requestPaint(); + } + + function updateHueFromPoint(x, y) { + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const dx = x - cx; + const dy = y - cy; + + const distance = Math.sqrt(dx * dx + dy * dy); + const radius = (Math.min(canvas.width, canvas.height) - handleSize - 8) / 2; + + if (distance < radius - 24 || distance > radius + 24) + return; + + const angle = normalizeAngle(Math.atan2(dy, dx)); + const start = normalizeAngle(arcStartAngle); + + let relative = angle - start; + if (relative < 0) + relative += Math.PI * 2; + + if (relative > arcSweep) { + const gap = Math.PI * 2 - arcSweep; + relative = relative < arcSweep + gap / 2 ? arcSweep : 0; + } + + currentHue = relative / arcSweep; + lastChromaticHue = currentHue; + drawing.penColor = Qt.hsla(currentHue, 1.0, 0.5, 1.0); + } + + implicitHeight: 180 + implicitWidth: 220 + + Component.onCompleted: syncFromPenColor() + onCurrentHueChanged: canvas.requestPaint() + onDrawingChanged: syncFromPenColor() + + Connections { + function onPenColorChanged() { + root.syncFromPenColor(); + } + + target: root.drawing + } + + Canvas { + id: canvas + + anchors.fill: parent + renderStrategy: Canvas.Threaded + renderTarget: Canvas.Image + + Component.onCompleted: requestPaint() + onHeightChanged: requestPaint() + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + const cx = width / 2; + const cy = height / 2; + const radius = (Math.min(width, height) - root.handleSize - 8) / 2; + + ctx.beginPath(); + ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); + ctx.lineWidth = root.ringThickness + 4; + ctx.lineCap = "round"; + ctx.strokeStyle = Qt.rgba(1, 1, 1, 0.12); + ctx.stroke(); + + for (let i = 0; i < root.segmentCount; ++i) { + const t1 = i / root.segmentCount; + const t2 = (i + 1) / root.segmentCount; + const a1 = root.arcStartAngle + root.arcSweep * t1; + const a2 = root.arcStartAngle + root.arcSweep * t2; + + ctx.beginPath(); + ctx.arc(cx, cy, radius, a1, a2); + ctx.lineWidth = root.ringThickness; + ctx.lineCap = "round"; + ctx.strokeStyle = Qt.hsla(t1, 1.0, 0.5, 1.0); + ctx.stroke(); + } + + const handleAngle = root.hueToAngle(root.currentHue); + const hx = cx + radius * Math.cos(handleAngle); + const hy = cy + radius * Math.sin(handleAngle); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2, 0, Math.PI * 2); + ctx.fillStyle = Qt.rgba(1, 1, 1, 0.95); + ctx.fill(); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2, 0, Math.PI * 2); + ctx.lineWidth = 1.5; + ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.18); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2 - 6, 0, Math.PI * 2); + ctx.fillStyle = root.drawing.penColor; + ctx.fill(); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2 - 6, 0, Math.PI * 2); + ctx.lineWidth = 1; + ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.20); + ctx.stroke(); + } + onWidthChanged: requestPaint() + } + + MouseArea { + acceptedButtons: Qt.LeftButton + anchors.fill: parent + + onPositionChanged: mouse => { + if (mouse.buttons & Qt.LeftButton) + root.updateHueFromPoint(mouse.x, mouse.y); + } + onPressed: mouse => root.updateHueFromPoint(mouse.x, mouse.y) + } +} diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index bcd1488..2eab002 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -11,7 +11,7 @@ import qs.Modules.Dashboard as Dashboard import qs.Modules.Osd as Osd import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources - +import qs.Modules.Drawing as Drawing import qs.Modules.Settings as Settings Shape { @@ -31,6 +31,12 @@ Shape { } } + Drawing.Background { + startX: 0 + startY: wrapper.y - rounding + wrapper: root.panels.drawing + } + Resources.Background { startX: 0 - rounding startY: 0 diff --git a/Drawers/Bar.qml b/Drawers/Bar.qml index 02d3e3c..be1bd99 100644 --- a/Drawers/Bar.qml +++ b/Drawers/Bar.qml @@ -32,19 +32,9 @@ Variants { WlrLayershell.namespace: "ZShell-Bar" color: "transparent" contentItem.focus: true + mask: visibilities.isDrawing ? null : region screen: scope.modelData - mask: Region { - id: region - - height: bar.screen.height - backgroundRect.implicitHeight - intersection: Intersection.Xor - regions: popoutRegions.instances - width: bar.width - x: 0 - y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height - } - contentItem.Keys.onEscapePressed: { if (Config.barConfig.autoHide) visibilities.bar = false; @@ -55,6 +45,17 @@ Variants { visibilities.resources = false; } + Region { + id: region + + height: bar.screen.height - backgroundRect.implicitHeight + intersection: Intersection.Xor + regions: popoutRegions.instances + width: bar.width + x: 0 + y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height + } + PanelWindow { id: exclusionZone @@ -117,6 +118,7 @@ Variants { property bool bar property bool dashboard + property bool isDrawing property bool launcher property bool notif: NotifServer.popups.length > 0 property bool osd @@ -157,20 +159,42 @@ Variants { } } + Drawing { + id: drawing + + anchors.fill: parent + z: 2 + } + + DrawingInput { + id: input + + bar: backgroundRect + drawing: drawing + panels: panels + popout: panels.drawing + visibilities: visibilities + z: 2 + } + Interactions { id: mouseArea anchors.fill: parent bar: barLoader + drawing: drawing + input: input panels: panels popouts: panels.popouts screen: scope.modelData visibilities: visibilities + z: 1 Panels { id: panels bar: backgroundRect + drawingItem: drawing screen: scope.modelData visibilities: visibilities } diff --git a/Drawers/Drawing.qml b/Drawers/Drawing.qml new file mode 100644 index 0000000..6a30ff2 --- /dev/null +++ b/Drawers/Drawing.qml @@ -0,0 +1,186 @@ +import QtQuick + +Canvas { + id: root + + property rect dirtyRect: Qt.rect(0, 0, 0, 0) + property bool frameQueued: false + property bool fullRepaintPending: true + property point lastPoint: Qt.point(0, 0) + property real minPointDistance: 2.0 + property color penColor: "white" + property real penWidth: 4 + property var pendingSegments: [] + property bool strokeActive: false + property var strokes: [] + + function appendPoint(x, y) { + if (!strokeActive || strokes.length === 0) + return; + const dx = x - lastPoint.x; + const dy = y - lastPoint.y; + + if ((dx * dx + dy * dy) < (minPointDistance * minPointDistance)) + return; + const x1 = lastPoint.x; + const y1 = lastPoint.y; + const x2 = x; + const y2 = y; + + strokes[strokes.length - 1].push(Qt.point(x2, y2)); + + pendingSegments.push({ + dot: false, + x1: x1, + y1: y1, + x2: x2, + y2: y2 + }); + + lastPoint = Qt.point(x2, y2); + queueDirty(segmentDirtyRect(x1, y1, x2, y2)); + } + + function beginStroke(x, y) { + const p = Qt.point(x, y); + strokes.push([p]); + lastPoint = p; + strokeActive = true; + + pendingSegments.push({ + dot: true, + x: x, + y: y + }); + + queueDirty(pointDirtyRect(x, y)); + } + + function clear() { + strokes = []; + pendingSegments = []; + dirtyRect = Qt.rect(0, 0, 0, 0); + fullRepaintPending = true; + markDirty(Qt.rect(0, 0, width, height)); + } + + function drawDot(ctx, x, y) { + ctx.beginPath(); + ctx.arc(x, y, penWidth / 2, 0, Math.PI * 2); + ctx.fill(); + } + + function drawSegment(ctx, x1, y1, x2, y2) { + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + + function endStroke() { + strokeActive = false; + } + + function pointDirtyRect(x, y) { + const pad = penWidth + 2; + return Qt.rect(x - pad, y - pad, pad * 2, pad * 2); + } + + function queueDirty(r) { + dirtyRect = unionRects(dirtyRect, r); + + if (frameQueued) + return; + frameQueued = true; + + requestAnimationFrame(function () { + frameQueued = false; + + if (dirtyRect.width > 0 && dirtyRect.height > 0) { + markDirty(dirtyRect); + dirtyRect = Qt.rect(0, 0, 0, 0); + } + }); + } + + function replayAll(ctx) { + ctx.clearRect(0, 0, width, height); + + for (const stroke of strokes) { + if (!stroke || stroke.length === 0) + continue; + if (stroke.length === 1) { + const p = stroke[0]; + drawDot(ctx, p.x, p.y); + continue; + } + + ctx.beginPath(); + ctx.moveTo(stroke[0].x, stroke[0].y); + for (let i = 1; i < stroke.length; ++i) + ctx.lineTo(stroke[i].x, stroke[i].y); + ctx.stroke(); + } + } + + function requestFullRepaint() { + fullRepaintPending = true; + markDirty(Qt.rect(0, 0, width, height)); + } + + function segmentDirtyRect(x1, y1, x2, y2) { + const pad = penWidth + 2; + const left = Math.min(x1, x2) - pad; + const top = Math.min(y1, y2) - pad; + const right = Math.max(x1, x2) + pad; + const bottom = Math.max(y1, y2) + pad; + return Qt.rect(left, top, right - left, bottom - top); + } + + function unionRects(a, b) { + if (a.width <= 0 || a.height <= 0) + return b; + if (b.width <= 0 || b.height <= 0) + return a; + + const left = Math.min(a.x, b.x); + const top = Math.min(a.y, b.y); + const right = Math.max(a.x + a.width, b.x + b.width); + const bottom = Math.max(a.y + a.height, b.y + b.height); + + return Qt.rect(left, top, right - left, bottom - top); + } + + anchors.fill: parent + contextType: "2d" + renderStrategy: Canvas.Threaded + renderTarget: Canvas.Image + + onHeightChanged: requestFullRepaint() + onPaint: region => { + const ctx = getContext("2d"); + + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.lineWidth = penWidth; + ctx.strokeStyle = penColor; + ctx.fillStyle = penColor; + + if (fullRepaintPending) { + fullRepaintPending = false; + replayAll(ctx); + pendingSegments = []; + return; + } + + for (const seg of pendingSegments) { + if (seg.dot) + drawDot(ctx, seg.x, seg.y); + else + drawSegment(ctx, seg.x1, seg.y1, seg.x2, seg.y2); + } + + pendingSegments = []; + } + onWidthChanged: requestFullRepaint() +} diff --git a/Drawers/DrawingInput.qml b/Drawers/DrawingInput.qml new file mode 100644 index 0000000..51a785f --- /dev/null +++ b/Drawers/DrawingInput.qml @@ -0,0 +1,53 @@ +import Quickshell +import QtQuick +import qs.Components + +CustomMouseArea { + id: root + + required property var bar + required property Drawing drawing + required property Panels panels + required property var popout + required property PersistentProperties visibilities + + function inLeftPanel(panel: Item, x: real, y: real): bool { + return x < panel.x + panel.width && withinPanelHeight(panel, x, y); + } + + function withinPanelHeight(panel: Item, x: real, y: real): bool { + const panelY = panel.y + bar.implicitHeight; + return y >= panelY && y <= panelY + panel.height; + } + + anchors.fill: root.visibilities.isDrawing ? parent : undefined + hoverEnabled: true + visible: root.visibilities.isDrawing + + onPositionChanged: event => { + const x = event.x; + const y = event.y; + + if (event.buttons & Qt.LeftButton) + root.drawing.appendPoint(x, y); + + if (root.inLeftPanel(root.popout, x, y)) { + root.z = -2; + root.panels.drawing.expanded = true; + } + } + onPressed: event => { + const x = event.x; + const y = event.y; + + if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) { + root.panels.drawing.expanded = false; + root.drawing.beginStroke(x, y); + return; + } + } + onReleased: { + if (root.visibilities.isDrawing) + root.drawing.endStroke(); + } +} diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 11f6561..79b0f72 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -10,6 +10,8 @@ CustomMouseArea { required property Item bar property bool dashboardShortcutActive property point dragStart + required property Drawing drawing + required property DrawingInput input property bool osdShortcutActive required property Panels panels required property BarPopouts.Wrapper popouts @@ -51,20 +53,10 @@ CustomMouseArea { anchors.fill: parent hoverEnabled: true - - // onPressed: event => { - // if ( root.popouts.hasCurrent && !inTopPanel( root.popouts, event.x, event.y )) { - // root.popouts.hasCurrent = false; - // } else if (root.visibilities.sidebar && !inRightPanel( panels.sidebar, event.x, event.y )) { - // root.visibilities.sidebar = false; - // } else if (root.visibilities.dashboard && !inTopPanel( panels.dashboard, event.x, event.y )) { - // root.visibilities.dashboard = false; - // } - // } + propagateComposedEvents: true onContainsMouseChanged: { if (!containsMouse) { - // Only hide if not activated by shortcut if (!osdShortcutActive) { visibilities.osd = false; root.panels.osd.hovered = false; @@ -87,120 +79,42 @@ CustomMouseArea { const dragX = x - dragStart.x; const dragY = y - dragStart.y; - // Show bar in non-exclusive mode on hover + if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) { + root.input.z = 2; + root.panels.drawing.expanded = false; + } + if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin) visibilities.bar = true; if (panels.sidebar.width === 0) { - // Show osd on hover const showOsd = inRightPanel(panels.osd, x, y); - // // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { - visibilities.osd = showOsd; - root.panels.osd.hovered = showOsd; - } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control + if (showOsd) { osdShortcutActive = false; root.panels.osd.hovered = true; } - - // const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; - // - // // Show/hide session on drag - // if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // - // // Show sidebar on drag if in session area and session is nearly fully visible - // if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) - // visibilities.sidebar = true; - // } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { - // // Show sidebar on drag if not in session area - // visibilities.sidebar = true; - // } } else { const outOfSidebar = x < width - panels.sidebar.width; - // Show osd on hover const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); - // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { visibilities.osd = showOsd; root.panels.osd.hovered = showOsd; } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control osdShortcutActive = false; root.panels.osd.hovered = true; } - // - // // Show/hide session on drag - // if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // } - // - // // Hide sidebar on drag - // if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold) - // visibilities.sidebar = false; } - // Show launcher on hover, or show/hide on drag if hover is disabled - // if (Config.launcher.showOnHover) { - // if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y)) - // visibilities.launcher = true; - // } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) { - // if (dragY < -Config.launcher.dragThreshold) - // visibilities.launcher = true; - // else if (dragY > Config.launcher.dragThreshold) - // visibilities.launcher = false; - // } - // - // // Show dashboard on hover - // const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!dashboardShortcutActive) { - // visibilities.dashboard = showDashboard; - // } else if (showDashboard) { - // // If hovering over dashboard area while in shortcut mode, transition to hover control - // dashboardShortcutActive = false; - // } - // - // // Show/hide dashboard on drag (for touchscreen devices) - // if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) { - // if (dragY > Config.dashboard.dragThreshold) - // visibilities.dashboard = true; - // else if (dragY < -Config.dashboard.dragThreshold) - // visibilities.dashboard = false; - // } - // - // // Show utilities on hover - // const showUtilities = inBottomPanel(panels.utilities, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!utilitiesShortcutActive) { - // visibilities.utilities = showUtilities; - // } else if (showUtilities) { - // // If hovering over utilities area while in shortcut mode, transition to hover control - // utilitiesShortcutActive = false; - // } - - // Show popouts on hover if (y < bar.implicitHeight) { bar.checkPopout(x); } } - // Monitor individual visibility changes Connections { function onDashboardChanged() { if (root.visibilities.dashboard) { - // Dashboard became visible, immediately check if this should be shortcut mode const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); if (!inDashboardArea) { root.dashboardShortcutActive = true; @@ -209,20 +123,21 @@ CustomMouseArea { root.visibilities.sidebar = false; root.popouts.hasCurrent = false; } else { - // Dashboard hidden, clear shortcut flag root.dashboardShortcutActive = false; - // root.visibilities.bar = false; } } + function onIsDrawingChanged() { + if (!root.visibilities.isDrawing) + root.drawing.clear(); + } + function onLauncherChanged() { - // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { root.dashboardShortcutActive = false; root.osdShortcutActive = false; root.utilitiesShortcutActive = false; - // Also hide dashboard and OSD if they're not being hovered const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inOsdArea) { @@ -234,13 +149,11 @@ CustomMouseArea { function onOsdChanged() { if (root.visibilities.osd) { - // OSD became visible, immediately check if this should be shortcut mode const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inOsdArea) { root.osdShortcutActive = true; } } else { - // OSD hidden, clear shortcut flag root.osdShortcutActive = false; } } @@ -260,13 +173,11 @@ CustomMouseArea { function onUtilitiesChanged() { if (root.visibilities.utilities) { - // Utilities became visible, immediately check if this should be shortcut mode const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY); if (!inUtilitiesArea) { root.utilitiesShortcutActive = true; } } else { - // Utilities hidden, clear shortcut flag root.utilitiesShortcutActive = false; } } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 7dea587..0c81408 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -11,6 +11,7 @@ import qs.Components.Toast as Toasts import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources import qs.Modules.Settings as Settings +import qs.Modules.Drawing as Drawing import qs.Config Item { @@ -18,6 +19,8 @@ Item { required property Item bar readonly property alias dashboard: dashboard + readonly property alias drawing: drawing + required property Canvas drawingItem readonly property alias launcher: launcher readonly property alias notifications: notifications readonly property alias osd: osd @@ -47,6 +50,16 @@ Item { visibilities: root.visibilities } + Drawing.Wrapper { + id: drawing + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + drawing: root.drawingItem + screen: root.screen + visibilities: root.visibilities + } + Osd.Wrapper { id: osd diff --git a/Modules/Drawing/Background.qml b/Modules/Drawing/Background.qml new file mode 100644 index 0000000..f19ba9a --- /dev/null +++ b/Modules/Drawing/Background.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Shapes +import qs.Components +import qs.Config + +ShapePath { + id: root + + readonly property bool flatten: wrapper.width < rounding * 2 + readonly property real rounding: Appearance.rounding.normal + readonly property real roundingX: flatten ? wrapper.width / 2 : rounding + required property Wrapper wrapper + + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 + + Behavior on fillColor { + CAnim { + } + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: root.wrapper.width - root.roundingX * 2 + relativeY: 0 + } + + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.rounding * 2 + } + + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: -root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: -(root.wrapper.width - root.roundingX * 2) + relativeY: 0 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: -root.roundingX + relativeY: root.rounding + } +} diff --git a/Modules/Drawing/Content.qml b/Modules/Drawing/Content.qml new file mode 100644 index 0000000..4bcffa0 --- /dev/null +++ b/Modules/Drawing/Content.qml @@ -0,0 +1,76 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Config +import qs.Components + +Item { + id: root + + readonly property var colors: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"] + required property Canvas drawing + required property var visibilities + + implicitHeight: huePicker.implicitHeight + 12 + palette.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: huePicker.implicitWidth + Appearance.padding.normal * 2 + + Column { + anchors.centerIn: parent + spacing: 12 + + ColorArcPicker { + id: huePicker + + drawing: root.drawing + } + + GridLayout { + id: palette + + anchors.left: parent.left + anchors.right: parent.right + columns: 5 + rowSpacing: 8 + rows: 2 + + Repeater { + model: root.colors + + delegate: Item { + required property color modelData + readonly property bool selected: Qt.colorEqual(root.drawing.penColor, modelData) + + Layout.fillWidth: true + height: 28 + + CustomRect { + anchors.centerIn: parent + border.color: selected ? "#ffffff" : Qt.rgba(1, 1, 1, 0.28) + border.width: selected ? 3 : 1 + color: "transparent" + height: parent.height + radius: width / 2 + width: parent.height + } + + CustomRect { + anchors.centerIn: parent + border.color: Qt.rgba(0, 0, 0, 0.25) + border.width: Qt.colorEqual(modelData, "#ffffff") ? 1 : 0 + color: modelData + height: 20 + radius: width / 2 + width: 20 + } + + MouseArea { + anchors.fill: parent + + onClicked: root.drawing.penColor = parent.modelData + } + } + } + } + } +} diff --git a/Modules/Drawing/Wrapper.qml b/Modules/Drawing/Wrapper.qml new file mode 100644 index 0000000..6908fd8 --- /dev/null +++ b/Modules/Drawing/Wrapper.qml @@ -0,0 +1,133 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import qs.Components +import qs.Helpers +import qs.Config +import qs.Daemons + +Item { + id: root + + required property Canvas drawing + property bool expanded: true + required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.isDrawing + required property var visibilities + + function show(): void { + visibilities.isDrawing = true; + timer.restart(); + } + + implicitHeight: content.implicitHeight + implicitWidth: 0 + visible: width > 0 + + states: [ + State { + name: "hidden" + when: !root.shouldBeActive + + PropertyChanges { + root.implicitWidth: 0 + } + + PropertyChanges { + icon.opacity: 0 + } + + PropertyChanges { + content.opacity: 0 + } + }, + State { + name: "collapsed" + when: root.shouldBeActive && !root.expanded + + PropertyChanges { + root.implicitWidth: icon.implicitWidth + } + + PropertyChanges { + icon.opacity: 1 + } + + PropertyChanges { + content.opacity: 0 + } + }, + State { + name: "visible" + when: root.shouldBeActive && root.expanded + + PropertyChanges { + root.implicitWidth: content.implicitWidth + } + + PropertyChanges { + icon.opacity: 0 + } + + PropertyChanges { + content.opacity: 1 + } + } + ] + transitions: [ + Transition { + from: "*" + to: "*" + + ParallelAnimation { + Anim { + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitWidth" + target: root + } + + Anim { + duration: Appearance.anim.durations.small + property: "opacity" + target: icon + } + + Anim { + duration: Appearance.anim.durations.small + property: "opacity" + target: content + } + } + } + ] + + Loader { + id: icon + + active: Qt.binding(() => root.shouldBeActive || root.visible) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: content.contentItem.height + opacity: 1 + + sourceComponent: MaterialIcon { + font.pointSize: 14 + text: "arrow_forward_ios" + } + } + + Loader { + id: content + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + sourceComponent: Content { + drawing: root.drawing + visibilities: root.visibilities + } + + Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) + } +} diff --git a/Modules/Shortcuts.qml b/Modules/Shortcuts.qml index 7da6200..ced923c 100644 --- a/Modules/Shortcuts.qml +++ b/Modules/Shortcuts.qml @@ -24,6 +24,15 @@ Scope { } } + CustomShortcut { + name: "toggle-drawing" + + onPressed: { + const visibilities = Visibilities.getForActive(); + visibilities.isDrawing = !visibilities.isDrawing; + } + } + CustomShortcut { name: "toggle-nc"