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

This commit is contained in:
2026-10-30 14:16:19 +01:00
parent c5608dc9b9
commit 566b71d386
12 changed files with 467 additions and 539 deletions
+6
View File
@@ -29,3 +29,9 @@ qml_module(ZShell-internal
PkgConfig::GL
ZShell-config
)
qt_add_custom_brush_shaders(ZShell-internal "internal_brush_shaders"
PREFIX "/"
FILES
shaders/strokeholemask.frag
)
@@ -0,0 +1,22 @@
#version 440
QC_INCLUDE "customfrag.glsl"
float sdRoundRect(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + vec2(r);
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
}
void main() {
float keep = 1.0;
if (data4.x > 0.5) {
vec2 p = fragCoord - data1.xy;
float feather = max(data3.y, 0.0001);
keep = smoothstep(-feather, feather, sdRoundRect(p, data1.zw, data3.x));
}
float a = keep * data2.w * globalAlpha * antialiasingAlpha();
fragColor = vec4(data2.rgb * a, a);
}
+39 -12
View File
@@ -71,11 +71,23 @@ static QCanvasPath buildStrokePath(
void StrokeCanvasItem::undoLastGroup() {
if (m_strokes.isEmpty()) return;
const int lastGroup = m_strokes.last().groupId;
QVector<Stroke> group;
for (int i = static_cast<int>(m_strokes.size()) - 1;
i >= 0 && m_strokes[i].groupId == lastGroup;
--i)
m_strokes.remove(i);
group.prepend(m_strokes.takeAt(i));
m_redoStack.append(group);
update();
}
void StrokeCanvasItem::redo() {
if (m_redoStack.isEmpty()) return;
m_strokes.append(m_redoStack.takeLast());
update();
}
@@ -106,19 +118,14 @@ void StrokeCanvasItem::setHoverPoint(const QPointF& point) {
emit hoverPointChanged();
}
void StrokeCanvasItem::showHover(qreal x, qreal y) {
const QPointF newPoint{x, y};
const bool pointChanged = (m_hoverPoint != newPoint);
const bool visibleChanged = !m_hoverVisible;
void StrokeCanvasItem::showHover() {
if (m_hoverVisible) return;
if (pointChanged || visibleChanged) {
m_hoverVisible = true;
m_hoverPoint = newPoint;
if (pointChanged) emit hoverPointChanged();
if (visibleChanged) emit hoverVisibleChanged();
update();
}
m_hoverVisible = true;
update();
emit hoverVisibleChanged();
}
void StrokeCanvasItem::hideHover() {
@@ -139,6 +146,24 @@ void StrokeCanvasItem::setPenWidth(qreal width) {
emit penWidthChanged();
}
void StrokeCanvasItem::setHole(const QRectF& hole) {
if (m_hole == hole) return;
m_hole = hole;
update();
emit holeChanged();
}
void StrokeCanvasItem::setHoleRadius(qreal radius) {
if (qFuzzyCompare(m_holeRadius, radius)) return;
m_holeRadius = radius;
update();
emit holeRadiusChanged();
}
void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) {
m_isDrawing = true;
m_currentStroke.points.clear();
@@ -199,6 +224,7 @@ void StrokeCanvasItem::endStroke() {
m_currentStroke.points.clear();
m_strokes.append(m_currentStroke);
m_currentStroke = {};
m_redoStack.clear();
update();
}
@@ -206,6 +232,7 @@ void StrokeCanvasItem::endStroke() {
void StrokeCanvasItem::clear() {
m_strokes.clear();
m_currentStroke = {};
m_redoStack.clear();
update();
}
+16 -1
View File
@@ -3,6 +3,7 @@
#include <QCanvasPainterItem>
#include <QColor>
#include <QPointF>
#include <QRectF>
#include <QVector>
#include <qcolor.h>
#include <qcontainerfwd.h>
@@ -27,6 +28,10 @@ class StrokeCanvasItem : public QCanvasPainterItem {
hoverPointChanged)
Q_PROPERTY(
qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
Q_PROPERTY(QRectF hole READ hole WRITE setHole NOTIFY holeChanged)
Q_PROPERTY(
qreal holeRadius READ holeRadius WRITE setHoleRadius NOTIFY
holeRadiusChanged)
public:
explicit StrokeCanvasItem(QQuickItem* parent = nullptr);
@@ -37,15 +42,20 @@ class StrokeCanvasItem : public QCanvasPainterItem {
void setHoverVisible(bool visible);
void setHoverPoint(const QPointF& point);
Q_INVOKABLE void showHover(qreal x, qreal y);
Q_INVOKABLE void showHover();
Q_INVOKABLE void hideHover();
Q_INVOKABLE void undoLastGroup();
Q_INVOKABLE void redo();
[[nodiscard]] QColor penColor() const { return m_penColor; }
[[nodiscard]] qreal penWidth() const { return m_penWidth; }
[[nodiscard]] QRectF hole() const { return m_hole; }
[[nodiscard]] qreal holeRadius() const { return m_holeRadius; }
void setPenColor(const QColor& color);
void setPenWidth(qreal width);
void setHole(const QRectF& hole);
void setHoleRadius(qreal radius);
Q_INVOKABLE void clear();
@@ -56,6 +66,8 @@ class StrokeCanvasItem : public QCanvasPainterItem {
signals:
void penColorChanged();
void penWidthChanged();
void holeChanged();
void holeRadiusChanged();
void hoverVisibleChanged();
void hoverPointChanged();
@@ -72,9 +84,12 @@ class StrokeCanvasItem : public QCanvasPainterItem {
qreal m_penWidth = 4.0;
bool m_isDrawing = false;
QRectF m_hole;
qreal m_holeRadius = 0.0;
int m_nextGroupId = 0;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
QVector<QVector<Stroke>> m_redoStack;
};
}; // namespace ZShell::internal
+104 -11
View File
@@ -1,11 +1,98 @@
#include "strokecanvasrenderer.hpp"
#include "strokecanvasitem.hpp"
#include <QCanvasPainter>
#include <QQuickWindow>
#include <QVector4D>
#include <qcolor.h>
#include <QSet>
namespace ZShell::internal {
static void drawStroke(
using Qt::StringLiterals::operator""_s;
static const QString kStrokeHoleShader = u":/shaders/strokeholemask.frag.qsb"_s;
namespace {
struct HoleUniforms {
QVector4D data1; // center.xy, halfSize.xy
QVector4D data3; // x: radius, y: feather in logical px
QVector4D data4; // x: hole enabled
};
static HoleUniforms holeUniforms(
const QRectF& hole, qreal radius, qreal devicePixelRatio) {
const bool active = hole.isValid() && hole.width() > 0 && hole.height() > 0;
if (!active) return {QVector4D(), QVector4D(), QVector4D()};
const QPointF center = hole.center();
const QPointF half(hole.width() * 0.5, hole.height() * 0.5);
const qreal maxRadius = qMin(half.x(), half.y());
return {
QVector4D(
static_cast<float>(center.x()),
static_cast<float>(center.y()),
static_cast<float>(half.x()),
static_cast<float>(half.y())),
QVector4D(
static_cast<float>(qMin(radius, maxRadius)),
static_cast<float>(1.0 / devicePixelRatio),
0.0f,
0.0f),
QVector4D(1.0, 0.0, 0.0, 0.0),
};
}
static void applyHole(QCanvasCustomBrush& brush, const HoleUniforms& u) {
brush.setData1(u.data1);
brush.setData3(u.data3);
brush.setData4(u.data4);
}
}; // namespace
void StrokeCanvasRenderer::updateBrushHole() {
const HoleUniforms uniforms =
holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio);
if (uniforms.data1 == m_lastHoleData1 &&
uniforms.data3 == m_lastHoleData3 && uniforms.data4 == m_lastHoleData4)
return;
m_lastHoleData1 = uniforms.data1;
m_lastHoleData3 = uniforms.data3;
m_lastHoleData4 = uniforms.data4;
++m_holeToken;
for (auto& entry : m_brushes) {
if (entry.holeToken == m_holeToken) continue;
applyHole(entry.brush, uniforms);
entry.holeToken = m_holeToken;
}
}
QCanvasCustomBrush& StrokeCanvasRenderer::brushFor(const QColor& color) {
const QRgb key = color.rgba();
if (auto it = m_brushes.find(key); it != m_brushes.end()) return it->brush;
const HoleUniforms uniforms =
holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio);
BrushEntry entry;
entry.brush.setFragmentShader(kStrokeHoleShader);
applyHole(entry.brush, uniforms);
entry.brush.setData2(
QVector4D(color.redF(), color.greenF(), color.blueF(), color.alphaF()));
entry.holeToken = m_holeToken;
return m_brushes.insert(key, entry)->brush;
}
void StrokeCanvasRenderer::drawStroke(
QCanvasPainter* painter,
const QVector<QPointF>& points,
const QColor& color,
@@ -13,8 +100,10 @@ static void drawStroke(
bool polyline) {
if (points.isEmpty()) return;
painter->setStrokeStyle(color);
painter->setFillStyle(color);
QCanvasCustomBrush& brush = brushFor(color);
painter->setStrokeStyle(brush);
painter->setFillStyle(brush);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -65,7 +154,7 @@ static void drawStroke(
painter->stroke();
}
static void drawHoverCursor(
void StrokeCanvasRenderer::drawHoverCursor(
QCanvasPainter* painter,
const QPointF& point,
qreal penWidth,
@@ -74,7 +163,7 @@ static void drawHoverCursor(
const qreal radius = penWidth * 0.5;
if (isDrawing) {
painter->setFillStyle(penColor);
painter->setFillStyle(brushFor(penColor));
painter->beginPath();
painter->circle(point, radius);
painter->fill();
@@ -85,7 +174,7 @@ static void drawHoverCursor(
const bool useDashes = penWidth > 10.0;
auto drawOutline = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color);
painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -117,7 +206,7 @@ static void drawHoverCursor(
};
auto drawCrosshair = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color);
painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -148,8 +237,11 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
m_penColor = canvas->m_penColor;
m_penWidth = canvas->m_penWidth;
m_hole = canvas->m_hole;
m_holeRadius = canvas->m_holeRadius;
m_devicePixelRatio =
canvas->window() ? canvas->window()->effectiveDevicePixelRatio() : 1.0;
// Detect removed strokes: present in renderer but no longer in canvas
QSet<int> canvasGroupIds;
for (const auto& stroke : canvas->m_strokes)
canvasGroupIds.insert(stroke.groupId);
@@ -162,7 +254,6 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
}
}
// Detect added strokes: present in canvas but not yet in renderer
QSet<int> rendererGroupIds;
for (const auto& stroke : m_strokes)
rendererGroupIds.insert(stroke.groupId);
@@ -180,6 +271,8 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
}
void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
updateBrushHole();
for (int id : m_pendingGroupRemovals)
painter->removePathGroup(id);
m_pendingGroupRemovals.clear();
@@ -187,8 +280,8 @@ void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
painter->clearRect(0, 0, width(), height());
for (const auto& stroke : m_strokes) {
painter->setStrokeStyle(stroke.color);
painter->setFillStyle(stroke.color);
painter->setStrokeStyle(brushFor(stroke.color));
painter->setFillStyle(brushFor(stroke.color));
painter->setLineWidth(stroke.width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -1,26 +1,58 @@
#pragma once
#include <QCanvasCustomBrush>
#include <QCanvasPainterItemRenderer>
#include <QHash>
#include <QRectF>
#include <QVector4D>
#include <qcontainerfwd.h>
#include "stroke.hpp"
namespace ZShell::internal {
struct BrushEntry {
QCanvasCustomBrush brush;
qint64 holeToken = -1; // hole generation this brush was built with
};
class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer {
public:
void synchronizeData(QCanvasPainterItem* item) override;
void paint(QCanvasPainter* painter) override;
private:
QCanvasCustomBrush& brushFor(const QColor& color);
void updateBrushHole();
void drawStroke(
QCanvasPainter* painter,
const QVector<QPointF>& points,
const QColor& color,
qreal width,
bool polyline);
void drawHoverCursor(
QCanvasPainter* painter,
const QPointF& point,
qreal penWidth,
QColor penColor,
bool isDrawing);
QColor m_penColor;
qreal m_penWidth = 4.0;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
bool m_isDrawing = false;
QRectF m_hole;
qreal m_holeRadius = 0.0;
qreal m_devicePixelRatio = 1.0;
QVector4D m_lastHoleData1;
QVector4D m_lastHoleData3;
QVector4D m_lastHoleData4;
qint64 m_holeToken = 0;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
QVector<int> m_pendingGroupRemovals;
QHash<QRgb, BrushEntry> m_brushes;
};
}; // namespace ZShell::internal