summaryrefslogtreecommitdiffstats
path: root/src/display/control
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/display/control
parentInitial commit. (diff)
downloadinkscape-upstream.tar.xz
inkscape-upstream.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/display/control')
-rw-r--r--src/display/control/README112
-rw-r--r--src/display/control/canvas-item-bpath.cpp223
-rw-r--r--src/display/control/canvas-item-bpath.h80
-rw-r--r--src/display/control/canvas-item-buffer.h50
-rw-r--r--src/display/control/canvas-item-catchall.cpp66
-rw-r--r--src/display/control/canvas-item-catchall.h52
-rw-r--r--src/display/control/canvas-item-context.cpp43
-rw-r--r--src/display/control/canvas-item-context.h68
-rw-r--r--src/display/control/canvas-item-ctrl.cpp1181
-rw-r--r--src/display/control/canvas-item-ctrl.h105
-rw-r--r--src/display/control/canvas-item-curve.cpp207
-rw-r--r--src/display/control/canvas-item-curve.h74
-rw-r--r--src/display/control/canvas-item-drawing.cpp220
-rw-r--r--src/display/control/canvas-item-drawing.h94
-rw-r--r--src/display/control/canvas-item-enums.h89
-rw-r--r--src/display/control/canvas-item-grid.cpp522
-rw-r--r--src/display/control/canvas-item-grid.h123
-rw-r--r--src/display/control/canvas-item-group.cpp118
-rw-r--r--src/display/control/canvas-item-group.h70
-rw-r--r--src/display/control/canvas-item-guideline.cpp312
-rw-r--r--src/display/control/canvas-item-guideline.h104
-rw-r--r--src/display/control/canvas-item-ptr.h36
-rw-r--r--src/display/control/canvas-item-quad.cpp170
-rw-r--r--src/display/control/canvas-item-quad.h69
-rw-r--r--src/display/control/canvas-item-rect.cpp262
-rw-r--r--src/display/control/canvas-item-rect.h77
-rw-r--r--src/display/control/canvas-item-text.cpp303
-rw-r--r--src/display/control/canvas-item-text.h89
-rw-r--r--src/display/control/canvas-item.cpp334
-rw-r--r--src/display/control/canvas-item.h169
-rw-r--r--src/display/control/canvas-page.cpp282
-rw-r--r--src/display/control/canvas-page.h78
-rw-r--r--src/display/control/canvas-temporary-item-list.cpp72
-rw-r--r--src/display/control/canvas-temporary-item-list.h58
-rw-r--r--src/display/control/canvas-temporary-item.cpp53
-rw-r--r--src/display/control/canvas-temporary-item.h59
-rw-r--r--src/display/control/snap-indicator.cpp648
-rw-r--r--src/display/control/snap-indicator.h85
38 files changed, 6757 insertions, 0 deletions
diff --git a/src/display/control/README b/src/display/control/README
new file mode 100644
index 0000000..680f0bf
--- /dev/null
+++ b/src/display/control/README
@@ -0,0 +1,112 @@
+
+This directory contains code for handling on-canvas editing objects as
+well as document display.
+
+Historically, the code originates from GnomeCanvas but at this point it
+has been completely rewritten for Inkscape's needs.
+
+One can think of a CanvasItem as a light-weight widget. There is a
+selection mechanism based on an item's position and a method to handle
+events. The selection mechanism is currently very simplistic,
+selecting the top-most item whose bounding box contains the
+cursor. Probably as a result, many items do not use these capabilities
+with external code replacing this functionality (e.g CanvasItemQuad,
+CanvasItemCurve).
+
+Points are stored as document coordinates. They are converted to
+screen coordinates by multiplying by the document to window affine.
+When an object's geometry changes, it must call request_update() to
+flag that its bounds need to be recalculated (_need_update =
+true). This will also cause all ancestors to also be marked and
+finally the Canvas. Before picking or drawing, all bounds must be
+up-to-date. (Changing the Canvas affine will also require bounds to be
+recalculated.) This mechanism ensures that bounds are correct in the
+most efficient manner. If only the style is changed (without geometric
+ramifications) then call canvas->redraw_area() to trigger a redraw.
+
+CanvasItemGroup keeps a list of child items using a Boost intrusive
+list. The pointers between nodes are kept inside the items. This
+allows for quick deletion by avoiding the need to search through the
+list to find the item to delete. This is important when a path
+contains hundress of nodes. However, a Boost intrusive list cannot be
+used with C++ smart pointers. Deleting an item can be done by either
+calling CanvasItemGroup::remove(CanvasItem*) or by directly deleting
+the item. Deleting a CanvasItemGroup will delete it and all of its
+children.
+
+
+Contents (x == pickable):
+
+* CanvasItem: Abstract base class.
+* CanvasItemBPath: x An item representing a Bezier path.
+* CanvasItemCatchall: x An infinite item, for capturing left-over events.
+* CanvasItemCtrl: x An item representing a control point (knot, node, etc.).
+* CanvasItemCurve: A single Bezier curve item (line or cubic). Not pickable!
+* CanvasItemDrawing: x The SVG drawing.
+* CanvasItemGrid: Base class for snapping grids.
+* CanvasItemGroup: x An item that contains other items.
+* CanvasItemGuideline: x A guideline for snapping.
+* CanvasItemQuad: An object defined by four points (only used by Text tool).
+* CanvasItemRect: An axis aligned rectangle item.
+* CanvasItemRotate: x For previewing the rotation of the canvas.
+* CanvasItemText: A text item.
+
+* CanvasItemEnum: All the enums you'll want to use!
+* CanvasItemBuffer: A class that wraps a Cairo buffer for drawing.
+
+Classes here that use CanvasItem's:
+
+* CanvasGridXY: A Cartesian grid for snapping.
+* CanvasGridAxonom An Axonometric grid for snapping.
+* SnapIndicator: A class for showing a snap possibility on canvas.
+* TemporaryItem: A class to manage the lifetime of a temporary CanvasItem.
+* TemporaryItemList: A class to track TemporaryItem's.
+
+
+Notes:
+
+CanvasItemCtrl (a.k.a. "Node", "Knot", "Handle", "Dragger", "Ctrl", "Mouse Grab", "Control Point")
+
+ Used by several groups of classes:
+
+ * Knot
+ ** KnotHolderEntity
+ *** live_effects/...
+ *** KnotHolder: Contains one or more KnotHolderEntity's.
+ Example: an object's fill and stroke gradients could have
+ overlapping knot-entities and are moved together via the knot holder.
+ **** shape-editor-knotholders.cpp
+ Classes derived from KnotHolder which contain classes derived from KnotHolderEntity for
+ editing shapes.
+ **** ShapeEditor contains two KnotHolders, one for shapes, one for LPE's.
+
+ ** ui/tools/tool-base.h
+ ** seltrans.h,.cpp
+ ** display/snap-indicator.h
+ ** gradient-drag.cpp
+ ** vanishing-point.h
+
+
+ * ControlPoint, SelectorPoint, SelectableControlPoint, Handle, Node, CurveDragPoint, TransformHandle, plus
+ auxiliary classes (manipulator...).
+ * drag-anchor
+ * pen-tool
+ * measure-tool
+ * guide-line
+ * snap-indicator
+
+
+TODO:
+
+Move files that use CanvasItem's to more appropriate places.
+
+All files that use CanvasItem's in src directory:
+* Move rubberband.h/.cpp to src/ui
+* Move gradient-drag to src/ui
+* Move selcue* to src/ui
+* Move seltrans* to src/ui
+* Move vanishing-point to src/ui
+* Move snap code to src/ui/snap display/snap-indicator.h/.cpp snap.h etc.
+
+See also src/ui/tool and src/ui/knot.
+
diff --git a/src/display/control/canvas-item-bpath.cpp b/src/display/control/canvas-item-bpath.cpp
new file mode 100644
index 0000000..d99588e
--- /dev/null
+++ b/src/display/control/canvas-item-bpath.cpp
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a Bezier path.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasBPath
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-bpath.h"
+
+#include "color.h" // SP_RGBA_x_F
+#include "display/curve.h"
+#include "display/cairo-utils.h"
+#include "helper/geom.h" // bounds_exact_transformed()
+
+namespace Inkscape {
+
+/**
+ * Create a null control bpath.
+ */
+CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemBpath:Null";
+ _pickable = true; // For now, everyone gets events from this class!
+}
+
+/**
+ * Create a control bpath. Path is in document coordinates.
+ */
+CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line)
+ : CanvasItem(group)
+ , _path(std::move(path))
+ , _phantom_line(phantom_line)
+{
+ _name = "CanvasItemBpath";
+ _pickable = true; // For now, everyone gets events from this class!
+ request_update(); // Render immediately or temporary bpaths won't show.
+}
+
+/**
+ * Set a control bpath. Curve is in document coordinates.
+ */
+void CanvasItemBpath::set_bpath(SPCurve const *curve, bool phantom_line)
+{
+ set_bpath(curve ? curve->get_pathvector() : Geom::PathVector(), phantom_line);
+}
+
+/**
+ * Set a control bpath. Path is in document coordinates.
+ */
+void CanvasItemBpath::set_bpath(Geom::PathVector path, bool phantom_line)
+{
+ defer([=, path = std::move(path)] () mutable {
+ _path = std::move(path);
+ _phantom_line = phantom_line;
+ request_update();
+ });
+}
+
+/**
+ * Set the fill color and fill rule.
+ */
+void CanvasItemBpath::set_fill(uint32_t fill, SPWindRule fill_rule)
+{
+ defer([=] {
+ if (_fill == fill && _fill_rule == fill_rule) return;
+ _fill = fill;
+ _fill_rule = fill_rule;
+ request_redraw();
+ });
+}
+
+void CanvasItemBpath::set_dashes(std::vector<double> &&dashes)
+{
+ defer([=, dashes = std::move(dashes)] () mutable {
+ _dashes = std::move(dashes);
+ });
+}
+
+/**
+ * Set the stroke width
+ */
+void CanvasItemBpath::set_stroke_width(double width)
+{
+ defer([=] {
+ if (_stroke_width == width) return;
+ _stroke_width = width;
+ request_redraw();
+ });
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on bpath.
+ */
+double CanvasItemBpath::closest_distance_to(Geom::Point const &p) const
+{
+ double d = Geom::infinity();
+
+ // Convert p to document coordinates (quicker than converting path to canvas units).
+ Geom::Point p_doc = p * affine().inverse();
+ _path.nearestTime(p_doc, &d);
+ d *= affine().descrim(); // Uniform scaling and rotation only.
+
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of bpath.
+ */
+bool CanvasItemBpath::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance == 0) {
+ tolerance = 1; // Need a minimum tolerance value or always returns false.
+ }
+
+ // Check for 'inside' a filled bpath if a fill is being used.
+ if ((_fill & 0xff) != 0) {
+ Geom::Point p_doc = p * affine().inverse();
+ if (_path.winding(p_doc) % 2 != 0) {
+ return true;
+ }
+ }
+
+ // Otherwise see how close we are to the outside line.
+ return closest_distance_to(p) < tolerance;
+}
+
+/**
+ * Update and redraw control bpath.
+ */
+void CanvasItemBpath::_update(bool)
+{
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ if (_path.empty()) {
+ _bounds = {};
+ return;
+ }
+
+ _bounds = expandedBy(bounds_exact_transformed(_path, affine()), 2);
+
+ // Queue redraw of new area
+ request_redraw();
+}
+
+/**
+ * Render bpath to screen via Cairo.
+ */
+void CanvasItemBpath::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ bool do_fill = (_fill & 0xff) != 0; // Not invisible.
+ bool do_stroke = (_stroke & 0xff) != 0; // Not invisible.
+
+ if (!do_fill && !do_stroke) {
+ // Both fill and stroke invisible.
+ return;
+ }
+
+ buf.cr->save();
+
+ // Setup path
+ buf.cr->set_tolerance(0.5);
+ buf.cr->begin_new_path();
+
+ feed_pathvector_to_cairo(buf.cr->cobj(), _path, affine(), buf.rect,
+ /* optimize_stroke */ !do_fill, 1);
+
+ // Do fill
+ if (do_fill) {
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill));
+ buf.cr->set_fill_rule(_fill_rule == SP_WIND_RULE_EVENODD ?
+ Cairo::FILL_RULE_EVEN_ODD : Cairo::FILL_RULE_WINDING);
+ buf.cr->fill_preserve();
+ }
+
+ // Do stroke
+ if (do_stroke) {
+
+ if (!_dashes.empty()) {
+ buf.cr->set_dash(_dashes, 0.0); // 0.0 is offset
+ }
+
+ if (_phantom_line) {
+ buf.cr->set_source_rgba(1.0, 1.0, 1.0, 0.25);
+ buf.cr->set_line_width(2.0);
+ buf.cr->stroke_preserve();
+ }
+
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ buf.cr->set_line_width(_stroke_width);
+ buf.cr->stroke();
+
+ } else {
+ buf.cr->begin_new_path(); // Clears path
+ }
+
+ buf.cr->restore();
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-bpath.h b/src/display/control/canvas-item-bpath.h
new file mode 100644
index 0000000..3b87579
--- /dev/null
+++ b/src/display/control/canvas-item-bpath.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_BPATH_H
+#define SEEN_CANVAS_ITEM_BPATH_H
+
+/**
+ * A class to represent a Bezier path.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasBPath
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/transforms.h>
+#include <2geom/pathvector.h>
+
+#include "canvas-item.h"
+
+#include "style-enums.h" // Fill rule
+
+class SPCurve;
+
+namespace Inkscape {
+
+class CanvasItemBpath final : public CanvasItem
+{
+public:
+ CanvasItemBpath(CanvasItemGroup *group);
+ CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line = false);
+
+ // Geometry
+ void set_bpath(SPCurve const *curve, bool phantom_line = false);
+ void set_bpath(Geom::PathVector path, bool phantom_line = false);
+
+ double closest_distance_to(Geom::Point const &p) const;
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Properties
+ void set_fill(uint32_t rgba, SPWindRule fill_rule);
+ void set_dashes(std::vector<double> &&dashes);
+ void set_stroke_width(double width);
+
+protected:
+ ~CanvasItemBpath() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ // Geometry
+ Geom::PathVector _path;
+
+ // Properties
+ SPWindRule _fill_rule = SP_WIND_RULE_EVENODD;
+ std::vector<double> _dashes;
+ bool _phantom_line = false;
+ double _stroke_width = 1.0;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_BPATH_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-buffer.h b/src/display/control/canvas-item-buffer.h
new file mode 100644
index 0000000..a66cb4c
--- /dev/null
+++ b/src/display/control/canvas-item-buffer.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_BUFFER_H
+#define SEEN_CANVAS_ITEM_BUFFER_H
+
+/**
+ * Buffer for rendering canvas items.
+ */
+
+/*
+ * Author:
+ * See git history.
+ *
+ * Copyright (C) 2020 Authors
+ *
+ * Rewrite of SPCanvasBuf.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include <2geom/rect.h>
+#include <cairomm/context.h>
+
+namespace Inkscape {
+
+/**
+ * Class used when rendering canvas items.
+ */
+struct CanvasItemBuffer
+{
+ Geom::IntRect rect;
+ int device_scale; // For high DPI monitors.
+ Cairo::RefPtr<Cairo::Context> cr;
+ bool outline_pass;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_BUFFER_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-catchall.cpp b/src/display/control/canvas-item-catchall.cpp
new file mode 100644
index 0000000..e6fbd47
--- /dev/null
+++ b/src/display/control/canvas-item-catchall.cpp
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to catch events after everyone else has had a go.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasAcetate.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-catchall.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control catchall.
+ */
+CanvasItemCatchall::CanvasItemCatchall(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemCatchall";
+ _pickable = true; // Duh! That's the purpose of this class!
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of catchall.
+ */
+bool CanvasItemCatchall::contains(Geom::Point const &p, double tolerance)
+{
+ return true; // We contain every place!
+}
+
+/**
+ * Update and redraw control catchall.
+ */
+void CanvasItemCatchall::_update(bool)
+{
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+}
+
+/**
+ * Render catchall to screen via Cairo.
+ */
+void CanvasItemCatchall::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // Do nothing! (Needed as CanvasItem is abstract.)
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-catchall.h b/src/display/control/canvas-item-catchall.h
new file mode 100644
index 0000000..6e0270a
--- /dev/null
+++ b/src/display/control/canvas-item-catchall.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_CATCHALL_H
+#define SEEN_CANVAS_ITEM_CATCHALL_H
+
+/**
+ * A class to catch events after everyone else has had a go.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasAcetate.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemCatchall final : public CanvasItem
+{
+public:
+ CanvasItemCatchall(CanvasItemGroup *group);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance) override;
+
+protected:
+ ~CanvasItemCatchall() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_CATCHALL_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-context.cpp b/src/display/control/canvas-item-context.cpp
new file mode 100644
index 0000000..99afdc4
--- /dev/null
+++ b/src/display/control/canvas-item-context.cpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "canvas-item-context.h"
+#include "canvas-item-group.h"
+
+namespace Inkscape {
+
+CanvasItemContext::CanvasItemContext(UI::Widget::Canvas *canvas)
+ : _canvas(canvas)
+ , _root(new CanvasItemGroup(this))
+{
+}
+
+CanvasItemContext::~CanvasItemContext()
+{
+ delete _root;
+}
+
+void CanvasItemContext::snapshot()
+{
+ assert(!_snapshotted);
+ _snapshotted = true;
+}
+
+void CanvasItemContext::unsnapshot()
+{
+ assert(_snapshotted);
+ _snapshotted = false;
+ _funclog();
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-context.h b/src/display/control/canvas-item-context.h
new file mode 100644
index 0000000..117110a
--- /dev/null
+++ b/src/display/control/canvas-item-context.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * The context in which a single CanvasItem tree exists. Holds the root node and common state.
+ */
+#ifndef SEEN_CANVAS_ITEM_CONTEXT_H
+#define SEEN_CANVAS_ITEM_CONTEXT_H
+
+#include <2geom/affine.h>
+#include "util/funclog.h"
+
+namespace Inkscape {
+
+namespace UI::Widget { class Canvas; }
+class CanvasItemGroup;
+
+class CanvasItemContext
+{
+public:
+ CanvasItemContext(UI::Widget::Canvas *canvas);
+ CanvasItemContext(CanvasItemContext const &) = delete;
+ CanvasItemContext &operator=(CanvasItemContext const &) = delete;
+ ~CanvasItemContext();
+
+ // Structure
+ UI::Widget::Canvas *canvas() const { return _canvas; }
+ CanvasItemGroup *root() const { return _root; }
+
+ // Geometry
+ Geom::Affine const &affine() const { return _affine; }
+ void setAffine(Geom::Affine const &affine) { _affine = affine; }
+
+ // Snapshotting
+ void snapshot();
+ void unsnapshot();
+ bool snapshotted() const { return _snapshotted; }
+
+ template<typename F>
+ void defer(F &&f) { _snapshotted ? _funclog.emplace(std::forward<F>(f)) : f(); }
+
+private:
+ // Structure
+ UI::Widget::Canvas *_canvas;
+ CanvasItemGroup *_root;
+
+ // Geometry
+ Geom::Affine _affine;
+
+ // Snapshotting
+ char _cacheline_separator[127];
+
+ bool _snapshotted = false;
+ Util::FuncLog _funclog;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_CONTEXT_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp
new file mode 100644
index 0000000..e0dcd90
--- /dev/null
+++ b/src/display/control/canvas-item-ctrl.cpp
@@ -0,0 +1,1181 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control node.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrl
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/transforms.h>
+
+#include "canvas-item-ctrl.h"
+#include "helper/geom.h"
+
+#include "preferences.h" // Default size.
+#include "display/cairo-utils.h" // argb32_from_rgba()
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create a null control node.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemCtrl:Null";
+ _pickable = true; // Everybody gets events from this class!
+}
+
+/**
+ * Create a control ctrl. Shape auto-set by type.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type)
+ : CanvasItem(group)
+ , _type(type)
+{
+ _name = "CanvasItemCtrl:Type_" + std::to_string(_type);
+ _pickable = true; // Everybody gets events from this class!
+
+ // Use _type to set default values:
+ set_shape_default();
+ set_size_default();
+}
+
+/**
+ * Create a control ctrl. Point is in document coordinates.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type, Geom::Point const &p)
+ : CanvasItemCtrl(group, type)
+{
+ _position = p;
+ request_update();
+}
+
+/**
+ * Create a control ctrl.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape)
+ : CanvasItem(group)
+ , _shape(shape)
+ , _type(CANVAS_ITEM_CTRL_TYPE_DEFAULT)
+{
+ _name = "CanvasItemCtrl:Shape_" + std::to_string(_shape);
+ _pickable = true; // Everybody gets events from this class!
+}
+
+/**
+ * Create a control ctrl. Point is in document coordinates.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape, Geom::Point const &p)
+ : CanvasItemCtrl(group, shape)
+{
+ _position = p;
+}
+
+/**
+ * Set the position. Point is in document coordinates.
+ */
+void CanvasItemCtrl::set_position(Geom::Point const &position)
+{
+ // std::cout << "CanvasItemCtrl::set_ctrl: " << _name << ": " << position << std::endl;
+ defer([=] {
+ if (_position == position) return;
+ _position = position;
+ request_update();
+ });
+}
+
+/**
+ * Returns distance between point in canvas units and position of ctrl.
+ */
+double CanvasItemCtrl::closest_distance_to(Geom::Point const &p) const
+{
+ // TODO: Different criteria for different shapes.
+ return Geom::distance(p, _position * affine());
+}
+
+/**
+ * If tolerance is zero, returns true if point p (in canvas units) is inside bounding box,
+ * else returns true if p (in canvas units) is within tolerance (canvas units) distance of ctrl.
+ * The latter assumes ctrl center anchored.
+ */
+bool CanvasItemCtrl::contains(Geom::Point const &p, double tolerance)
+{
+ // TODO: Different criteria for different shapes.
+ if (!_bounds) return false;
+ if (tolerance == 0) {
+ return _bounds->interiorContains(p);
+ } else {
+ return closest_distance_to(p) <= tolerance;
+ }
+}
+
+static auto angle_of(Geom::Affine const &affine)
+{
+ return std::atan2(affine[1], affine[0]);
+}
+
+/**
+ * Update and redraw control ctrl.
+ */
+void CanvasItemCtrl::_update(bool)
+{
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ // Setting the position to (inf, inf) to hide it is a pervasive hack we need to support.
+ if (!_position.isFinite()) {
+ _bounds = {};
+ return;
+ }
+
+ // Width and height are always odd.
+ assert(_width % 2 == 1);
+ assert(_height % 2 == 1);
+
+ // Get half width and height, rounded down.
+ int const w_half = _width / 2;
+ int const h_half = _height / 2;
+
+ // Set _angle, and compute adjustment for anchor.
+ int dx = 0;
+ int dy = 0;
+
+ switch (_shape) {
+ case CANVAS_ITEM_CTRL_SHAPE_DARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN:
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN:
+ {
+ double angle = _anchor * M_PI_4 + angle_of(affine());
+ double const half = _width / 2.0;
+
+ dx = -(half + 2) * cos(angle); // Add a bit to prevent tip from overlapping due to rounding errors.
+ dy = -(half + 2) * sin(angle);
+
+ switch (_shape) {
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW:
+ angle += 5 * M_PI_4;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW:
+ angle += M_PI_2;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN:
+ dx = -(half / 2 + 2) * cos(angle);
+ dy = -(half / 2 + 2) * sin(angle);
+ angle -= M_PI_2;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN:
+ angle -= M_PI_4;
+ dx = (half / 2 + 2) * ( sin(angle) - cos(angle));
+ dy = (half / 2 + 2) * (-sin(angle) - cos(angle));
+ break;
+
+ default:
+ break;
+ }
+
+ if (_angle != angle) {
+ _angle = angle;
+ _built.reset();
+ }
+
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_PIVOT:
+ case CANVAS_ITEM_CTRL_SHAPE_MALIGN: {
+ double const angle = angle_of(affine());
+ if (_angle != angle) {
+ _angle = angle;
+ _built.reset();
+ }
+ break;
+ }
+
+ default:
+ switch (_anchor) {
+ case SP_ANCHOR_N:
+ case SP_ANCHOR_CENTER:
+ case SP_ANCHOR_S:
+ break;
+
+ case SP_ANCHOR_NW:
+ case SP_ANCHOR_W:
+ case SP_ANCHOR_SW:
+ dx = w_half;
+ break;
+
+ case SP_ANCHOR_NE:
+ case SP_ANCHOR_E:
+ case SP_ANCHOR_SE:
+ dx = -w_half;
+ break;
+ }
+
+ switch (_anchor) {
+ case SP_ANCHOR_W:
+ case SP_ANCHOR_CENTER:
+ case SP_ANCHOR_E:
+ break;
+
+ case SP_ANCHOR_NW:
+ case SP_ANCHOR_N:
+ case SP_ANCHOR_NE:
+ dy = h_half;
+ break;
+
+ case SP_ANCHOR_SW:
+ case SP_ANCHOR_S:
+ case SP_ANCHOR_SE:
+ dy = -h_half;
+ break;
+ }
+ break;
+ }
+
+ auto const pt = Geom::IntPoint(-w_half, -h_half) + Geom::IntPoint(dx, dy) + (_position * affine()).floor();
+ _bounds = Geom::IntRect(pt, pt + Geom::IntPoint(_width, _height));
+
+ // Queue redraw of new area
+ request_redraw();
+}
+
+static inline uint32_t compose_xor(uint32_t bg, uint32_t fg, uint32_t a)
+{
+ uint32_t c = bg * (255 - a) + (((bg ^ ~fg) + (bg >> 2) - (bg > 127 ? 63 : 0)) & 255) * a;
+ return (c + 127) / 255;
+}
+
+/**
+ * Render ctrl to screen via Cairo.
+ */
+void CanvasItemCtrl::_render(CanvasItemBuffer &buf) const
+{
+ _built.init([&, this] {
+ build_cache(buf.device_scale);
+ });
+
+ Geom::Point c = _bounds->min() - buf.rect.min();
+ int x = c.x(); // Must be pixel aligned.
+ int y = c.y();
+
+ buf.cr->save();
+
+ // This code works regardless of source type.
+
+ // 1. Copy the affected part of output to a temporary surface
+
+ // Size in device pixels. Does not set device scale.
+ int width = _width * buf.device_scale;
+ int height = _height * buf.device_scale;
+ auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width, height);
+ cairo_surface_set_device_scale(work->cobj(), buf.device_scale, buf.device_scale); // No C++ API!
+
+ auto cr = Cairo::Context::create(work);
+ cr->translate(-_bounds->left(), -_bounds->top());
+ cr->set_source(buf.cr->get_target(), buf.rect.left(), buf.rect.top());
+ cr->paint();
+ // static int a = 0;
+ // std::string name0 = "ctrl0_" + _name + "_" + std::to_string(a++) + ".png";
+ // work->write_to_png(name0);
+
+ // 2. Composite the control on a temporary surface
+ work->flush();
+ int strideb = work->get_stride();
+ unsigned char *pxb = work->get_data();
+
+ // this code allow background become isolated from rendering so we can do things like outline overlay
+ uint32_t backcolor = get_canvas()->get_effective_background();
+ uint32_t *p = _cache.get();
+ for (int i = 0; i < height; ++i) {
+ auto pb = reinterpret_cast<uint32_t*>(pxb + i * strideb);
+ for (int j = 0; j < width; ++j) {
+ uint32_t base = *pb;
+ uint32_t cc = *p++;
+ uint32_t ac = cc & 0xff;
+ if (*pb == 0 && cc != 0) {
+ base = backcolor;
+ }
+ if (ac == 0 && cc != 0) {
+ *pb++ = argb32_from_rgba(cc | 0x000000ff);
+ } else if (ac == 0) {
+ *pb++ = base;
+ } else if (
+ _mode == CANVAS_ITEM_CTRL_MODE_XOR ||
+ _mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR ||
+ _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR)
+ {
+ EXTRACT_ARGB32(base, ab,rb,gb,bb)
+ // here we get canvas color and if color to draw
+ // has opacity, we override base colors
+ // flattenig canvas color
+ EXTRACT_ARGB32(backcolor, abb,rbb,gbb,bbb)
+ if (abb != ab) {
+ rb = (ab/255.0) * rb + (1-(ab/255.0)) * rbb;
+ gb = (ab/255.0) * gb + (1-(ab/255.0)) * gbb;
+ bb = (ab/255.0) * bb + (1-(ab/255.0)) * bbb;
+ ab = 255;
+ }
+ uint32_t ro = compose_xor(rb, (cc & 0xff000000) >> 24, ac);
+ uint32_t go = compose_xor(gb, (cc & 0x00ff0000) >> 16, ac);
+ uint32_t bo = compose_xor(bb, (cc & 0x0000ff00) >> 8, ac);
+ if (_mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR ||
+ _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) {
+ uint32_t gray = ro * 0.299 + go * 0.587 + bo * 0.114;
+ if (_mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) {
+ double f = 0.85; // desaturate by 15%
+ double p = sqrt(ro * ro * 0.299 + go * go * 0.587 + bo * bo * 0.114);
+ ro = p + (ro - p) * f;
+ go = p + (go - p) * f;
+ bo = p + (bo - p) * f;
+ } else {
+ ro = gray;
+ go = gray;
+ bo = gray;
+ }
+ }
+ ASSEMBLE_ARGB32(px, ab,ro,go,bo)
+ *pb++ = px;
+ } else {
+ *pb++ = argb32_from_rgba(cc | 0x000000ff);
+ }
+ }
+ }
+ work->mark_dirty();
+ // std::string name1 = "ctrl1_" + _name + "_" + std::to_string(a) + ".png";
+ // work->write_to_png(name1);
+
+ // 3. Replace the affected part of output with contents of temporary surface
+ buf.cr->set_source(work, x, y);
+
+ buf.cr->rectangle(x, y, _width, _height);
+ buf.cr->clip();
+ buf.cr->set_operator(Cairo::OPERATOR_SOURCE);
+ buf.cr->paint();
+ buf.cr->restore();
+}
+
+void CanvasItemCtrl::set_fill(uint32_t fill)
+{
+ defer([=] {
+ if (_fill == fill) return;
+ _fill = fill;
+ _built.reset();
+ request_redraw();
+ });
+}
+
+void CanvasItemCtrl::set_stroke(uint32_t stroke)
+{
+ defer([=] {
+ if (_stroke == stroke) return;
+ _stroke = stroke;
+ _built.reset();
+ request_redraw();
+ });
+}
+
+void CanvasItemCtrl::set_shape(CanvasItemCtrlShape shape)
+{
+ defer([=] {
+ if (_shape == shape) return;
+ _shape = shape;
+ _built.reset();
+ request_update(); // Geometry could change
+ });
+}
+
+void CanvasItemCtrl::set_shape_default()
+{
+ switch (_type) {
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_DARROW;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_SARROW;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CARROW;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_PIVOT;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_SALIGN;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CALIGN;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_MALIGN;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO:
+ case CANVAS_ITEM_CTRL_TYPE_ROTATE:
+ case CANVAS_ITEM_CTRL_TYPE_MARGIN:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CIRCLE;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_CENTER:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_PLUS;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_SHAPER:
+ case CANVAS_ITEM_CTRL_TYPE_LPE:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_DIAMOND;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_POINT:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CROSS;
+ break;
+
+ default:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE;
+ }
+}
+
+void CanvasItemCtrl::set_mode(CanvasItemCtrlMode mode)
+{
+ defer([=] {
+ if (_mode == mode) return;
+ _mode = mode;
+ _built.reset();
+ request_update();
+ });
+}
+
+void CanvasItemCtrl::set_pixbuf(Glib::RefPtr<Gdk::Pixbuf> pixbuf)
+{
+ defer([=, pixbuf = std::move(pixbuf)] () mutable {
+ if (_pixbuf != pixbuf) return;
+ _pixbuf = std::move(pixbuf);
+ _width = _pixbuf->get_width();
+ _height = _pixbuf->get_height();
+ _built.reset();
+ request_update();
+ });
+}
+
+// Nominally width == height == size except possibly for pixmaps.
+void CanvasItemCtrl::set_size(int size)
+{
+ defer([=] {
+ if (_pixbuf) {
+ // std::cerr << "CanvasItemCtrl::set_size: Attempting to set size on pixbuf control!" << std::endl;
+ return;
+ }
+ if (_width == size + _extra && _height == size + _extra) return;
+ _width = size + _extra;
+ _height = size + _extra;
+ _built.reset();
+ request_update(); // Geometry change
+ });
+}
+
+void CanvasItemCtrl::set_size_via_index(int size_index)
+{
+ // Size must always be an odd number to center on pixel.
+
+ if (size_index < 1 || size_index > 15) {
+ std::cerr << "CanvasItemCtrl::set_size_via_index: size_index out of range!" << std::endl;
+ size_index = 3;
+ }
+
+ int size = 0;
+ switch (_type) {
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW:
+ size = size_index * 2 + 7;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER:
+ size = size_index * 2 + 9; // 2 larger than HANDLE/SKEW
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN:
+ size = size_index * 4 + 5; // Needs to be larger to allow for rotating.
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_POINT:
+ case CANVAS_ITEM_CTRL_TYPE_ROTATE:
+ case CANVAS_ITEM_CTRL_TYPE_MARGIN:
+ case CANVAS_ITEM_CTRL_TYPE_CENTER:
+ case CANVAS_ITEM_CTRL_TYPE_SIZER:
+ case CANVAS_ITEM_CTRL_TYPE_SHAPER:
+ case CANVAS_ITEM_CTRL_TYPE_LPE:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP:
+ size = size_index * 2 + 5;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL:
+ size = size_index * 2 + 3;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_INVISIPOINT:
+ size = 1;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ANCHOR: // vanishing point for 3D box and anchor for pencil
+ size = size_index * 2 + 1;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_DEFAULT:
+ size = size_index * 2 + 1;
+ break;
+
+ default:
+ g_warning("set_size_via_index: missing case for handle type: %d", static_cast<int>(_type));
+ size = size_index * 2 + 1;
+ break;
+ }
+
+ set_size(size);
+}
+
+void CanvasItemCtrl::set_size_default()
+{
+ int size = Preferences::get()->getIntLimited("/options/grabsize/value", 3, 1, 15);
+ set_size_via_index(size);
+}
+
+void CanvasItemCtrl::set_size_extra(int extra)
+{
+ defer([=] {
+ if (_extra == extra || _pixbuf) return; // Don't enlarge pixbuf!
+ _width += extra - _extra;
+ _height += extra - _extra;
+ _extra = extra;
+ _built.reset();
+ request_update(); // Geometry change
+ });
+}
+
+void CanvasItemCtrl::set_type(CanvasItemCtrlType type)
+{
+ defer([=] {
+ if (_type == type) return;
+ _type = type;
+
+ // Use _type to set default values.
+ set_shape_default();
+ set_size_default();
+ _built.reset();
+ request_update(); // Possible geometry change
+ });
+}
+
+void CanvasItemCtrl::set_angle(double angle)
+{
+ defer([=] {
+ if (_angle == angle) return;
+ _angle = angle;
+ _built.reset();
+ request_update(); // Geometry change
+ });
+}
+
+void CanvasItemCtrl::set_anchor(SPAnchorType anchor)
+{
+ defer([=] {
+ if (_anchor == anchor) return;
+ _anchor = anchor;
+ request_update(); // Geometry change
+ });
+}
+
+// ---------- Protected ----------
+
+static void draw_darrow(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ // Find points, starting from tip of one arrowhead, working clockwise.
+ /* 1 4
+ ╱│ │╲
+ ╱ └────────┘ ╲
+ 0╱ 2 3 ╲5
+ ╲ 8 7 ╱
+ ╲ ┌────────┐ ╱
+ ╲│9 6│╱
+ */
+
+ // Length of arrowhead (not including stroke).
+ double delta = (size-1)/4.0; // Use unscaled width.
+
+ // Tip of arrow (0)
+ double tip_x = 0.5; // At edge, allow room for stroke.
+ double tip_y = size/2.0; // Center, assuming width == height.
+
+ // Outer corner (1)
+ double out_x = tip_x + delta;
+ double out_y = tip_y - delta;
+
+ // Inner corner (2)
+ double in_x = out_x;
+ double in_y = out_y + (delta/2.0);
+
+ double x0 = tip_x; double y0 = tip_y;
+ double x1 = out_x; double y1 = out_y;
+ double x2 = in_x; double y2 = in_y;
+ double x3 = size - in_x; double y3 = in_y;
+ double x4 = size - out_x; double y4 = out_y;
+ double x5 = size - tip_x; double y5 = tip_y;
+ double x6 = size - out_x; double y6 = size - out_y;
+ double x7 = size - in_x; double y7 = size - in_y;
+ double x8 = in_x; double y8 = size - in_y;
+ double x9 = out_x; double y9 = size - out_y;
+
+ // Draw arrow
+ cr->move_to(x0, y0);
+ cr->line_to(x1, y1);
+ cr->line_to(x2, y2);
+ cr->line_to(x3, y3);
+ cr->line_to(x4, y4);
+ cr->line_to(x5, y5);
+ cr->line_to(x6, y6);
+ cr->line_to(x7, y7);
+ cr->line_to(x8, y8);
+ cr->line_to(x9, y9);
+ cr->close_path();
+}
+
+static void draw_carrow(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ // Length of arrowhead (not including stroke).
+ double delta = (size-3)/4.0; // Use unscaled width.
+
+ // Tip of arrow
+ double tip_x = 1.5; // Edge, allow room for stroke when rotated.
+ double tip_y = delta + 1.5;
+
+ // Outer corner (1)
+ double out_x = tip_x + delta;
+ double out_y = tip_y - delta;
+
+ // Inner corner (2)
+ double in_x = out_x;
+ double in_y = out_y + (delta/2.0);
+
+ double x0 = tip_x; double y0 = tip_y;
+ double x1 = out_x; double y1 = out_y;
+ double x2 = in_x; double y2 = in_y;
+ double x3 = size - in_y; //double y3 = size - in_x;
+ double x4 = size - out_y; double y4 = size - out_x;
+ double x5 = size - tip_y; double y5 = size - tip_x;
+ double x6 = x5 - delta; double y6 = y4;
+ double x7 = x5 - delta/2.0; double y7 = y4;
+ double x8 = x1; //double y8 = y0 + delta/2.0;
+ double x9 = x1; double y9 = y0 + delta;
+
+ // Draw arrow
+ cr->move_to(x0, y0);
+ cr->line_to(x1, y1);
+ cr->line_to(x2, y2);
+ cr->arc(x1, y4, x3-x2, 3.0*M_PI/2.0, 0);
+ cr->line_to(x4, y4);
+ cr->line_to(x5, y5);
+ cr->line_to(x6, y6);
+ cr->line_to(x7, y7);
+ cr->arc_negative(x1, y4, x7-x8, 0, 3.0*M_PI/2.0);
+ cr->line_to(x9, y9);
+ cr->close_path();
+}
+
+static void draw_triangle(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ // Construct an arrowhead (triangle)
+ double s = size/2.0;
+ double wcos = s * cos( M_PI/6 );
+ double hsin = s * sin( M_PI/6 );
+ // Construct a smaller arrow head for fill.
+ Geom::Point p1f(1, s);
+ Geom::Point p2f(s + wcos - 1, s + hsin);
+ Geom::Point p3f(s + wcos - 1, s - hsin);
+ // Draw arrow
+ cr->move_to(p1f[0], p1f[1]);
+ cr->line_to(p2f[0], p2f[1]);
+ cr->line_to(p3f[0], p3f[1]);
+ cr->close_path();
+}
+
+static void draw_triangle_angled(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ // Construct an arrowhead (triangle) of half size.
+ double s = size/2.0;
+ double wcos = s * cos( M_PI/9 );
+ double hsin = s * sin( M_PI/9 );
+ Geom::Point p1f(s + 1, s);
+ Geom::Point p2f(s + wcos - 1, s + hsin - 1);
+ Geom::Point p3f(s + wcos - 1, s - (hsin - 1));
+ // Draw arrow
+ cr->move_to(p1f[0], p1f[1]);
+ cr->line_to(p2f[0], p2f[1]);
+ cr->line_to(p3f[0], p3f[1]);
+ cr->close_path();
+}
+
+static void draw_pivot(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ double delta4 = (size-5)/4.0; // Keep away from edge or will clip when rotating.
+ double delta8 = delta4/2;
+
+ // Line start
+ double center = size/2.0;
+
+ cr->move_to (center - delta8, center - 2*delta4 - delta8);
+ cr->rel_line_to ( delta4, 0 );
+ cr->rel_line_to ( 0, delta4);
+
+ cr->rel_line_to ( delta4, delta4);
+
+ cr->rel_line_to ( delta4, 0 );
+ cr->rel_line_to ( 0, delta4);
+ cr->rel_line_to (-delta4, 0 );
+
+ cr->rel_line_to (-delta4, delta4);
+
+ cr->rel_line_to ( 0, delta4);
+ cr->rel_line_to (-delta4, 0 );
+ cr->rel_line_to ( 0, -delta4);
+
+ cr->rel_line_to (-delta4, -delta4);
+
+ cr->rel_line_to (-delta4, 0 );
+ cr->rel_line_to ( 0, -delta4);
+ cr->rel_line_to ( delta4, 0 );
+
+ cr->rel_line_to ( delta4, -delta4);
+ cr->close_path();
+
+ cr->begin_new_sub_path();
+ cr->arc_negative(center, center, delta4, 0, -2 * M_PI);
+}
+
+static void draw_salign(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ // Triangle pointing at line.
+
+ // Basic units.
+ double delta4 = (size-1)/4.0; // Use unscaled width.
+ double delta8 = delta4/2;
+ if (delta8 < 2) {
+ // Keep a minimum gap of at least one pixel (after stroking).
+ delta8 = 2;
+ }
+
+ // Tip of triangle
+ double tip_x = size/2.0; // Center (also rotation point).
+ double tip_y = size/2.0;
+
+ // Corner triangle position.
+ double outer = size/2.0 - delta4;
+
+ // Outer line position
+ double oline = size/2.0 + (int)delta4;
+
+ // Inner line position
+ double iline = size/2.0 + (int)delta8;
+
+ // Draw triangle
+ cr->move_to(tip_x, tip_y);
+ cr->line_to(outer, outer);
+ cr->line_to(size - outer, outer);
+ cr->close_path();
+
+ // Draw line
+ cr->move_to(outer, iline);
+ cr->line_to(size - outer, iline);
+ cr->line_to(size - outer, oline);
+ cr->line_to(outer, oline);
+ cr->close_path();
+}
+
+static void draw_calign(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ // Basic units.
+ double delta4 = (size-1)/4.0; // Use unscaled width.
+ double delta8 = delta4/2;
+ if (delta8 < 2) {
+ // Keep a minimum gap of at least one pixel (after stroking).
+ delta8 = 2;
+ }
+
+ // Tip of triangle
+ double tip_x = size/2.0; // Center (also rotation point).
+ double tip_y = size/2.0;
+
+ // Corner triangle position.
+ double outer = size/2.0 - delta8 - delta4;
+
+ // End of line positin
+ double eline = size/2.0 - delta8;
+
+ // Outer line position
+ double oline = size/2.0 + (int)delta4;
+
+ // Inner line position
+ double iline = size/2.0 + (int)delta8;
+
+ // Draw triangle
+ cr->move_to(tip_x, tip_y);
+ cr->line_to(outer, tip_y);
+ cr->line_to(tip_x, outer);
+ cr->close_path();
+
+ // Draw line
+ cr->move_to(iline, iline);
+ cr->line_to(iline, eline);
+ cr->line_to(oline, eline);
+ cr->line_to(oline, oline);
+ cr->line_to(eline, oline);
+ cr->line_to(eline, iline);
+ cr->close_path();
+}
+
+static void draw_malign(Cairo::RefPtr<Cairo::Context> const &cr, double size)
+{
+ // Basic units.
+ double delta4 = (size-1)/4.0; // Use unscaled width.
+ double delta8 = delta4/2;
+ if (delta8 < 2) {
+ // Keep a minimum gap of at least one pixel (after stroking).
+ delta8 = 2;
+ }
+
+ // Tip of triangle
+ double tip_0 = size/2.0;
+ double tip_1 = size/2.0 - delta8;
+
+ // Draw triangles
+ cr->move_to(tip_0, tip_1);
+ cr->line_to(tip_0 - delta4, tip_1 - delta4);
+ cr->line_to(tip_0 + delta4, tip_1 - delta4);
+ cr->close_path();
+
+ cr->move_to(size - tip_1, tip_0);
+ cr->line_to(size - tip_1 + delta4, tip_0 - delta4);
+ cr->line_to(size - tip_1 + delta4, tip_0 + delta4);
+ cr->close_path();
+
+ cr->move_to(size - tip_0, size - tip_1);
+ cr->line_to(size - tip_0 + delta4, size - tip_1 + delta4);
+ cr->line_to(size - tip_0 - delta4, size - tip_1 + delta4);
+ cr->close_path();
+
+ cr->move_to(tip_1, tip_0);
+ cr->line_to(tip_1 - delta4, tip_0 + delta4);
+ cr->line_to(tip_1 - delta4, tip_0 - delta4);
+ cr->close_path();
+}
+
+void CanvasItemCtrl::build_cache(int device_scale) const
+{
+ if (_width < 2 || _height < 2) {
+ return; // Nothing to render
+ }
+
+ if (_shape != CANVAS_ITEM_CTRL_SHAPE_BITMAP) {
+ if (_width % 2 == 0 || _height % 2 == 0) {
+ std::cerr << "CanvasItemCtrl::build_cache: Width and/or height not odd integer! "
+ << _name << ": width: " << _width << " height: " << _height << std::endl;
+ }
+ }
+
+ // Get memory for cache.
+ int width = _width * device_scale; // Not unsigned or math errors occur!
+ int height = _height * device_scale;
+ int size = width * height;
+
+ _cache = std::make_unique<uint32_t[]>(size);
+ auto p = _cache.get();
+
+ switch (_shape) {
+ case CANVAS_ITEM_CTRL_SHAPE_SQUARE:
+ // Actually any rectanglular shape.
+ for (int i = 0; i < width; ++i) {
+ for (int j = 0; j < width; ++j) {
+ if (i + 1 > device_scale && device_scale < width - i &&
+ j + 1 > device_scale && device_scale < height - j)
+ {
+ *p++ = _fill;
+ } else {
+ *p++ = _stroke;
+ }
+ }
+ }
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_DIAMOND: {
+ // Assume width == height.
+ int m = (width+1)/2;
+
+ for (int i = 0; i < width; ++i) {
+ for (int j = 0; j < height; ++j) {
+ if ( i + j > m-1+device_scale &&
+ (width-1-i) + j > m-1+device_scale &&
+ (width-1-i) + (height-1-j) > m-1+device_scale &&
+ i + (height-1-j) > m-1+device_scale ) {
+ *p++ = _fill;
+ } else
+ if ( i + j > m-2 &&
+ (width-1-i) + j > m-2 &&
+ (width-1-i) + (height-1-j) > m-2 &&
+ i + (height-1-j) > m-2 ) {
+ *p++ = _stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_CIRCLE: {
+ // Assume width == height.
+ double rs = width/2.0;
+ double rs2 = rs*rs;
+ double rf = rs-device_scale;
+ double rf2 = rf*rf;
+
+ for (int i = 0; i < width; ++i) {
+ for (int j = 0; j < height; ++j) {
+
+ double rx = i - (width /2.0) + 0.5;
+ double ry = j - (height/2.0) + 0.5;
+ double r2 = rx*rx + ry*ry;
+
+ if (r2 < rf2) {
+ *p++ = _fill;
+ } else if (r2 < rs2) {
+ *p++ = _stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_CROSS:
+ // Actually an 'X'.
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ if ( abs(x - y) < device_scale ||
+ abs(width - 1 - x - y) < device_scale ) {
+ *p++ = _stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_PLUS:
+ // Actually an '+'.
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ if ( std::abs(x-width/2) < device_scale ||
+ std::abs(y-height/2) < device_scale ) {
+ *p++ = _stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ break;
+ case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE: //triangle optionaly rotated
+ case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED: // triangle with pointing to center of knot and rotated this way
+ case CANVAS_ITEM_CTRL_SHAPE_DARROW: // Double arrow
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW: // Same shape as darrow but rendered rotated 90 degrees.
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW: // Double corner arrow
+ case CANVAS_ITEM_CTRL_SHAPE_PIVOT: // Fancy "plus"
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN: // Side align (triangle pointing toward line)
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN: // Corner align (triangle pointing into "L")
+ case CANVAS_ITEM_CTRL_SHAPE_MALIGN: // Middle align (four triangles poining inward)
+ {
+ double size = _width; // Use unscaled width.
+
+ auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, device_scale * size, device_scale * size);
+ cairo_surface_set_device_scale(work->cobj(), device_scale, device_scale); // No C++ API!
+ auto cr = Cairo::Context::create(work);
+
+ // Rotate around center
+ cr->translate( size/2.0, size/2.0);
+ cr->rotate(_angle);
+ cr->translate(-size/2.0, -size/2.0);
+
+ // Construct path
+ bool triangles = _shape == CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED || _shape == CANVAS_ITEM_CTRL_SHAPE_TRIANGLE;
+ switch (_shape) {
+ case CANVAS_ITEM_CTRL_SHAPE_DARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW:
+ draw_darrow(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE:
+ draw_triangle(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED:
+ draw_triangle_angled(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW:
+ draw_carrow(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_PIVOT:
+ draw_pivot(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN:
+ draw_salign(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN:
+ draw_calign(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_MALIGN:
+ draw_malign(cr, size);
+ break;
+
+ default:
+ // Shouldn't happen
+ break;
+ }
+
+ // Fill and stroke.
+ cr->set_source_rgba(SP_RGBA32_R_F(_fill),
+ SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill),
+ SP_RGBA32_A_F(_fill));
+ cr->fill_preserve();
+ cr->set_source_rgba(SP_RGBA32_R_F(_stroke),
+ SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke),
+ SP_RGBA32_A_F(_stroke));
+ cr->set_line_width(1);
+ cr->stroke();
+
+ // Copy to buffer.
+ work->flush();
+ int strideb = work->get_stride();
+ unsigned char* pxb = work->get_data();
+ auto p = _cache.get();
+ for (int i = 0; i < device_scale * size; ++i) {
+ auto pb = reinterpret_cast<uint32_t*>(pxb + i * strideb);
+ for (int j = 0; j < width; ++j) {
+
+ if (triangles) {
+ *p++ = rgba_from_argb32(*pb);
+ } else {
+ uint32_t color = 0x0;
+
+ // Need to un-premultiply alpha and change order argb -> rgba.
+ uint32_t alpha = (*pb & 0xff000000) >> 24;
+ if (alpha == 0x0) {
+ color = 0x0;
+ } else {
+ uint32_t rgb = unpremul_alpha(*pb & 0xffffff, alpha);
+ color = (rgb << 8) + alpha;
+ }
+ *p++ = color;
+ }
+ pb++;
+ }
+ }
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_BITMAP:
+ {
+ if (_pixbuf) {
+ unsigned char* px = _pixbuf->get_pixels();
+ unsigned int rs = _pixbuf->get_rowstride();
+ for (int y = 0; y < height/device_scale; y++){
+ for (int x = 0; x < width/device_scale; x++) {
+ unsigned char *s = px + rs*y + 4*x;
+ uint32_t color;
+ if (s[3] < 0x80) {
+ color = 0;
+ } else if (s[0] < 0x80) {
+ color = _stroke;
+ } else {
+ color = _fill;
+ }
+
+ // Fill in device_scale x device_scale block
+ for (int i = 0; i < device_scale; ++i) {
+ for (int j = 0; j < device_scale; ++j) {
+ auto p = _cache.get() +
+ (x * device_scale + i) + // Column
+ (y * device_scale + j) * width; // Row
+ *p = color;
+ }
+ }
+ }
+ }
+ } else {
+ std::cerr << "CanvasItemCtrl::build_cache: No bitmap!" << std::endl;
+ auto p = _cache.get();
+ for (int y = 0; y < height/device_scale; y++){
+ for (int x = 0; x < width/device_scale; x++) {
+ if (x == y) {
+ *p++ = 0xffff0000;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_IMAGE:
+ std::cerr << "CanvasItemCtrl::build_cache: image: UNIMPLEMENTED" << std::endl;
+ break;
+
+ default:
+ std::cerr << "CanvasItemCtrl::build_cache: unhandled shape!" << std::endl;
+ break;
+ }
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-ctrl.h b/src/display/control/canvas-item-ctrl.h
new file mode 100644
index 0000000..6c072a2
--- /dev/null
+++ b/src/display/control/canvas-item-ctrl.h
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_CTRL_H
+#define SEEN_CANVAS_ITEM_CTRL_H
+
+/**
+ * A class to represent a control node.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrl
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <memory>
+#include <2geom/point.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include "canvas-item.h"
+#include "canvas-item-enums.h"
+
+#include "enums.h" // SP_ANCHOR_X
+#include "display/initlock.h"
+
+namespace Inkscape {
+
+class CanvasItemCtrl : public CanvasItem
+{
+public:
+ CanvasItemCtrl(CanvasItemGroup *group);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type, Geom::Point const &p);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape, Geom::Point const &p);
+
+ // Geometry
+ void set_position(Geom::Point const &position);
+
+ double closest_distance_to(Geom::Point const &p) const;
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Properties
+ void set_fill(uint32_t rgba) override;
+ void set_stroke(uint32_t rgba) override;
+ void set_shape(CanvasItemCtrlShape shape);
+ void set_shape_default(); // Use type to determine shape.
+ void set_mode(CanvasItemCtrlMode mode);
+ void set_mode_default();
+ void set_size(int size);
+ virtual void set_size_via_index(int size_index);
+ void set_size_default(); // Use preference and type to set size.
+ void set_size_extra(int extra); // Used to temporary increase size of ctrl.
+ void set_anchor(SPAnchorType anchor);
+ void set_angle(double angle);
+ void set_type(CanvasItemCtrlType type);
+ void set_pixbuf(Glib::RefPtr<Gdk::Pixbuf> pixbuf);
+
+protected:
+ ~CanvasItemCtrl() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ void build_cache(int device_scale) const;
+
+ // Geometry
+ Geom::Point _position;
+
+ // Display
+ InitLock _built;
+ mutable std::unique_ptr<uint32_t[]> _cache;
+
+ // Properties
+ CanvasItemCtrlType _type = CANVAS_ITEM_CTRL_TYPE_DEFAULT;
+ CanvasItemCtrlShape _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE;
+ CanvasItemCtrlMode _mode = CANVAS_ITEM_CTRL_MODE_XOR;
+ int _width = 5; // Nominally width == height == size... unless we use a pixmap.
+ int _height = 5;
+ int _extra = 0; // Used to temporarily increase size.
+ double _angle = 0; // Used for triangles, could be used for arrows.
+ SPAnchorType _anchor = SP_ANCHOR_CENTER;
+ Glib::RefPtr<Gdk::Pixbuf> _pixbuf;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_CTRL_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-curve.cpp b/src/display/control/canvas-item-curve.cpp
new file mode 100644
index 0000000..c37c1a0
--- /dev/null
+++ b/src/display/control/canvas-item-curve.cpp
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a single Bezier control curve, either a line or a cubic Bezier.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlLine and SPCtrlCurve
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/bezier-curve.h>
+
+#include "canvas-item-curve.h"
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "helper/geom.h"
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control curve.
+ */
+CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemCurve:Null";
+}
+
+/**
+ * Create a linear control curve. Points are in document coordinates.
+ */
+CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1)
+ : CanvasItem(group)
+ , _curve(std::make_unique<Geom::LineSegment>(p0, p1))
+{
+ _name = "CanvasItemCurve:Line";
+}
+
+/**
+ * Create a cubic Bezier control curve. Points are in document coordinates.
+ */
+CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group,
+ Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3)
+ : CanvasItem(group)
+ , _curve(std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3))
+{
+ _name = "CanvasItemCurve:CubicBezier";
+}
+
+/**
+ * Set a linear control curve. Points are in document coordinates.
+ */
+void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1)
+{
+ defer([=] {
+ _name = "CanvasItemCurve:Line";
+ _curve = std::make_unique<Geom::LineSegment>(p0, p1);
+ request_update();
+ });
+}
+
+/**
+ * Set a cubic Bezier control curve. Points are in document coordinates.
+ */
+void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3)
+{
+ defer([=] {
+ _name = "CanvasItemCurve:CubicBezier";
+ _curve = std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3);
+ request_update();
+ });
+}
+
+/**
+ * Set stroke width.
+ */
+void CanvasItemCurve::set_width(int width)
+{
+ defer([=] {
+ if (_width == width) return;
+ _width = width;
+ request_update();
+ });
+}
+
+/**
+ * Set background stroke alpha.
+ */
+void CanvasItemCurve::set_bg_alpha(float alpha)
+{
+ defer([=] {
+ if (bg_alpha == alpha) return;
+ bg_alpha = alpha;
+ request_update();
+ });
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on curve.
+ */
+double CanvasItemCurve::closest_distance_to(Geom::Point const &p) const
+{
+ double d = Geom::infinity();
+ if (_curve) {
+ Geom::BezierCurve curve = *_curve;
+ curve *= affine(); // Document to canvas.
+ Geom::Point n = curve.pointAt(curve.nearestTime(p));
+ d = Geom::distance(p, n);
+ }
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of curve.
+ */
+bool CanvasItemCurve::contains(Geom::Point const &p, double tolerance)
+{
+ return closest_distance_to(p) <= tolerance;
+}
+
+/**
+ * Update and redraw control curve.
+ */
+void CanvasItemCurve::_update(bool)
+{
+ // Queue redraw of old area (erase previous content).
+ request_redraw(); // This is actually never useful as curves are always deleted
+ // and recreated when a node is moved! But keep it in case we change that.
+
+ if (!_curve || _curve->isDegenerate()) {
+ _bounds = {};
+ return; // No curve! Can happen - see node.h.
+ }
+
+ // Tradeoff between updating a larger area (typically twice for Beziers?) vs computation time for bounds.
+ _bounds = expandedBy(_curve->boundsExact() * affine(), 2); // Room for stroke.
+
+ // Queue redraw of new area
+ request_redraw();
+}
+
+/**
+ * Render curve to screen via Cairo.
+ */
+void CanvasItemCurve::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ assert(_curve); // Not called if _curve is null, since _bounds would be null.
+
+ // Todo: Transform, rather than copy.
+ Geom::BezierCurve curve = *_curve;
+ curve *= affine(); // Document to canvas.
+ curve *= Geom::Translate(-buf.rect.min()); // Canvas to screen.
+
+ buf.cr->save();
+
+ buf.cr->begin_new_path();
+
+ if (curve.size() == 2) {
+ // Line
+ buf.cr->move_to(curve[0].x(), curve[0].y());
+ buf.cr->line_to(curve[1].x(), curve[1].y());
+ } else {
+ // Curve
+ buf.cr->move_to(curve[0].x(), curve[0].y());
+ buf.cr->curve_to(curve[1].x(), curve[1].y(), curve[2].x(), curve[2].y(), curve[3].x(), curve[3].y());
+ }
+
+ buf.cr->set_source_rgba(1.0, 1.0, 1.0, bg_alpha);
+ buf.cr->set_line_width(background_width);
+ buf.cr->stroke_preserve();
+
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ buf.cr->set_line_width(_width);
+ buf.cr->stroke();
+
+ // Uncomment to show bounds
+ // Geom::Rect bounds = _bounds;
+ // bounds.expandBy(-1);
+ // bounds -= buf.rect.min();
+ // buf.cr->set_source_rgba(1.0, 0.0, 0.0, 1.0);
+ // buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ // buf.cr->stroke();
+
+ buf.cr->restore();
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-curve.h b/src/display/control/canvas-item-curve.h
new file mode 100644
index 0000000..8e1f9b5
--- /dev/null
+++ b/src/display/control/canvas-item-curve.h
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_CURVE_H
+#define SEEN_CANVAS_ITEM_CURVE_H
+
+/**
+ * A class to represent a single Bezier control curve.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlLine and SPCtrlCurve
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <memory>
+#include <2geom/path.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemCurve final : public CanvasItem
+{
+public:
+ CanvasItemCurve(CanvasItemGroup *group);
+ CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1);
+ CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3);
+
+ // Geometry
+ void set_coords(Geom::Point const &p0, Geom::Point const &p1);
+ void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3);
+ void set_width(int width);
+ void set_bg_alpha(float alpha);
+ bool is_line() const { return _curve->size() == 2; }
+
+ double closest_distance_to(Geom::Point const &p) const;
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+protected:
+ ~CanvasItemCurve() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ // Display
+ std::unique_ptr<Geom::BezierCurve> _curve;
+
+ int _width = 1;
+ int background_width = 3; // this should be an odd number so that the background appears on both the sides of the curve.
+ float bg_alpha = 0.5f;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_CURVE_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-drawing.cpp b/src/display/control/canvas-item-drawing.cpp
new file mode 100644
index 0000000..cfa527a
--- /dev/null
+++ b/src/display/control/canvas-item-drawing.cpp
@@ -0,0 +1,220 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to render the SVG drawing.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of _SPCanvasArena.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-drawing.h"
+
+#include "desktop.h"
+
+#include "display/drawing.h"
+#include "display/drawing-context.h"
+#include "display/drawing-item.h"
+#include "display/drawing-group.h"
+
+#include "helper/geom.h"
+#include "ui/widget/canvas.h"
+#include "ui/modifiers.h"
+
+namespace Inkscape {
+
+/**
+ * Create the drawing. One per window!
+ */
+CanvasItemDrawing::CanvasItemDrawing(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemDrawing";
+ _pickable = true;
+
+ _drawing = std::make_unique<Drawing>(this);
+ auto root = new DrawingGroup(*_drawing);
+ root->setPickChildren(true);
+ _drawing->setRoot(root);
+}
+
+/**
+ * Returns true if point p (in canvas units) is inside some object in drawing.
+ */
+bool CanvasItemDrawing::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance != 0) {
+ std::cerr << "CanvasItemDrawing::contains: Non-zero tolerance not implemented!" << std::endl;
+ }
+
+ _picked_item = _drawing->pick(p, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE);
+
+ if (_picked_item) {
+ // This will trigger a signal that is handled by our event handler. Seems a bit of a
+ // round-about way of doing things but it matches what other pickable canvas-item classes do.
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Update and redraw drawing.
+ */
+void CanvasItemDrawing::_update(bool)
+{
+ // Undo y-axis flip. This should not be here!!!!
+ auto new_drawing_affine = affine();
+ if (auto desktop = get_canvas()->get_desktop()) {
+ new_drawing_affine = desktop->doc2dt() * new_drawing_affine;
+ }
+
+ bool affine_changed = _drawing_affine != new_drawing_affine;
+ if (affine_changed) {
+ _drawing_affine = new_drawing_affine;
+ }
+
+ _drawing->update(Geom::IntRect::infinite(), _drawing_affine, DrawingItem::STATE_ALL, affine_changed * DrawingItem::STATE_ALL);
+
+ _bounds = expandedBy(_drawing->root()->drawbox(), 1); // Avoid aliasing artifacts
+
+ // Todo: This should be managed elsewhere.
+ if (_cursor) {
+ /* Mess with enter/leave notifiers */
+ DrawingItem *new_drawing_item = _drawing->pick(_c, _delta, _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE);
+ if (_active_item != new_drawing_item) {
+
+ GdkEventCrossing ec;
+ ec.window = get_canvas()->get_window()->gobj();
+ ec.send_event = true;
+ ec.subwindow = ec.window;
+ ec.time = GDK_CURRENT_TIME;
+ ec.x = _c.x();
+ ec.y = _c.y();
+
+ /* fixme: Why? */
+ if (_active_item) {
+ ec.type = GDK_LEAVE_NOTIFY;
+ _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+
+ _active_item = new_drawing_item;
+
+ if (_active_item) {
+ ec.type = GDK_ENTER_NOTIFY;
+ _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+ }
+ }
+}
+
+/**
+ * Render drawing to screen via Cairo.
+ */
+void CanvasItemDrawing::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ auto dc = Inkscape::DrawingContext(buf.cr->cobj(), buf.rect.min());
+ _drawing->render(dc, buf.rect, buf.outline_pass * DrawingItem::RENDER_OUTLINE);
+}
+
+/**
+ * Handle events directed at the drawing. We first attempt to handle them here.
+ */
+bool CanvasItemDrawing::handle_event(GdkEvent *event)
+{
+ bool retval = false;
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ if (!_cursor) {
+ if (_active_item) {
+ std::cerr << "CanvasItemDrawing::event_handler: cursor entered drawing with an active item!" << std::endl;
+ }
+ _cursor = true;
+
+ /* TODO ... event -> arena transform? */
+ _c = Geom::Point(event->crossing.x, event->crossing.y);
+
+ _active_item = _drawing->pick(_c, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE);
+ retval = _drawing_event_signal.emit(event, _active_item);
+ }
+ break;
+
+ case GDK_LEAVE_NOTIFY:
+ if (_cursor) {
+ retval = _drawing_event_signal.emit(event, _active_item);
+ _active_item = nullptr;
+ _cursor = false;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ {
+ /* TODO ... event -> arena transform? */
+ _c = Geom::Point(event->motion.x, event->motion.y);
+
+ auto new_drawing_item = _drawing->pick(_c, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE);
+ if (_active_item != new_drawing_item) {
+
+ GdkEventCrossing ec;
+ ec.window = event->motion.window;
+ ec.send_event = event->motion.send_event;
+ ec.subwindow = event->motion.window;
+ ec.time = event->motion.time;
+ ec.x = event->motion.x;
+ ec.y = event->motion.y;
+
+ /* fixme: What is wrong? */
+ if (_active_item) {
+ ec.type = GDK_LEAVE_NOTIFY;
+ retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+
+ _active_item = new_drawing_item;
+
+ if (_active_item) {
+ ec.type = GDK_ENTER_NOTIFY;
+ retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+ }
+ retval = retval || _drawing_event_signal.emit(event, _active_item);
+ break;
+ }
+
+ case GDK_SCROLL:
+ {
+ if (Modifiers::Modifier::get(Modifiers::Type::CANVAS_ZOOM)->active(event->scroll.state)) {
+ /* Zoom is emitted by the canvas as well, ignore here */
+ return false;
+ }
+ retval = _drawing_event_signal.emit(event, _active_item);
+ break;
+ }
+
+ default:
+ /* Just send event */
+ retval = _drawing_event_signal.emit(event, _active_item);
+ break;
+ }
+
+ return retval;
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-drawing.h b/src/display/control/canvas-item-drawing.h
new file mode 100644
index 0000000..f3fd494
--- /dev/null
+++ b/src/display/control/canvas-item-drawing.h
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_DRAWING_H
+#define SEEN_CANVAS_ITEM_DRAWING_H
+
+/**
+ * A class to render the SVG drawing.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of _SPCanvasArena.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/sigc++.h>
+
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class Drawing;
+class DrawingItem;
+class Updatecontext;
+
+class CanvasItemDrawing final : public CanvasItem
+{
+public:
+ CanvasItemDrawing(CanvasItemGroup *group);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ Inkscape::Drawing *get_drawing() { return _drawing.get(); }
+
+ // Drawing items
+ void set_active(Inkscape::DrawingItem *active) { _active_item = active; }
+ Inkscape::DrawingItem *get_active() { return _active_item; }
+
+ // Events
+ bool handle_event(GdkEvent *event) override;
+ void set_sticky(bool sticky) { _sticky = sticky; }
+ void set_pick_outline(bool pick_outline) { _pick_outline = pick_outline; }
+
+ // Signals
+ sigc::connection connect_drawing_event(sigc::slot<bool (GdkEvent*, Inkscape::DrawingItem *)> slot) {
+ return _drawing_event_signal.connect(slot);
+ }
+
+protected:
+ ~CanvasItemDrawing() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ // Selection
+ Geom::Point _c;
+ double _delta = Geom::infinity();
+ Inkscape::DrawingItem *_active_item = nullptr;
+ Inkscape::DrawingItem *_picked_item = nullptr;
+
+ // Display
+ std::unique_ptr<Inkscape::Drawing> _drawing;
+ Geom::Affine _drawing_affine;
+
+ // Events
+ bool _cursor = false;
+ bool _sticky = false; // Pick anything, even if hidden.
+ bool _pick_outline = false;
+
+ // Signals
+ sigc::signal<bool (GdkEvent*, Inkscape::DrawingItem *)> _drawing_event_signal;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_DRAWING_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-enums.h b/src/display/control/canvas-item-enums.h
new file mode 100644
index 0000000..ab173f3
--- /dev/null
+++ b/src/display/control/canvas-item-enums.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Enums for CanvasItems.
+ */
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ */
+
+#ifndef SEEN_CANVAS_ITEM_ENUMS_H
+#define SEEN_CANVAS_ITEM_ENUMS_H
+
+namespace Inkscape {
+
+enum CanvasItemColor {
+ CANVAS_ITEM_PRIMARY,
+ CANVAS_ITEM_SECONDARY,
+ CANVAS_ITEM_TERTIARY
+};
+
+enum CanvasItemCtrlShape {
+ CANVAS_ITEM_CTRL_SHAPE_SQUARE,
+ CANVAS_ITEM_CTRL_SHAPE_DIAMOND,
+ CANVAS_ITEM_CTRL_SHAPE_CIRCLE,
+ CANVAS_ITEM_CTRL_SHAPE_TRIANGLE,
+ CANVAS_ITEM_CTRL_SHAPE_CROSS,
+ CANVAS_ITEM_CTRL_SHAPE_PLUS,
+ CANVAS_ITEM_CTRL_SHAPE_PIVOT, // Fancy "plus"
+ CANVAS_ITEM_CTRL_SHAPE_DARROW, // Double headed arrow.
+ CANVAS_ITEM_CTRL_SHAPE_SARROW, // Double headed arrow, rotated (skew).
+ CANVAS_ITEM_CTRL_SHAPE_CARROW, // Double headed curved arrow.
+ CANVAS_ITEM_CTRL_SHAPE_SALIGN, // Side alignment.
+ CANVAS_ITEM_CTRL_SHAPE_CALIGN, // Corner alignment.
+ CANVAS_ITEM_CTRL_SHAPE_MALIGN, // Center (middle) alignment.
+ CANVAS_ITEM_CTRL_SHAPE_BITMAP,
+ CANVAS_ITEM_CTRL_SHAPE_IMAGE,
+ CANVAS_ITEM_CTRL_SHAPE_LINE,
+ CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED,
+};
+
+// Applies to control points.
+enum CanvasItemCtrlType {
+ CANVAS_ITEM_CTRL_TYPE_DEFAULT,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE, // Stretch & Scale
+ CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN,
+ CANVAS_ITEM_CTRL_TYPE_ANCHOR,
+ CANVAS_ITEM_CTRL_TYPE_POINT,
+ CANVAS_ITEM_CTRL_TYPE_ROTATE,
+ CANVAS_ITEM_CTRL_TYPE_MARGIN,
+ CANVAS_ITEM_CTRL_TYPE_CENTER,
+ CANVAS_ITEM_CTRL_TYPE_SIZER,
+ CANVAS_ITEM_CTRL_TYPE_SHAPER,
+ CANVAS_ITEM_CTRL_TYPE_LPE,
+ CANVAS_ITEM_CTRL_TYPE_NODE_AUTO,
+ CANVAS_ITEM_CTRL_TYPE_NODE_CUSP,
+ CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH,
+ CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL,
+ CANVAS_ITEM_CTRL_TYPE_INVISIPOINT
+};
+
+enum CanvasItemCtrlMode {
+ CANVAS_ITEM_CTRL_MODE_COLOR,
+ CANVAS_ITEM_CTRL_MODE_XOR,
+ CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR,
+ CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_ENUMS_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-grid.cpp b/src/display/control/canvas-item-grid.cpp
new file mode 100644
index 0000000..2c14ae2
--- /dev/null
+++ b/src/display/control/canvas-item-grid.cpp
@@ -0,0 +1,522 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of GridCanvasItem.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/line.h>
+
+#include "canvas-item-grid.h"
+#include "color.h"
+#include "helper/geom.h"
+
+enum Dim3 { X, Y, Z };
+
+static int calculate_scaling_factor(double length, int major)
+{
+ int multiply = 1;
+ int step = std::max(major, 1);
+ int watchdog = 0;
+
+ while (length * multiply < 8.0 && watchdog < 100) {
+ multiply *= step;
+ // First pass, go up to the major line spacing, then keep increasing by two.
+ step = 2;
+ watchdog++;
+ }
+
+ return multiply;
+}
+
+namespace Inkscape {
+
+/**
+ * Create a null control grid.
+ */
+CanvasItemGrid::CanvasItemGrid(CanvasItemGroup *group)
+ : CanvasItem(group)
+ , _origin(0, 0)
+ , _spacing(1, 1)
+ , _minor_color(GRID_DEFAULT_MINOR_COLOR)
+ , _major_color(GRID_DEFAULT_MAJOR_COLOR)
+ , _major_line_interval(5)
+ , _dotted(false)
+{
+ _no_emp_when_zoomed_out = Preferences::get()->getBool("/options/grids/no_emphasize_when_zoomedout");
+ _pref_tracker = Preferences::PreferencesObserver::create("/options/grids/no_emphasize_when_zoomedout", [this] (auto &entry) {
+ set_no_emp_when_zoomed_out(entry.getBool());
+ });
+
+ request_update();
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of grid.
+ */
+bool CanvasItemGrid::contains(Geom::Point const &p, double tolerance)
+{
+ return false; // We're not pickable!
+}
+
+// Find the signed distance of a point to a line. The distance is negative if
+// the point lies to the left of the line considering the line's versor.
+static double signed_distance(Geom::Point const &point, Geom::Line const &line)
+{
+ return Geom::cross(point - line.initialPoint(), line.versor());
+}
+
+// Find intersections of line with rectangle. There should be zero or two.
+// If line is degenerate with rectangle side, two corner points are returned.
+static std::vector<Geom::Point> intersect_line_rectangle(Geom::Line const &line, Geom::Rect const &rect)
+{
+ std::vector<Geom::Point> intersections;
+ for (int i = 0; i < 4; ++i) {
+ Geom::LineSegment side(rect.corner(i), rect.corner((i + 1) % 4));
+ try {
+ if (auto oc = Geom::intersection(line, side)) {
+ intersections.emplace_back(line.pointAt(oc->ta));
+ }
+ } catch (Geom::InfiniteSolutions const &) {
+ return { side.pointAt(0), side.pointAt(1) };
+ }
+ }
+ return intersections;
+}
+
+void CanvasItemGrid::set_origin(Geom::Point const &point)
+{
+ defer([=] {
+ if (_origin == point) return;
+ _origin = point;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_major_color(uint32_t color)
+{
+ defer([=] {
+ if (_major_color == color) return;
+ _major_color = color;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_minor_color(uint32_t color)
+{
+ defer([=] {
+ if (_minor_color == color) return;
+ _minor_color = color;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_dotted(bool dotted)
+{
+ defer([=] {
+ if (_dotted == dotted) return;
+ _dotted = dotted;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_spacing(Geom::Point const &point)
+{
+ defer([=] {
+ if (_spacing == point) return;
+ _spacing = point;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_major_line_interval(int n)
+{
+ if (n < 1) return;
+ defer([=] {
+ if (_major_line_interval == n) return;
+ _major_line_interval = n;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_no_emp_when_zoomed_out(bool noemp)
+{
+ if (_no_emp_when_zoomed_out != noemp) {
+ _no_emp_when_zoomed_out = noemp;
+ request_redraw();
+ }
+}
+
+/** ====== Rectangular Grid ====== **/
+
+CanvasItemGridXY::CanvasItemGridXY(Inkscape::CanvasItemGroup *group)
+ : CanvasItemGrid(group)
+{
+ _name = "CanvasItemGridXY";
+}
+
+void CanvasItemGridXY::_update(bool)
+{
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+
+ // Queue redraw of grid area
+ ow = _origin * affine();
+ sw[0] = Geom::Point(_spacing[0], 0) * affine().withoutTranslation();
+ sw[1] = Geom::Point(0, _spacing[1]) * affine().withoutTranslation();
+
+ // Find suitable grid spacing for display
+ for (int dim : {0, 1}) {
+ int const scaling_factor = calculate_scaling_factor(sw[dim].length(), _major_line_interval);
+ sw[dim] *= scaling_factor;
+ scaled[dim] = scaling_factor > 1;
+ }
+
+ request_redraw();
+}
+
+void CanvasItemGridXY::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // no_emphasize_when_zoomedout determines color (minor or major) when only major grid lines/dots shown.
+ uint32_t empcolor = ((scaled[Geom::X] || scaled[Geom::Y]) && _no_emp_when_zoomed_out) ? _minor_color : _major_color;
+ uint32_t color = _minor_color;
+
+ buf.cr->save();
+ buf.cr->translate(-buf.rect.left(), -buf.rect.top());
+ buf.cr->set_line_width(1.0);
+ buf.cr->set_line_cap(Cairo::LINE_CAP_SQUARE);
+
+ // Add a 2px margin to the buffer rectangle to avoid missing intersections (in case of rounding errors, and due to adding 0.5 below)
+ auto const buf_rect_with_margin = expandedBy(buf.rect, 2);
+
+ for (int dim : {0, 1}) {
+ int const nrm = dim ^ 0x1;
+
+ // Construct an axis line through origin with direction normal to grid spacing.
+ Geom::Line axis = Geom::Line::from_origin_and_vector(ow, sw[dim]);
+ Geom::Line orth = Geom::Line::from_origin_and_vector(ow, sw[nrm]);
+
+ double spacing = sw[nrm].length(); // Spacing between grid lines.
+ double dash = sw[dim].length(); // Total length of dash pattern.
+
+ // Find the minimum and maximum distances of the buffer corners from axis.
+ double min = Geom::infinity();
+ double max = -Geom::infinity();
+ for (int c = 0; c < 4; ++c) {
+
+ // We need signed distance... lib2geom offers only positive distance.
+ double distance = signed_distance(buf_rect_with_margin.corner(c), axis);
+
+ // Correct it for coordinate flips (inverts handedness).
+ if (Geom::cross(axis.vector(), orth.vector()) > 0) {
+ distance = -distance;
+ }
+
+ min = std::min(min, distance);
+ max = std::max(max, distance);
+ }
+ int start = std::floor(min / spacing);
+ int stop = std::floor(max / spacing);
+
+ // Loop over grid lines that intersected buf rectangle.
+ for (int j = start + 1; j <= stop; ++j) {
+
+ Geom::Line grid_line = Geom::make_parallel_line(ow + j * sw[nrm], axis);
+
+ std::vector<Geom::Point> x = intersect_line_rectangle(grid_line, buf_rect_with_margin);
+
+ // If we have two intersections, grid line intersects buffer rectangle.
+ if (x.size() == 2) {
+ // Make sure lines are always drawn in the same direction (or dashes misplaced).
+ Geom::Line vector(x[0], x[1]);
+ if (Geom::dot(vector.vector(), axis.vector()) < 0.0) {
+ std::swap(x[0], x[1]);
+ }
+
+ // Set up line. Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel, without any aliasing.
+ // For this we need to position the lines at exactly half pixels, see https://www.cairographics.org/FAQ/#sharp_lines
+ // Must be consistent with the pixel alignment of the guide lines, see CanvasItemGridXY::render(), and the drawing of the rulers
+ buf.cr->move_to(floor(x[0].x()) + 0.5, floor(x[0].y()) + 0.5);
+ buf.cr->line_to(floor(x[1].x()) + 0.5, floor(x[1].y()) + 0.5);
+
+ // Determine whether to draw with the emphasis color.
+ bool const noemp = !scaled[dim] && j % _major_line_interval != 0;
+
+ // Set dash pattern and color.
+ if (_dotted) {
+ // alpha needs to be larger than in the line case to maintain a similar
+ // visual impact but setting it to the maximal value makes the dots
+ // dominant in some cases. Solution, increase the alpha by a factor of
+ // 4. This then allows some user adjustment.
+ uint32_t _empdot = (empcolor & 0xff) << 2;
+ if (_empdot > 0xff)
+ _empdot = 0xff;
+ _empdot += (empcolor & 0xffffff00);
+
+ uint32_t _colordot = (color & 0xff) << 2;
+ if (_colordot > 0xff)
+ _colordot = 0xff;
+ _colordot += (color & 0xffffff00);
+
+ // Dash pattern must use spacing from orthogonal direction.
+ // Offset is to center dash on orthogonal lines.
+ double offset = std::fmod(signed_distance(x[0], orth), sw[dim].length());
+ if (Geom::cross(axis.vector(), orth.vector()) > 0) {
+ offset = -offset;
+ }
+
+ std::vector<double> dashes;
+ if (noemp) {
+ // Minor lines
+ dashes.push_back(1.0);
+ dashes.push_back(dash - 1.0);
+ offset -= 0.5;
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_colordot), SP_RGBA32_G_F(_colordot),
+ SP_RGBA32_B_F(_colordot), SP_RGBA32_A_F(_colordot));
+ } else {
+ // Major lines
+ dashes.push_back(3.0);
+ dashes.push_back(dash - 3.0);
+ offset -= 1.5; // Center dash on intersection.
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_empdot), SP_RGBA32_G_F(_empdot),
+ SP_RGBA32_B_F(_empdot), SP_RGBA32_A_F(_empdot));
+ }
+
+ buf.cr->set_line_cap(Cairo::LINE_CAP_BUTT);
+ buf.cr->set_dash(dashes, -offset);
+
+ } else {
+ // Solid lines
+ uint32_t col = noemp ? color : empcolor;
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(col), SP_RGBA32_G_F(col),
+ SP_RGBA32_B_F(col), SP_RGBA32_A_F(col));
+ }
+
+ buf.cr->stroke();
+
+ } else {
+ std::cerr << "CanvasItemGridXY::render: Grid line doesn't intersect!" << std::endl;
+ }
+ }
+ }
+
+ buf.cr->restore();
+}
+
+/** ========= Axonometric Grids ======== */
+
+/*
+ * Current limits are: one axis (y-axis) is always vertical. The other two
+ * axes are bound to a certain range of angles. The z-axis always has an angle
+ * smaller than 90 degrees (measured from horizontal, 0 degrees being a line extending
+ * to the right). The x-axis will always have an angle between 0 and 90 degrees.
+ */
+CanvasItemGridAxonom::CanvasItemGridAxonom(Inkscape::CanvasItemGroup *group)
+ : CanvasItemGrid(group)
+{
+ _name = "CanvasItemGridAxonom";
+
+ angle_deg[X] = 30.0;
+ angle_deg[Y] = 30.0;
+ angle_deg[Z] = 0.0;
+
+ angle_rad[X] = Geom::rad_from_deg(angle_deg[X]);
+ angle_rad[Y] = Geom::rad_from_deg(angle_deg[Y]);
+ angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]);
+
+ tan_angle[X] = std::tan(angle_rad[X]);
+ tan_angle[Y] = std::tan(angle_rad[Y]);
+ tan_angle[Z] = std::tan(angle_rad[Z]);
+}
+
+void CanvasItemGridAxonom::_update(bool)
+{
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+
+ ow = _origin * affine();
+ lyw = _spacing.y() * affine().descrim();
+
+ int const scaling_factor = calculate_scaling_factor(lyw, _major_line_interval);
+ lyw *= scaling_factor;
+ scaled = scaling_factor > 1;
+
+ spacing_ylines = lyw / (tan_angle[X] + tan_angle[Z]);
+ lxw_x = Geom::are_near(tan_angle[X], 0) ? Geom::infinity() : lyw / tan_angle[X];
+ lxw_z = Geom::are_near(tan_angle[Z], 0) ? Geom::infinity() : lyw / tan_angle[Z];
+
+ if (_major_line_interval == 0) {
+ scaled = true;
+ }
+
+ request_redraw();
+}
+
+// expects value given to be in degrees
+void CanvasItemGridAxonom::set_angle_x(double deg)
+{
+ defer([=] {
+ angle_deg[X] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns
+ angle_rad[X] = Geom::rad_from_deg(angle_deg[X]);
+ tan_angle[X] = std::tan(angle_rad[X]);
+ request_update();
+ });
+}
+
+// expects value given to be in degrees
+void CanvasItemGridAxonom::set_angle_z(double deg)
+{
+ defer([=] {
+ angle_deg[Z] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns
+ angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]);
+ tan_angle[Z] = std::tan(angle_rad[Z]);
+ request_update();
+ });
+}
+
+static void drawline(Inkscape::CanvasItemBuffer &buf, int x0, int y0, int x1, int y1, uint32_t rgba)
+{
+ buf.cr->move_to(0.5 + x0, 0.5 + y0);
+ buf.cr->line_to(0.5 + x1, 0.5 + y1);
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba),
+ SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+ buf.cr->stroke();
+}
+
+static void vline(Inkscape::CanvasItemBuffer &buf, int x, int ys, int ye, uint32_t rgba)
+{
+ if (x < buf.rect.left() || x >= buf.rect.right())
+ return;
+
+ buf.cr->move_to(0.5 + x, 0.5 + ys);
+ buf.cr->line_to(0.5 + x, 0.5 + ye);
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba),
+ SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+ buf.cr->stroke();
+}
+
+/**
+ * This function calls Cairo to render a line on a particular canvas buffer.
+ * Coordinates are interpreted as SCREENcoordinates
+ */
+void CanvasItemGridAxonom::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // Set correct coloring, depending preference (when zoomed out, always major coloring or minor coloring)
+ uint32_t empcolor = (scaled && _no_emp_when_zoomed_out) ? _minor_color : _major_color;
+ uint32_t color = _minor_color;
+
+ buf.cr->save();
+ buf.cr->translate(-buf.rect.left(), -buf.rect.top());
+ buf.cr->set_line_width(1.0);
+ buf.cr->set_line_cap(Cairo::LINE_CAP_SQUARE);
+
+ // gc = gridcoordinates (the coordinates calculated from the grids origin 'grid->ow'.
+ // sc = screencoordinates ( for example "buf.rect.left()" is in screencoordinates )
+ // bc = buffer patch coordinates (x=0 on left side of page, y=0 on bottom of page)
+
+ // tl = topleft
+ auto const buf_tl_gc = buf.rect.min() - ow;
+
+ // render the three separate line groups representing the main-axes
+
+ // x-axis always goes from topleft to bottomright. (0,0) - (1,1)
+ double const xintercept_y_bc = (buf_tl_gc.x() * tan_angle[X]) - buf_tl_gc.y();
+ double const xstart_y_sc = (xintercept_y_bc - std::floor(xintercept_y_bc / lyw) * lyw) + buf.rect.top();
+ int const xlinestart = std::round((xstart_y_sc - buf_tl_gc.x() * tan_angle[X] - ow.y()) / lyw);
+ int xlinenum = xlinestart;
+
+ // lines starting on left side.
+ for (double y = xstart_y_sc; y < buf.rect.bottom(); y += lyw, xlinenum++) {
+ int const x0 = buf.rect.left();
+ int const y0 = round(y);
+ int x1 = x0 + round((buf.rect.bottom() - y) / tan_angle[X]);
+ int y1 = buf.rect.bottom();
+ if (Geom::are_near(tan_angle[X], 0)) {
+ x1 = buf.rect.right();
+ y1 = y0;
+ }
+
+ bool const noemp = !scaled && xlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+
+ // lines starting from top side
+ if (!Geom::are_near(tan_angle[X], 0)) {
+ double const xstart_x_sc = buf.rect.left() + (lxw_x - (xstart_y_sc - buf.rect.top()) / tan_angle[X]);
+ xlinenum = xlinestart-1;
+ for (double x = xstart_x_sc; x < buf.rect.right(); x += lxw_x, xlinenum--) {
+ int const y0 = buf.rect.top();
+ int const y1 = buf.rect.bottom();
+ int const x0 = round(x);
+ int const x1 = x0 + round((y1 - y0) / tan_angle[X]);
+
+ bool const noemp = !scaled && xlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+ }
+
+ // y-axis lines (vertical)
+ double const ystart_x_sc = floor (buf_tl_gc[Geom::X] / spacing_ylines) * spacing_ylines + ow[Geom::X];
+ int const ylinestart = round((ystart_x_sc - ow[Geom::X]) / spacing_ylines);
+ int ylinenum = ylinestart;
+ for (double x = ystart_x_sc; x < buf.rect.right(); x += spacing_ylines, ylinenum++) {
+ int const x0 = floor(x); // sp_grid_vline will add 0.5 again, so we'll pre-emptively use floor()
+ // instead of round() to avoid biasing the vertical lines to the right by half a pixel; see
+ // CanvasItemGridXY::render() for more details
+ bool const noemp = !scaled && ylinenum % _major_line_interval != 0;
+ vline(buf, x0, buf.rect.top(), buf.rect.bottom() - 1, noemp ? color : empcolor);
+ }
+
+ // z-axis always goes from bottomleft to topright. (0,1) - (1,0)
+ double const zintercept_y_bc = (buf_tl_gc.x() * -tan_angle[Z]) - buf_tl_gc.y();
+ double const zstart_y_sc = (zintercept_y_bc - std::floor(zintercept_y_bc / lyw) * lyw) + buf.rect.top();
+ int const zlinestart = std::round((zstart_y_sc + buf_tl_gc.x() * tan_angle[Z] - ow.y()) / lyw);
+ int zlinenum = zlinestart;
+ // lines starting from left side
+ double next_y = zstart_y_sc;
+ for (double y = zstart_y_sc; y < buf.rect.bottom(); y += lyw, zlinenum++, next_y = y) {
+ int const x0 = buf.rect.left();
+ int const y0 = round(y);
+ int x1 = x0 + round((y - buf.rect.top()) / tan_angle[Z]);
+ int y1 = buf.rect.top();
+ if (Geom::are_near(tan_angle[Z], 0)) {
+ x1 = buf.rect.right();
+ y1 = y0;
+ }
+
+ bool const noemp = !scaled && zlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+
+ // draw lines from bottom-up
+ if (!Geom::are_near(tan_angle[Z], 0)) {
+ double const zstart_x_sc = buf.rect.left() + (next_y - buf.rect.bottom()) / tan_angle[Z];
+ for (double x = zstart_x_sc; x < buf.rect.right(); x += lxw_z, zlinenum++) {
+ int const y0 = buf.rect.bottom();
+ int const y1 = buf.rect.top();
+ int const x0 = round(x);
+ int const x1 = x0 + round(buf.rect.height() / tan_angle[Z]);
+
+ bool const noemp = !scaled && zlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+ }
+
+ buf.cr->restore();
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-grid.h b/src/display/control/canvas-item-grid.h
new file mode 100644
index 0000000..3e8bc28
--- /dev/null
+++ b/src/display/control/canvas-item-grid.h
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of GridCanvasItem.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_CANVAS_ITEM_GRID_H
+#define SEEN_CANVAS_ITEM_GRID_H
+
+#include <cstdint>
+#include <2geom/point.h>
+
+#include "canvas-item.h"
+#include "preferences.h"
+
+uint32_t constexpr GRID_DEFAULT_MAJOR_COLOR = 0x0099e54d;
+uint32_t constexpr GRID_DEFAULT_MINOR_COLOR = 0x0099e526;
+
+namespace Inkscape {
+
+class CanvasItemGrid : public CanvasItem
+{
+public:
+ CanvasItemGrid(CanvasItemGroup *group);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Properties
+ void set_major_color(uint32_t color);
+ void set_minor_color(uint32_t color);
+ void set_origin(Geom::Point const &point);
+ void set_spacing(Geom::Point const &point);
+ void set_dotted(bool b);
+ void set_major_line_interval(int n);
+ void set_no_emp_when_zoomed_out(bool noemp);
+
+protected:
+ ~CanvasItemGrid() override = default;
+
+ bool _dotted;
+
+ Geom::Point _origin;
+
+ Geom::Point _spacing; /**< Spacing between elements of the grid */
+
+ int _major_line_interval;
+ bool _no_emp_when_zoomed_out;
+ uint32_t _major_color;
+ uint32_t _minor_color;
+
+private:
+ std::unique_ptr<Preferences::PreferencesObserver> _pref_tracker;
+};
+
+/** Canvas Item for rectangular grids */
+class CanvasItemGridXY final : public CanvasItemGrid
+{
+public:
+ CanvasItemGridXY(CanvasItemGroup *group);
+
+protected:
+ friend class GridSnapperXY;
+
+ void _update(bool propagate) override;
+ void _render(CanvasItemBuffer &buf) const override;
+
+ bool scaled[2]; /**< Whether the grid is in scaled mode, which can
+ be different in the X or Y direction, hence two
+ variables */
+ Geom::Point ow; /**< Transformed origin by the affine for the zoom */
+ Geom::Point sw[2]; /**< Transformed spacing by the affine for the zoom */
+};
+
+/** Canvas Item for axonometric grids */
+class CanvasItemGridAxonom final : public CanvasItemGrid
+{
+public:
+ CanvasItemGridAxonom(CanvasItemGroup *group);
+
+ // Properties
+ void set_angle_x(double value);
+ void set_angle_z(double value);
+
+protected:
+ friend class GridSnapperAxonom;
+
+ void _update(bool propagate) override;
+ void _render(CanvasItemBuffer &buf) const override;
+
+ bool scaled; /**< Whether the grid is in scaled mode */
+
+ double angle_deg[3]; /**< Angle of each axis (note that angle[2] == 0) */
+ double angle_rad[3]; /**< Angle of each axis (note that angle[2] == 0) */
+ double tan_angle[3]; /**< tan(angle[.]) */
+
+ double lyw = 1.0; /**< Transformed length y by the affine for the zoom */
+ double lxw_x = 1.0;
+ double lxw_z = 1.0;
+ double spacing_ylines = 1.0;
+
+ Geom::Point ow; /**< Transformed origin by the affine for the zoom */
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_GRID_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-group.cpp b/src/display/control/canvas-item-group.cpp
new file mode 100644
index 0000000..93193c3
--- /dev/null
+++ b/src/display/control/canvas-item-group.cpp
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A CanvasItem that contains other CanvasItem's.
+ */
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasGroup
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <boost/range/adaptor/reversed.hpp>
+#include "canvas-item-group.h"
+
+constexpr bool DEBUG_LOGGING = false;
+
+namespace Inkscape {
+
+CanvasItemGroup::CanvasItemGroup(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemGroup";
+ _pickable = true; // For now all groups are pickable... look into turning this off for some groups (e.g. temp).
+}
+
+CanvasItemGroup::CanvasItemGroup(CanvasItemContext *context)
+ : CanvasItem(context)
+{
+ _name = "CanvasItemGroup:Root";
+ _pickable = true; // see above
+}
+
+CanvasItemGroup::~CanvasItemGroup()
+{
+ items.clear_and_dispose([] (auto c) { delete c; });
+}
+
+void CanvasItemGroup::_update(bool propagate)
+{
+ _bounds = {};
+
+ // Update all children and calculate new bounds.
+ for (auto &item : items) {
+ item.update(propagate);
+ _bounds |= item.get_bounds();
+ }
+}
+
+void CanvasItemGroup::_mark_net_invisible()
+{
+ if (!_net_visible) {
+ return;
+ }
+ _net_visible = false;
+ _need_update = false;
+ for (auto &item : items) {
+ item._mark_net_invisible();
+ }
+ _bounds = {};
+}
+
+void CanvasItemGroup::visit_page_rects(std::function<void(Geom::Rect const &)> const &f) const
+{
+ for (auto &item : items) {
+ if (!item.is_visible()) continue;
+ item.visit_page_rects(f);
+ }
+}
+
+void CanvasItemGroup::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ for (auto &item : items) {
+ item.render(buf);
+ }
+}
+
+// Return last visible and pickable item that contains point.
+// SPCanvasGroup returned distance but it was not used.
+CanvasItem *CanvasItemGroup::pick_item(Geom::Point const &p)
+{
+ if constexpr (DEBUG_LOGGING) {
+ std::cout << "CanvasItemGroup::pick_item:" << std::endl;
+ std::cout << " PICKING: In group: " << _name << " bounds: " << _bounds << std::endl;
+ }
+
+ for (auto &item : boost::adaptors::reverse(items)) {
+ if constexpr (DEBUG_LOGGING) std::cout << " PICKING: Checking: " << item.get_name() << " bounds: " << item.get_bounds() << std::endl;
+
+ if (item.is_visible() && item.is_pickable() && item.contains(p)) {
+ if (auto group = dynamic_cast<CanvasItemGroup*>(&item)) {
+ if (auto ret = group->pick_item(p)) {
+ return ret;
+ }
+ } else {
+ return &item;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-group.h b/src/display/control/canvas-item-group.h
new file mode 100644
index 0000000..4ab5884
--- /dev/null
+++ b/src/display/control/canvas-item-group.h
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_GROUP_H
+#define SEEN_CANVAS_ITEM_GROUP_H
+
+/**
+ * A CanvasItem that contains other CanvasItems.
+ */
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasGroup
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemGroup final : public CanvasItem
+{
+public:
+ CanvasItemGroup(CanvasItemGroup *group);
+ CanvasItemGroup(CanvasItemContext *context);
+
+ // Geometry
+ void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const override;
+
+ // Selection
+ CanvasItem *pick_item(Geom::Point const &p);
+
+protected:
+ friend class CanvasItem; // access to items
+ friend class CanvasItemContext; // access to destructor
+
+ ~CanvasItemGroup() override;
+
+ void _update(bool propagate) override;
+ void _mark_net_invisible() override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ /**
+ * Type for linked list storing CanvasItems.
+ * Used to speed deletion when a group contains a large number of items (as in nodes for a complex path).
+ */
+ using CanvasItemList = boost::intrusive::list<
+ Inkscape::CanvasItem,
+ boost::intrusive::member_hook<Inkscape::CanvasItem, boost::intrusive::list_member_hook<>,
+ &Inkscape::CanvasItem::member_hook>>;
+
+ CanvasItemList items;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-guideline.cpp b/src/display/control/canvas-item-guideline.cpp
new file mode 100644
index 0000000..7cefc2b
--- /dev/null
+++ b/src/display/control/canvas-item-guideline.cpp
@@ -0,0 +1,312 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control guide line.
+ */
+
+/*
+ * Authors:
+ * Tavmjong Bah - Rewrite of SPGuideLine
+ * Rafael Siejakowski - Tweaks to handle appearance
+ *
+ * Copyright (C) 2020-2022 the Authors.
+ *
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/line.h>
+
+#include "canvas-item-guideline.h"
+#include "canvas-item-ctrl.h"
+
+#include "desktop.h" // Canvas orientation so label is orientated correctly.
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create a control guide line. Points are in document units.
+ */
+CanvasItemGuideLine::CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label,
+ Geom::Point const &origin, Geom::Point const &normal)
+ : CanvasItem(group)
+ , _origin(origin)
+ , _normal(normal)
+ , _label(std::move(label))
+{
+ _name = "CanvasItemGuideLine:" + _label;
+ _pickable = true; // For now, everybody gets events from this class!
+
+ // Control to move guide line.
+ _origin_ctrl = make_canvasitem<CanvasItemGuideHandle>(group, _origin, this);
+ _origin_ctrl->set_name("CanvasItemGuideLine:Ctrl:" + _label);
+ _origin_ctrl->set_size_default();
+ _origin_ctrl->set_pickable(true); // The handle will also react to dragging
+ set_locked(false); // Init _origin_ctrl shape and stroke.
+}
+
+/**
+ * Sets origin of guide line (place where handle is located).
+ */
+void CanvasItemGuideLine::set_origin(Geom::Point const &origin)
+{
+ if (_origin != origin) {
+ _origin = origin;
+ _origin_ctrl->set_position(_origin);
+ request_update();
+ }
+}
+
+/**
+ * Sets orientation of guide line.
+ */
+void CanvasItemGuideLine::set_normal(Geom::Point const &normal)
+{
+ if (_normal != normal) {
+ _normal = normal;
+ request_update();
+ }
+}
+
+/**
+ * Sets the inverted nature of the line
+ */
+void CanvasItemGuideLine::set_inverted(bool inverted)
+{
+ if (_inverted != inverted) {
+ _inverted = inverted;
+ request_update();
+ }
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on guideLine.
+ */
+double CanvasItemGuideLine::closest_distance_to(Geom::Point const &p)
+{
+ // Maybe store guide as a Geom::Line?
+ auto guide = Geom::Line::from_origin_and_vector(_origin, Geom::rot90(_normal));
+ guide *= affine();
+ return Geom::distance(p, guide);
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of guideLine (or 1 if tolerance is zero).
+ */
+bool CanvasItemGuideLine::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance == 0) {
+ tolerance = 1; // Can't pick of zero!
+ }
+
+ return closest_distance_to(p) < tolerance;
+}
+
+/**
+ * Returns the pointer to the origin control (the "dot")
+ */
+CanvasItemGuideHandle* CanvasItemGuideLine::dot() const
+{
+ return _origin_ctrl.get();
+}
+
+/**
+ * Update and redraw control guideLine.
+ */
+void CanvasItemGuideLine::_update(bool)
+{
+ // Required when rotating canvas
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+
+ // Queue redraw of new area (and old too).
+ request_redraw();
+}
+
+/**
+ * Render guideLine to screen via Cairo.
+ */
+void CanvasItemGuideLine::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // Document to canvas
+ Geom::Point const normal = _normal * affine().withoutTranslation(); // Direction only
+ Geom::Point const origin = _origin * affine();
+
+ /* Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel,
+ * without any aliasing. For this we need to position the lines at exactly half pixels, see
+ * https://www.cairographics.org/FAQ/#sharp_lines
+ * Must be consistent with the pixel alignment of the grid lines, see CanvasXYGrid::Render(),
+ * and the drawing of the rulers.
+ * Lastly, the origin control is also pixel-aligned and we want to visually cut through its
+ * exact center.
+ */
+ Geom::Point const aligned_origin = origin.floor() + Geom::Point(0.5, 0.5);
+
+ // Set up the Cairo rendering context
+ auto ctx = buf.cr;
+ ctx->save();
+ ctx->translate(-buf.rect.left(), -buf.rect.top()); // Canvas to screen
+ ctx->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ ctx->set_line_width(1);
+
+ if (_inverted) {
+ // operator not available in cairo C++ bindings
+ cairo_set_operator(ctx->cobj(), CAIRO_OPERATOR_DIFFERENCE);
+ }
+
+ if (!_label.empty()) { // Render text label
+ ctx->save();
+ ctx->translate(aligned_origin.x(), aligned_origin.y());
+
+ auto desktop = get_canvas()->get_desktop();
+ ctx->rotate(atan2(normal.cw()) + M_PI * (desktop && desktop->is_yaxisdown() ? 1 : 0));
+ ctx->translate(0, -(_origin_ctrl->radius() + LABEL_SEP)); // Offset by dot radius + 2
+ ctx->move_to(0, 0);
+ ctx->show_text(_label);
+ ctx->restore();
+ }
+
+ // Draw guide.
+ // Special case: horizontal and vertical lines (easier calculations)
+
+ // Don't use isHorizontal()/isVertical() as they test only exact matches.
+ if (Geom::are_near(normal.y(), 0.0)) {
+ // Vertical
+ double const position = aligned_origin.x();
+ ctx->move_to(position, buf.rect.top() + 0.5);
+ ctx->line_to(position, buf.rect.bottom() - 0.5);
+ } else if (Geom::are_near(normal.x(), 0.0)) {
+ // Horizontal
+ double position = aligned_origin.y();
+ ctx->move_to(buf.rect.left() + 0.5, position);
+ ctx->line_to(buf.rect.right() - 0.5, position);
+ } else {
+ // Angled
+ Geom::Line line = Geom::Line::from_origin_and_vector(aligned_origin, Geom::rot90(normal));
+
+ // Find intersections of the line with buf rectangle. There should be zero or two.
+ std::vector<Geom::Point> intersections;
+ for (unsigned i = 0; i < 4; ++i) {
+ Geom::LineSegment side(buf.rect.corner(i), buf.rect.corner((i+1)%4));
+ try {
+ Geom::OptCrossing oc = Geom::intersection(line, side);
+ if (oc) {
+ intersections.push_back(line.pointAt(oc->ta));
+ }
+ } catch (Geom::InfiniteSolutions const &) {
+ // Shouldn't happen as we have already taken care of horizontal/vertical guides.
+ std::cerr << "CanvasItemGuideLine::render: Error: Infinite intersections." << std::endl;
+ }
+ }
+
+ if (intersections.size() == 2) {
+ double const x0 = intersections[0].x();
+ double const x1 = intersections[1].x();
+ double const y0 = intersections[0].y();
+ double const y1 = intersections[1].y();
+ ctx->move_to(x0, y0);
+ ctx->line_to(x1, y1);
+ }
+ }
+ ctx->stroke();
+
+ ctx->restore();
+}
+
+void CanvasItemGuideLine::set_visible(bool visible)
+{
+ CanvasItem::set_visible(visible);
+ _origin_ctrl->set_visible(visible);
+}
+
+void CanvasItemGuideLine::set_stroke(uint32_t color)
+{
+ // Make sure the fill of the control is the same as the stroke
+ // of the guide-line:
+ _origin_ctrl->set_fill(color);
+ CanvasItem::set_stroke(color);
+}
+
+void CanvasItemGuideLine::set_label(Glib::ustring &&label)
+{
+ defer([=, label = std::move(label)] () mutable {
+ if (_label == label) return;
+ _label = std::move(label);
+ request_update();
+ });
+}
+
+void CanvasItemGuideLine::set_locked(bool locked)
+{
+ defer([=] {
+ if (_locked == locked) return;
+ _locked = locked;
+ if (_locked) {
+ _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CROSS);
+ _origin_ctrl->set_stroke(CONTROL_LOCKED_COLOR);
+ _origin_ctrl->set_fill(0x00000000); // no fill
+ } else {
+ _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ _origin_ctrl->set_stroke(0x00000000); // no stroke
+ _origin_ctrl->set_fill(_stroke); // fill the control with this guide's color
+ }
+ });
+}
+
+//===============================================================================================
+
+/**
+ * @brief Create a handle ("dot") along a guide line
+ * @param group - the associated canvas item group
+ * @param pos - position
+ * @param line - pointer to the corresponding guide line
+ */
+CanvasItemGuideHandle::CanvasItemGuideHandle(CanvasItemGroup *group,
+ Geom::Point const &pos,
+ CanvasItemGuideLine* line)
+ : CanvasItemCtrl(group, CANVAS_ITEM_CTRL_SHAPE_CIRCLE, pos)
+ , _my_line(line) // Save a pointer to our guide line
+{
+}
+
+/**
+ * Return the radius of the handle dot
+ */
+double CanvasItemGuideHandle::radius() const
+{
+ return 0.5 * static_cast<double>(_width); // radius is half the width
+}
+
+/**
+ * Update the size of the handle based on the index from Preferences
+ */
+void CanvasItemGuideHandle::set_size_via_index(int index)
+{
+ double const r = static_cast<double>(index) * SCALE;
+ unsigned long const rounded_diameter = std::lround(r * 2.0); // diameter is twice the radius
+ unsigned long size = rounded_diameter | 0x1; // make sure the size is always odd
+ if (size < MINIMUM_SIZE) {
+ size = MINIMUM_SIZE;
+ }
+ defer([=] {
+ if (_width == size) return;
+ _width = size;
+ _height = size;
+ _built.reset();
+ request_update();
+ _my_line->request_update();
+ });
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-guideline.h b/src/display/control/canvas-item-guideline.h
new file mode 100644
index 0000000..424fb27
--- /dev/null
+++ b/src/display/control/canvas-item-guideline.h
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_GUIDELINE_H
+#define SEEN_CANVAS_ITEM_GUIDELINE_H
+
+/**
+ * A class to represent a control guide line.
+ */
+
+/*
+ * Authors:
+ * Tavmjong Bah - Rewrite of SPGuideLine
+ * Rafael Siejakowski - Tweaks to handle appearance
+ *
+ * Copyright (C) 2020-2022 the Authors.
+ *
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+#include "canvas-item-ctrl.h"
+#include "canvas-item-ptr.h"
+
+namespace Inkscape {
+
+class CanvasItemGuideHandle;
+
+class CanvasItemGuideLine final : public CanvasItem
+{
+public:
+ CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label, Geom::Point const &origin, Geom::Point const &normal);
+
+ // Geometry
+ void set_origin(Geom::Point const &origin);
+ void set_normal(Geom::Point const &normal);
+ double closest_distance_to(Geom::Point const &p);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Properties
+ void set_visible(bool visible) override;
+ void set_stroke(uint32_t color) override;
+ void set_label(Glib::ustring &&label);
+ void set_locked(bool locked);
+ void set_inverted(bool inverted);
+
+ // Getters
+ CanvasItemGuideHandle *dot() const;
+
+protected:
+ ~CanvasItemGuideLine() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ Geom::Point _origin;
+ Geom::Point _normal = Geom::Point(0, 1);
+ Glib::ustring _label;
+ bool _locked = true; // Flipped in constructor to trigger init of _origin_ctrl.
+ bool _inverted = false;
+ CanvasItemPtr<CanvasItemGuideHandle> _origin_ctrl;
+
+ static constexpr uint32_t CONTROL_LOCKED_COLOR = 0x00000080; // RGBA black semitranslucent
+ static constexpr double LABEL_SEP = 2.0; // Distance between the label and the origin control
+};
+
+// A handle ("dot") serving as draggable origin control
+class CanvasItemGuideHandle final : public CanvasItemCtrl
+{
+public:
+ CanvasItemGuideHandle(CanvasItemGroup *group, Geom::Point const &pos, CanvasItemGuideLine *line);
+ double radius() const;
+ void set_size_via_index(int index) override;
+
+protected:
+ ~CanvasItemGuideHandle() override = default;
+
+ CanvasItemGuideLine *_my_line; // The guide line we belong to
+
+ // static data
+ static constexpr double SCALE = 0.55; // handle size relative to an auto-smooth node
+ static constexpr unsigned MINIMUM_SIZE = 7; // smallest handle size, must be an odd int
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_GUIDELINE_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-ptr.h b/src/display/control/canvas-item-ptr.h
new file mode 100644
index 0000000..b8c198c
--- /dev/null
+++ b/src/display/control/canvas-item-ptr.h
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_PTR_H
+#define SEEN_CANVAS_ITEM_PTR_H
+
+/*
+ * An entirely analogous file to display/drawing-item-ptr.h.
+ */
+
+#include <memory>
+#include <type_traits>
+
+namespace Inkscape { class CanvasItem; }
+
+/// Deleter object which calls the unlink() method of CanvasItem.
+struct CanvasItemUnlinkDeleter
+{
+ template <typename T>
+ void operator()(T *t)
+ {
+ static_assert(std::is_base_of_v<Inkscape::CanvasItem, T>);
+ t->unlink();
+ }
+};
+
+/// Smart pointer used to hold CanvasItems, like std::unique_ptr.
+template <typename T>
+using CanvasItemPtr = std::unique_ptr<T, CanvasItemUnlinkDeleter>;
+
+/// Convienence function to create a CanvasItemPtr, like std::make_unique.
+template <typename T, typename... Args>
+auto make_canvasitem(Args&&... args)
+{
+ return CanvasItemPtr<T>(new T(std::forward<Args>(args)...));
+};
+
+#endif // SEEN_CANVAS_ITEM_PTR_H
diff --git a/src/display/control/canvas-item-quad.cpp b/src/display/control/canvas-item-quad.cpp
new file mode 100644
index 0000000..06914de
--- /dev/null
+++ b/src/display/control/canvas-item-quad.cpp
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control quadrilateral. Used to highlight selected text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlQuadr
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cassert>
+
+#include "canvas-item-quad.h"
+
+#include "color.h" // SP_RGBA_x_F
+#include "helper/geom.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control quad.
+ */
+CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemQuad:Null";
+}
+
+/**
+ * Create a control quad. Points are in document coordinates.
+ */
+CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group,
+ Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3)
+ : CanvasItem(group)
+ , _p0(p0)
+ , _p1(p1)
+ , _p2(p2)
+ , _p3(p3)
+{
+ _name = "CanvasItemQuad";
+}
+
+/**
+ * Set a control quad. Points are in document coordinates.
+ */
+void CanvasItemQuad::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3)
+{
+ defer([=] {
+ _p0 = p0;
+ _p1 = p1;
+ _p2 = p2;
+ _p3 = p3;
+ request_update();
+ });
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of quad.
+ */
+bool CanvasItemQuad::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance != 0) {
+ std::cerr << "CanvasItemQuad::contains: Non-zero tolerance not implemented!" << std::endl;
+ }
+
+ Geom::Point p0 = _p0 * affine();
+ Geom::Point p1 = _p1 * affine();
+ Geom::Point p2 = _p2 * affine();
+ Geom::Point p3 = _p3 * affine();
+
+ // From 2geom rotated-rect.cpp
+ return
+ Geom::cross(p1 - p0, p - p0) >= 0 &&
+ Geom::cross(p2 - p1, p - p1) >= 0 &&
+ Geom::cross(p3 - p2, p - p2) >= 0 &&
+ Geom::cross(p0 - p3, p - p3) >= 0;
+}
+
+/**
+ * Update and redraw control quad.
+ */
+void CanvasItemQuad::_update(bool)
+{
+ if (_p0 == _p1 || _p1 == _p2 || _p2 == _p3 || _p3 == _p0) {
+ _bounds = {};
+ return; // Not quad or not initialized.
+ }
+
+ // Queue redraw of old area (erase previous content).
+ request_redraw(); // This is actually never useful as quads are always deleted
+ // and recreated when a node is moved! But keep it in case we change that.
+
+ _bounds = expandedBy(bounds_of(_p0, _p1, _p2, _p3) * affine(), 2); // Room for anti-aliasing effects.
+
+ // Queue redraw of new area
+ request_redraw();
+}
+
+/**
+ * Render quad to screen via Cairo.
+ */
+void CanvasItemQuad::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // Document to canvas
+ Geom::Point p0 = _p0 * affine();
+ Geom::Point p1 = _p1 * affine();
+ Geom::Point p2 = _p2 * affine();
+ Geom::Point p3 = _p3 * affine();
+
+ // Canvas to screen
+ p0 *= Geom::Translate(-buf.rect.min());
+ p1 *= Geom::Translate(-buf.rect.min());
+ p2 *= Geom::Translate(-buf.rect.min());
+ p3 *= Geom::Translate(-buf.rect.min());
+
+ buf.cr->save();
+
+ buf.cr->begin_new_path();
+
+ buf.cr->move_to(p0.x(), p0.y());
+ buf.cr->line_to(p1.x(), p1.y());
+ buf.cr->line_to(p2.x(), p2.y());
+ buf.cr->line_to(p3.x(), p3.y());
+ buf.cr->close_path();
+
+ if (_inverted) {
+ cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE);
+ }
+
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill));
+ buf.cr->fill_preserve();
+
+ buf.cr->set_line_width(1);
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ buf.cr->stroke_preserve();
+ buf.cr->begin_new_path();
+
+ buf.cr->restore();
+}
+
+void CanvasItemQuad::set_inverted(bool inverted)
+{
+ defer([=] {
+ if (_inverted == inverted) return;
+ _inverted = inverted;
+ request_redraw();
+ });
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-quad.h b/src/display/control/canvas-item-quad.h
new file mode 100644
index 0000000..deb8e46
--- /dev/null
+++ b/src/display/control/canvas-item-quad.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_QUAD_H
+#define SEEN_CANVAS_ITEM_QUAD_H
+
+/**
+ * A class to represent a control quadrilateral. Used to highlight selected text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlQuadr
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemQuad final : public CanvasItem
+{
+public:
+ CanvasItemQuad(CanvasItemGroup *group);
+ CanvasItemQuad(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3);
+
+ // Geometry
+ void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ void set_inverted(bool inverted);
+
+protected:
+ ~CanvasItemQuad() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ Geom::Point _p0;
+ Geom::Point _p1;
+ Geom::Point _p2;
+ Geom::Point _p3;
+
+ bool _inverted = false;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_QUAD_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp
new file mode 100644
index 0000000..0204cdf
--- /dev/null
+++ b/src/display/control/canvas-item-rect.cpp
@@ -0,0 +1,262 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control rectangle. Used for rubberband selector, page outline, etc.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of CtrlRect
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cairo/cairo.h>
+
+#include "canvas-item-rect.h"
+
+#include "display/cairo-utils.h"
+#include "color.h" // SP_RGBA_x_F
+#include "helper/geom.h"
+#include "inkscape.h"
+#include "ui/util.h"
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control rect.
+ */
+CanvasItemRect::CanvasItemRect(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemRect:Null";
+}
+
+/**
+ * Create a control rect. Point are in document coordinates.
+ */
+CanvasItemRect::CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect)
+ : CanvasItem(group)
+ , _rect(rect)
+{
+ _name = "CanvasItemRect";
+}
+
+/**
+ * Set a control rect. Points are in document coordinates.
+ */
+void CanvasItemRect::set_rect(Geom::Rect const &rect)
+{
+ defer([=] {
+ if (_rect == rect) return;
+ _rect = rect;
+ request_update();
+ });
+}
+
+/**
+ * Run a callback for each rectangle that should be filled and painted in the background.
+ */
+void CanvasItemRect::visit_page_rects(std::function<void(Geom::Rect const &)> const &f) const
+{
+ if (_is_page && _fill != 0) {
+ f(_rect);
+ }
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of rect.
+ * Non-zero tolerance not implemented! Is valid for a rotated canvas.
+ */
+bool CanvasItemRect::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance != 0) {
+ std::cerr << "CanvasItemRect::contains: Non-zero tolerance not implemented!" << std::endl;
+ }
+
+ return _rect.contains(p * affine().inverse());
+}
+
+/**
+ * Update and redraw control rect.
+ */
+void CanvasItemRect::_update(bool)
+{
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ // Enlarge bbox by twice shadow size (to allow for shadow on any side with a 45deg rotation).
+ _bounds = _rect;
+ // note: add shadow size before applying transformation, since get_shadow_size accounts for scale
+ if (_shadow_width > 0 && !_dashed) {
+ _bounds->expandBy(2 * get_shadow_size());
+ }
+ *_bounds *= affine();
+ _bounds->expandBy(2); // Room for stroke.
+
+ // Queue redraw of new area
+ request_redraw();
+}
+
+/**
+ * Render rect to screen via Cairo.
+ */
+void CanvasItemRect::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // Are we axis aligned?
+ auto const &aff = affine();
+ bool const axis_aligned = (Geom::are_near(aff[1], 0) && Geom::are_near(aff[2], 0))
+ || (Geom::are_near(aff[0], 0) && Geom::are_near(aff[3], 0));
+
+ // If so, then snap the rectangle to the pixel grid.
+ auto rect = _rect;
+ if (axis_aligned) {
+ rect = (floor(_rect * aff) + Geom::Point(0.5, 0.5)) * aff.inverse();
+ }
+
+ buf.cr->save();
+ buf.cr->translate(-buf.rect.left(), -buf.rect.top());
+
+ if (_inverted) {
+ cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE);
+ }
+
+ // Draw shadow first. Shadow extends under rectangle to reduce aliasing effects. Canvas draws page shadows in OpenGL mode.
+ if (_shadow_width > 0 && !_dashed && !(_is_page && get_canvas()->get_opengl_enabled())) {
+ // There's only one UI knob to adjust border and shadow color, so instead of using border color
+ // transparency as is, it is boosted by this function, since shadow attenuates it.
+ auto const alpha = (std::exp(-3 * SP_RGBA32_A_F(_shadow_color)) - 1) / (std::exp(-3) - 1);
+
+ // Flip shadow upside-down if y-axis is inverted.
+ auto doc2dt = Geom::identity();
+ if (auto desktop = get_canvas()->get_desktop()) {
+ doc2dt = desktop->doc2dt();
+ }
+
+ buf.cr->save();
+ buf.cr->transform(geom_to_cairo(doc2dt * aff));
+ ink_cairo_draw_drop_shadow(buf.cr, rect * doc2dt, get_shadow_size(), _shadow_color, alpha);
+ buf.cr->restore();
+ }
+
+ // Get the points we need transformed into window coordinates.
+ buf.cr->begin_new_path();
+ for (int i = 0; i < 4; ++i) {
+ auto pt = rect.corner(i) * aff;
+ buf.cr->line_to(pt.x(), pt.y());
+ }
+ buf.cr->close_path();
+
+ // Draw border.
+ static std::valarray<double> dashes = {4.0, 4.0};
+ if (_dashed) {
+ buf.cr->set_dash(dashes, -0.5);
+ }
+ buf.cr->set_line_width(1);
+ // we maybe have painted the background, back to "normal" compositing
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ buf.cr->stroke_preserve();
+
+ // Highlight the border by drawing it in _shadow_color.
+ if (_shadow_width == 1 && _dashed) {
+ buf.cr->set_dash(dashes, 3.5); // Dash offset by dash length.
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_shadow_color), SP_RGBA32_G_F(_shadow_color),
+ SP_RGBA32_B_F(_shadow_color), SP_RGBA32_A_F(_shadow_color));
+ buf.cr->stroke_preserve();
+ }
+
+ buf.cr->begin_new_path(); // Clear path or get weird artifacts.
+
+ // Uncomment to show bounds
+ // Geom::Rect bounds = _bounds;
+ // bounds.expandBy(-1);
+ // bounds -= buf.rect.min();
+ // buf.cr->set_source_rgba(1.0, 0.0, _shadow_width / 3.0, 1.0);
+ // buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ // buf.cr->stroke();
+
+ buf.cr->restore();
+}
+
+void CanvasItemRect::set_is_page(bool is_page)
+{
+ defer([=] {
+ if (_is_page == is_page) return;
+ _is_page = is_page;
+ request_redraw();
+ });
+}
+
+void CanvasItemRect::set_fill(uint32_t fill)
+{
+ if (fill != _fill && _is_page) get_canvas()->set_page(fill);
+ CanvasItem::set_fill(fill);
+}
+
+void CanvasItemRect::set_dashed(bool dashed)
+{
+ defer([=] {
+ if (_dashed == dashed) return;
+ _dashed = dashed;
+ request_redraw();
+ });
+}
+
+void CanvasItemRect::set_inverted(bool inverted)
+{
+ defer([=] {
+ if (_inverted == inverted) return;
+ _inverted = inverted;
+ request_redraw();
+ });
+}
+
+void CanvasItemRect::set_shadow(uint32_t color, int width)
+{
+ defer([=] {
+ if (_shadow_color == color && _shadow_width == width) return;
+ _shadow_color = color;
+ _shadow_width = width;
+ request_redraw();
+ if (_is_page) get_canvas()->set_border(_shadow_width > 0 ? color : 0x0);
+ });
+}
+
+double CanvasItemRect::get_shadow_size() const
+{
+ // gradient drop shadow needs much more room than solid one, so inflating the size;
+ // fudge factor of 6 used to make sizes baked in svg documents work as steps:
+ // typical value of 2 will work out to 12 pixels which is a narrow shadow (b/c of exponential fall of)
+ auto size = _shadow_width * 6;
+ if (size < 0) {
+ size = 0;
+ } else if (size > 120) {
+ // arbitrarily selected max size, so Cairo gradient doesn't blow up if document has bogus shadow values
+ size = 120;
+ }
+ auto scale = affine().descrim();
+
+ // calculate space for gradient shadow; if divided by 'scale' it would be zoom independent (fixed in size);
+ // if 'scale' is not used, drop shadow will be getting smaller with document zoom;
+ // here hybrid approach is used: "unscaling" with square root of scale allows shadows to diminish
+ // more slowly at small zoom levels (so it's still perceptible) and grow more slowly at high mag (where it doesn't matter, b/c it's typically off-screen)
+ return size / (scale > 0 ? sqrt(scale) : 1);
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-rect.h b/src/display/control/canvas-item-rect.h
new file mode 100644
index 0000000..4f67734
--- /dev/null
+++ b/src/display/control/canvas-item-rect.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_RECT_H
+#define SEEN_CANVAS_ITEM_RECT_H
+
+/**
+ * A class to represent a control rectangle. Used for rubberband selector, page outline, etc.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of CtrlRect
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <memory>
+#include <2geom/path.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemRect final : public CanvasItem
+{
+public:
+ CanvasItemRect(CanvasItemGroup *group);
+ CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect);
+
+ // Geometry
+ void set_rect(Geom::Rect const &rect);
+ void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const override;
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Properties
+ void set_is_page(bool is_page);
+ void set_fill(uint32_t color) override;
+ void set_dashed(bool dash = true);
+ void set_inverted(bool inverted = false);
+ void set_shadow(uint32_t color, int width);
+
+protected:
+ ~CanvasItemRect() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ // Geometry
+ double get_shadow_size() const;
+
+ Geom::Rect _rect;
+ bool _is_page = false;
+ bool _dashed = false;
+ bool _inverted = false;
+ int _shadow_width = 0;
+ uint32_t _shadow_color = 0x0;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_RECT_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-text.cpp b/src/display/control/canvas-item-text.cpp
new file mode 100644
index 0000000..5049543
--- /dev/null
+++ b/src/display/control/canvas-item-text.cpp
@@ -0,0 +1,303 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control textrilateral. Used to highlight selected text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlTextr
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-text.h"
+
+#include <cmath>
+#include <utility> // std::move
+#include <glibmm/i18n.h>
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "ui/util.h"
+
+namespace Inkscape {
+
+/**
+ * Create a null control text.
+ */
+CanvasItemText::CanvasItemText(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemText";
+ _fill = 0x33337fff; // Override CanvasItem default.
+}
+
+/**
+ * Create a control text. Point are in document coordinates.
+ */
+CanvasItemText::CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text, bool scaled)
+ : CanvasItem(group)
+ , _p(p)
+ , _text(std::move(text))
+ , _scaled(scaled)
+{
+ _name = "CanvasItemText";
+ _fill = 0x33337fff; // Override CanvasItem default.
+
+ request_update();
+}
+
+/**
+ * Set a text position. Position is in document coordinates.
+ */
+void CanvasItemText::set_coord(Geom::Point const &p)
+{
+ defer([=] {
+ if (_p == p) return;
+ _p = p;
+ request_update();
+ });
+}
+
+/**
+ * Set a text position. Position is in document coordinates.
+ */
+void CanvasItemText::set_bg_radius(double rad)
+{
+ defer([=] {
+ if (_bg_rad == rad) return;
+ _bg_rad = rad;
+ request_update();
+ });
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of text.
+ */
+bool CanvasItemText::contains(Geom::Point const &p, double tolerance)
+{
+ return false; // We never select text.
+}
+
+/**
+ * Update and redraw control text.
+ */
+void CanvasItemText::_update(bool)
+{
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ // Point needs to be scaled manually if not cairo scaling
+ Geom::Point p = _scaled ? _p : _p * affine();
+
+ // Measure text size
+ _text_box = load_text_extents();
+
+ // Offset relative to requested point
+ double offset_x = -(_anchor_position.x() * _text_box.width());
+ double offset_y = -(_anchor_position.y() * _text_box.height());
+ offset_x += p.x() + _adjust_offset.x();
+ offset_y += p.y() + _adjust_offset.y();
+ _text_box *= Geom::Translate(Geom::Point(offset_x, offset_y).floor());
+
+ // Pixel alignment of background. Avoid aliasing artifacts on redraw.
+ _text_box = _text_box.roundOutwards();
+
+ // Don't apply affine here, to keep text at the same size in screen coords.
+ _bounds = _text_box;
+ if (_scaled && _bounds) {
+ *_bounds *= affine();
+ _bounds = _bounds->roundOutwards();
+ }
+
+ // Queue redraw of new area
+ request_redraw();
+}
+
+/**
+ * Render text to screen via Cairo.
+ */
+void CanvasItemText::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ buf.cr->save();
+
+ // Screen to desktop coords.
+ buf.cr->translate(-buf.rect.left(), -buf.rect.top());
+
+ if (_scaled) {
+ // Convert from canvas space to document space
+ buf.cr->transform(geom_to_cairo(affine()));
+ }
+
+ double x = _text_box.min().x();
+ double y = _text_box.min().y();
+ double w = _text_box.width();
+ double h = _text_box.height();
+
+ // Background
+ if (_use_background) {
+ if (_bg_rad == 0.0) {
+ buf.cr->rectangle(x, y, w, h);
+ } else {
+ double radius = _bg_rad * (std::min(w ,h) / 2);
+ buf.cr->arc(x + w - radius, y + radius, radius, -M_PI_2, 0);
+ buf.cr->arc(x + w - radius, y + h - radius, radius, 0, M_PI_2);
+ buf.cr->arc(x + radius, y + h - radius, radius, M_PI_2, M_PI);
+ buf.cr->arc(x + radius, y + radius, radius, M_PI, 3*M_PI_2);
+ }
+ buf.cr->set_line_width(2);
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_background), SP_RGBA32_G_F(_background),
+ SP_RGBA32_B_F(_background), SP_RGBA32_A_F(_background));
+ buf.cr->fill();
+ }
+
+ // Center the text inside the draw background box
+ auto bx = x + w / 2.0;
+ auto by = y + h / 2.0 + 1;
+ buf.cr->move_to(int(bx - _text_size.x_bearing - _text_size.width/2.0),
+ int(by - _text_size.y_bearing - _text_extent.height/2.0));
+
+ buf.cr->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL);
+ buf.cr->set_font_size(_fontsize);
+ buf.cr->text_path(_text);
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill));
+ buf.cr->fill();
+ buf.cr->restore();
+}
+
+void CanvasItemText::set_text(Glib::ustring text)
+{
+ defer([=, text = std::move(text)] () mutable {
+ if (_text == text) return;
+ _text = std::move(text);
+ request_update(); // Might be larger than before!
+ });
+}
+
+void CanvasItemText::set_fontsize(double fontsize)
+{
+ defer([=] {
+ if (_fontsize == fontsize) return;
+ _fontsize = fontsize;
+ request_update(); // Might be larger than before!
+ });
+}
+
+/**
+ * Load the sizes of the text extent using the given font.
+ */
+Geom::Rect CanvasItemText::load_text_extents()
+{
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 1, 1);
+ auto context = Cairo::Context::create(surface);
+ context->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL);
+ context->set_font_size(_fontsize);
+ context->get_text_extents(_text, _text_size);
+
+ if (_fixed_line) {
+ // TRANSLATORS: This is a set of letters to test for font ascender and descenders.
+ context->get_text_extents(_("lg1p$"), _text_extent);
+ } else {
+ _text_extent = _text_size;
+ }
+
+ return Geom::Rect::from_xywh(0, 0,
+ _text_size.x_advance + _border * 2,
+ _text_extent.height + _border * 2);
+}
+
+void CanvasItemText::set_background(uint32_t background)
+{
+ defer([=] {
+ if (_background != background) {
+ _background = background;
+ request_redraw();
+ }
+ _use_background = true;
+ });
+}
+
+/**
+ * Set the anchor point, x and y between 0.0 and 1.0.
+ */
+void CanvasItemText::set_anchor(Geom::Point const &anchor_pt)
+{
+ defer([=] {
+ if (_anchor_position == anchor_pt) return;
+ _anchor_position = anchor_pt;
+ request_update();
+ });
+}
+
+void CanvasItemText::set_adjust(Geom::Point const &adjust_pt)
+{
+ defer([=] {
+ if (_adjust_offset == adjust_pt) return;
+ _adjust_offset = adjust_pt;
+ request_update();
+ });
+}
+
+void CanvasItemText::set_fixed_line(bool fixed_line)
+{
+ defer([=] {
+ if (_fixed_line == fixed_line) return;
+ _fixed_line = fixed_line;
+ request_update();
+ });
+}
+
+void CanvasItemText::set_border(double border)
+{
+ defer([=] {
+ if (_border == border) return;
+ _border = border;
+ request_update();
+ });
+}
+
+} // namespace Inkscape
+
+/* FROM: http://lists.cairographics.org/archives/cairo-bugs/2009-March/003014.html
+ - Glyph surfaces: In most font rendering systems, glyph surfaces
+ have an origin at (0,0) and a bounding box that is typically
+ represented as (x_bearing,y_bearing,width,height). Depending on
+ which way y progresses in the system, y_bearing may typically be
+ negative (for systems similar to cairo, with origin at top left),
+ or be positive (in systems like PDF with origin at bottom left).
+ No matter which is the case, it is important to note that
+ (x_bearing,y_bearing) is the coordinates of top-left of the glyph
+ relative to the glyph origin. That is, for example:
+
+ Scaled-glyph space:
+
+ (x_bearing,y_bearing) <-- negative numbers
+ +----------------+
+ | . |
+ | . |
+ |......(0,0) <---|-- glyph origin
+ | |
+ | |
+ +----------------+
+ (width+x_bearing,height+y_bearing)
+
+ Note the similarity of the origin to the device space. That is
+ exactly how we use the device_offset to represent scaled glyphs:
+ to use the device-space origin as the glyph origin.
+*/
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item-text.h b/src/display/control/canvas-item-text.h
new file mode 100644
index 0000000..a3ad4f4
--- /dev/null
+++ b/src/display/control/canvas-item-text.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_TEXT_H
+#define SEEN_CANVAS_ITEM_TEXT_H
+
+/**
+ * A class to represent on-screen text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasText.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include <glibmm/ustring.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemText final : public CanvasItem
+{
+public:
+ CanvasItemText(CanvasItemGroup *group);
+ CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text, bool scaled = false);
+
+ // Geometry
+ void set_coord(Geom::Point const &p);
+ void set_bg_radius(double rad);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Properties
+ void set_text(Glib::ustring text);
+ void set_fontsize(double fontsize);
+ void set_border(double border);
+ void set_background(uint32_t background);
+ void set_anchor(Geom::Point const &anchor_pt);
+ void set_adjust(Geom::Point const &adjust_pt);
+ void set_fixed_line(bool fixed_line);
+
+protected:
+ ~CanvasItemText() override = default;
+
+ void _update(bool propagate) override;
+ void _render(Inkscape::CanvasItemBuffer &buf) const override;
+
+ Geom::Point _p; // Position of text (not box around text).
+ Cairo::TextExtents _text_extent;
+ Cairo::TextExtents _text_size;
+ Geom::Point _anchor_position;
+ Geom::Point _adjust_offset;
+ Geom::Rect _text_box;
+ Glib::ustring _text;
+ std::string _fontname = "sans-serif";
+ double _fontsize = 10;
+ double _border = 3;
+ double _bg_rad = 0;
+ uint32_t _background = 0x0000007f;
+ bool _use_background = false;
+ bool _fixed_line = false; // Correction for font heights
+ bool _scaled = false;
+
+ Geom::Rect load_text_extents();
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_TEXT_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item.cpp b/src/display/control/canvas-item.cpp
new file mode 100644
index 0000000..984d24d
--- /dev/null
+++ b/src/display/control/canvas-item.cpp
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Abstract base class for on-canvas control items.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasItem
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item.h"
+#include "canvas-item-group.h"
+#include "canvas-item-ctrl.h"
+
+#include "ui/widget/canvas.h"
+
+constexpr bool DEBUG_LOGGING = false;
+constexpr bool DEBUG_BOUNDS = false;
+
+namespace Inkscape {
+
+CanvasItem::CanvasItem(CanvasItemContext *context)
+ : _context(context)
+ , _parent(nullptr)
+{
+ if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: create root " << get_name() << std::endl;
+ request_update();
+}
+
+CanvasItem::CanvasItem(CanvasItemGroup *parent)
+ : _context(parent->_context)
+ , _parent(parent)
+{
+ if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: add " << get_name() << " to " << parent->get_name() << " " << parent->items.size() << std::endl;
+ defer([=] {
+ parent->items.push_back(*this);
+ request_update();
+ });
+}
+
+void CanvasItem::unlink()
+{
+ defer([=] {
+ // Clear canvas of item.
+ request_redraw();
+
+ // Remove from parent.
+ if (_parent) {
+ if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: remove " << get_name() << " from " << _parent->get_name() << " " << _parent->items.size() << std::endl;
+ auto it = _parent->items.iterator_to(*this);
+ assert(it != _parent->items.end());
+ _parent->items.erase(it);
+ _parent->request_update();
+ } else {
+ if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: destroy root " << get_name() << std::endl;
+ }
+
+ delete this;
+ });
+}
+
+CanvasItem::~CanvasItem()
+{
+ // Clear any pointers to this object in canvas.
+ get_canvas()->canvas_item_destructed(this);
+}
+
+bool CanvasItem::is_descendant_of(CanvasItem const *ancestor) const
+{
+ auto item = this;
+ while (item) {
+ if (item == ancestor) {
+ return true;
+ }
+ item = item->_parent;
+ }
+ return false;
+}
+
+void CanvasItem::set_z_position(int zpos)
+{
+ if (!_parent) {
+ std::cerr << "CanvasItem::set_z_position: No parent!" << std::endl;
+ return;
+ }
+
+ defer([=] {
+ _parent->items.erase(_parent->items.iterator_to(*this));
+
+ if (zpos <= 0) {
+ _parent->items.push_front(*this);
+ } else if (zpos >= _parent->items.size() - 1) {
+ _parent->items.push_back(*this);
+ } else {
+ auto it = _parent->items.begin();
+ std::advance(it, zpos);
+ _parent->items.insert(it, *this);
+ }
+ });
+}
+
+void CanvasItem::raise_to_top()
+{
+ if (!_parent) {
+ std::cerr << "CanvasItem::raise_to_top: No parent!" << std::endl;
+ return;
+ }
+
+ defer([=] {
+ _parent->items.erase(_parent->items.iterator_to(*this));
+ _parent->items.push_back(*this);
+ });
+}
+
+void CanvasItem::lower_to_bottom()
+{
+ if (!_parent) {
+ std::cerr << "CanvasItem::lower_to_bottom: No parent!" << std::endl;
+ return;
+ }
+
+ defer([=] {
+ _parent->items.erase(_parent->items.iterator_to(*this));
+ _parent->items.push_front(*this);
+ });
+}
+
+// Indicate geometry changed and bounds needs recalculating.
+void CanvasItem::request_update()
+{
+ if (_need_update || !_visible) {
+ return;
+ }
+
+ _need_update = true;
+
+ if (_parent) {
+ _parent->request_update();
+ } else {
+ get_canvas()->request_update();
+ }
+}
+
+void CanvasItem::update(bool propagate)
+{
+ if (!_visible) {
+ _mark_net_invisible();
+ return;
+ }
+
+ bool reappearing = !_net_visible;
+ _net_visible = true;
+
+ if (!_need_update && !reappearing && !propagate) {
+ return;
+ }
+
+ _need_update = false;
+
+ // Get new bounds
+ _update(propagate);
+
+ if (reappearing) {
+ request_redraw();
+ }
+}
+
+void CanvasItem::_mark_net_invisible()
+{
+ if (!_net_visible) {
+ return;
+ }
+ _net_visible = false;
+ _need_update = false;
+ request_redraw();
+ _bounds = {};
+}
+
+// Grab all events!
+void CanvasItem::grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> const &cursor)
+{
+ if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem::grab: " << _name << std::endl;
+
+ auto canvas = get_canvas();
+
+ // Don't grab if we already have a grabbed item!
+ if (canvas->get_grabbed_canvas_item()) {
+ return;
+ }
+
+ gtk_grab_add(GTK_WIDGET(canvas->gobj()));
+
+ canvas->set_grabbed_canvas_item(this, event_mask);
+ canvas->set_current_canvas_item(this); // So that all events go to grabbed item.
+}
+
+void CanvasItem::ungrab()
+{
+ if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem::ungrab: " << _name << std::endl;
+
+ auto canvas = get_canvas();
+
+ if (canvas->get_grabbed_canvas_item() != this) {
+ return; // Sanity check
+ }
+
+ canvas->set_grabbed_canvas_item(nullptr, (Gdk::EventMask)0); // Zero mask
+
+ gtk_grab_remove(GTK_WIDGET(canvas->gobj()));
+}
+
+void CanvasItem::render(CanvasItemBuffer &buf) const
+{
+ if (_visible && _bounds && _bounds->interiorIntersects(buf.rect)) {
+ _render(buf);
+ if constexpr (DEBUG_BOUNDS) {
+ auto bounds = *_bounds;
+ bounds.expandBy(-1);
+ bounds -= buf.rect.min();
+ buf.cr->set_source_rgba(1.0, 0.0, 0.0, 1.0);
+ buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ buf.cr->stroke();
+ }
+ }
+}
+
+/*
+ * The main invariant of the invisibility system is
+ *
+ * x needs update and is visible ==> parent(x) needs update or is invisible
+ *
+ * When x belongs to the visible subtree, meaning it and all its parents are visible,
+ * this condition reduces to
+ *
+ * x needs update ==> parent(x) needs update
+ *
+ * Thus within the visible subtree, the subset of nodes that need updating forms a subtree.
+ *
+ * In the update() function, we only walk this latter subtree.
+ */
+
+void CanvasItem::set_visible(bool visible)
+{
+ defer([=] {
+ if (_visible == visible) return;
+ if (_visible) {
+ request_update();
+ _visible = false;
+ } else {
+ _visible = true;
+ _need_update = false;
+ request_update();
+ }
+ });
+}
+
+void CanvasItem::request_redraw()
+{
+ // Queue redraw request
+ if (_bounds) {
+ get_canvas()->redraw_area(*_bounds);
+ }
+}
+
+void CanvasItem::set_fill(uint32_t fill)
+{
+ defer([=] {
+ if (_fill == fill) return;
+ _fill = fill;
+ request_redraw();
+ });
+}
+
+void CanvasItem::set_stroke(uint32_t stroke)
+{
+ defer([=] {
+ if (_stroke == stroke) return;
+ _stroke = stroke;
+ request_redraw();
+ });
+}
+
+void CanvasItem::update_canvas_item_ctrl_sizes(int size_index)
+{
+ if (auto ctrl = dynamic_cast<CanvasItemCtrl*>(this)) {
+ // We can't use set_size_default as the preference file is updated ->after<- the signal is emitted!
+ ctrl->set_size_via_index(size_index);
+ } else if (auto group = dynamic_cast<CanvasItemGroup*>(this)) {
+ for (auto &item : group->items) {
+ item.update_canvas_item_ctrl_sizes(size_index);
+ }
+ }
+}
+
+void CanvasItem::canvas_item_print_tree(int level, int zorder) const
+{
+ if (level == 0) {
+ std::cout << "Canvas Item Tree" << std::endl;
+ }
+
+ std::cout << "CC: ";
+ for (int i = 0; i < level; ++i) {
+ std::cout << " ";
+ }
+
+ std::cout << zorder << ": " << _name << std::endl;
+
+ if (auto group = dynamic_cast<Inkscape::CanvasItemGroup const*>(this)) {
+ int i = 0;
+ for (auto &item : group->items) {
+ item.canvas_item_print_tree(level + 1, i);
+ i++;
+ }
+ }
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-item.h b/src/display/control/canvas-item.h
new file mode 100644
index 0000000..fc34176
--- /dev/null
+++ b/src/display/control/canvas-item.h
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_H
+#define SEEN_CANVAS_ITEM_H
+
+/**
+ * Abstract base class for on-canvas control items.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasItem
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ * A note about coordinates:
+ *
+ * 1. Canvas items are constructed using document (SVG) coordinates.
+ * 2. Calculations are made in canvas units, which is equivalent of SVG units multiplied by zoom factor.
+ * This is true for bounds and closest distance calculations.
+ * 3 Drawing is done in screen units which is the same as canvas units but translated.
+ * The document and canvas origins overlap.
+ * The affine contains only scaling and rotating components.
+ */
+
+#include <cstdint>
+#include <boost/intrusive/list.hpp>
+#include <2geom/rect.h>
+#include <sigc++/sigc++.h>
+
+#include <gdkmm/device.h> // Gdk::EventMask
+#include <gdk/gdk.h> // GdkEvent
+
+#include "canvas-item-enums.h"
+#include "canvas-item-buffer.h"
+#include "canvas-item-context.h"
+
+class SPItem;
+
+namespace Inkscape {
+
+inline constexpr uint32_t CANVAS_ITEM_COLORS[] = { 0x0000ff7f, 0xff00007f, 0xffff007f };
+
+namespace UI::Widget { class Canvas; }
+class CanvasItemGroup;
+
+class CanvasItem
+{
+public:
+ CanvasItem(CanvasItemContext *context);
+ CanvasItem(CanvasItemGroup *parent);
+ CanvasItem(CanvasItem const &) = delete;
+ CanvasItem &operator=(CanvasItem const &) = delete;
+ void unlink();
+
+ // Structure
+ UI::Widget::Canvas *get_canvas() const { return _context->canvas(); }
+ CanvasItemGroup *get_parent() const { return _parent; }
+ bool is_descendant_of(CanvasItem const *ancestor) const;
+
+ // Z Position
+ void set_z_position(int zpos);
+ void raise_to_top(); // Move to top of group (last entry).
+ void lower_to_bottom(); // Move to bottom of group (first entry).
+
+ // Geometry
+ void request_update();
+ void update(bool propagate);
+ virtual void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const {}
+ Geom::OptRect const &get_bounds() const { return _bounds; }
+
+ // Selection
+ virtual bool contains(Geom::Point const &p, double tolerance = 0) { return _bounds && _bounds->interiorContains(p); }
+ void grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> const & = {});
+ void ungrab();
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer &buf) const;
+ bool is_visible() const { return _visible; }
+ virtual void set_visible(bool visible);
+ void show() { set_visible(true); }
+ void hide() { set_visible(false); }
+ void request_redraw(); // queue redraw request
+
+ // Properties
+ virtual void set_fill(uint32_t rgba);
+ void set_fill(CanvasItemColor color) { set_fill(CANVAS_ITEM_COLORS[color]); }
+ virtual void set_stroke(uint32_t rgba);
+ void set_stroke(CanvasItemColor color) { set_stroke(CANVAS_ITEM_COLORS[color]); }
+ void set_name(std::string &&name) { _name = std::move(name); }
+ std::string const &get_name() const { return _name; }
+ void update_canvas_item_ctrl_sizes(int size_index);
+
+ // Events
+ void set_pickable(bool pickable) { _pickable = pickable; }
+ bool is_pickable() const { return _pickable; }
+ sigc::connection connect_event(sigc::slot<bool(GdkEvent*)> const &slot) {
+ return _event_signal.connect(slot);
+ }
+ virtual bool handle_event(GdkEvent *event) {
+ return _event_signal.emit(event); // Default just emits event.
+ }
+
+ // Recursively print CanvasItem tree.
+ void canvas_item_print_tree(int level = 0, int zorder = 0) const;
+
+ // Boost linked list member hook, speeds deletion.
+ boost::intrusive::list_member_hook<> member_hook;
+
+protected:
+ friend class CanvasItemGroup;
+
+ virtual ~CanvasItem();
+
+ // Structure
+ CanvasItemContext *_context;
+ CanvasItemGroup *_parent;
+
+ // Geometry
+ Geom::OptRect _bounds;
+ bool _need_update = false;
+ Geom::Affine const &affine() const { return _context->affine(); }
+ virtual void _update(bool propagate) = 0;
+ virtual void _mark_net_invisible();
+
+ // Display
+ bool _visible = true;
+ bool _net_visible = true;
+ virtual void _render(Inkscape::CanvasItemBuffer &buf) const = 0;
+
+ // Selection
+ bool _pickable = false; // Most items are just for display and are not pickable!
+
+ // Properties
+ uint32_t _fill = CANVAS_ITEM_COLORS[CANVAS_ITEM_SECONDARY];
+ uint32_t _stroke = CANVAS_ITEM_COLORS[CANVAS_ITEM_PRIMARY];
+ std::string _name; // For debugging
+
+ // Events
+ sigc::signal<bool (GdkEvent*)> _event_signal;
+
+ // Snapshotting
+ template<typename F>
+ void defer(F &&f) { _context->defer(std::forward<F>(f)); }
+};
+
+} // namespace Inkscape
+
+// Todo: Move to lib2geom.
+inline auto &operator<<(std::ostream &s, Geom::OptRect const &rect)
+{
+ return rect ? (s << *rect) : (s << "(empty)");
+}
+
+#endif // SEEN_CANVAS_ITEM_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-page.cpp b/src/display/control/canvas-page.cpp
new file mode 100644
index 0000000..5fa337d
--- /dev/null
+++ b/src/display/control/canvas-page.cpp
@@ -0,0 +1,282 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape pages implementation
+ *
+ * Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2021 Martin Owens
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-page.h"
+#include "canvas-item-rect.h"
+#include "canvas-item-text.h"
+#include "color.h"
+
+namespace Inkscape {
+
+CanvasPage::CanvasPage() = default;
+
+CanvasPage::~CanvasPage() = default;
+
+/**
+ * Add the page canvas to the given canvas item groups (canvas view is implicit)
+ */
+void CanvasPage::add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *border_group)
+{
+ // Foreground 'border'
+ if (auto item = new CanvasItemRect(border_group, size)) {
+ item->set_name("foreground");
+ item->set_is_page(true);
+ canvas_items.emplace_back(item);
+ }
+
+ // Background rectangle 'fill'
+ if (auto item = new CanvasItemRect(background_group, size)) {
+ item->set_name("background");
+ item->set_is_page(true);
+ item->set_dashed(false);
+ item->set_inverted(false);
+ item->set_stroke(0x00000000);
+ canvas_items.emplace_back(item);
+ }
+
+ if (auto item = new CanvasItemRect(border_group, size)) {
+ item->set_name("margin");
+ item->set_dashed(false);
+ item->set_inverted(false);
+ item->set_stroke(_margin_color);
+ canvas_items.emplace_back(item);
+ }
+
+ if (auto item = new CanvasItemRect(border_group, size)) {
+ item->set_name("bleed");
+ item->set_dashed(false);
+ item->set_inverted(false);
+ item->set_stroke(_bleed_color);
+ canvas_items.emplace_back(item);
+ }
+
+ if (auto label = new CanvasItemText(border_group, Geom::Point(0, 0), "{Page Label}")) {
+ label->set_fixed_line(false);
+ canvas_items.emplace_back(label);
+ }
+}
+/**
+ * Hide the page in the given canvas widget.
+ */
+void CanvasPage::remove(UI::Widget::Canvas *canvas)
+{
+ g_assert(canvas != nullptr);
+ for (auto it = canvas_items.begin(); it != canvas_items.end();) {
+ if (canvas == (*it)->get_canvas()) {
+ it = canvas_items.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+void CanvasPage::show()
+{
+ for (auto &item : canvas_items) {
+ item->show();
+ }
+}
+
+void CanvasPage::hide()
+{
+ for (auto &item : canvas_items) {
+ item->hide();
+ }
+}
+
+void CanvasPage::set_guides_visible(bool show) {
+ for (auto& item: canvas_items) {
+ if (item->get_name() == "margin" || item->get_name() == "bleed") {
+ item->set_visible(show);
+ }
+ }
+}
+
+/**
+ * Update the visual representation of a page on screen.
+ *
+ * @param size - The size of the page in desktop units
+ * @param txt - An optional label for the page
+ * @param outline - Disable normal rendering and show as an outline.
+ */
+void CanvasPage::update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect bleed, const char *txt, bool outline)
+{
+ // Put these in the preferences?
+ bool border_on_top = _border_on_top;
+ guint32 shadow_color = _border_color; // there's no separate shadow color in the UI, border color is used
+ guint32 select_color = 0x000000cc;
+ guint32 border_color = _border_color;
+ guint32 margin_color = _margin_color;
+ guint32 bleed_color = _bleed_color;
+
+ // This is used when showing the viewport as *not a page* it's mostly
+ // never used as the first page is normally the viewport too.
+ if (outline) {
+ border_on_top = false;
+ _shadow_size = 0;
+ border_color = select_color;
+ }
+
+ for (auto &item : canvas_items) {
+ if (auto rect = dynamic_cast<CanvasItemRect *>(item.get())) {
+ if (rect->get_name() == "margin") {
+ rect->set_stroke(margin_color);
+ bool vis = margin && *margin != size;
+ rect->set_visible(vis);
+ if (vis) {
+ rect->set_rect(*margin);
+ }
+ continue;
+ }
+ if (rect->get_name() == "bleed") {
+ rect->set_stroke(bleed_color);
+ bool vis = bleed && *bleed != size;
+ rect->set_visible(vis);
+ if (vis) {
+ rect->set_rect(*bleed);
+ }
+ continue;
+ }
+
+ rect->set_rect(size);
+
+ bool is_foreground = (rect->get_name() == "foreground");
+ // This will put the border on the background OR foreground layer as needed.
+ if (is_foreground == border_on_top) {
+ rect->show();
+ rect->set_stroke(is_selected ? select_color : border_color);
+ } else {
+ rect->hide();
+ rect->set_stroke(0x0);
+ }
+ // This undoes the hide for the background rect, and additionally gives it a fill and shadow.
+ if (!is_foreground) {
+ rect->show();
+/*
+ if (_checkerboard) {
+ // draw checkerboard pattern, ignore alpha (background color doesn't support it)
+ rect->set_background_checkerboard(_background_color, false);
+ }
+ else {
+ // Background color does not support transparency; draw opaque pages
+ rect->set_background(_background_color | 0xff);
+ }
+*/
+ rect->set_fill(_background_color);
+ rect->set_shadow(shadow_color, _shadow_size);
+ } else {
+ rect->set_fill(0x0);
+ rect->set_shadow(0x0, 0);
+ }
+ } else if (auto label = dynamic_cast<CanvasItemText *>(item.get())) {
+ _updateTextItem(label, size, txt ? txt : "");
+ }
+ }
+}
+
+/**
+ * Update the page's textual label.
+ */
+void CanvasPage::_updateTextItem(CanvasItemText *label, Geom::Rect page, std::string txt)
+{
+ // Default style for the label
+ int fontsize = 10.0;
+ uint32_t foreground = 0xffffffff;
+ uint32_t background = 0x00000099;
+ uint32_t selected = 0x0e5bf199;
+ Geom::Point anchor(0.0, 1.0);
+ Geom::Point coord = page.corner(0);
+ double radius = 0.2;
+
+ // Change the colors for whiter/lighter backgrounds
+ unsigned char luminance = SP_RGBA32_LUMINANCE(_canvas_color);
+ if (luminance < 0x88) {
+ foreground = 0x000000ff;
+ background = 0xffffff99;
+ selected = 0x50afe7ff;
+ }
+
+ if (_label_style == "below") {
+ radius = 1.0;
+ fontsize = 14.0;
+ anchor = Geom::Point(0.5, -0.2);
+ coord = Geom::Point(page.midpoint()[Geom::X], page.bottom());
+
+ if (!txt.empty()) {
+ std::string bullet = is_selected ? " \u2022 " : " ";
+ txt = bullet + txt + bullet;
+ }
+ }
+
+ label->set_fontsize(fontsize);
+ label->set_fill(foreground);
+ label->set_background(is_selected ? selected : background);
+ label->set_bg_radius(radius);
+ label->set_anchor(anchor);
+ label->set_coord(coord);
+ label->set_visible(!txt.empty());
+ label->set_text(std::move(txt));
+ label->set_border(4.0);
+}
+
+bool CanvasPage::setOnTop(bool on_top)
+{
+ if (on_top != _border_on_top) {
+ _border_on_top = on_top;
+ return true;
+ }
+ return false;
+}
+
+bool CanvasPage::setShadow(int shadow)
+{
+ if (_shadow_size != shadow) {
+ _shadow_size = shadow;
+ return true;
+ }
+ return false;
+}
+
+bool CanvasPage::setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed)
+{
+ if (border != _border_color || bg != _background_color || canvas != _canvas_color) {
+ _border_color = border;
+ _background_color = bg;
+ _canvas_color = canvas;
+ _margin_color = margin;
+ _bleed_color = bleed;
+ return true;
+ }
+ return false;
+}
+
+bool CanvasPage::setLabelStyle(const std::string &style)
+{
+ if (style != _label_style) {
+ _label_style = style;
+ return true;
+ }
+ return false;
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-page.h b/src/display/control/canvas-page.h
new file mode 100644
index 0000000..e3227bf
--- /dev/null
+++ b/src/display/control/canvas-page.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ *
+ *//*
+ * Authors:
+ * Martin Owens 2021
+ *
+ * Copyright (C) 2021 Martin Owens
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_CANVAS_PAGE_H
+#define SEEN_CANVAS_PAGE_H
+
+#include <2geom/rect.h>
+#include <glib.h>
+#include <vector>
+
+#include "canvas-item-ptr.h"
+
+namespace Inkscape {
+namespace UI::Widget { class Canvas; }
+
+class CanvasItemGroup;
+class CanvasItemText;
+
+class CanvasPage
+{
+public:
+ CanvasPage();
+ ~CanvasPage();
+
+ void update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect bleed, const char *txt, bool outline = false);
+ void add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *foreground_group);
+ void remove(UI::Widget::Canvas *canvas);
+ void show();
+ void hide();
+ void set_guides_visible(bool show);
+
+ bool setOnTop(bool on_top);
+ bool setShadow(int shadow);
+ bool setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed);
+ bool setLabelStyle(const std::string &style);
+
+ bool is_selected = false;
+private:
+ void _updateTextItem(CanvasItemText *label, Geom::Rect page, std::string txt);
+
+ // This may make this look like a CanvasItemGroup, but it's not one. This
+ // isn't a collection of items, but a set of items in multiple Canvases.
+ // Each item can belong in either a foreground or background group.
+ std::vector<CanvasItemPtr<CanvasItem>> canvas_items;
+
+ int _shadow_size = 0;
+ bool _border_on_top = true;
+ uint32_t _background_color = 0xffffffff;
+ uint32_t _border_color = 0x00000040;
+ uint32_t _canvas_color = 0xffffffff;
+ uint32_t _margin_color = 0x1699d771; // Blue'ish
+ uint32_t _bleed_color = 0xbe310e62; // Red'ish
+
+ std::string _label_style = "default";
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_PAGE_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/display/control/canvas-temporary-item-list.cpp b/src/display/control/canvas-temporary-item-list.cpp
new file mode 100644
index 0000000..6ff516d
--- /dev/null
+++ b/src/display/control/canvas-temporary-item-list.cpp
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Provides a class that can contain active TemporaryItem's on a desktop
+ * Code inspired by message-stack.cpp
+ *
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <algorithm>
+#include "canvas-temporary-item.h"
+#include "canvas-temporary-item-list.h"
+
+namespace Inkscape {
+namespace Display {
+
+TemporaryItemList::~TemporaryItemList()
+{
+ // delete all items in list so the timeouts are removed
+ for (auto tempitem : itemlist) {
+ delete tempitem;
+ }
+ itemlist.clear();
+}
+
+// Note that TemporaryItem or TemporaryItemList is responsible for deletion and such, so this return pointer can safely be ignored.
+TemporaryItem *TemporaryItemList::add_item(CanvasItem *item, int lifetime_msecs)
+{
+ // beware of strange things happening due to very short timeouts
+ TemporaryItem *tempitem;
+ if (lifetime_msecs == 0)
+ tempitem = new TemporaryItem(item, 0);
+ else {
+ tempitem = new TemporaryItem(item, lifetime_msecs);
+ tempitem->signal_timeout.connect([this] (auto tempitem) { itemlist.remove(tempitem); });
+ // no need to delete the item, it does that itself after signal_timeout.emit() completes
+ }
+
+ itemlist.emplace_back(tempitem);
+ return tempitem;
+}
+
+void TemporaryItemList::delete_item(TemporaryItem *tempitem)
+{
+ // check if the item is in the list, if so, delete it. (in other words, don't wait for the item to delete itself)
+ auto it = std::find_if(itemlist.begin(), itemlist.end(), [=] (auto *item) {
+ return item == tempitem;
+ });
+
+ if (it != itemlist.end()) {
+ itemlist.erase(it);
+ delete tempitem;
+ }
+}
+
+} // namespace Display
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-temporary-item-list.h b/src/display/control/canvas-temporary-item-list.h
new file mode 100644
index 0000000..1321cbf
--- /dev/null
+++ b/src/display/control/canvas-temporary-item-list.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H
+#define INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H
+
+/*
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <list>
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItem;
+
+namespace Display {
+
+class TemporaryItem;
+
+/**
+ * Provides a class that can contain active TemporaryItems on a desktop.
+ */
+class TemporaryItemList final
+{
+public:
+ TemporaryItemList() = default;
+ TemporaryItemList(TemporaryItemList const &) = delete;
+ TemporaryItemList &operator=(TemporaryItemList const &) = delete;
+ ~TemporaryItemList();
+
+ TemporaryItem* add_item(CanvasItem *item, int lifetime_msecs);
+ void delete_item(TemporaryItem *tempitem);
+
+protected:
+ std::list<TemporaryItem *> itemlist; ///< List of temp items.
+};
+
+} // namespace Display
+} // namespace Inkscape
+
+#endif // INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-temporary-item.cpp b/src/display/control/canvas-temporary-item.cpp
new file mode 100644
index 0000000..d95ad21
--- /dev/null
+++ b/src/display/control/canvas-temporary-item.cpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Provides a class that can contain active TemporaryItem's on a desktop
+ * When the object is deleted, it also deletes the canvasitem it contains!
+ * This object should be created/managed by a TemporaryItemList.
+ * After its lifetime, it fires the timeout signal, afterwards *it deletes itself*.
+ *
+ * (part of code inspired by message-stack.cpp)
+ *
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/main.h>
+
+#include "canvas-temporary-item.h"
+#include "canvas-item.h"
+
+namespace Inkscape {
+namespace Display {
+
+TemporaryItem::TemporaryItem(CanvasItem *item, int lifetime_msecs)
+ : canvasitem(std::move(item))
+{
+ // Zero lifetime means stay forever, so do not add timeout event.
+ if (lifetime_msecs > 0) {
+ timeout_conn = Glib::signal_timeout().connect([this] {
+ signal_timeout.emit(this);
+ delete this;
+ return false;
+ }, lifetime_msecs);
+ }
+}
+
+TemporaryItem::~TemporaryItem() = default;
+
+} // namespace Display
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/canvas-temporary-item.h b/src/display/control/canvas-temporary-item.h
new file mode 100644
index 0000000..4ce2787
--- /dev/null
+++ b/src/display/control/canvas-temporary-item.h
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_H
+#define INKSCAPE_CANVAS_TEMPORARY_ITEM_H
+
+/*
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/signal.h>
+#include <sigc++/connection.h>
+#include "display/control/canvas-item-ptr.h"
+#include "helper/auto-connection.h"
+
+namespace Inkscape {
+
+class CanvasItem;
+
+namespace Display {
+
+/**
+ * Provides a class to put a canvasitem temporarily on-canvas.
+ */
+class TemporaryItem final
+{
+public:
+ TemporaryItem(CanvasItem *item, int lifetime_msecs);
+ TemporaryItem(TemporaryItem const &) = delete;
+ TemporaryItem &operator=(TemporaryItem const &) = delete;
+ ~TemporaryItem();
+
+ sigc::signal<void (TemporaryItem *)> signal_timeout;
+
+protected:
+ friend class TemporaryItemList;
+
+ CanvasItemPtr<CanvasItem> canvasitem; ///< The item we are holding on to.
+ auto_connection timeout_conn;
+};
+
+} //namespace Display
+} //namespace Inkscape
+
+#endif // INKSCAPE_CANVAS_TEMPORARY_ITEM_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/display/control/snap-indicator.cpp b/src/display/control/snap-indicator.cpp
new file mode 100644
index 0000000..8672d88
--- /dev/null
+++ b/src/display/control/snap-indicator.cpp
@@ -0,0 +1,648 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap
+ *
+ * Authors:
+ * Johan Engelen
+ * Diederik van Lierop
+ *
+ * Copyright (C) Johan Engelen 2009 <j.b.c.engelen@utwente.nl>
+ * Copyright (C) Diederik van Lierop 2010 - 2012 <mail@diedenrezi.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <string>
+#include <iomanip>
+#include <unordered_map>
+
+#include "snap-indicator.h"
+
+#include "desktop.h"
+#include "enums.h"
+#include "preferences.h"
+#include "util/units.h"
+#include "document.h"
+
+
+#include "canvas-item-ctrl.h"
+#include "canvas-item-rect.h"
+#include "canvas-item-text.h"
+#include "canvas-item-curve.h"
+
+#include "ui/tools/measure-tool.h"
+
+#define DISTANCE_BG_RADIUS 0.3
+
+namespace Inkscape {
+namespace Display {
+
+static std::map<SnapSourceType, Glib::ustring> source2string = {
+ {SNAPSOURCE_UNDEFINED, _("UNDEFINED")},
+ {SNAPSOURCE_BBOX_CORNER, _("Bounding box corner")},
+ {SNAPSOURCE_BBOX_MIDPOINT, _("Bounding box midpoint")},
+ {SNAPSOURCE_BBOX_EDGE_MIDPOINT, _("Bounding box side midpoint")},
+ {SNAPSOURCE_NODE_SMOOTH, _("Smooth node")},
+ {SNAPSOURCE_NODE_CUSP, _("Cusp node")},
+ {SNAPSOURCE_LINE_MIDPOINT, _("Line midpoint")},
+ {SNAPSOURCE_PATH_INTERSECTION, _("Path intersection")},
+ {SNAPSOURCE_RECT_CORNER, _("Corner")},
+ {SNAPSOURCE_CONVEX_HULL_CORNER, _("Convex hull corner")},
+ {SNAPSOURCE_ELLIPSE_QUADRANT_POINT, _("Quadrant point")},
+ {SNAPSOURCE_NODE_HANDLE, _("Handle")},
+ {SNAPSOURCE_GUIDE, _("Guide")},
+ {SNAPSOURCE_GUIDE_ORIGIN, _("Guide origin")},
+ {SNAPSOURCE_ROTATION_CENTER, _("Object rotation center")},
+ {SNAPSOURCE_OBJECT_MIDPOINT, _("Object midpoint")},
+ {SNAPSOURCE_IMG_CORNER, _("Corner")},
+ {SNAPSOURCE_TEXT_ANCHOR, _("Text anchor")},
+ {SNAPSOURCE_OTHER_HANDLE, _("Handle")},
+ {SNAPSOURCE_GRID_PITCH, _("Multiple of grid spacing")},
+ {SNAPSOURCE_PAGE_CORNER, _("Page corner")},
+ {SNAPSOURCE_PAGE_CENTER, _("Page center")},
+};
+
+static std::map<SnapTargetType, Glib::ustring> target2string = {
+ {SNAPTARGET_UNDEFINED, _("UNDEFINED")},
+ {SNAPTARGET_BBOX_CORNER, _("bounding box corner")},
+ {SNAPTARGET_BBOX_EDGE, _("bounding box side")},
+ {SNAPTARGET_BBOX_EDGE_MIDPOINT, _("bounding box side midpoint")},
+ {SNAPTARGET_BBOX_MIDPOINT, _("bounding box midpoint")},
+ {SNAPTARGET_NODE_SMOOTH, _("smooth node")},
+ {SNAPTARGET_NODE_CUSP, _("cusp node")},
+ {SNAPTARGET_LINE_MIDPOINT, _("line midpoint")},
+ {SNAPTARGET_PATH, _("path")},
+ {SNAPTARGET_PATH_PERPENDICULAR, _("path (perpendicular)")},
+ {SNAPTARGET_PATH_TANGENTIAL, _("path (tangential)")},
+ {SNAPTARGET_PATH_INTERSECTION, _("path intersection")},
+ {SNAPTARGET_PATH_GUIDE_INTERSECTION, _("guide-path intersection")},
+ {SNAPTARGET_PATH_CLIP, _("clip-path")},
+ {SNAPTARGET_PATH_MASK, _("mask-path")},
+ {SNAPTARGET_ELLIPSE_QUADRANT_POINT, _("quadrant point")},
+ {SNAPTARGET_RECT_CORNER, _("corner")},
+ {SNAPTARGET_GRID, _("grid line")},
+ {SNAPTARGET_GRID_INTERSECTION, _("grid intersection")},
+ {SNAPTARGET_GRID_PERPENDICULAR, _("grid line (perpendicular)")},
+ {SNAPTARGET_GUIDE, _("guide")},
+ {SNAPTARGET_GUIDE_INTERSECTION, _("guide intersection")},
+ {SNAPTARGET_GUIDE_ORIGIN, _("guide origin")},
+ {SNAPTARGET_GUIDE_PERPENDICULAR, _("guide (perpendicular)")},
+ {SNAPTARGET_GRID_GUIDE_INTERSECTION, _("grid-guide intersection")},
+ {SNAPTARGET_PAGE_EDGE_BORDER, _("page border")},
+ {SNAPTARGET_PAGE_EDGE_CORNER, _("page corner")},
+ {SNAPTARGET_PAGE_EDGE_CENTER, _("page center")},
+ {SNAPTARGET_PAGE_MARGIN_BORDER, _("page margin border")},
+ {SNAPTARGET_PAGE_MARGIN_CORNER, _("page margin corner")},
+ {SNAPTARGET_PAGE_MARGIN_CENTER, _("page margin center")},
+ {SNAPTARGET_PAGE_BLEED_BORDER, _("page bleed border")},
+ {SNAPTARGET_PAGE_BLEED_CORNER, _("page bleed corner")},
+ {SNAPTARGET_OBJECT_MIDPOINT, _("object midpoint")},
+ {SNAPTARGET_IMG_CORNER, _("corner")},
+ {SNAPTARGET_ROTATION_CENTER, _("object rotation center")},
+ {SNAPTARGET_TEXT_ANCHOR, _("text anchor")},
+ {SNAPTARGET_TEXT_BASELINE, _("text baseline")},
+ {SNAPTARGET_CONSTRAINED_ANGLE, _("constrained angle")},
+ {SNAPTARGET_CONSTRAINT, _("constraint")},
+};
+
+SnapIndicator::SnapIndicator(SPDesktop * desktop)
+ : _snaptarget(nullptr),
+ _snaptarget_tooltip(nullptr),
+ _snaptarget_bbox(nullptr),
+ _snapsource(nullptr),
+ _snaptarget_is_presnap(false),
+ _desktop(desktop)
+{
+}
+
+SnapIndicator::~SnapIndicator()
+{
+ // remove item that might be present
+ remove_snaptarget();
+ remove_snapsource();
+}
+
+void
+SnapIndicator::set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap)
+{
+ remove_snaptarget(); //only display one snaptarget at a time
+
+ g_assert(_desktop != nullptr);
+
+ if (!p.getSnapped()) {
+ return; // If we haven't snapped, then it is of no use to draw a snapindicator
+ }
+
+ if (p.getTarget() == SNAPTARGET_CONSTRAINT) {
+ // This is not a real snap, although moving along the constraint did affect the mouse pointer's position.
+ // Maybe we should only show a snap indicator when the user explicitly asked for a constraint by pressing ctrl?
+ // We should not show a snap indicator when stretching a selection box, which is also constrained. That would be
+ // too much information.
+ return;
+ }
+
+ bool is_alignment = p.getAlignmentTarget().has_value();
+ bool is_distribution = p.getTarget() & SNAPTARGET_DISTRIBUTION_CATEGORY;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0;
+
+ bool value = prefs->getBool("/options/snapindicator/value", true);
+
+ if (value) {
+ Glib::ustring target_name = _("UNDEFINED");
+ Glib::ustring source_name = _("UNDEFINED");
+
+ if (!is_alignment && !is_distribution) {
+ if (target2string.find(p.getTarget()) == target2string.end())
+ g_warning("Target type %i not present in target2string", p.getTarget());
+
+ if (source2string.find(p.getSource()) == source2string.end())
+ g_warning("Source type %i not present in target2string", p.getSource());
+
+ target_name = _(target2string[p.getTarget()].c_str());
+ source_name = _(source2string[p.getSource()].c_str());
+ }
+ //std::cout << "Snapped " << source_name << " to " << target_name << std::endl;
+
+ remove_snapsource(); // Don't set both the source and target indicators, as these will overlap
+
+ double timeout_val = prefs->getDouble("/options/snapindicatorpersistence/value", 2.0);
+ if (timeout_val < 0.1) {
+ timeout_val = 0.1; // a zero value would mean infinite persistence (i.e. until new snap occurs)
+ // Besides, negatives values would ....?
+ }
+
+ // TODO: should this be a constant or a separate prefrence
+ // we are using the preference of measure tool here.
+ double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0);
+
+ if (is_distribution) {
+ make_distribution_indicators(p, fontsize, scale);
+ }
+
+ if (is_alignment) {
+ auto color = pre_snap ? 0x7f7f7fff : get_guide_color(p.getAlignmentTargetType());
+ make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget(), color, fontsize, scale);
+ if (p.getAlignmentTargetType() == SNAPTARGET_ALIGNMENT_INTERSECTION) {
+ make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget2(), color, fontsize, scale);
+ }
+ }
+
+ _snaptarget_is_presnap = pre_snap;
+
+ // Display the snap indicator (i.e. the cross)
+ Inkscape::CanvasItemCtrl *ctrl;
+
+ if (!is_alignment && !is_distribution) {
+ // Display snap indicator at snap target
+ ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CROSS);
+ ctrl->set_size(11);
+ ctrl->set_stroke( pre_snap ? 0x7f7f7fff : 0xff0000ff);
+ ctrl->set_position(p.getPoint());
+
+ _snaptarget = _desktop->add_temporary_canvasitem(ctrl, timeout_val*1000.0);
+ // The snap indicator will be deleted after some time-out, and sp_canvas_item_dispose
+ // will be called. This will set canvas->current_item to NULL if the snap indicator was
+ // the current item, after which any events will go to the root handler instead of any
+ // item handler. Dragging an object which has just snapped might therefore not be possible
+ // without selecting / repicking it again. To avoid this, we make sure here that the
+ // snap indicator will never be picked, and will therefore never be the current item.
+ // Reported bugs:
+ // - scrolling when hovering above a pre-snap indicator won't work (for example)
+ // (https://bugs.launchpad.net/inkscape/+bug/522335/comments/8)
+ // - dragging doesn't work without repicking
+ // (https://bugs.launchpad.net/inkscape/+bug/1420301/comments/15)
+ ctrl->set_pickable(false);
+
+ // Display the tooltip, which reveals the type of snap source and the type of snap target
+ Glib::ustring tooltip_str;
+ if ( (p.getSource() != SNAPSOURCE_GRID_PITCH) && (p.getTarget() != SNAPTARGET_UNDEFINED) ) {
+ tooltip_str = source_name + _(" to ") + target_name;
+ } else if (p.getSource() != SNAPSOURCE_UNDEFINED) {
+ tooltip_str = source_name;
+ }
+
+
+ if (!tooltip_str.empty()) {
+ Geom::Point tooltip_pos = p.getPoint();
+ if (dynamic_cast<Inkscape::UI::Tools::MeasureTool *>(_desktop->event_context)) {
+ // Make sure that the snap tooltips do not overlap the ones from the measure tool
+ tooltip_pos += _desktop->w2d(Geom::Point(0, -3*fontsize));
+ } else {
+ tooltip_pos += _desktop->w2d(Geom::Point(0, -2*fontsize));
+ }
+
+ auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), tooltip_pos, tooltip_str);
+ canvas_tooltip->set_fontsize(fontsize);
+ canvas_tooltip->set_fill(0xffffffff);
+ canvas_tooltip->set_background(pre_snap ? 0x33337f40 : 0x33337f7f);
+
+ _snaptarget_tooltip = _desktop->add_temporary_canvasitem(canvas_tooltip, timeout_val*1000.0);
+ }
+
+ // Display the bounding box, if we snapped to one
+ Geom::OptRect const bbox = p.getTargetBBox();
+ if (bbox) {
+ auto box = new Inkscape::CanvasItemRect(_desktop->getCanvasTemp(), *bbox);
+ box->set_stroke(pre_snap ? 0x7f7f7fff : 0xff0000ff);
+ box->set_dashed(true);
+ box->set_pickable(false); // Is false by default.
+ box->lower_to_bottom();
+ _snaptarget_bbox = _desktop->add_temporary_canvasitem(box, timeout_val*1000.0);
+ }
+ }
+ }
+}
+
+void
+SnapIndicator::remove_snaptarget(bool only_if_presnap)
+{
+ if (only_if_presnap && !_snaptarget_is_presnap) {
+ return;
+ }
+
+ if (_snaptarget) {
+ _desktop->remove_temporary_canvasitem(_snaptarget);
+ _snaptarget = nullptr;
+ _snaptarget_is_presnap = false;
+ }
+
+ if (_snaptarget_tooltip) {
+ _desktop->remove_temporary_canvasitem(_snaptarget_tooltip);
+ _snaptarget_tooltip = nullptr;
+ }
+
+ if (_snaptarget_bbox) {
+ _desktop->remove_temporary_canvasitem(_snaptarget_bbox);
+ _snaptarget_bbox = nullptr;
+ }
+
+ for (auto *item : _alignment_snap_indicators) {
+ _desktop->remove_temporary_canvasitem(item);
+ }
+ _alignment_snap_indicators.clear();
+
+ for (auto *item : _distribution_snap_indicators) {
+ _desktop->remove_temporary_canvasitem(item);
+ }
+ _distribution_snap_indicators.clear();
+}
+
+void
+SnapIndicator::set_new_snapsource(Inkscape::SnapCandidatePoint const &p)
+{
+ remove_snapsource();
+
+ g_assert(_desktop != nullptr); // If this fails, then likely setup() has not been called on the snap manager (see snap.cpp -> setup())
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool value = prefs->getBool("/options/snapindicator/value", true);
+
+ if (value) {
+ auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ ctrl->set_size(7);
+ ctrl->set_stroke(0xff0000ff);
+ ctrl->set_position(p.getPoint());
+ _snapsource = _desktop->add_temporary_canvasitem(ctrl, 1000);
+ }
+}
+
+void
+SnapIndicator::set_new_debugging_point(Geom::Point const &p)
+{
+ g_assert(_desktop != nullptr);
+ auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND);
+ ctrl->set_size(11);
+ ctrl->set_stroke(0x00ff00ff);
+ ctrl->set_position(p);
+ _debugging_points.push_back(_desktop->add_temporary_canvasitem(ctrl, 5000));
+}
+
+void
+SnapIndicator::remove_snapsource()
+{
+ if (_snapsource) {
+ _desktop->remove_temporary_canvasitem(_snapsource);
+ _snapsource = nullptr;
+ }
+}
+
+void
+SnapIndicator::remove_debugging_points()
+{
+ for (std::list<TemporaryItem *>::const_iterator i = _debugging_points.begin(); i != _debugging_points.end(); ++i) {
+ _desktop->remove_temporary_canvasitem(*i);
+ }
+ _debugging_points.clear();
+}
+
+guint32 SnapIndicator::get_guide_color(SnapTargetType t)
+{
+ switch(t) {
+ case SNAPTARGET_ALIGNMENT_BBOX_CORNER:
+ case SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT:
+ case SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT:
+ return 0xff0000ff;
+ case SNAPTARGET_ALIGNMENT_PAGE_EDGE_CENTER:
+ case SNAPTARGET_ALIGNMENT_PAGE_EDGE_CORNER:
+ case SNAPTARGET_ALIGNMENT_PAGE_MARGIN_CENTER:
+ case SNAPTARGET_ALIGNMENT_PAGE_MARGIN_CORNER:
+ case SNAPTARGET_ALIGNMENT_PAGE_BLEED_CORNER:
+ return 0x00ff00ff;
+ case SNAPTARGET_ALIGNMENT_HANDLE:
+ return 0x0000ffff;
+ case SNAPTARGET_ALIGNMENT_INTERSECTION:
+ return 0xd13bd1ff;
+ default:
+ g_warning("Alignment guide color not handled %i", t);
+ return 0x000000ff;
+ }
+}
+
+std::pair<Geom::Coord, int> get_y_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset)
+{
+ Geom::Coord y;
+ int sign;
+
+ // We add a margin of 5px here to make sure that very small movements of mouse
+ // pointer do not cause the position of distribution indicator to change.
+ if (source.midpoint().y() < target.midpoint().y() + 5) {
+ y = source.max().y() + offset;
+ sign = 1;
+ } else {
+ y = source.min().y() - offset;
+ sign = -1;
+ }
+
+ return {y, sign};
+}
+
+std::pair<Geom::Coord, int> get_x_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset)
+{
+ Geom::Coord x;
+ int sign;
+
+ // We add a margin of 5px here to make sure that very small movements of mouse
+ // pointer do not cause the position of distribution indicator to change.
+ if (source.midpoint().x() < target.midpoint().x() + 5) {
+ x = source.max().x() + offset;
+ sign = 1;
+ } else {
+ x = source.min().x() - offset;
+ sign = -1;
+ }
+
+ return {x, sign};
+}
+
+void SnapIndicator::make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale)
+{
+ //make sure the line is straight
+ g_assert(p1.x() == p2.x() || p1.y() == p2.y());
+
+ Preferences *prefs = Preferences::get();
+ bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false);
+
+ Inkscape::CanvasItemCurve *line;
+
+ auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ ctrl->set_size(7);
+ ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR);
+ ctrl->set_stroke(0xffffffff);
+ ctrl->set_fill(color);
+ ctrl->set_position(p1);
+ ctrl->set_pickable(false);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0));
+
+ ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ ctrl->set_size(7);
+ ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR);
+ ctrl->set_stroke(0xffffffff);
+ ctrl->set_fill(color);
+ ctrl->set_position(p2);
+ ctrl->set_pickable(false);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0));
+
+ auto dist = Geom::L2(p2 - p1);
+ double offset = (fontsize + 5) / _desktop->current_zoom();
+ if (show_distance && dist > 2 * offset) {
+ auto direction = Geom::unit_vector(p1 - p2);
+ auto text_pos = (p1 + p2)/2;
+
+ Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str();
+ if (!unit_name.compare("")) {
+ unit_name = DEFAULT_UNIT_NAME;
+ }
+
+ dist = Inkscape::Util::Quantity::convert(dist, "px", unit_name);
+
+ Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*dist);
+
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance);
+ text->set_fontsize(fontsize);
+ text->set_fill(color);
+ text->set_background(0xffffffc8);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+
+ auto temp_point = text_pos + offset*direction;
+ line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, temp_point);
+ line->set_stroke(color);
+ line->set_bg_alpha(1.0f);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0));
+
+ temp_point = text_pos - offset*direction;
+ line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), temp_point, p2);
+ line->set_stroke(color);
+ line->set_bg_alpha(1.0f);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0));
+ } else {
+ line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line->set_stroke(color);
+ line->set_bg_alpha(1.0f);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0));
+ }
+}
+
+Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_v(Geom::Point const & p)
+{
+ Geom::Coord length = 10/_desktop->current_zoom();
+ auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(0, length/2), p - Geom::Point(0, length/2));
+ line->set_stroke(0xff5f1fff);
+ return line;
+}
+
+Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_h(Geom::Point const & p)
+{
+ Geom::Coord length = 10/_desktop->current_zoom();
+ auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(length/2, 0), p - Geom::Point(length/2, 0));
+ line->set_stroke(0xff5f1fff);
+ return line;
+}
+
+void SnapIndicator::make_distribution_indicators(SnappedPoint const &p,
+ double fontsize,
+ double scale)
+{
+ Preferences *prefs = Preferences::get();
+ bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false);
+
+ guint32 color = 0xff5f1fff;
+ guint32 text_fill = 0xffffffff;
+ guint32 text_bg = 0xff5f1fff; //0x33337f7f
+ Geom::Point text_pos;
+ double text_offset = (fontsize * 2);
+ // double line_offset = 5/_desktop->current_zoom();
+ double line_offset = 0;
+
+ Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str();
+ if (!unit_name.compare("")) {
+ unit_name = DEFAULT_UNIT_NAME;
+ }
+ auto equal_dist = Inkscape::Util::Quantity::convert(p.getDistributionDistance(), "px", unit_name);
+ Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist);
+
+ switch (p.getTarget()) {
+ case SNAPTARGET_DISTRIBUTION_Y:
+ case SNAPTARGET_DISTRIBUTION_X:
+ case SNAPTARGET_DISTRIBUTION_RIGHT:
+ case SNAPTARGET_DISTRIBUTION_LEFT:
+ case SNAPTARGET_DISTRIBUTION_UP:
+ case SNAPTARGET_DISTRIBUTION_DOWN: {
+ Geom::Point p1, p2;
+ Inkscape::CanvasItemCurve *point1, *point2;
+
+ for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) {
+ switch (p.getTarget()) {
+ case SNAPTARGET_DISTRIBUTION_RIGHT:
+ case SNAPTARGET_DISTRIBUTION_LEFT:
+ case SNAPTARGET_DISTRIBUTION_X: {
+ auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(it->max().x() + line_offset, y);
+ p2 = Geom::Point(std::next(it)->min().x() - line_offset, y);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset));
+
+ point1 = make_stub_line_v(p1);
+ point2 = make_stub_line_v(p2);
+ break;
+ }
+
+ case SNAPTARGET_DISTRIBUTION_DOWN:
+ case SNAPTARGET_DISTRIBUTION_UP:
+ case SNAPTARGET_DISTRIBUTION_Y: {
+ auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(x, it->max().y() + line_offset);
+ p2 = Geom::Point(x, std::next(it)->min().y() - line_offset);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0));
+
+ point1 = make_stub_line_h(p1);
+ point2 = make_stub_line_h(p2);
+ break;
+ }
+ }
+
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0));
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0));
+
+ auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line1->set_stroke(color);
+ line1->set_width(2);
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0));
+
+ if (show_distance) {
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance);
+ text->set_fontsize(fontsize);
+ text->set_fill(text_fill);
+ text->set_background(text_bg);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+ }
+ }
+ break;
+ }
+ case SNAPTARGET_DISTRIBUTION_XY: {
+ Geom::Point p1, p2;
+ Inkscape::CanvasItemCurve *point1, *point2;
+
+ auto equal_dist2 = Inkscape::Util::Quantity::convert(p.getDistributionDistance2(), "px", unit_name);
+ Glib::ustring distance2 = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist2);
+
+ for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) {
+ auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(it->max().x() + line_offset, y);
+ p2 = Geom::Point(std::next(it)->min().x() - line_offset, y);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset));
+
+ point1 = make_stub_line_v(p1);
+ point2 = make_stub_line_v(p2);
+
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0));
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0));
+
+ auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line1->set_stroke(color);
+ line1->set_width(2);
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0));
+
+ if (show_distance) {
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance);
+ text->set_fontsize(fontsize);
+ text->set_fill(text_fill);
+ text->set_background(text_bg);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+ }
+ }
+
+ for (auto it = p.getBBoxes2().begin(); it + 1 != p.getBBoxes2().end(); it++) {
+ auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(x, it->max().y() + line_offset);
+ p2 = Geom::Point(x, std::next(it)->min().y() - line_offset);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0));
+
+ point1 = make_stub_line_h(p1);
+ point2 = make_stub_line_h(p2);
+
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0));
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0));
+
+ auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line1->set_stroke(color);
+ line1->set_width(2);
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0));
+
+ if (show_distance) {
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance2);
+ text->set_fontsize(fontsize);
+ text->set_fill(text_fill);
+ text->set_background(text_bg);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+ }
+ }
+
+ break;
+ }
+ }
+}
+
+} //namespace Display
+} /* namespace Inkscape */
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=4:softtabstop=4 :
diff --git a/src/display/control/snap-indicator.h b/src/display/control/snap-indicator.h
new file mode 100644
index 0000000..ca4721b
--- /dev/null
+++ b/src/display/control/snap-indicator.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_DISPLAY_SNAP_INDICATOR_H
+#define INKSCAPE_DISPLAY_SNAP_INDICATOR_H
+
+/**
+ * @file
+ * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap
+ */
+/*
+ * Authors:
+ * Johan Engelen
+ * Diederik van Lierop
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ * Copyright (C) Diederik van Lierop 2010 <mail@diedenrezi.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "snap-enums.h"
+#include "snapped-point.h"
+#include "display/control/canvas-item-curve.h"
+
+#include <glib.h>
+#include <glibmm/i18n.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace Display {
+
+class TemporaryItem;
+
+class SnapIndicator {
+public:
+ SnapIndicator(SPDesktop *desktop);
+ virtual ~SnapIndicator();
+
+ void set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap = false);
+ void remove_snaptarget(bool only_if_presnap = false);
+
+ void set_new_snapsource(Inkscape::SnapCandidatePoint const &p);
+ void remove_snapsource();
+
+ void set_new_debugging_point(Geom::Point const &p);
+ void remove_debugging_points();
+
+protected:
+ TemporaryItem *_snaptarget;
+ TemporaryItem *_snaptarget_tooltip;
+ TemporaryItem *_snaptarget_bbox;
+ TemporaryItem *_snapsource;
+
+ std::list<TemporaryItem *> _alignment_snap_indicators;
+ std::list<TemporaryItem *> _distribution_snap_indicators;
+ std::list<TemporaryItem *> _debugging_points;
+ bool _snaptarget_is_presnap;
+ SPDesktop *_desktop;
+
+private:
+ SnapIndicator(const SnapIndicator&) = delete;
+ SnapIndicator& operator=(const SnapIndicator&) = delete;
+
+ void make_distribution_indicators(SnappedPoint const &p, double fontsize, double scale);
+ void make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale);
+ guint32 get_guide_color(SnapTargetType t);
+ Inkscape::CanvasItemCurve* make_stub_line_h(Geom::Point const &p);
+ Inkscape::CanvasItemCurve* make_stub_line_v(Geom::Point const &p);
+};
+
+} //namespace Display
+} //namespace Inkscape
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :