// SPDX-License-Identifier: GPL-2.0-or-later /* * Authors: * Tavmjong Bah * PBS * * Copyright (C) 2022 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include // Logging #include // Sort #include // Coarsener #include #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; // 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 struct Pref {}; template struct PrefBase { const char *path; T t, def; std::unique_ptr obs; std::function 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*>(this)->read(); act(); obs = Inkscape::Preferences::get()->createObserver(path, [this] (const Preferences::Entry &e) {t = static_cast*>(this)->changed(e); act();});} void disable() {t = def; act(); obs.reset();} void set_enabled(bool enabled) {enabled ? enable() : disable();} }; template<> struct Pref : PrefBase { 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 : PrefBase { 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 : PrefBase { 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 tile_size = Pref ("/options/rendering/tile-size", 16, 1, 10000); Pref tile_multiplier = Pref ("/options/rendering/tile-multiplier", 16, 1, 512); Pref x_ray_radius = Pref ("/options/rendering/xray-radius", 100, 1, 1500); Pref from_display = Pref ("/options/displayprofile/from_display"); Pref grabsize = Pref ("/options/grabsize/value", 3, 1, 15); Pref outline_overlay_opacity = Pref ("/options/rendering/outline-overlay-opacity", 50, 1, 100); // New parameters Pref update_strategy = Pref ("/options/rendering/update_strategy", 3, 1, 3); Pref render_time_limit = Pref ("/options/rendering/render_time_limit", 1000, 100, 1000000); Pref use_new_bisector = Pref ("/options/rendering/use_new_bisector", true); Pref new_bisector_size = Pref ("/options/rendering/new_bisector_size", 500, 1, 10000); Pref max_affine_diff = Pref("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0); Pref pad = Pref ("/options/rendering/pad", 200, 0, 1000); Pref coarsener_min_size = Pref ("/options/rendering/coarsener_min_size", 200, 0, 1000); Pref coarsener_glue_size = Pref ("/options/rendering/coarsener_glue_size", 80, 0, 1000); Pref coarsener_min_fullness = Pref("/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0); // Debug switches Pref debug_framecheck = Pref ("/options/rendering/debug_framecheck"); Pref debug_logging = Pref ("/options/rendering/debug_logging"); Pref debug_slow_redraw = Pref ("/options/rendering/debug_slow_redraw"); Pref debug_slow_redraw_time = Pref ("/options/rendering/debug_slow_redraw_time", 50, 0, 1000000); Pref debug_show_redraw = Pref ("/options/rendering/debug_show_redraw"); Pref debug_show_unclean = Pref ("/options/rendering/debug_show_unclean"); Pref debug_show_snapshot = Pref ("/options/rendering/debug_show_snapshot"); Pref debug_show_clean = Pref ("/options/rendering/debug_show_clean"); Pref debug_disable_redraw = Pref ("/options/rendering/debug_disable_redraw"); Pref debug_sticky_decoupled = Pref ("/options/rendering/debug_sticky_decoupled"); // Developer mode Pref devmode = Pref("/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 &cr, const Cairo::RefPtr ®) { 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 clean_region; Updater(Cairo::RefPtr 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 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 old_clean_region; public: FullredrawUpdater(Cairo::RefPtr 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 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> blocked; // The region blocked from being updated at each scale. public: MultiscaleUpdater(Cairo::RefPtr 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 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 make_updater(int type, Cairo::RefPtr clean_region = Cairo::Region::create()) { switch (type) { case 1: return std::make_unique(std::move(clean_region)); case 2: return std::make_unique(std::move(clean_region)); default: case 3: return std::make_unique(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; // 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 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; // 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 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 _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 _snapshot_store, _snapshot_outline_store; Cairo::RefPtr _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 const &store, bool is_backing_store, bool outline_overlay_pass); std::optional old_bisector(const Geom::IntRect &rect); std::optional 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 last_mouse; }; /* * Lifecycle */ Canvas::Canvas() : d(std::make_unique(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(); 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(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(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(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(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(key_event)); } bool Canvas::on_key_release_event(GdkEventKey *key_event) { return d->add_to_bucket(reinterpret_cast(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(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::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(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&) { 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::min(); constexpr Geom::Coord max_int = std::numeric_limits::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(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(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 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 &store, const Cairo::RefPtr &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& 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 ®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 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 processed; processed.reserve(nrects); // Removal lists. std::vector remove_rects; std::vector 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 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 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 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(prefs.coarsener_min_size, prefs.new_bisector_size / 2), std::min(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 &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 :