summaryrefslogtreecommitdiffstats
path: root/src/ui/widget/canvas.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/widget/canvas.cpp')
-rw-r--r--src/ui/widget/canvas.cpp2797
1 files changed, 2797 insertions, 0 deletions
diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp
new file mode 100644
index 0000000..8e6de8c
--- /dev/null
+++ b/src/ui/widget/canvas.cpp
@@ -0,0 +1,2797 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * Authors:
+ * Tavmjong Bah
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream> // Logging
+#include <algorithm> // Sort
+#include <set> // Coarsener
+
+#include <glibmm/i18n.h>
+
+#include <2geom/rect.h>
+
+#include "canvas.h"
+#include "canvas-grid.h"
+
+#include "color.h" // Background color
+#include "cms-system.h" // Color correction
+#include "desktop.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h" // Checkerboard background
+#include "display/drawing.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/snap-indicator.h"
+
+#include "ui/tools/tool-base.h" // Default cursor
+
+#include "framecheck.h" // For frame profiling
+#define framecheck_whole_function(D) auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event();
+
+/*
+ * The canvas is responsible for rendering the SVG drawing with various "control"
+ * items below and on top of the drawing. Rendering is triggered by a call to one of:
+ *
+ *
+ * * redraw_all() Redraws the entire canvas by calling redraw_area() with the canvas area.
+ *
+ * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't affect
+ * a CanvasItem's geometry or size.
+ *
+ * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a
+ * CanvasItem's geometry or size has changed.
+ *
+ * The first three functions add a request to the Gtk's "idle" list via
+ *
+ * * add_idle() Which causes Gtk to call when resources are available:
+ *
+ * * on_idle() Which sets up the backing stores, divides the area of the canvas that has been marked
+ * unclean into rectangles that are small enough to render quickly, and renders them outwards
+ * from the mouse with a call to:
+ *
+ * * paint_rect_internal() Which paints the rectangle using paint_single_buffer(). It renders onto a Cairo
+ * surface "backing_store". After a piece is rendered there is a call to:
+ *
+ * * queue_draw_area() A Gtk function for marking areas of the window as needing a repaint, which when
+ * the time is right calls:
+ *
+ * * on_draw() Which blits the Cairo surface to the screen.
+ *
+ * The other responsibility of the canvas is to determine where to send GUI events. It does this
+ * by determining which CanvasItem is "picked" and then forwards the events to that item. Not all
+ * items can be picked. As a last resort, the "CatchAll" CanvasItem will be picked as it is the
+ * lowest CanvasItem in the stack (except for the "root" CanvasItem). With a small be of work, it
+ * should be possible to make the "root" CanvasItem a "CatchAll" eliminating the need for a
+ * dedicated "CatchAll" CanvasItem. There probably could be efficiency improvements as some
+ * items that are not pickable probably should be which would save having to effectively pick
+ * them "externally" (e.g. gradient CanvasItemCurves).
+ */
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*
+ * GDK event utilities
+ */
+
+// GdkEvents can only be safely copied using gdk_event_copy. However, this function allocates. Therefore, we need the following smart pointer to wrap the result.
+struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}};
+using GdkEventUniqPtr = std::unique_ptr<GdkEvent, GdkEventFreer>;
+
+// Copies a GdkEvent, returning the result as a smart pointer.
+auto make_unique_copy(const GdkEvent &ev) {return GdkEventUniqPtr(gdk_event_copy(&ev));}
+
+/*
+ * Preferences
+ */
+
+template<typename T>
+struct Pref {};
+
+template<typename T>
+struct PrefBase
+{
+ const char *path;
+ T t, def;
+ std::unique_ptr<Preferences::PreferencesObserver> obs;
+ std::function<void()> action;
+ operator T() const {return t;}
+ PrefBase(const char *path, T def) : path(path), def(def) {}
+ void act() {if (action) action();}
+ void enable() {t = static_cast<Pref<T>*>(this)->read(); act(); obs = Inkscape::Preferences::get()->createObserver(path, [this] (const Preferences::Entry &e) {t = static_cast<Pref<T>*>(this)->changed(e); act();});}
+ void disable() {t = def; act(); obs.reset();}
+ void set_enabled(bool enabled) {enabled ? enable() : disable();}
+};
+
+template<>
+struct Pref<bool> : PrefBase<bool>
+{
+ Pref(const char *path, bool def = false) : PrefBase(path, def) {enable();}
+ bool read() {return Inkscape::Preferences::get()->getBool(path, def);}
+ bool changed(const Preferences::Entry &e) {return e.getBool(def);}
+};
+
+template<>
+struct Pref<int> : PrefBase<int>
+{
+ int min, max;
+ Pref(const char *path, int def, int min, int max) : PrefBase(path, def), min(min), max(max) {enable();}
+ int read() {return Inkscape::Preferences::get()->getIntLimited(path, def, min, max);}
+ int changed(const Preferences::Entry &e) {return e.getIntLimited(def, min, max);}
+};
+
+template<>
+struct Pref<double> : PrefBase<double>
+{
+ double min, max;
+ Pref(const char *path, double def, double min, double max) : PrefBase(path, def), min(min), max(max) {enable();}
+ double read() {return Inkscape::Preferences::get()->getDoubleLimited(path, def, min, max);}
+ double changed(const Preferences::Entry &e) {return e.getDoubleLimited(def, min, max);}
+};
+
+struct Prefs
+{
+ // Original parameters
+ Pref<int> tile_size = Pref<int> ("/options/rendering/tile-size", 16, 1, 10000);
+ Pref<int> tile_multiplier = Pref<int> ("/options/rendering/tile-multiplier", 16, 1, 512);
+ Pref<int> x_ray_radius = Pref<int> ("/options/rendering/xray-radius", 100, 1, 1500);
+ Pref<bool> from_display = Pref<bool> ("/options/displayprofile/from_display");
+ Pref<int> grabsize = Pref<int> ("/options/grabsize/value", 3, 1, 15);
+ Pref<int> outline_overlay_opacity = Pref<int> ("/options/rendering/outline-overlay-opacity", 50, 1, 100);
+
+ // New parameters
+ Pref<int> update_strategy = Pref<int> ("/options/rendering/update_strategy", 3, 1, 3);
+ Pref<int> render_time_limit = Pref<int> ("/options/rendering/render_time_limit", 1000, 100, 1000000);
+ Pref<bool> use_new_bisector = Pref<bool> ("/options/rendering/use_new_bisector", true);
+ Pref<int> new_bisector_size = Pref<int> ("/options/rendering/new_bisector_size", 500, 1, 10000);
+ Pref<double> max_affine_diff = Pref<double>("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0);
+ Pref<int> pad = Pref<int> ("/options/rendering/pad", 200, 0, 1000);
+ Pref<int> coarsener_min_size = Pref<int> ("/options/rendering/coarsener_min_size", 200, 0, 1000);
+ Pref<int> coarsener_glue_size = Pref<int> ("/options/rendering/coarsener_glue_size", 80, 0, 1000);
+ Pref<double> coarsener_min_fullness = Pref<double>("/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0);
+
+ // Debug switches
+ Pref<bool> debug_framecheck = Pref<bool> ("/options/rendering/debug_framecheck");
+ Pref<bool> debug_logging = Pref<bool> ("/options/rendering/debug_logging");
+ Pref<bool> debug_slow_redraw = Pref<bool> ("/options/rendering/debug_slow_redraw");
+ Pref<int> debug_slow_redraw_time = Pref<int> ("/options/rendering/debug_slow_redraw_time", 50, 0, 1000000);
+ Pref<bool> debug_show_redraw = Pref<bool> ("/options/rendering/debug_show_redraw");
+ Pref<bool> debug_show_unclean = Pref<bool> ("/options/rendering/debug_show_unclean");
+ Pref<bool> debug_show_snapshot = Pref<bool> ("/options/rendering/debug_show_snapshot");
+ Pref<bool> debug_show_clean = Pref<bool> ("/options/rendering/debug_show_clean");
+ Pref<bool> debug_disable_redraw = Pref<bool> ("/options/rendering/debug_disable_redraw");
+ Pref<bool> debug_sticky_decoupled = Pref<bool> ("/options/rendering/debug_sticky_decoupled");
+
+ // Developer mode
+ Pref<bool> devmode = Pref<bool>("/options/rendering/devmode");
+ void set_devmode(bool on);
+};
+
+void Prefs::set_devmode(bool on)
+{
+ tile_size.set_enabled(on);
+ render_time_limit.set_enabled(on);
+ use_new_bisector.set_enabled(on);
+ new_bisector_size.set_enabled(on);
+ max_affine_diff.set_enabled(on);
+ pad.set_enabled(on);
+ coarsener_min_size.set_enabled(on);
+ coarsener_glue_size.set_enabled(on);
+ coarsener_min_fullness.set_enabled(on);
+ debug_framecheck.set_enabled(on);
+ debug_logging.set_enabled(on);
+ debug_slow_redraw.set_enabled(on);
+ debug_slow_redraw_time.set_enabled(on);
+ debug_show_redraw.set_enabled(on);
+ debug_show_unclean.set_enabled(on);
+ debug_show_snapshot.set_enabled(on);
+ debug_show_clean.set_enabled(on);
+ debug_disable_redraw.set_enabled(on);
+ debug_sticky_decoupled.set_enabled(on);
+}
+
+/*
+ * Conversion functions
+ */
+
+auto geom_to_cairo(Geom::IntRect rect)
+{
+ return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()};
+}
+
+auto cairo_to_geom(Cairo::RectangleInt rect)
+{
+ return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height);
+}
+
+auto geom_to_cairo(Geom::Affine affine)
+{
+ return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]);
+}
+
+auto geom_act(Geom::Affine a, Geom::IntPoint p)
+{
+ Geom::Point p2 = p;
+ p2 *= a;
+ return Geom::IntPoint(std::round(p2.x()), std::round(p2.y()));
+}
+
+void region_to_path(const Cairo::RefPtr<Cairo::Context> &cr, const Cairo::RefPtr<Cairo::Region> &reg)
+{
+ for (int i = 0; i < reg->get_num_rectangles(); i++) {
+ auto rect = reg->get_rectangle(i);
+ cr->rectangle(rect.x, rect.y, rect.width, rect.height);
+ }
+}
+
+/*
+ * Update strategy
+ */
+
+// A class hierarchy for controlling what order to update invalidated regions.
+class Updater
+{
+public:
+ // The subregion of the store with up-to-date content.
+ Cairo::RefPtr<Cairo::Region> clean_region;
+
+ Updater(Cairo::RefPtr<Cairo::Region> clean_region) : clean_region(std::move(clean_region)) {}
+
+ virtual void reset() {clean_region = Cairo::Region::create();} // Reset the clean region to empty.
+ virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position; clip everything to the new store rectangle.
+ virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract (geom_to_cairo(rect));} // Called on every invalidate event.
+ virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn.
+
+ virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() {return clean_region;}; // Called by on_idle to determine what regions to consider clean for the current redraw.
+ virtual bool report_finished () {return false;} // Called in on_idle if the redraw has finished. Returns true to indicate that further redraws are required with a different clean region.
+ virtual void frame () {} // Called by on_draw to notify the updater of the display of the frame.
+ virtual ~Updater() = default;
+};
+
+// Responsive updater: As soon as a region is invalidated, redraw it.
+using ResponsiveUpdater = Updater;
+
+// Full redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed.
+class FullredrawUpdater : public Updater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null.
+ Cairo::RefPtr<Cairo::Region> old_clean_region;
+
+public:
+
+ FullredrawUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {}
+
+ void reset() override
+ {
+ Updater::reset();
+ inprogress = false;
+ old_clean_region.clear();
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ Updater::intersect(rect);
+ if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect));
+ }
+
+ void mark_dirty(const Geom::IntRect &rect) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ Updater::mark_dirty(rect);
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ Updater::mark_clean(rect);
+ if (old_clean_region) old_clean_region->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!old_clean_region) {
+ return clean_region;
+ } else {
+ return old_clean_region;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!old_clean_region) {
+ // Completed redraw without being damaged => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => ask for another redraw, using the up-to-date clean region.
+ old_clean_region.clear();
+ return true;
+ }
+ }
+};
+
+// Multiscale updater: Updates tiles near the mouse faster. Gives the best of both.
+class MultiscaleUpdater : public Updater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Whether damage events occurred during the current redraw.
+ bool activated = false;
+
+ int counter; // A steadily incrementing counter from which the current scale is derived.
+ int scale; // The current scale to process updates at.
+ int elapsed; // How much time has been spent at the current scale.
+ std::vector<Cairo::RefPtr<Cairo::Region>> blocked; // The region blocked from being updated at each scale.
+
+public:
+
+ MultiscaleUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {}
+
+ void reset() override
+ {
+ Updater::reset();
+ inprogress = activated = false;
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ Updater::intersect(rect);
+ if (activated) {
+ for (auto &reg : blocked) {
+ reg->intersect(geom_to_cairo(rect));
+ }
+ }
+ }
+
+ void mark_dirty(const Geom::IntRect &rect) override
+ {
+ Updater::mark_dirty(rect);
+ if (inprogress && !activated) {
+ counter = scale = elapsed = 0;
+ blocked = {Cairo::Region::create()};
+ activated = true;
+ }
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ Updater::mark_clean(rect);
+ if (activated) blocked[scale]->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!activated) {
+ return clean_region;
+ } else {
+ auto result = clean_region->copy();
+ result->do_union(blocked[scale]);
+ return result;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!activated) {
+ // Completed redraw without damage => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => begin updating any remaining damaged regions.
+ activated = false;
+ blocked.clear();
+ return true;
+ }
+ }
+
+ void frame() override
+ {
+ if (!activated) return;
+
+ // Stay at the current scale for 2^scale frames.
+ elapsed++;
+ if (elapsed < (1 << scale)) return;
+ elapsed = 0;
+
+ // Adjust the counter, which causes scale to hop around the values 0, 1, 2... spending half as much time at each subsequent scale.
+ counter++;
+ scale = 0;
+ for (int tmp = counter; tmp % 2 == 1; tmp /= 2) {
+ scale++;
+ }
+
+ // Ensure sufficiently many blocked zones exist.
+ if (scale == blocked.size()) {
+ blocked.emplace_back();
+ }
+
+ // Recreate the current blocked zone as the union of the clean region and lower-scale blocked zones.
+ blocked[scale] = clean_region->copy();
+ for (int i = 0; i < scale; i++) {
+ blocked[scale]->do_union(blocked[i]);
+ }
+ }
+};
+
+std::unique_ptr<Updater>
+make_updater(int type, Cairo::RefPtr<Cairo::Region> clean_region = Cairo::Region::create())
+{
+ switch (type) {
+ case 1: return std::make_unique<ResponsiveUpdater>(std::move(clean_region));
+ case 2: return std::make_unique<FullredrawUpdater>(std::move(clean_region));
+ default:
+ case 3: return std::make_unique<MultiscaleUpdater>(std::move(clean_region));
+ }
+}
+
+/*
+ * Implementation class
+ */
+
+class CanvasPrivate
+{
+public:
+
+ friend class Canvas;
+ Canvas *q;
+ CanvasPrivate(Canvas *q) : q(q) {}
+
+ // Lifecycle
+ bool active = false;
+ void update_active();
+ void activate();
+ void deactivate();
+
+ // Preferences
+ Prefs prefs;
+
+ // Update strategy; tracks the unclean region and decides how to redraw it.
+ std::unique_ptr<Updater> updater;
+
+ // Event processor. Events that interact with the Canvas are buffered here until the start of the next frame. They are processed by a separate object so that deleting the Canvas mid-event can be done safely.
+ struct EventProcessor
+ {
+ std::vector<GdkEventUniqPtr> events;
+ int pos;
+ GdkEvent *ignore = nullptr;
+ CanvasPrivate *canvasprivate; // Nulled on destruction.
+ bool in_processing = false; // For handling recursion due to nested GTK main loops.
+ void process();
+ int gobble_key_events(guint keyval, guint mask);
+ void gobble_motion_events(guint mask);
+ };
+ std::shared_ptr<EventProcessor> eventprocessor; // Usually held by CanvasPrivate, but temporarily also held by itself while processing so that it is not deleted mid-event.
+ bool add_to_bucket(GdkEvent*);
+ bool process_bucketed_event(const GdkEvent&);
+ bool pick_current_item(const GdkEvent&);
+ bool emit_event(const GdkEvent&);
+ Inkscape::CanvasItem *pre_scroll_grabbed_item;
+
+ // State for determining when to run event processor.
+ bool pending_draw = false;
+ sigc::connection bucket_emptier;
+ std::optional<guint> bucket_emptier_tick_callback;
+ void schedule_bucket_emptier();
+
+ // Idle system. The high priority idle ensures at least one idle cycle between add_idle and on_draw.
+ void add_idle();
+ sigc::connection hipri_idle;
+ sigc::connection lopri_idle;
+ bool on_hipri_idle();
+ bool on_lopri_idle();
+ bool idle_running = false;
+
+ // Important global properties of all the stores. If these change, all the stores must be recreated.
+ int _device_scale = 1;
+ bool _store_solid_background;
+
+ // The backing store.
+ Geom::IntRect _store_rect;
+ Geom::Affine _store_affine;
+ Cairo::RefPtr<Cairo::ImageSurface> _backing_store, _outline_store;
+
+ // The snapshot store. Used to mask redraw delay on zoom/rotate.
+ Geom::IntRect _snapshot_rect;
+ Geom::Affine _snapshot_affine;
+ Geom::IntPoint _snapshot_static_offset = {0, 0};
+ Cairo::RefPtr<Cairo::ImageSurface> _snapshot_store, _snapshot_outline_store;
+ Cairo::RefPtr<Cairo::Region> _snapshot_clean_region;
+
+ Geom::Affine geom_affine; // The affine the geometry was last imbued with.
+ bool decoupled_mode = false;
+
+ bool solid_background; // Whether the last background set is solid.
+ bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY;}
+
+ // Drawing
+ bool on_idle();
+ void paint_rect_internal(Geom::IntRect const &rect);
+ void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr<Cairo::ImageSurface> const &store, bool is_backing_store, bool outline_overlay_pass);
+ std::optional<Geom::Dim2> old_bisector(const Geom::IntRect &rect);
+ std::optional<Geom::Dim2> new_bisector(const Geom::IntRect &rect);
+
+ // Trivial overload of GtkWidget function.
+ void queue_draw_area(Geom::IntRect &rect);
+
+ // For tracking the last known mouse position. (The function Gdk::Window::get_device_position cannot be used because of slow X11 round-trips. Remove this workaround when X11 dies.)
+ std::optional<Geom::Point> last_mouse;
+};
+
+/*
+ * Lifecycle
+ */
+
+Canvas::Canvas()
+ : d(std::make_unique<CanvasPrivate>(this))
+{
+ set_name("InkscapeCanvas");
+
+ // Events
+ add_events(Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::ENTER_NOTIFY_MASK |
+ Gdk::LEAVE_NOTIFY_MASK |
+ Gdk::FOCUS_CHANGE_MASK |
+ Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::SCROLL_MASK |
+ Gdk::SMOOTH_SCROLL_MASK );
+
+ // Set up EventProcessor
+ d->eventprocessor = std::make_shared<CanvasPrivate::EventProcessor>();
+ d->eventprocessor->canvasprivate = d.get();
+
+ // Updater
+ d->updater = make_updater(d->prefs.update_strategy);
+
+ // Preferences
+ d->prefs.grabsize.action = [=] {_canvas_item_root->update_canvas_item_ctrl_sizes(d->prefs.grabsize);};
+ d->prefs.debug_show_unclean.action = [=] {queue_draw();};
+ d->prefs.debug_show_clean.action = [=] {queue_draw();};
+ d->prefs.debug_disable_redraw.action = [=] {d->add_idle();};
+ d->prefs.debug_sticky_decoupled.action = [=] {d->add_idle();};
+ d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));};
+ d->prefs.outline_overlay_opacity.action = [=] {queue_draw();};
+
+ // Developer mode master switch
+ d->prefs.devmode.action = [=] {d->prefs.set_devmode(d->prefs.devmode);};
+ d->prefs.devmode.action();
+
+ // Cavas item root
+ _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr);
+ _canvas_item_root->set_name("CanvasItemGroup:Root");
+ _canvas_item_root->set_canvas(this);
+
+ // Background
+ _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0);
+ d->solid_background = true;
+}
+
+void CanvasPrivate::activate()
+{
+ // Event handling/item picking
+ q->_pick_event.type = GDK_LEAVE_NOTIFY;
+ q->_pick_event.crossing.x = 0;
+ q->_pick_event.crossing.y = 0;
+
+ q->_in_repick = false;
+ q->_left_grabbed_item = false;
+ q->_all_enter_events = false;
+ q->_is_dragging = false;
+ q->_state = 0;
+
+ q->_current_canvas_item = nullptr;
+ q->_current_canvas_item_new = nullptr;
+ q->_grabbed_canvas_item = nullptr;
+ q->_grabbed_event_mask = (Gdk::EventMask)0;
+ pre_scroll_grabbed_item = nullptr;
+
+ // Drawing
+ q->_drawing_disabled = false;
+ q->_need_update = true;
+
+ // Split view
+ q->_split_direction = Inkscape::SplitDirection::EAST;
+ q->_split_position = {-1, -1}; // initialize with off-canvas coordinates
+ q->_hover_direction = Inkscape::SplitDirection::NONE;
+ q->_split_dragging = false;
+
+ add_idle();
+}
+
+void CanvasPrivate::deactivate()
+{
+ // Disconnect signals and timeouts. (Note: They will never be rescheduled while inactive.)
+ hipri_idle.disconnect();
+ lopri_idle.disconnect();
+ bucket_emptier.disconnect();
+ if (bucket_emptier_tick_callback) q->remove_tick_callback(*bucket_emptier_tick_callback);
+}
+
+Canvas::~Canvas()
+{
+ // Not necessary as GTK guarantees realization is always followed by unrealization. But just in case that invariant breaks, we deal with it.
+ if (d->active) {
+ std::cerr << "Canvas destructed while realized!" << std::endl;
+ d->deactivate();
+ }
+
+ // Disconnect from EventProcessor.
+ d->eventprocessor->canvasprivate = nullptr;
+
+ // Remove entire CanvasItem tree.
+ delete _canvas_item_root;
+}
+
+void CanvasPrivate::update_active()
+{
+ bool new_active = q->_drawing && q->get_realized();
+ if (new_active != active) {
+ active = new_active;
+ active ? activate() : deactivate();
+ }
+}
+
+void Canvas::set_drawing(Drawing *drawing)
+{
+ _drawing = drawing;
+ d->update_active();
+}
+
+void
+Canvas::on_realize()
+{
+ parent_type::on_realize();
+ assert(get_realized());
+ d->update_active();
+}
+
+void Canvas::on_unrealize()
+{
+ parent_type::on_unrealize();
+ assert(!get_realized());
+ d->update_active();
+}
+
+/*
+ * Events system
+ */
+
+// The following protected functions of Canvas are where all incoming events initially arrive.
+// Those that do not interact with the Canvas are processed instantaneously, while the rest are
+// delayed by placing them into the bucket.
+
+bool
+Canvas::on_scroll_event(GdkEventScroll *scroll_event)
+{
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(scroll_event));
+}
+
+bool
+Canvas::on_button_press_event(GdkEventButton *button_event)
+{
+ return on_button_event(button_event);
+}
+
+bool
+Canvas::on_button_release_event(GdkEventButton *button_event)
+{
+ return on_button_event(button_event);
+}
+
+// Unified handler for press and release events.
+bool
+Canvas::on_button_event(GdkEventButton *button_event)
+{
+ // Sanity-check event type.
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ break; // Good
+ default:
+ std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl;
+ return false;
+ }
+
+ // Drag the split view controller.
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_dragging = true;
+ _split_drag_start = Geom::Point(button_event->x, button_event->y);
+ return true;
+ }
+ break;
+ case GDK_2BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_direction = _hover_direction;
+ _split_dragging = false;
+ queue_draw();
+ return true;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ _split_dragging = false;
+ break;
+ }
+
+ // Otherwise, handle as a delayed event.
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(button_event));
+}
+
+bool
+Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event)
+{
+ if (crossing_event->window != get_window()->gobj()) {
+ return false;
+ }
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(crossing_event));
+}
+
+bool
+Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event)
+{
+ if (crossing_event->window != get_window()->gobj()) {
+ return false;
+ }
+ d->last_mouse = {};
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(crossing_event));
+}
+
+bool
+Canvas::on_focus_in_event(GdkEventFocus *focus_event)
+{
+ grab_focus();
+ return false;
+}
+
+bool
+Canvas::on_key_press_event(GdkEventKey *key_event)
+{
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool
+Canvas::on_key_release_event(GdkEventKey *key_event)
+{
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool
+Canvas::on_motion_notify_event(GdkEventMotion *motion_event)
+{
+ // Record the last mouse position.
+ d->last_mouse = Geom::Point(motion_event->x, motion_event->y);
+
+ // Handle interactions with the split view controller.
+ Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y);
+
+ // Check if we are near the edge. If so, revert to normal mode.
+ if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) {
+ if (cursor_position.x() < 5 ||
+ cursor_position.y() < 5 ||
+ cursor_position.x() - get_allocation().get_width() > -5 ||
+ cursor_position.y() - get_allocation().get_height() > -5 ) {
+
+ // Reset everything.
+ _split_mode = Inkscape::SplitMode::NORMAL;
+ _split_position = Geom::Point(-1, -1);
+ _hover_direction = Inkscape::SplitDirection::NONE;
+ set_cursor();
+ queue_draw();
+
+ // Update action (turn into utility function?).
+ auto window = dynamic_cast<Gtk::ApplicationWindow *>(get_toplevel());
+ if (!window) {
+ std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl;
+ return true;
+ }
+
+ auto action = window->lookup_action("canvas-split-mode");
+ if (!action) {
+ std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl;
+ return true;
+ }
+
+ auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(action);
+ if (!saction) {
+ std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl;
+ return true;
+ }
+
+ saction->change_state((int)Inkscape::SplitMode::NORMAL);
+
+ return true;
+ }
+ }
+
+ if (_split_mode == Inkscape::SplitMode::XRAY) {
+ _split_position = cursor_position;
+ queue_draw();
+ }
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ Inkscape::SplitDirection hover_direction = Inkscape::SplitDirection::NONE;
+ Geom::Point difference(cursor_position - _split_position);
+
+ // Move controller
+ if (_split_dragging) {
+ Geom::Point delta = cursor_position - _split_drag_start; // We don't use _split_position
+ if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) {
+ _split_position += Geom::Point(0, delta.y());
+ } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) {
+ _split_position += Geom::Point(delta.x(), 0);
+ } else {
+ _split_position += delta;
+ }
+ _split_drag_start = cursor_position;
+ queue_draw();
+ return true;
+ }
+
+ if (Geom::distance(cursor_position, _split_position) < 20 * d->_device_scale) {
+ // We're hovering over circle, figure out which direction we are in.
+ if (difference.y() - difference.x() > 0) {
+ if (difference.y() + difference.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::SOUTH;
+ } else {
+ hover_direction = Inkscape::SplitDirection::WEST;
+ }
+ } else {
+ if (difference.y() + difference.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::EAST;
+ } else {
+ hover_direction = Inkscape::SplitDirection::NORTH;
+ }
+ }
+ } else if (_split_direction == Inkscape::SplitDirection::NORTH ||
+ _split_direction == Inkscape::SplitDirection::SOUTH) {
+ if (std::abs(difference.y()) < 3 * d->_device_scale) {
+ // We're hovering over horizontal line
+ hover_direction = Inkscape::SplitDirection::HORIZONTAL;
+ }
+ } else {
+ if (std::abs(difference.x()) < 3 * d->_device_scale) {
+ // We're hovering over vertical line
+ hover_direction = Inkscape::SplitDirection::VERTICAL;
+ }
+ }
+
+ if (_hover_direction != hover_direction) {
+ _hover_direction = hover_direction;
+ set_cursor();
+ queue_draw();
+ }
+
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ // We're hovering, don't pick or emit event.
+ return true;
+ }
+ }
+
+ // Otherwise, handle as a delayed event.
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(motion_event));
+}
+
+// Most events end up here. We store them in the bucket, and process them as soon as possible after
+// the next 'on_draw'. If 'on_draw' isn't pending, we use the 'tick_callback' signal to process them
+// when 'on_draw' would have run anyway. If 'on_draw' later becomes pending, we remove this signal.
+
+// Add an event to the bucket and ensure it will be emptied in the near future.
+bool
+CanvasPrivate::add_to_bucket(GdkEvent *event)
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ std::cerr << "Canvas::add_to_bucket: Called while not active!" << std::endl;
+ return false;
+ }
+
+ // Prevent re-fired events from going through again.
+ if (event == eventprocessor->ignore) {
+ return false;
+ }
+
+ // If this is the first event, ensure event processing will run on the main loop as soon as possible after the next frame has started.
+ if (eventprocessor->events.empty() && !pending_draw) {
+#ifndef NDEBUG
+ if (bucket_emptier_tick_callback) {
+ g_warning("bucket_emptier_tick_callback not empty");
+ }
+#endif
+ bucket_emptier_tick_callback = q->add_tick_callback([this] (const Glib::RefPtr<Gdk::FrameClock>&) {
+ assert(active);
+ bucket_emptier_tick_callback.reset();
+ schedule_bucket_emptier();
+ return false;
+ });
+ }
+
+ // Add a copy to the queue.
+ eventprocessor->events.emplace_back(gdk_event_copy(event));
+
+ // Tell GTK the event was handled.
+ return true;
+}
+
+void CanvasPrivate::schedule_bucket_emptier()
+{
+ if (!active) {
+ std::cerr << "Canvas::schedule_bucket_emptier: Called while not active!" << std::endl;
+ return;
+ }
+
+ if (!bucket_emptier.connected()) {
+ bucket_emptier = Glib::signal_idle().connect([this] {
+ assert(active);
+ eventprocessor->process();
+ return false;
+ }, G_PRIORITY_HIGH_IDLE + 14); // before hipri_idle
+ }
+}
+
+// The following functions run at the start of the next frame on the GTK main loop.
+// (Note: It is crucial that it runs on the main loop and not in any frame clock tick callbacks. GTK does not allow widgets to be deleted in the latter; only the former.)
+
+// Process bucketed events.
+void
+CanvasPrivate::EventProcessor::process()
+{
+ framecheck_whole_function(canvasprivate)
+
+ // Ensure the EventProcessor continues to live even if the Canvas is destroyed during event processing.
+ auto self = canvasprivate->eventprocessor;
+
+ // Check if toplevel or recursive. (Recursive calls happen if processing an event starts its own nested GTK main loop.)
+ bool toplevel = !in_processing;
+ in_processing = true;
+
+ // If toplevel, initialise the iteration index. It may be incremented externally by gobblers or recursive calls.
+ if (toplevel) {
+ pos = 0;
+ }
+
+ while (pos < events.size()) {
+ // Extract next event.
+ auto event = std::move(events[pos]);
+ pos++;
+
+ // Fire the event at the CanvasItems and see if it was handled.
+ bool handled = canvasprivate->process_bucketed_event(*event);
+
+ if (!handled) {
+ // Re-fire the event at the window, and ignore it when it comes back here again.
+ ignore = event.get();
+ canvasprivate->q->get_toplevel()->event(event.get());
+ ignore = nullptr;
+ }
+
+ // If the Canvas was destroyed or deactivated during event processing, exit now.
+ if (!canvasprivate || !canvasprivate->active) return;
+ }
+
+ // Otherwise, clear the list of events that was just processed.
+ events.clear();
+
+ // Reset the variable to track recursive calls.
+ if (toplevel) {
+ in_processing = false;
+ }
+}
+
+// Called during event processing by some tools to batch backlogs of key events that may have built up after a freeze.
+int
+Canvas::gobble_key_events(guint keyval, guint mask)
+{
+ return d->eventprocessor->gobble_key_events(keyval, mask);
+}
+
+int
+CanvasPrivate::EventProcessor::gobble_key_events(guint keyval, guint mask)
+{
+ int count = 0;
+
+ while (pos < events.size()) {
+ auto &event = events[pos];
+ if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) && event->key.keyval == keyval && (!mask || (event->key.state & mask))) {
+ // Discard event and continue.
+ if (event->type == GDK_KEY_PRESS) count++;
+ pos++;
+ }
+ else {
+ // Stop discarding.
+ break;
+ }
+ }
+
+ if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " key press(es)" << std::endl;
+
+ return count;
+}
+
+// Called during event processing by some tools to ignore backlogs of motion events that may have built up after a freeze.
+void
+Canvas::gobble_motion_events(guint mask)
+{
+ d->eventprocessor->gobble_motion_events(mask);
+}
+
+void
+CanvasPrivate::EventProcessor::gobble_motion_events(guint mask)
+{
+ int count = 0;
+
+ while (pos < events.size()) {
+ auto &event = events[pos];
+ if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) {
+ // Discard event and continue.
+ count++;
+ pos++;
+ }
+ else {
+ // Stop discarding.
+ break;
+ }
+ }
+
+ if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " motion event(s)" << std::endl;
+}
+
+// From now on Inkscape's regular event processing logic takes place. The only thing to remember is that
+// all of this happens at a slight delay after the original GTK events. Therefore, it's important to make
+// sure that stateful variables like '_current_canvas_item' and friends are ONLY read/written within these
+// functions, not during the earlier GTK event handlers. Otherwise state confusion will ensue.
+
+bool
+CanvasPrivate::process_bucketed_event(const GdkEvent &event)
+{
+ auto calc_button_mask = [&] () -> int {
+ switch (event.button.button) {
+ case 1: return GDK_BUTTON1_MASK; break;
+ case 2: return GDK_BUTTON2_MASK; break;
+ case 3: return GDK_BUTTON3_MASK; break;
+ case 4: return GDK_BUTTON4_MASK; break;
+ case 5: return GDK_BUTTON5_MASK; break;
+ default: return 0; // Buttons can range at least to 9 but mask defined only to 5.
+ }
+ };
+
+ // Do event-specific processing.
+ switch (event.type) {
+
+ case GDK_SCROLL:
+ {
+ // Save the current event-receiving item just before scrolling starts. It will continue to receive scroll events until the mouse is moved.
+ if (!pre_scroll_grabbed_item) {
+ pre_scroll_grabbed_item = q->_current_canvas_item;
+ if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
+ pre_scroll_grabbed_item = q->_grabbed_canvas_item;
+ }
+ }
+
+ // Process the scroll event...
+ bool retval = emit_event(event);
+
+ // ...then repick.
+ q->_state = event.scroll.state;
+ pick_current_item(event);
+
+ return retval;
+ }
+
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ {
+ pre_scroll_grabbed_item = nullptr;
+
+ // Pick the current item as if the button were not pressed...
+ q->_state = event.button.state;
+ pick_current_item(event);
+
+ // ...then process the event.
+ q->_state ^= calc_button_mask();
+ bool retval = emit_event(event);
+
+ return retval;
+ }
+
+ case GDK_BUTTON_RELEASE:
+ {
+ pre_scroll_grabbed_item = nullptr;
+
+ // Process the event as if the button were pressed...
+ q->_state = event.button.state;
+ bool retval = emit_event(event);
+
+ // ...then repick after the button has been released.
+ auto event_copy = make_unique_copy(event);
+ event_copy->button.state ^= calc_button_mask();
+ q->_state = event_copy->button.state;
+ pick_current_item(*event_copy);
+
+ return retval;
+ }
+
+ case GDK_ENTER_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event.crossing.state;
+ return pick_current_item(event);
+
+ case GDK_LEAVE_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event.crossing.state;
+ // This is needed to remove alignment or distribution snap indicators.
+ if (q->_desktop) {
+ q->_desktop->snapindicator->remove_snaptarget();
+ }
+ return pick_current_item(event);
+
+ case GDK_KEY_PRESS:
+ case GDK_KEY_RELEASE:
+ return emit_event(event);
+
+ case GDK_MOTION_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event.motion.state;
+ pick_current_item(event);
+ return emit_event(event);
+
+ default:
+ return false;
+ }
+}
+
+// This function is called by 'process_bucketed_event' to manipulate the state variables relating
+// to the current object under the mouse, for example, to generate enter and leave events.
+// (A more detailed explanation by Tavmjong follows.)
+// --------
+// This routine reacts to events from the canvas. It's main purpose is to find the canvas item
+// closest to the cursor where the event occurred and then send the event (sometimes modified) to
+// that item. The event then bubbles up the canvas item tree until an object handles it. If the
+// widget is redrawn, this routine may be called again for the same event.
+//
+// Canvas items register their interest by connecting to the "event" signal.
+// Example in desktop.cpp:
+// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this));
+bool
+CanvasPrivate::pick_current_item(const GdkEvent &event)
+{
+ // Ensure requested geometry updates are performed first.
+ if (q->_need_update) {
+ q->_canvas_item_root->update(geom_affine);
+ q->_need_update = false;
+ }
+
+ int button_down = 0;
+ if (!q->_all_enter_events) {
+ // Only set true in connector-tool.cpp.
+
+ // If a button is down, we'll perform enter and leave events on the
+ // current item, but not enter on any other item. This is more or
+ // less like X pointer grabbing for canvas items.
+ button_down = q->_state & (GDK_BUTTON1_MASK |
+ GDK_BUTTON2_MASK |
+ GDK_BUTTON3_MASK |
+ GDK_BUTTON4_MASK |
+ GDK_BUTTON5_MASK);
+ if (!button_down) q->_left_grabbed_item = false;
+ }
+
+ // Save the event in the canvas. This is used to synthesize enter and
+ // leave events in case the current item changes. It is also used to
+ // re-pick the current item if the current one gets deleted. Also,
+ // synthesize an enter event.
+ if (&event != &q->_pick_event) {
+ if (event.type == GDK_MOTION_NOTIFY || event.type == GDK_SCROLL || event.type == GDK_BUTTON_RELEASE) {
+ // Convert to GDK_ENTER_NOTIFY
+
+ // These fields have the same offsets in all types of events.
+ q->_pick_event.crossing.type = GDK_ENTER_NOTIFY;
+ q->_pick_event.crossing.window = event.motion.window;
+ q->_pick_event.crossing.send_event = event.motion.send_event;
+ q->_pick_event.crossing.subwindow = nullptr;
+ q->_pick_event.crossing.x = event.motion.x;
+ q->_pick_event.crossing.y = event.motion.y;
+ q->_pick_event.crossing.mode = GDK_CROSSING_NORMAL;
+ q->_pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR;
+ q->_pick_event.crossing.focus = false;
+
+ // These fields don't have the same offsets in all types of events.
+ // (Todo: With C++20, can reduce the code repetition here using a templated lambda.)
+ switch (event.type)
+ {
+ case GDK_MOTION_NOTIFY:
+ q->_pick_event.crossing.state = event.motion.state;
+ q->_pick_event.crossing.x_root = event.motion.x_root;
+ q->_pick_event.crossing.y_root = event.motion.y_root;
+ break;
+ case GDK_SCROLL:
+ q->_pick_event.crossing.state = event.scroll.state;
+ q->_pick_event.crossing.x_root = event.scroll.x_root;
+ q->_pick_event.crossing.y_root = event.scroll.y_root;
+ break;
+ case GDK_BUTTON_RELEASE:
+ q->_pick_event.crossing.state = event.button.state;
+ q->_pick_event.crossing.x_root = event.button.x_root;
+ q->_pick_event.crossing.y_root = event.button.y_root;
+ break;
+ default:
+ assert(false);
+ }
+
+ } else {
+ q->_pick_event = event;
+ }
+ }
+
+ if (q->_in_repick) {
+ // Don't do anything else if this is a recursive call.
+ return false;
+ }
+
+ // Find new item
+ q->_current_canvas_item_new = nullptr;
+
+ if (q->_pick_event.type != GDK_LEAVE_NOTIFY && q->_canvas_item_root->is_visible()) {
+ // Leave notify means there is no current item.
+ // Find closest item.
+ double x = 0.0;
+ double y = 0.0;
+
+ if (q->_pick_event.type == GDK_ENTER_NOTIFY) {
+ x = q->_pick_event.crossing.x;
+ y = q->_pick_event.crossing.y;
+ } else {
+ x = q->_pick_event.motion.x;
+ y = q->_pick_event.motion.y;
+ }
+
+ // If in split mode, look at where cursor is to see if one should pick with outline mode.
+ if (q->_split_mode == Inkscape::SplitMode::SPLIT && q->_render_mode != Inkscape::RenderMode::OUTLINE_OVERLAY) {
+ if ((q->_split_direction == Inkscape::SplitDirection::NORTH && y > q->_split_position.y()) ||
+ (q->_split_direction == Inkscape::SplitDirection::SOUTH && y < q->_split_position.y()) ||
+ (q->_split_direction == Inkscape::SplitDirection::WEST && x > q->_split_position.x()) ||
+ (q->_split_direction == Inkscape::SplitDirection::EAST && x < q->_split_position.x()) ) {
+ q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE);
+ }
+ }
+ // Convert to world coordinates.
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (decoupled_mode) {
+ p *= _store_affine * q->_affine.inverse();
+ }
+
+ q->_current_canvas_item_new = q->_canvas_item_root->pick_item(p);
+ // if (q->_current_canvas_item_new) {
+ // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl;
+ // } else {
+ // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl;
+ // }
+
+ // Reset the drawing back to the requested render mode.
+ q->_drawing->setRenderMode(q->_render_mode);
+ }
+
+ if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) {
+ // Current item did not change!
+ return false;
+ }
+
+ // Synthesize events for old and new current items.
+ bool retval = false;
+ if (q->_current_canvas_item_new != q->_current_canvas_item &&
+ q->_current_canvas_item != nullptr &&
+ !q->_left_grabbed_item ) {
+
+ GdkEvent new_event;
+ new_event = q->_pick_event;
+ new_event.type = GDK_LEAVE_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ q->_in_repick = true;
+ retval = emit_event(new_event);
+ q->_in_repick = false;
+ }
+
+ if (q->_all_enter_events == false) {
+ // new_current_item may have been set to nullptr during the call to emitEvent() above.
+ if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) {
+ q->_left_grabbed_item = true;
+ return retval;
+ }
+ }
+
+ // Handle the rest of cases
+ q->_left_grabbed_item = false;
+ q->_current_canvas_item = q->_current_canvas_item_new;
+
+ if (q->_current_canvas_item != nullptr) {
+ GdkEvent new_event;
+ new_event = q->_pick_event;
+ new_event.type = GDK_ENTER_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ retval = emit_event(new_event);
+ }
+
+ return retval;
+}
+
+// Fires an event at the canvas, after a little pre-processing. Returns true if handled.
+bool
+CanvasPrivate::emit_event(const GdkEvent &event)
+{
+ // Handle grabbed items.
+ if (q->_grabbed_canvas_item) {
+ auto mask = (Gdk::EventMask)0;
+
+ switch (event.type) {
+ case GDK_ENTER_NOTIFY:
+ mask = Gdk::ENTER_NOTIFY_MASK;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ mask = Gdk::LEAVE_NOTIFY_MASK;
+ break;
+ case GDK_MOTION_NOTIFY:
+ mask = Gdk::POINTER_MOTION_MASK;
+ break;
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ mask = Gdk::BUTTON_PRESS_MASK;
+ break;
+ case GDK_BUTTON_RELEASE:
+ mask = Gdk::BUTTON_RELEASE_MASK;
+ break;
+ case GDK_KEY_PRESS:
+ mask = Gdk::KEY_PRESS_MASK;
+ break;
+ case GDK_KEY_RELEASE:
+ mask = Gdk::KEY_RELEASE_MASK;
+ break;
+ case GDK_SCROLL:
+ mask = Gdk::SCROLL_MASK;
+ mask |= Gdk::SMOOTH_SCROLL_MASK;
+ break;
+ default:
+ break;
+ }
+
+ if (!(mask & q->_grabbed_event_mask)) {
+ return false;
+ }
+ }
+
+ // Convert to world coordinates. We have two different cases due to different event structures.
+ auto conv = [&, this] (double &x, double &y) {
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (decoupled_mode) {
+ p *= _store_affine * q->_affine.inverse();
+ }
+ x = p.x();
+ y = p.y();
+ };
+
+ auto event_copy = make_unique_copy(event);
+
+ switch (event.type) {
+ case GDK_ENTER_NOTIFY:
+ case GDK_LEAVE_NOTIFY:
+ conv(event_copy->crossing.x, event_copy->crossing.y);
+ break;
+ case GDK_MOTION_NOTIFY:
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ conv(event_copy->motion.x, event_copy->motion.y);
+ break;
+ default:
+ break;
+ }
+
+ // Block undo/redo while anything is dragged.
+ if (event.type == GDK_BUTTON_PRESS && event.button.button == 1) {
+ q->_is_dragging = true;
+ } else if (event.type == GDK_BUTTON_RELEASE) {
+ q->_is_dragging = false;
+ }
+
+ if (q->_current_canvas_item) {
+ // Choose where to send event.
+ auto item = q->_current_canvas_item;
+
+ if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
+ item = q->_grabbed_canvas_item;
+ }
+
+ if (pre_scroll_grabbed_item && event.type == GDK_SCROLL) {
+ item = pre_scroll_grabbed_item;
+ }
+
+ // Propagate the event up the canvas item hierarchy until handled.
+ while (item) {
+ if (item->handle_event(event_copy.get())) return true;
+ item = item->get_parent();
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Protected functions
+ */
+
+Geom::IntPoint
+Canvas::get_dimensions() const
+{
+ Gtk::Allocation allocation = get_allocation();
+ return {allocation.get_width(), allocation.get_height()};
+}
+
+/**
+ * Is world point inside canvas area?
+ */
+bool
+Canvas::world_point_inside_canvas(Geom::Point const &world) const
+{
+ return get_area_world().contains(world.floor());
+}
+
+/**
+ * Translate point in canvas to world coordinates.
+ */
+Geom::Point
+Canvas::canvas_to_world(Geom::Point const &point) const
+{
+ return point + _pos;
+}
+
+/**
+ * Return the area shown in the canvas in world coordinates.
+ */
+Geom::IntRect
+Canvas::get_area_world() const
+{
+ return Geom::IntRect(_pos, _pos + get_dimensions());
+}
+
+/**
+ * Set the affine for the canvas.
+ */
+void
+Canvas::set_affine(Geom::Affine const &affine)
+{
+ if (_affine == affine) {
+ return;
+ }
+
+ _affine = affine;
+
+ d->add_idle();
+ queue_draw();
+}
+
+void CanvasPrivate::queue_draw_area(Geom::IntRect &rect)
+{
+ q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height());
+}
+
+/**
+ * Invalidate drawing and redraw during idle.
+ */
+void
+Canvas::redraw_all()
+{
+ if (!d->active) {
+ // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
+ // We need to ignore their requests!
+ return;
+ }
+ d->updater->reset(); // Empty region (i.e. everything is dirty).
+ d->add_idle();
+ if (d->prefs.debug_show_unclean) queue_draw();
+}
+
+/**
+ * Redraw the given area during idle.
+ */
+void
+Canvas::redraw_area(int x0, int y0, int x1, int y1)
+{
+ if (!d->active) {
+ // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
+ // We need to ignore their requests!
+ return;
+ }
+
+ // Clamp area to Cairo's technically supported max size (-2^30..+2^30-1).
+ // This ensures that the rectangle dimensions don't overflow and wrap around.
+ constexpr int min_coord = -(1 << 30);
+ constexpr int max_coord = (1 << 30) - 1;
+
+ x0 = std::clamp(x0, min_coord, max_coord);
+ y0 = std::clamp(y0, min_coord, max_coord);
+ x1 = std::clamp(x1, min_coord, max_coord);
+ y1 = std::clamp(y1, min_coord, max_coord);
+
+ if (x0 >= x1 || y0 >= y1) {
+ return;
+ }
+
+ auto rect = Geom::IntRect::from_xywh(x0, y0, x1 - x0, y1 - y0);
+ d->updater->mark_dirty(rect);
+ d->add_idle();
+ if (d->prefs.debug_show_unclean) queue_draw();
+}
+
+void
+Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1)
+{
+ // Handle overflow during conversion gracefully.
+ // Round outward to make sure integral coordinates cover the entire area.
+ constexpr Geom::Coord min_int = std::numeric_limits<int>::min();
+ constexpr Geom::Coord max_int = std::numeric_limits<int>::max();
+
+ redraw_area(
+ (int)std::floor(std::clamp(x0, min_int, max_int)),
+ (int)std::floor(std::clamp(y0, min_int, max_int)),
+ (int)std::ceil (std::clamp(x1, min_int, max_int)),
+ (int)std::ceil (std::clamp(y1, min_int, max_int))
+ );
+}
+
+void
+Canvas::redraw_area(Geom::Rect &area)
+{
+ redraw_area(area.left(), area.top(), area.right(), area.bottom());
+}
+
+/**
+ * Redraw after changing canvas item geometry.
+ */
+void
+Canvas::request_update()
+{
+ // Flag geometry as needing update.
+ _need_update = true;
+
+ // Trigger the idle process to perform the update.
+ d->add_idle();
+}
+
+/**
+ * Scroll window so drawing point 'pos' is at upper left corner of canvas.
+ */
+void
+Canvas::set_pos(Geom::IntPoint const &pos)
+{
+ if (pos == _pos) {
+ return;
+ }
+
+ _pos = pos;
+
+ d->add_idle();
+ queue_draw();
+
+ if (auto grid = dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(get_parent())) {
+ grid->UpdateRulers();
+ }
+}
+
+/**
+ * Set canvas background color (display only).
+ */
+void
+Canvas::set_background_color(guint32 rgba)
+{
+ double r = SP_RGBA32_R_F(rgba);
+ double g = SP_RGBA32_G_F(rgba);
+ double b = SP_RGBA32_B_F(rgba);
+
+ _background = Cairo::SolidPattern::create_rgb(r, g, b);
+ d->solid_background = true;
+
+ redraw_all();
+}
+
+/**
+ * Set canvas background to a checkerboard pattern.
+ */
+void
+Canvas::set_background_checkerboard(guint32 rgba, bool use_alpha)
+{
+ auto pattern = ink_cairo_pattern_create_checkerboard(rgba, use_alpha);
+ _background = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(pattern));
+ d->solid_background = false;
+ redraw_all();
+}
+
+void Canvas::set_drawing_disabled(bool disable)
+{
+ _drawing_disabled = disable;
+ if (!disable) {
+ d->add_idle();
+ }
+}
+
+void
+Canvas::set_render_mode(Inkscape::RenderMode mode)
+{
+ if (_render_mode != mode) {
+ _render_mode = mode;
+ _drawing->setRenderMode(_render_mode);
+ redraw_all();
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void
+Canvas::set_color_mode(Inkscape::ColorMode mode)
+{
+ if (_color_mode != mode) {
+ _color_mode = mode;
+ redraw_all();
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void
+Canvas::set_split_mode(Inkscape::SplitMode mode)
+{
+ if (_split_mode != mode) {
+ _split_mode = mode;
+ redraw_all();
+ }
+}
+
+Cairo::RefPtr<Cairo::ImageSurface>
+Canvas::get_backing_store() const
+{
+ return d->_backing_store;
+}
+
+/**
+ * Clear current and grabbed items.
+ */
+void
+Canvas::canvas_item_destructed(Inkscape::CanvasItem* item)
+{
+ if (item == _current_canvas_item) {
+ _current_canvas_item = nullptr;
+ }
+
+ if (item == _current_canvas_item_new) {
+ _current_canvas_item_new = nullptr;
+ }
+
+ if (item == _grabbed_canvas_item) {
+ _grabbed_canvas_item = nullptr;
+ auto const display = Gdk::Display::get_default();
+ auto const seat = display->get_default_seat();
+ seat->ungrab();
+ }
+
+ if (item == d->pre_scroll_grabbed_item) {
+ d->pre_scroll_grabbed_item = nullptr;
+ }
+}
+
+// Change cursor
+void
+Canvas::set_cursor() {
+
+ if (!_desktop) {
+ return;
+ }
+
+ auto display = Gdk::Display::get_default();
+
+ switch (_hover_direction) {
+
+ case Inkscape::SplitDirection::NONE:
+ _desktop->event_context->use_tool_cursor();
+ break;
+
+ case Inkscape::SplitDirection::NORTH:
+ case Inkscape::SplitDirection::EAST:
+ case Inkscape::SplitDirection::SOUTH:
+ case Inkscape::SplitDirection::WEST:
+ {
+ auto cursor = Gdk::Cursor::create(display, "pointer");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ case Inkscape::SplitDirection::HORIZONTAL:
+ {
+ auto cursor = Gdk::Cursor::create(display, "ns-resize");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ case Inkscape::SplitDirection::VERTICAL:
+ {
+ auto cursor = Gdk::Cursor::create(display, "ew-resize");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ default:
+ // Shouldn't reach.
+ std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl;
+ }
+}
+
+void
+Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
+{
+ minimum_width = natural_width = 256;
+}
+
+void
+Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
+{
+ minimum_height = natural_height = 256;
+}
+
+void
+Canvas::on_size_allocate(Gtk::Allocation &allocation)
+{
+ parent_type::on_size_allocate(allocation);
+ assert(allocation == get_allocation());
+ d->add_idle(); // Trigger the size update to be applied to the stores before the next call to on_draw.
+}
+
+/*
+ * Drawing
+ */
+
+/*
+ * The on_draw() function is called whenever Gtk wants to update the window. This function:
+ *
+ * 1. Ensures that if the idle process was started, at least one cycle has run.
+ *
+ * 2. Blits the store(s) onto the canvas, clipping the outline store as required.
+ * (Or composites them with the transformed snapshot store(s) in decoupled mode.)
+ *
+ * 3. Draws the "controller" in the 'split' split mode.
+ */
+bool
+Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr)
+{
+ auto f = FrameCheck::Event();
+
+ if (!d->active) {
+ std::cerr << "Canvas::on_draw: Called while not active!" << std::endl;
+ return true;
+ }
+
+ // sp_canvas_item_recursive_print_tree(0, _root);
+ // canvas_item_print_tree(_canvas_item_root);
+
+ assert(_drawing);
+
+ // Although hipri_idle is scheduled at a priority higher than draw, and should therefore always be called first if asked, there are times when GTK simply decides to call on_draw anyway.
+ // Here we ensure that that call has taken place. This is problematic because if hipri_idle does rendering, enlarging the damage rect, then our drawing will still be clipped to the old
+ // damage rect. It was precisely this problem that lead to the introduction of hipri_idle. Fortunately, the following failsafe only seems to execute once during initialisation, and
+ // once on further resize events. Both these events seem to trigger a full damage, hence we are ok.
+ if (d->hipri_idle.connected()) {
+ d->hipri_idle.disconnect();
+ d->on_hipri_idle();
+ }
+
+ // Blit background if not solid. (If solid, it is baked into the stores.)
+ if (!d->solid_background) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("background");
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(_background);
+ cr->paint();
+ cr->restore();
+ }
+
+ auto draw_store = [&, this] (const Cairo::RefPtr<Cairo::ImageSurface> &store, const Cairo::RefPtr<Cairo::ImageSurface> &snapshot_store, bool is_backing_store) {
+ if (!d->decoupled_mode) {
+ // Blit store to screen.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw");
+ cr->save();
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ cr->set_source(store, d->_store_rect.left() - _pos.x(), d->_store_rect.top() - _pos.y());
+ cr->paint();
+ cr->restore();
+ } else {
+ // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step.
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+
+ // Blit untransformed snapshot store to complement of transformed snapshot clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 2);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height());
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_snapshot_affine.inverse()));
+ region_to_path(cr, d->_snapshot_clean_region);
+ cr->clip();
+ cr->transform(geom_to_cairo(d->_snapshot_affine * _affine.inverse()));
+ cr->translate(_pos.x(), _pos.y());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ cr->set_source(snapshot_store, d->_snapshot_static_offset.x(), d->_snapshot_static_offset.y());
+ cr->paint();
+ cr->restore();
+
+ // Draw transformed snapshot, clipped to its clean region and the complement of the store's clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 1);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height());
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ region_to_path(cr, d->updater->clean_region);
+ cr->clip();
+ cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse()));
+ region_to_path(cr, d->_snapshot_clean_region);
+ cr->clip();
+ cr->set_source(snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ if (d->prefs.debug_show_snapshot) {
+ cr->set_source_rgba(0, 0, 1, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->paint();
+ }
+ cr->restore();
+
+ // Draw transformed store, clipped to clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0);
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ region_to_path(cr, d->updater->clean_region);
+ cr->clip();
+ cr->set_source(store, d->_store_rect.left(), d->_store_rect.top());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ cr->restore();
+ }
+ };
+
+ // Draw the backing store.
+ draw_store(d->_backing_store, d->_snapshot_store, true);
+
+ // Draw overlay if required.
+ if (_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) {
+ assert(d->_outline_store);
+
+ double outline_overlay_opacity = 1.0 - d->prefs.outline_overlay_opacity / 100.0;
+
+ // Partially obscure drawing by painting semi-transparent white.
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->paint_with_alpha(outline_overlay_opacity);
+
+ // Overlay outline.
+ draw_store(d->_outline_store, d->_snapshot_outline_store, false);
+ }
+
+ // Draw split if required.
+ if (_split_mode != Inkscape::SplitMode::NORMAL) {
+ assert(d->_outline_store);
+
+ // Move split position to center if not in canvas.
+ auto const rect = Geom::Rect(Geom::Point(), get_dimensions());
+ if (!rect.contains(_split_position)) {
+ _split_position = rect.midpoint();
+ }
+
+ // Add clipping path and blit background.
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(_background);
+ add_clippath(cr);
+ cr->paint();
+ cr->restore();
+
+ // Add clipping path and draw outline store.
+ cr->save();
+ add_clippath(cr);
+ draw_store(d->_outline_store, d->_snapshot_outline_store, false);
+ cr->restore();
+ }
+
+ // Paint unclean regions in red.
+ if (d->prefs.debug_show_unclean) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_unclean");
+ auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect));
+ reg->subtract(d->updater->clean_region);
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ if (d->decoupled_mode) {
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ }
+ cr->set_source_rgba(1, 0, 0, 0.2);
+ region_to_path(cr, reg);
+ cr->fill();
+ cr->restore();
+ }
+
+ // Paint internal edges of clean region in green.
+ if (d->prefs.debug_show_clean) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_clean");
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ if (d->decoupled_mode) {
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ }
+ cr->set_source_rgba(0, 0.7, 0, 0.4);
+ region_to_path(cr, d->updater->clean_region);
+ cr->stroke();
+ cr->restore();
+ }
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ // Add dividing line.
+ cr->save();
+ cr->set_source_rgb(0, 0, 0);
+ cr->set_line_width(1);
+ if (_split_direction == Inkscape::SplitDirection::EAST ||
+ _split_direction == Inkscape::SplitDirection::WEST) {
+ cr->move_to((int)_split_position.x() + 0.5, 0);
+ cr->line_to((int)_split_position.x() + 0.5, get_dimensions().y());
+ cr->stroke();
+ } else {
+ cr->move_to( 0, (int)_split_position.y() + 0.5);
+ cr->line_to(get_dimensions().x(), (int)_split_position.y() + 0.5);
+ cr->stroke();
+ }
+ cr->restore();
+
+ // Add controller image.
+ double a = _hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0;
+ cr->save();
+ cr->set_source_rgba(0.2, 0.2, 0.2, a);
+ cr->arc(_split_position.x(), _split_position.y(), 20 * d->_device_scale, 0, 2 * M_PI);
+ cr->fill();
+ cr->restore();
+
+ cr->save();
+ for (int i = 0; i < 4; ++i) {
+ // The four direction triangles.
+ cr->save();
+
+ // Position triangle.
+ cr->translate(_split_position.x(), _split_position.y());
+ cr->rotate((i + 2) * M_PI / 2.0);
+
+ // Draw triangle.
+ cr->move_to(-5 * d->_device_scale, 8 * d->_device_scale);
+ cr->line_to( 0, 18 * d->_device_scale);
+ cr->line_to( 5 * d->_device_scale, 8 * d->_device_scale);
+ cr->close_path();
+
+ double b = (int)_hover_direction == (i + 1) ? 0.9 : 0.7;
+ cr->set_source_rgba(b, b, b, a);
+ cr->fill();
+
+ cr->restore();
+ }
+ cr->restore();
+ }
+
+ // Process bucketed events as soon as possible after draw. We cannot process them now, because we have
+ // a frame to get out as soon as possible, and processing events may take a while. Instead, we schedule
+ // it with a signal callback on the main loop that runs as soon as this function is completed.
+ if (!d->eventprocessor->events.empty()) d->schedule_bucket_emptier();
+
+ // Record the fact that a draw is no longer pending.
+ d->pending_draw = false;
+
+ // Notify the update strategy that another frame has passed.
+ d->updater->frame();
+
+ // Just-for-1.2 flicker "prevention": save the last offset the store was drawn at outside of decoupled mode,
+ // so we can continue to draw a static snapshot upon next going into decoupled mode.
+ if (!d->decoupled_mode) {
+ d->_snapshot_static_offset = d->_store_rect.min() - _pos;
+ }
+
+ return true;
+}
+
+// Sets clip path for Split and X-Ray modes.
+void
+Canvas::add_clippath(const Cairo::RefPtr<Cairo::Context>& cr)
+{
+ double width = get_allocation().get_width();
+ double height = get_allocation().get_height();
+ double sx = _split_position.x();
+ double sy = _split_position.y();
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ // We're clipping the outline region... so it's backwards.
+ switch (_split_direction) {
+ case Inkscape::SplitDirection::SOUTH:
+ cr->rectangle(0, 0, width, sy);
+ break;
+ case Inkscape::SplitDirection::NORTH:
+ cr->rectangle(0, sy, width, height - sy);
+ break;
+ case Inkscape::SplitDirection::EAST:
+ cr->rectangle(0, 0, sx, height );
+ break;
+ case Inkscape::SplitDirection::WEST:
+ cr->rectangle(sx, 0, width - sx, height );
+ break;
+ default:
+ // no clipping (for NONE, HORIZONTAL, VERTICAL)
+ break;
+ }
+ } else {
+ cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI);
+ }
+
+ cr->clip();
+}
+
+void
+CanvasPrivate::add_idle()
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ // We can safely discard events until active, because we will run add_idle on activation later in initialisation.
+ return;
+ }
+
+ if (!hipri_idle.connected()) {
+ hipri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_hipri_idle), G_PRIORITY_HIGH_IDLE + 15); // after resize, before draw
+ }
+
+ if (!lopri_idle.connected()) {
+ lopri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_lopri_idle), G_PRIORITY_DEFAULT_IDLE);
+ }
+
+ idle_running = true;
+}
+
+auto
+distSq(const Geom::IntPoint pt, const Geom::IntRect &rect)
+{
+ auto v = rect.clamp(pt) - pt;
+ return v.x() * v.x() + v.y() * v.y();
+}
+
+auto
+calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) {
+ auto c = a.inverse() * b;
+ return std::abs(c[0] - 1) + std::abs(c[1]) + std::abs(c[2]) + std::abs(c[3] - 1);
+}
+
+// Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.)
+auto
+coarsen(const Cairo::RefPtr<Cairo::Region> &region, int min_size, int glue_size, double min_fullness)
+{
+ // Sort the rects by minExtent.
+ struct Compare
+ {
+ bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const {
+ return a.minExtent() < b.minExtent();
+ }
+ };
+ std::multiset<Geom::IntRect, Compare> rects;
+ int nrects = region->get_num_rectangles();
+ for (int i = 0; i < nrects; i++) {
+ rects.emplace(cairo_to_geom(region->get_rectangle(i)));
+ }
+
+ // List of processed rectangles.
+ std::vector<Geom::IntRect> processed;
+ processed.reserve(nrects);
+
+ // Removal lists.
+ std::vector<decltype(rects)::iterator> remove_rects;
+ std::vector<int> remove_processed;
+
+ // Repeatedly expand small rectangles by absorbing their nearby small rectangles.
+ while (!rects.empty() && rects.begin()->minExtent() < min_size) {
+ // Extract the smallest unprocessed rectangle.
+ auto rect = *rects.begin();
+ rects.erase(rects.begin());
+
+ // Initialise the effective glue size.
+ int effective_glue_size = glue_size;
+
+ while (true) {
+ // Find the glue zone.
+ auto glue_zone = rect;
+ glue_zone.expandBy(effective_glue_size);
+
+ // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast.
+ auto newrect = rect;
+ int absorbed_area = 0;
+
+ remove_rects.clear();
+ for (auto it = rects.begin(); it != rects.end(); ++it) {
+ if (glue_zone.contains(*it)) {
+ newrect.unionWith(*it);
+ absorbed_area += it->area();
+ remove_rects.emplace_back(it);
+ }
+ }
+
+ remove_processed.clear();
+ for (int i = 0; i < processed.size(); i++) {
+ auto &r = processed[i];
+ if (glue_zone.contains(r)) {
+ newrect.unionWith(r);
+ absorbed_area += r.area();
+ remove_processed.emplace_back(i);
+ }
+ }
+
+ // If the result was too empty, try again with a smaller glue size.
+ double fullness = (double)(rect.area() + absorbed_area) / newrect.area();
+ if (fullness < min_fullness) {
+ effective_glue_size /= 2;
+ continue;
+ }
+
+ // Commit the change.
+ rect = newrect;
+
+ for (auto &it : remove_rects) {
+ rects.erase(it);
+ }
+
+ for (int j = (int)remove_processed.size() - 1; j >= 0; j--) {
+ int i = remove_processed[j];
+ processed[i] = processed.back();
+ processed.pop_back();
+ }
+
+ // Stop growing if not changed or now big enough.
+ bool finished = absorbed_area == 0 || rect.minExtent() >= min_size;
+ if (finished) {
+ break;
+ }
+
+ // Otherwise, continue normally.
+ effective_glue_size = glue_size;
+ }
+
+ // Put the finished rectangle in processed.
+ processed.emplace_back(rect);
+ }
+
+ // Put any remaining rectangles in processed.
+ for (auto &rect : rects) {
+ processed.emplace_back(rect);
+ }
+
+ return processed;
+}
+
+std::optional<Geom::Dim2>
+CanvasPrivate::old_bisector(const Geom::IntRect &rect)
+{
+ int bw = rect.width();
+ int bh = rect.height();
+
+ /*
+ * Determine redraw strategy:
+ *
+ * bw < bh (strips mode): Draw horizontal strips starting from cursor position.
+ * Seems to be faster for drawing many smaller objects zoomed out.
+ *
+ * bw > hb (chunks mode): Splits across the larger dimension of the rectangle, painting
+ * in almost square chunks (from the cursor.
+ * Seems to be faster for drawing a few blurred objects across the entire screen.
+ * Seems to be somewhat psychologically faster.
+ *
+ * Default is for strips mode.
+ */
+
+ int max_pixels;
+ if (q->_render_mode != Inkscape::RenderMode::OUTLINE) {
+ // Can't be too small or large gradient will be rerendered too many times!
+ max_pixels = 65536 * prefs.tile_multiplier;
+ } else {
+ // Paths only. 1M is catched buffer and we need four channels.
+ max_pixels = 262144;
+ }
+
+ if (bw * bh > max_pixels) {
+ if (bw < bh || bh < 2 * prefs.tile_size) {
+ return Geom::X;
+ } else {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+std::optional<Geom::Dim2>
+CanvasPrivate::new_bisector(const Geom::IntRect &rect)
+{
+ int bw = rect.width();
+ int bh = rect.height();
+
+ // Chop in half along the bigger dimension if the bigger dimension is too big.
+ if (bw > bh) {
+ if (bw > prefs.new_bisector_size) {
+ return Geom::X;
+ }
+ } else {
+ if (bh > prefs.new_bisector_size) {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+bool
+CanvasPrivate::on_hipri_idle()
+{
+ assert(active);
+ if (idle_running) {
+ idle_running = on_idle();
+ }
+ return false;
+}
+
+bool
+CanvasPrivate::on_lopri_idle()
+{
+ assert(active);
+ if (idle_running) {
+ idle_running = on_idle();
+ }
+ return idle_running;
+}
+
+bool
+CanvasPrivate::on_idle()
+{
+ framecheck_whole_function(this)
+
+ assert(q->_canvas_item_root);
+
+ // Quit idle process if not supposed to be drawing.
+ if (!q->_drawing || q->_drawing_disabled) {
+ return false;
+ }
+
+ const Geom::IntPoint pad(prefs.pad, prefs.pad);
+ auto recreate_store = [&, this] {
+ // Recreate the store at the current affine so that it covers the visible region.
+ _store_rect = q->get_area_world();
+ _store_rect.expandBy(pad);
+ Geom::IntRect expanded = _store_rect;
+ Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2);
+ expanded.expandBy(expansion);
+ q->_drawing->setCacheLimit(expanded);
+ _store_affine = q->_affine;
+ int desired_width = _store_rect.width() * _device_scale;
+ int desired_height = _store_rect.height() * _device_scale;
+ if (!_backing_store || _backing_store->get_width() != desired_width || _backing_store->get_height() != desired_height) {
+ _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height);
+ cairo_surface_set_device_scale(_backing_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ }
+ auto cr = Cairo::Context::create(_backing_store);
+ if (solid_background) {
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(q->_background);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ }
+ cr->paint();
+ if (need_outline_store()) {
+ if (!_outline_store || _outline_store->get_width() != desired_width || _outline_store->get_height() != desired_height) {
+ _outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height);
+ cairo_surface_set_device_scale(_outline_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ }
+ auto cr = Cairo::Context::create(_outline_store);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ updater->reset();
+ if (prefs.debug_show_unclean) q->queue_draw();
+ };
+
+ // Determine whether the rendering parameters have changed, and reset if so.
+ if (!_backing_store || (need_outline_store() && !_outline_store) || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) {
+ _device_scale = q->get_scale_factor();
+ _store_solid_background = solid_background;
+ recreate_store();
+ decoupled_mode = false;
+ if (prefs.debug_logging) std::cout << "Full reset" << std::endl;
+ }
+
+ // Make sure to clear the outline store when not in use, so we don't accidentally re-use it when it is required again.
+ if (!need_outline_store()) {
+ _outline_store.clear();
+ }
+
+ auto shift_store = [&, this] {
+ // Recreate the store, but keep re-usable content from the old store.
+ auto store_rect = q->get_area_world();
+ store_rect.expandBy(pad);
+ Geom::IntRect expanded = _store_rect;
+ Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2);
+ expanded.expandBy(expansion);
+ q->_drawing->setCacheLimit(expanded);
+ auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale);
+ cairo_surface_set_device_scale(backing_store->cobj(), _device_scale, _device_scale); // No C++ API!
+
+ // Determine the geometry of the shift.
+ auto shift = store_rect.min() - _store_rect.min();
+ auto reuse_rect = store_rect & _store_rect;
+ assert(reuse_rect); // Should not be called if there is no overlap.
+ auto cr = Cairo::Context::create(backing_store);
+
+ // Paint background into region not covered by next operation.
+ if (solid_background) {
+ auto reg = Cairo::Region::create(geom_to_cairo(store_rect));
+ reg->subtract(geom_to_cairo(*reuse_rect));
+ reg->translate(-store_rect.left(), -store_rect.top());
+ cr->save();
+ if (solid_background) {
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(q->_background);
+ }
+ region_to_path(cr, reg);
+ cr->fill();
+ cr->restore();
+ }
+
+ // Copy re-usuable contents of old store into new store, shifted.
+ cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(_backing_store, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+
+ // Set the result as the new backing store.
+ _store_rect = store_rect;
+ assert(_store_affine == q->_affine); // Should not be called if the affine has changed.
+ _backing_store = std::move(backing_store);
+
+ // Do the same for the outline store
+ if (_outline_store) {
+ auto outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale);
+ cairo_surface_set_device_scale(outline_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ auto cr = Cairo::Context::create(outline_store);
+ cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(_outline_store, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+ _outline_store = std::move(outline_store);
+ }
+
+ updater->intersect(_store_rect);
+ if (prefs.debug_show_unclean) q->queue_draw();
+ };
+
+ auto take_snapshot = [&, this] {
+ // Copy the backing store to the snapshot, leaving us temporarily in an invalid state.
+ std::swap(_snapshot_store, _backing_store); // This will re-use the old snapshot store later if possible.
+ _snapshot_rect = _store_rect;
+ _snapshot_affine = _store_affine;
+ _snapshot_clean_region = updater->clean_region->copy();
+
+ // Do the same for the outline store
+ std::swap(_snapshot_outline_store, _outline_store);
+
+ // Recreate the backing store, making the state valid again.
+ recreate_store();
+ };
+
+ // Handle transitions and actions in response to viewport changes.
+ if (!decoupled_mode) {
+ // Enter decoupled mode if the affine has changed from what the backing store was drawn at.
+ if (q->_affine != _store_affine) {
+ // Snapshot and reset the backing store.
+ take_snapshot();
+
+ // Enter decoupled mode.
+ if (prefs.debug_logging) std::cout << "Entering decoupled mode" << std::endl;
+ decoupled_mode = true;
+
+ // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving
+ // it again performs exactly the same rendering operations as if we had not gone into it at all. Also, no extra copies
+ // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost.
+ } else {
+ // Get visible rectangle in canvas coordinates.
+ auto const visible = q->get_area_world();
+ if (!_store_rect.intersects(visible)) {
+ // If the store has gone completely off-screen, recreate it.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Recreated store" << std::endl;
+ } else if (!_store_rect.contains(visible)) {
+ // If the store has gone partially off-screen, shift it.
+ shift_store();
+ if (prefs.debug_logging) std::cout << "Shifted store" << std::endl;
+ }
+ // After these operations, the store should now be fully on-screen.
+ assert(_store_rect.contains(visible));
+ }
+ } else { // if (decoupled_mode)
+ // Completely cancel the previous redraw and start again if the viewing parameters have changed too much.
+ if (!prefs.debug_sticky_decoupled) {
+ auto pl = Geom::Parallelogram(q->get_area_world());
+ pl *= _store_affine * q->_affine.inverse();
+ if (!pl.intersects(_store_rect)) {
+ // Store has gone off the screen.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Restarting redraw (store off-screen)" << std::endl;
+ } else {
+ auto diff = calc_affine_diff(q->_affine, _store_affine);
+ if (diff > prefs.max_affine_diff) {
+ // Affine has changed too much.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl;
+ }
+ }
+ }
+ }
+
+ // Assert that _clean_region is a subregion of _store_rect.
+ #ifndef NDEBUG
+ auto tmp = updater->clean_region->copy();
+ tmp->subtract(geom_to_cairo(_store_rect));
+ assert(tmp->empty());
+ #endif
+
+ // Ensure the geometry is up-to-date and in the right place.
+ auto affine = decoupled_mode ? _store_affine : q->_affine;
+ if (q->_need_update || geom_affine != affine) {
+ q->_canvas_item_root->update(affine);
+ geom_affine = affine;
+ q->_need_update = false;
+ }
+
+ // If asked to, don't paint anything and instead halt the idle process.
+ if (prefs.debug_disable_redraw) {
+ return false;
+ }
+
+ // Get the subrectangle of store that is visible.
+ Geom::OptIntRect visible_rect;
+ if (!decoupled_mode) {
+ // By a previous assertion, this always lies within the store.
+ visible_rect = q->get_area_world();
+ } else {
+ // Get the window rectangle transformed into canvas space.
+ auto pl = Geom::Parallelogram(q->get_area_world());
+ pl *= _store_affine * q->_affine.inverse();
+
+ // Get its bounding box, rounded outwards.
+ auto b = pl.bounds();
+ auto bi = Geom::IntRect(b.min().floor(), b.max().ceil());
+
+ // The visible rect is the intersection of this with the store
+ visible_rect = bi & _store_rect;
+ }
+ // The visible rectangle must be a subrectangle of store.
+ assert(_store_rect.contains(visible_rect));
+
+ // Get the mouse position in screen space.
+ Geom::IntPoint mouse_loc = (last_mouse ? *last_mouse : Geom::Point(q->get_dimensions()) / 2).round();
+
+ // Map the mouse to canvas space.
+ mouse_loc += q->_pos;
+ if (decoupled_mode) {
+ mouse_loc = geom_act(_store_affine * q->_affine.inverse(), mouse_loc);
+ }
+
+ // Begin processing redraws.
+ auto start_time = g_get_monotonic_time();
+ while (true) {
+ // Get the clean region for the next redraw as reported by the updater.
+ auto clean_region = updater->get_next_clean_region();
+
+ // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store).
+ Cairo::RefPtr<Cairo::Region> paint_region;
+ if (visible_rect) {
+ paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect));
+ paint_region->subtract(clean_region);
+ } else {
+ paint_region = Cairo::Region::create();
+ }
+
+ Geom::OptIntRect dragged = Geom::OptIntRect();
+ if (q->_grabbed_canvas_item) {
+ dragged = q->_grabbed_canvas_item->get_bounds().roundOutwards();
+ if (dragged) {
+ (*dragged).expandBy(prefs.pad);
+ dragged = dragged & visible_rect;
+ if (dragged) {
+ paint_region->subtract(geom_to_cairo(*dragged));
+ }
+ }
+ }
+ // Get the list of rectangles to paint, coarsened to avoid fragmentation.
+ auto rects = coarsen(paint_region,
+ std::min<int>(prefs.coarsener_min_size, prefs.new_bisector_size / 2),
+ std::min<int>(prefs.coarsener_glue_size, prefs.new_bisector_size / 2),
+ prefs.coarsener_min_fullness);
+ if (dragged) {
+ // this become the first after look for cursor
+ rects.push_back(*dragged);
+ }
+ // Ensure that all the rectangles lie within the visible rect (and therefore within the store).
+ #ifndef NDEBUG
+ for (auto &rect : rects) {
+ assert(visible_rect.contains(rect));
+ }
+ #endif
+
+ // Put the rectangles into a heap sorted by distance from mouse.
+ auto cmp = [&] (const Geom::IntRect &a, const Geom::IntRect &b) {
+ return distSq(mouse_loc, a) > distSq(mouse_loc, b);
+ };
+ std::make_heap(rects.begin(), rects.end(), cmp);
+
+ // Process rectangles until none left or timed out.
+ bool start = true;
+ while (!rects.empty()) {
+ // Extract the closest rectangle to the mouse.
+ std::pop_heap(rects.begin(), rects.end(), cmp);
+ auto rect = rects.back();
+ rects.pop_back();
+
+ // Cull empty rectangles.
+ if (rect.width() == 0 || rect.height() == 0) {
+ start = false;
+ continue;
+ }
+
+ // Cull rectangles that lie entirely inside the clean region.
+ // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.)
+ if (clean_region->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) {
+ start = false;
+ continue;
+ }
+
+ // Lambda to add a rectangle to the heap.
+ auto add_rect = [&] (const Geom::IntRect &rect) {
+ rects.emplace_back(rect);
+ std::push_heap(rects.begin(), rects.end(), cmp);
+ };
+
+ // If the rectangle needs bisecting, bisect it and put it back on the heap.
+ auto axis = prefs.use_new_bisector ? new_bisector(rect) : old_bisector(rect);
+ if (axis && !(dragged && start)) {
+ int mid = rect[*axis].middle();
+ auto lo = rect; lo[*axis].setMax(mid); add_rect(lo);
+ auto hi = rect; hi[*axis].setMin(mid); add_rect(hi);
+ start = false;
+ continue;
+ }
+ start = false;
+ // Paint the rectangle.
+ paint_rect_internal(rect);
+
+ // Check for timeout.
+ auto now = g_get_monotonic_time();
+ auto elapsed = now - start_time;
+ bool fixchoping = false;
+ #ifdef _WIN32
+ fixchoping = true;
+ #elif defined(__APPLE__)
+ fixchoping = true;
+ #endif
+ if (elapsed > ((fixchoping && prefs.render_time_limit == 1000) ? 80000 : prefs.render_time_limit)) {
+ // Timed out. Temporarily return to GTK main loop, and come back here when next idle.
+ if (prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - start_time << " us" << std::endl;
+ framecheckobj.subtype = 1;
+ return true;
+ }
+ }
+
+ // Report the redraw as finished. Exit if there's no more redraws to process.
+ bool keep_going = updater->report_finished();
+ if (!keep_going) break;
+ }
+
+ // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine.
+ if (decoupled_mode) {
+ if (prefs.debug_sticky_decoupled) {
+ // Debug feature: quit idle process, but stay in decoupled mode.
+ return false;
+ } else if (_store_affine == q->_affine) {
+ // Content is rendered at the correct affine - exit decoupled mode and quit idle process.
+ if (prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl;
+ // Exit decoupled mode.
+ decoupled_mode = false;
+ // Quit idle process.
+ return false;
+ } else {
+ // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine.
+ if (prefs.debug_logging) std::cout << "Scheduling final redraw" << std::endl;
+ // Snapshot and reset the backing store.
+ take_snapshot();
+ // Continue idle process.
+ return true;
+ }
+ } else {
+ // All done, quit the idle process.
+ framecheckobj.subtype = 3;
+ return false;
+ }
+}
+
+void
+CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect)
+{
+ // Paint the rectangle.
+ q->_drawing->setColorMode(q->_color_mode);
+ paint_single_buffer(rect, _backing_store, true, false);
+
+ if (_outline_store) {
+ q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE);
+ paint_single_buffer(rect, _outline_store, false, q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY);
+ q->_drawing->setRenderMode(q->_render_mode); // Leave the drawing in the requested render mode.
+ }
+
+ // Introduce an artificial delay for each rectangle.
+ if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time);
+
+ // Mark the rectangle as clean.
+ updater->mark_clean(rect);
+
+ // Mark the screen dirty.
+ if (!decoupled_mode) {
+ // Get rectangle needing repaint
+ auto repaint_rect = rect - q->_pos;
+
+ // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles)
+ auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height());
+ assert(repaint_rect & screen_rect);
+
+ // Schedule repaint
+ queue_draw_area(repaint_rect); // Guarantees on_draw will be called in the future.
+ if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();}
+ pending_draw = true;
+ } else {
+ // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards)
+ auto pl = Geom::Parallelogram(rect);
+ pl *= q->_affine * _store_affine.inverse();
+ pl *= Geom::Translate(-q->_pos);
+ auto b = pl.bounds();
+ auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil());
+
+ // Check if repaint is necessary - some rectangles could be entirely off-screen.
+ auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height());
+ if (repaint_rect & screen_rect) {
+ // Schedule repaint
+ queue_draw_area(repaint_rect);
+ if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();}
+ pending_draw = true;
+ }
+ }
+}
+
+void
+CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo::RefPtr<Cairo::ImageSurface> &store, bool is_backing_store, bool outline_overlay_pass)
+{
+ // Make sure the following code does not go outside of store's data.
+ assert(store);
+ assert(store->get_format() == Cairo::FORMAT_ARGB32);
+ assert(_store_rect.contains(paint_rect)); // FIXME: Observed to fail once when hitting Ctrl+O while Canvas was busy. Haven't managed to reproduce it. Doesn't mean it's fixed.
+
+ // Create temporary surface that draws directly to store.
+ store->flush();
+ unsigned char *data = store->get_data();
+ int stride = store->get_stride();
+
+ // Check we are using the correct device scale.
+ double x_scale = 1.0;
+ double y_scale = 1.0;
+ cairo_surface_get_device_scale(store->cobj(), &x_scale, &y_scale); // No C++ API!
+ assert (_device_scale == (int) x_scale);
+ assert (_device_scale == (int) y_scale);
+
+ // Move to the correct row.
+ data += stride * (paint_rect.top() - _store_rect.top()) * (int)y_scale;
+ // Move to the correct column.
+ data += 4 * (paint_rect.left() - _store_rect.left()) * (int)x_scale;
+ auto imgs = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32,
+ paint_rect.width() * _device_scale,
+ paint_rect.height() * _device_scale,
+ stride);
+
+ cairo_surface_set_device_scale(imgs->cobj(), _device_scale, _device_scale); // No C++ API!
+
+ auto cr = Cairo::Context::create(imgs);
+
+ // Clear background
+ cr->save();
+ if (is_backing_store && solid_background) {
+ cr->set_source(q->_background);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ }
+ cr->paint();
+ cr->restore();
+
+ // Render drawing on top of background.
+ if (q->_canvas_item_root->is_visible()) {
+ auto buf = Inkscape::CanvasItemBuffer{ paint_rect, _device_scale, outline_overlay_pass, cr };
+ q->_canvas_item_root->render(&buf);
+ }
+
+ // Paint over newly drawn content with a translucent random colour.
+ if (prefs.debug_show_redraw) {
+ cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->rectangle(0, 0, imgs->get_width(), imgs->get_height());
+ cr->fill();
+ }
+
+ if (q->_cms_active) {
+ auto transf = prefs.from_display
+ ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key)
+ : Inkscape::CMSSystem::getDisplayTransform();
+
+ if (transf) {
+ imgs->flush();
+ auto px = imgs->get_data();
+ int stride = imgs->get_stride();
+ for (int i = 0; i < paint_rect.height(); i++) {
+ auto row = px + i * stride;
+ Inkscape::CMSSystem::doTransform(transf, row, row, paint_rect.width());
+ }
+ imgs->mark_dirty();
+ }
+ }
+
+ store->mark_dirty();
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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:fileencoding=utf-8:textwidth=99 :