feat: drawing popout now clips strokes again + add redo + add undo and redo keybinds
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Python / verify (pull_request) Failing after 1m41s
Rust / build (pull_request) Successful in 1m15s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m6s
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Python / verify (pull_request) Failing after 1m41s
Rust / build (pull_request) Successful in 1m15s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m6s
This commit is contained in:
@@ -6,6 +6,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
GuiPrivate
|
||||
Quick
|
||||
CanvasPainter
|
||||
CanvasPainterTools
|
||||
QuickPrivate
|
||||
QuickLayouts
|
||||
QuickLayoutsPrivate
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user