12.3b: rulers, the grid and guides on the canvas, guide drags (Compositor 1.1.7)

This commit is contained in:
雷电芽衣 committed 2026-09-26 20:55:55 -04:00
1 parent 338451fc35
commit 78bfa2445d
19 files changed
+718 -10

No files matched your search

+15 -1
View File
@@ -1,4 +1,5 @@
#include "ContentView.h"
#include "UI/CanvasRulers.h"
#include "UI/ColorPaletteControls.h"
#include "Logging.h"
#include "Document/ProjectWorkspace.h"
@@ -245,7 +246,18 @@ ContentView::ContentView(EditorSession &session, ProjectController *projects, QW
middle->setSpacing(0);
middle->addWidget(rail);
middle->addWidget(divider(QFrame::VLine, this));
middle->addWidget(canvas, 1);
// The rulers frame the canvas above and left, Swift's stacks.
auto *framed = new QWidget(this);
auto *rulers = new QGridLayout(framed);
rulers->setContentsMargins(0, 0, 0, 0);
rulers->setSpacing(0);
m_rulers = {new CanvasRulerCorner(framed), new CanvasRulerView(session, CanvasGuide::Axis::horizontal, *m_canvas, framed),
new CanvasRulerView(session, CanvasGuide::Axis::vertical, *m_canvas, framed)};
rulers->addWidget(m_rulers[0], 0, 0);
rulers->addWidget(m_rulers[1], 0, 1);
rulers->addWidget(m_rulers[2], 1, 0);
rulers->addWidget(canvas, 1, 1);
middle->addWidget(framed, 1);
m_layersPanel->setFixedWidth(int(layersPanelWidth()));
middle->addWidget(new PanelResizeEdge(*m_layersPanel, this));
middle->addWidget(m_layersPanel);
@@ -372,6 +384,8 @@ void ContentView::synchronize()
}
const std::optional<CanvasDocument> &document = m_session.document();
showWelcome(!document);
for (QWidget *ruler : m_rulers)
ruler->setVisible(m_session.showsRulers() && document);
m_zoom->setVisible(document.has_value());
m_dimensions->setVisible(document.has_value());
if (document) {
+2
View File
@@ -69,6 +69,8 @@ private:
LayersPanel *const m_layersPanel;
// An accent ring while a drop may land.
QWidget *const m_dropRing;
// Swift 1.1.7's rulers: the corner, then the two strips.
std::vector<QWidget *> m_rulers;
ProjectWorkspace *m_workspace = nullptr;
// A document that appears hands the canvas the keys.
bool m_hadDocument = false;
+2
View File
@@ -1,5 +1,7 @@
// Swift's Guides extension, and the View menu's guide state.
public:
// Cyan, Photoshop's default guide colour.
static inline const QColor guideColor = QColor::fromRgbF(0, 1, 1, 0.9f);
static constexpr double guideHitDistance = 5;
bool showsGrid() const { return m_showsGrid; }
void setShowsGrid(bool shows);
+56
View File
@@ -0,0 +1,56 @@
#include "Rendering/EditorCanvas.h"
// Swift's isOverRuler: rulers sit above and left.
bool CanvasView::isOverRuler(QPointF point) const
{
return m_session.showsRulers() && (point.x() < 0 || point.y() < 0);
}
std::optional<double> CanvasView::documentPosition(CanvasGuide::Axis axis, QPointF point) const
{
const std::optional<CanvasDocument> &document = m_session.document();
if (!document)
return std::nullopt;
const QPointF pixel = m_session.viewport.documentPoint(point, document->size());
return axis == CanvasGuide::Axis::vertical ? pixel.x() : pixel.y();
}
// A press on a guide moves it, before any layer.
bool CanvasView::beginGuideDrag(QPointF point)
{
const std::optional<CanvasGuide> guide = m_session.canEditGuides() ? m_session.hitGuide(point) : std::nullopt;
if (!guide)
return false;
m_session.beginGuideMove(*guide);
m_guideDragging = true;
m_dragCursor = QCursor(guide->axis == CanvasGuide::Axis::vertical ? Qt::SizeHorCursor : Qt::SizeVerCursor);
setCursor(*m_dragCursor);
return true;
}
// A move without the left button: Qt lost the release.
bool CanvasView::guideMove(QPointF point, Qt::MouseButtons buttons)
{
if (!m_guideDragging)
return false;
if (!buttons.testFlag(Qt::LeftButton)) {
endGuideDrag(point);
return true;
}
const std::optional<GuideDrag> &drag = m_session.guideDrag();
if (const std::optional<double> position = drag ? documentPosition(drag->axis, point) : std::nullopt)
m_session.moveGuideDrag(*position);
synchronizeDisplay();
return true;
}
// Released over a ruler, the guide goes.
void CanvasView::endGuideDrag(QPointF point)
{
if (!m_guideDragging)
return;
m_guideDragging = false;
m_dragCursor.reset();
m_session.finishGuideDrag(isOverRuler(point));
updateCursor();
}
+5
View File
@@ -111,6 +111,11 @@ void CanvasView::pressKey(QKeyEvent *event, int physical)
m_session.cancelGradient();
} else if (cropKey(*event)) {
return;
} else if (event->key() == Qt::Key_Escape && m_session.guideDrag()) {
m_session.cancelGuideDrag();
m_guideDragging = false;
m_dragCursor.reset();
updateCursor();
} else if ((event->key() == Qt::Key_Escape || enter) && m_session.transformEdit()) {
// The keys end a drag with its edit, cursor included.
m_transformDrag.reset();
+9 -4
View File
@@ -29,7 +29,8 @@ bool CanvasView::event(QEvent *event)
if (event->type() == QEvent::NativeGesture) {
const auto *gesture = static_cast<QNativeGestureEvent *>(event);
// A pinch, as Swift's magnify; not mid-drag or mid-stroke.
if (gesture->gestureType() == Qt::ZoomNativeGesture && !m_transformDrag && !m_cropDrag && !m_session.brushStroke() && !m_session.warpStroke()) {
if (gesture->gestureType() == Qt::ZoomNativeGesture && !m_transformDrag && !m_cropDrag && !m_guideDragging && !m_session.brushStroke()
&& !m_session.warpStroke()) {
m_session.zoom(m_session.viewport.zoom() * (1 + gesture->value()), gesture->position());
event->accept();
return true;
@@ -97,7 +98,7 @@ void CanvasView::consumeFocusRequest(int request)
void CanvasView::wheelEvent(QWheelEvent *event)
{
if (!m_session.document() || m_transformDrag || m_cropDrag || m_session.brushStroke() || m_session.warpStroke())
if (!m_session.document() || m_transformDrag || m_cropDrag || m_guideDragging || m_session.brushStroke() || m_session.warpStroke())
return;
// Trackpads report pixels; a wheel reports notches, lines each.
const bool precise = !event->pixelDelta().isNull();
@@ -146,7 +147,8 @@ void CanvasView::mousePressEvent(QMouseEvent *event)
m_lastDragPoint = point;
updateCursor();
} else if (m_session.tool() == NavigationTool::move) {
beginTransformDrag(point, event->modifiers());
if (!beginGuideDrag(point))
beginTransformDrag(point, event->modifiers());
} else if (isSelectionTool(m_session.tool())) {
lassoMouseDown(point, event->modifiers());
updateCursor();
@@ -211,7 +213,7 @@ void CanvasView::mouseMoveEvent(QMouseEvent *event)
if (m_inlineTextEditor && !held)
m_inlineTextEditor->release();
if (pickingMove(point, event->buttons()) || targetingMove(point, event->buttons(), event->modifiers())
|| cropMove(point, event->buttons(), event->modifiers()))
|| cropMove(point, event->buttons(), event->modifiers()) || guideMove(point, event->buttons()))
return;
if (m_transformDrag) {
dragTransform(point, event->modifiers());
@@ -280,6 +282,7 @@ void CanvasView::mouseReleaseEvent(QMouseEvent *event)
return;
}
m_session.snapGuides = {};
endGuideDrag(event->position());
pickingRelease();
targetingRelease();
brushMouseUp(event->position());
@@ -323,6 +326,8 @@ void CanvasView::focusOutEvent(QFocusEvent *event)
updateBrushCursor();
cancelTransformDrag();
endCropDrag();
// A lost release: the guide stays where it was dragged.
endGuideDrag(QPointF(0, 0));
m_gradientDrag.reset();
m_session.cancelShape();
endSelectionGestures();
+3
View File
@@ -178,6 +178,9 @@ QCursor CanvasView::transformCursor(QPointF point, Qt::KeyboardModifiers modifie
const std::optional<TransformOverlayGeometry> geometry = m_overlay.geometry();
const std::optional<TransformDrag::Mode> hit = geometry ? geometry->hit(point) : std::nullopt;
if (!hit) {
// Guides lie under the handles, over a layer's drag.
if (const std::optional<CanvasGuide> guide = m_session.hitGuide(point))
return QCursor(guide->axis == CanvasGuide::Axis::vertical ? Qt::SizeHorCursor : Qt::SizeVerCursor);
if (!pressMovesLayer(point, modifiers))
return QCursor(Qt::ArrowCursor);
return duplicate ? duplicateCursor(ratio) : moveCursor(ratio);
+5
View File
@@ -161,6 +161,11 @@ bool CanvasView::synchronizeDisplay()
} else if (m_displayedTool != m_session.tool() || m_displayedBlocked != blocked || m_displayedSelectionCursor != cursorState) {
updateCursor();
}
// The grid and guides redraw the view, as Swift's observation.
if (const GuidesShown lines{m_session.showsGrid(), m_session.showsGuides(), m_session.displayedGuides()}; m_displayedGuideLines != lines) {
update();
m_displayedGuideLines = lines;
}
// A shape draft redraws the view, as Swift's observation does.
if (const std::optional<ShapeShown> shape = shownShape(); m_displayedShape != shape) {
update();
+16
View File
@@ -58,6 +58,9 @@ public:
const SampleRingOverlay &sampleRing() const { return m_sampleRing; }
// The text typed on the canvas, while one is open.
InlineTextEditor *inlineTextEditor() const { return m_inlineTextEditor.get(); }
// Released on the rulers, just outside the canvas.
bool isOverRuler(QPointF point) const;
std::optional<double> documentPosition(CanvasGuide::Axis axis, QPointF point) const;
protected:
bool event(QEvent *event) override;
@@ -234,6 +237,10 @@ private:
void dragCrop(QPointF point, Qt::KeyboardModifiers modifiers);
void endCropDrag();
bool cropKey(const QKeyEvent &key);
// The Move tool's guide drags (EditorCanvas+Guides.cpp).
bool beginGuideDrag(QPointF point);
bool guideMove(QPointF point, Qt::MouseButtons buttons);
void endGuideDrag(QPointF point);
// Swift's targeted drag: right raises; Ctrl drags the hue.
bool targetingMove(QPointF point, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers);
void targetingRelease();
@@ -348,4 +355,13 @@ private:
std::optional<CropSnap> m_cropSnap;
// The crop frame as last shown, in view points.
std::optional<QRectF> m_displayedCrop;
bool m_guideDragging = false;
// The grid and guides as drawn: a change redraws.
struct GuidesShown {
bool grid;
bool guides;
std::vector<CanvasGuide> lines;
friend bool operator==(const GuidesShown &, const GuidesShown &) = default;
};
std::optional<GuidesShown> m_displayedGuideLines;
};
+66
View File
@@ -181,6 +181,8 @@ std::optional<std::pair<QPointF, QPointF>> TransformOverlay::gradientLine() cons
void TransformOverlay::draw(QPainter &context, const QPalette &palette) const
{
drawLayoutGrid(context);
drawGuides(context);
if (m_session.tool() == NavigationTool::crop)
drawCrop(context);
else if (const std::optional<std::pair<QPointF, QPointF>> line = gradientLine())
@@ -359,6 +361,70 @@ void TransformOverlay::drawTransformHandles(QPainter &context, const QPalette &p
context.restore();
}
// The layout grid: solid majors, dotted subdivisions when roomy.
void TransformOverlay::drawLayoutGrid(QPainter &context) const
{
const std::optional<CanvasDocument> &document = m_session.document();
if (!m_session.showsGrid() || !document)
return;
const QSizeF size = document->size();
const QTransform map = documentToView();
const double hairline = 1 / std::max(m_session.viewport.backingScale, 1.0);
const auto lines = [&](bool majors) {
QPainterPath path;
for (const double x : LayoutGrid::lines(size.width())) {
if (LayoutGrid::isMajor(x) == majors) {
path.moveTo(map.map(QPointF(x, 0)));
path.lineTo(map.map(QPointF(x, size.height())));
}
}
for (const double y : LayoutGrid::lines(size.height())) {
if (LayoutGrid::isMajor(y) == majors) {
path.moveTo(map.map(QPointF(0, y)));
path.lineTo(map.map(QPointF(size.width(), y)));
}
}
return path;
};
context.save();
context.setRenderHint(QPainter::Antialiasing, true);
context.setBrush(Qt::NoBrush);
if (LayoutGrid::step * m_session.viewport.pointsPerPixel() >= 4) {
QPen dotted(QColor::fromRgbF(0.55, 0.55, 0.55, 0.28), hairline, Qt::CustomDashLine, Qt::FlatCap);
// One point on, two off: Qt counts in pen widths.
dotted.setDashPattern({1 / hairline, 2 / hairline});
context.setPen(dotted);
context.drawPath(lines(false));
}
context.setPen(QPen(QColor::fromRgbF(0.7, 0.7, 0.7, 0.45), hairline, Qt::SolidLine, Qt::FlatCap));
context.drawPath(lines(true));
context.restore();
}
// Guides span the whole view, past the document too.
void TransformOverlay::drawGuides(QPainter &context) const
{
const std::optional<CanvasDocument> &document = m_session.document();
if (!m_session.showsGuides() || !document)
return;
const std::vector<CanvasGuide> guides = m_session.displayedGuides();
if (guides.empty())
return;
const CanvasViewport &viewport = m_session.viewport;
const QSizeF view = viewport.viewSize;
context.save();
context.setRenderHint(QPainter::Antialiasing, true);
context.setPen(QPen(EditorSession::guideColor, 1 / std::max(viewport.backingScale, 1.0), Qt::SolidLine, Qt::FlatCap));
for (const CanvasGuide &guide : guides) {
const QPointF at = viewport.viewPoint(QPointF(guide.position, guide.position), document->size());
if (guide.axis == CanvasGuide::Axis::vertical)
context.drawLine(QPointF(at.x(), 0), QPointF(at.x(), view.height()));
else
context.drawLine(QPointF(0, at.y()), QPointF(view.width(), at.y()));
}
context.restore();
}
// While a move snaps, a line along what it met.
void TransformOverlay::drawSnapGuides(QPainter &context, const QPalette &palette) const
{
+2
View File
@@ -51,6 +51,8 @@ public:
private:
QTransform documentToView() const;
void drawLayoutGrid(QPainter &context) const;
void drawGuides(QPainter &context) const;
void drawTransformHandles(QPainter &context, const QPalette &palette) const;
void drawGradientLine(QPainter &context, QPointF start, QPointF end) const;
void drawCrop(QPainter &context) const;
+141
View File
@@ -0,0 +1,141 @@
#include "UI/CanvasRulers.h"
#include "Rendering/EditorCanvas.h"
#include <QMouseEvent>
#include <QPainter>
#include <array>
#include <cmath>
CanvasRulerCorner::CanvasRulerCorner(QWidget *parent) : QWidget(parent)
{
setFixedSize(CanvasRuler::thickness, CanvasRuler::thickness);
}
void CanvasRulerCorner::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(rect(), QColor::fromRgbF(0.2, 0.2, 0.2));
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(QPen(QColor::fromRgbF(1, 1, 1, 0.28), 1));
const double side = CanvasRuler::thickness;
painter.drawLine(QPointF(5, side - 4), QPointF(side - 4, 5));
}
CanvasRulerView::CanvasRulerView(EditorSession &session, CanvasGuide::Axis axis, CanvasView &canvas, QWidget *parent)
: QWidget(parent), m_session(session), m_axis(axis), m_canvas(canvas)
{
const bool vertical = axis == CanvasGuide::Axis::vertical;
setAccessibleName(vertical ? QStringLiteral("Vertical ruler") : QStringLiteral("Horizontal ruler"));
setCursor(vertical ? Qt::SizeHorCursor : Qt::SizeVerCursor);
if (vertical)
setFixedWidth(CanvasRuler::thickness);
else
setFixedHeight(CanvasRuler::thickness);
connect(&session, &EditorSession::changed, this, qOverload<>(&QWidget::update));
}
double CanvasRulerView::majorStep(double pointsPerPixel)
{
const double target = 70 / std::max(pointsPerPixel, 0.0001);
static constexpr std::array<double, 18> nice{1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1'000, 2'000, 2'500, 5'000, 10'000, 20'000, 25'000};
for (const double step : nice) {
if (step >= target)
return step;
}
return 50'000;
}
QString CanvasRulerView::label(double value)
{
return QString::number(qint64(std::round(value)));
}
void CanvasRulerView::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(rect(), QColor::fromRgbF(0.2, 0.2, 0.2));
const std::optional<CanvasDocument> &document = m_session.document();
if (!document)
return;
const bool horizontal = m_axis == CanvasGuide::Axis::horizontal;
const QSizeF size = document->size();
const CanvasViewport &viewport = m_session.viewport;
const double step = majorStep(viewport.pointsPerPixel());
const double minor = step / 10;
const double hairline = 1 / std::max(devicePixelRatioF(), 1.0);
// The canvas's own points, wherever the ruler sits beside it.
const QPointF offset = inCanvas(QPointF(0, 0));
const QPointF from = viewport.documentPoint(offset, size), to = viewport.documentPoint(offset + QPointF(width(), height()), size);
const double start = horizontal ? from.x() : from.y(), end = horizontal ? to.x() : to.y();
const double first = std::floor(std::min(start, end) / minor) * minor, last = std::ceil(std::max(start, end) / minor) * minor;
if (!(minor > 0) || !std::isfinite(first) || !std::isfinite(last))
return;
QFont font = painter.font();
font.setPixelSize(8);
painter.setFont(font);
const QColor tick = QColor::fromRgbF(0.62, 0.62, 0.62), words = QColor::fromRgbF(0.78, 0.78, 0.78);
for (double value = first; value <= last + 0.001; value += minor) {
const QPointF at = viewport.viewPoint(QPointF(value, value), size) - offset;
const double view = horizontal ? at.x() : at.y();
const double remainder = std::abs(std::remainder(value, step));
const bool major = remainder < 0.001 || std::abs(remainder - step) < 0.001;
const bool middle = !major && std::abs(std::remainder(value, step / 2)) < 0.001;
const double length = major ? 8 : middle ? 5 : 3;
painter.fillRect(horizontal ? QRectF(view - hairline / 2, height() - length, hairline, length)
: QRectF(width() - length, view - hairline / 2, length, hairline),
tick);
if (!major)
continue;
const QString text = label(value);
const QSizeF drawn = painter.fontMetrics().size(Qt::TextSingleLine, text);
painter.setPen(words);
if (horizontal) {
painter.drawText(QRectF(QPointF(view + 2, 0), drawn), Qt::AlignLeft | Qt::AlignTop, text);
} else {
// Turned along the tick, as Swift draws it.
painter.save();
painter.translate(1, view + 2);
painter.rotate(-90);
painter.drawText(QRectF(QPointF(-drawn.width(), 0), drawn), Qt::AlignLeft | Qt::AlignTop, text);
painter.restore();
}
}
const QColor edge = QColor::fromRgbF(0.08, 0.08, 0.08);
painter.fillRect(horizontal ? QRectF(0, height() - hairline, width(), hairline) : QRectF(width() - hairline, 0, hairline, height()), edge);
}
// The canvas is a sibling: points go by the screen.
QPointF CanvasRulerView::inCanvas(QPointF point) const
{
return m_canvas.mapFromGlobal(mapToGlobal(point));
}
std::optional<double> CanvasRulerView::documentPosition(QPointF point) const
{
return m_canvas.documentPosition(m_axis, inCanvas(point));
}
void CanvasRulerView::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton)
return;
m_canvas.setFocus(Qt::MouseFocusReason);
const std::optional<double> position = documentPosition(event->position());
if (!m_session.canEditGuides() || !position)
return;
m_session.beginGuideCreation(m_axis, *position);
}
void CanvasRulerView::mouseMoveEvent(QMouseEvent *event)
{
if (!m_session.guideDrag())
return;
if (const std::optional<double> position = documentPosition(event->position()))
m_session.moveGuideDrag(*position);
}
void CanvasRulerView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || !m_session.guideDrag())
return;
m_session.finishGuideDrag(m_canvas.isOverRuler(inCanvas(event->position())));
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "Document/EditorSession.h"
#include <QWidget>
class CanvasView;
namespace CanvasRuler {
inline constexpr int thickness = 18;
}
// The square where the two rulers meet.
class CanvasRulerCorner : public QWidget {
Q_OBJECT
public:
explicit CanvasRulerCorner(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
};
// Swift's CanvasRulerNSView: ticks in document pixels; drag out a guide.
class CanvasRulerView : public QWidget {
Q_OBJECT
public:
CanvasRulerView(EditorSession &session, CanvasGuide::Axis axis, CanvasView &canvas, QWidget *parent = nullptr);
// Numbered ticks about 70 points apart, 1-2-5 steps.
static double majorStep(double pointsPerPixel);
static QString label(double value);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
QPointF inCanvas(QPointF point) const;
std::optional<double> documentPosition(QPointF point) const;
EditorSession &m_session;
const CanvasGuide::Axis m_axis;
CanvasView &m_canvas;
};