diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/widget/canvas.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/ui/widget/canvas.cpp | 2797 |
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> ®) +{ + 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 ® : 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> ®ion, 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 : |