diff --git a/src/desktop.cpp b/src/desktop.cpp index 524bb38917fd399e697d77f2e9e0886288311518..56778dafd64eec1d5cf258ddd9c5f5a03b1944e0 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -298,7 +298,6 @@ void SPDesktop::destroy() namedview->hide(this); _sel_changed_connection.disconnect(); - _commit_connection.disconnect(); _reconstruction_start_connection.disconnect(); _reconstruction_finish_connection.disconnect(); @@ -581,9 +580,8 @@ SPDesktop::set_display_area (bool log) // Scroll Geom::Point offset = _current_affine.getOffset(); - canvas->scroll_to(offset, true); + canvas->scroll_to(offset); canvas->set_affine(_current_affine.d2w()); // For CanvasItem's. - // To do: if transform unchanged call with 'false' (redraw only newly exposed areas). /* Update perspective lines if we are in the 3D box tool (so that infinite ones are shown * correctly) */ @@ -625,7 +623,7 @@ void SPDesktop::set_display_area( Geom::Rect const &r, double border, bool log) { // Create a rectangle the size of the window aligned with origin. - Geom::Rect w( Geom::Point(), canvas->get_area_world().dimensions() ); + Geom::Rect w( Geom::Point(), canvas->get_dimensions() ); // Shrink window to account for border padding. w.expandBy( -border ); @@ -651,10 +649,10 @@ SPDesktop::set_display_area( Geom::Rect const &r, double border, bool log) /** * Return canvas viewbox in desktop coordinates */ -Geom::Parallelogram SPDesktop::get_display_area(bool use_integer_viewbox) const +Geom::Parallelogram SPDesktop::get_display_area() const { // viewbox in world coordinates - Geom::Rect const viewbox = use_integer_viewbox ? canvas->get_area_world_int() : canvas->get_area_world(); + Geom::Rect const viewbox = canvas->get_area_world(); // display area in desktop coordinates return Geom::Parallelogram(viewbox) * w2d(); @@ -769,7 +767,7 @@ SPDesktop::zoom_selection() } Geom::Point SPDesktop::current_center() const { - return canvas->get_area_world().midpoint() * _current_affine.w2d(); + return Geom::Rect(canvas->get_area_world()).midpoint() * _current_affine.w2d(); } /** @@ -895,7 +893,6 @@ SPDesktop::rotate_relative_center_point (Geom::Point const &c, double rotate) set_display_area(c, viewbox.midpoint()); } - /** * Set new flip direction, keeping the point 'c' fixed in the desktop window. * @@ -968,7 +965,7 @@ SPDesktop::is_flipped (CanvasFlip flip) void SPDesktop::scroll_absolute (Geom::Point const &point, bool is_scrolling) { - canvas->scroll_to(point, false); + canvas->scroll_to(point); _current_affine.setOffset( point ); /* update perspective lines if we are in the 3D box tool (so that infinite ones are shown correctly) */ @@ -1285,11 +1282,6 @@ sigc::connection SPDesktop::connectToolSubselectionChangedEx(const sigc::slotredraw_now(); -} - void SPDesktop::updateDialogs() { getContainer()->set_inkscape_window(getInkscapeWindow()); @@ -1410,9 +1402,6 @@ SPDesktop::setDocument (SPDocument *doc) selection->setDocument(doc); - _commit_connection.disconnect(); - _commit_connection = doc->connectCommit(sigc::mem_fun(*this, &SPDesktop::updateNow)); - /// \todo fixme: This condition exists to make sure the code /// inside is NOT called on initialization, only on replacement. But there /// are surely more safe methods to accomplish this. diff --git a/src/desktop.h b/src/desktop.h index b8123c47406b19b021b83aff190a6cdb392c7216..2660162cc91d5edb0dc10b8f5a93aee0d3606143 100644 --- a/src/desktop.h +++ b/src/desktop.h @@ -354,7 +354,7 @@ public: void set_display_area (bool log = true); void set_display_area (Geom::Point const &c, Geom::Point const &w, bool log = true); void set_display_area (Geom::Rect const &a, Geom::Coord border, bool log = true); - Geom::Parallelogram get_display_area(bool use_integer_viewbox = false) const; + Geom::Parallelogram get_display_area() const; void set_display_width(Geom::Rect const &a, Geom::Coord border); void set_display_center(Geom::Rect const &a); @@ -419,7 +419,6 @@ public: Gtk::Widget *get_toolbox() const; void setToolboxAdjustmentValue (gchar const* id, double val); bool isToolboxButtonActive (gchar const *id); - void updateNow(); void updateCanvasNow(); void updateDialogs(); void storeDesktopPosition(); @@ -614,7 +613,6 @@ private: sigc::connection _sel_changed_connection; sigc::connection _reconstruction_start_connection; sigc::connection _reconstruction_finish_connection; - sigc::connection _commit_connection; void onResized (double, double) override; void onRedrawRequested() override; diff --git a/src/display/control/canvas-item-buffer.h b/src/display/control/canvas-item-buffer.h index 0215b5175330a0238e3731e3119a78fa3e7fb2df..5bc5846edcf780796b8809353c364de05ecb5ff9 100644 --- a/src/display/control/canvas-item-buffer.h +++ b/src/display/control/canvas-item-buffer.h @@ -26,23 +26,10 @@ namespace Inkscape { /** * Class used when rendering canvas items. */ -class CanvasItemBuffer { -public: - CanvasItemBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, int device_scale) - : rect(paint_rect) - , canvas_rect(canvas_rect) - , device_scale(device_scale) - {} - ~CanvasItemBuffer() = default; - +struct CanvasItemBuffer { Geom::IntRect rect; - Geom::IntRect canvas_rect; // visible window in world coordinates (i.e. offset by _x0, _y0) int device_scale; // For high DPI monitors. - Cairo::RefPtr cr; - unsigned char *buf = nullptr; - int buf_rowstride = 0; - bool is_empty = true; }; } // Namespace Inkscape diff --git a/src/display/control/canvas-item-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp index 6bb301348772e587dfa4df9f7ddfdb2a7a326d73..f2696c8aae1a45c85aaa3104c100a6351c710256 100644 --- a/src/display/control/canvas-item-ctrl.cpp +++ b/src/display/control/canvas-item-ctrl.cpp @@ -320,7 +320,7 @@ void CanvasItemCtrl::render(Inkscape::CanvasItemBuffer *buf) unsigned char *pxb = work->get_data(); // this code allow background become isolated from rendering so we can do things like outline overlay - cairo_pattern_t *pattern = _canvas->get_background_store()->cobj(); + cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj(); guint32 backcolor = ink_cairo_pattern_get_argb32(pattern); guint32 *p = _cache; for (int i = 0; i < height; ++i) { diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp index 96d6514ce04c0ecefde59ca6faf34398c43e77cc..c207f4088a4407a41fe396aed286966fff550ee1 100644 --- a/src/display/control/canvas-item-rect.cpp +++ b/src/display/control/canvas-item-rect.cpp @@ -199,7 +199,7 @@ void CanvasItemRect::render(Inkscape::CanvasItemBuffer *buf) buf->cr->restore(); } - cairo_pattern_t *pattern = _canvas->get_background_store()->cobj(); + cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj(); guint32 backcolor = ink_cairo_pattern_get_argb32(pattern); EXTRACT_ARGB32(backcolor, ab,rb,gb,bb) diff --git a/src/gradient-drag.cpp b/src/gradient-drag.cpp index 06f110a208da33ddd9579e99b3a0ddd852729705..7c353d8195f44a4576dc40e6a77a9c787551c2b0 100644 --- a/src/gradient-drag.cpp +++ b/src/gradient-drag.cpp @@ -1062,8 +1062,6 @@ static void gr_knot_mousedown_handler(SPKnot */*knot*/, unsigned int /*state*/, if (dragger_corner) { dragger_corner->highlightCorner(true); } - - dragger->parent->desktop->getCanvas()->forced_redraws_start(5); } /** @@ -1073,8 +1071,6 @@ static void gr_knot_ungrabbed_handler(SPKnot *knot, unsigned int state, gpointer { GrDragger *dragger = (GrDragger *) data; - dragger->parent->desktop->getCanvas()->forced_redraws_stop(); - dragger->point_original = dragger->point = knot->pos; if ((state & GDK_CONTROL_MASK) && (state & GDK_SHIFT_MASK)) { @@ -2822,7 +2818,7 @@ bool GrDrag::key_press_handler(GdkEvent *event) y_dir *= -desktop->yaxisdir(); - gint mul = 1 + Inkscape::UI::Tools::gobble_key_events(keyval, 0); // with any mask + gint mul = 1 + desktop->canvas->gobble_key_events(keyval, 0); // with any mask if (MOD__SHIFT(event)) { mul *= 10; diff --git a/src/object/sp-namedview.cpp b/src/object/sp-namedview.cpp index c20d6b983571a9dc7975365b3cde919b9b42b05e..06e15b9f8d64d98c69b5d934a364084c4d44e1f5 100644 --- a/src/object/sp-namedview.cpp +++ b/src/object/sp-namedview.cpp @@ -322,7 +322,6 @@ void SPNamedView::update(SPCtx *ctx, guint flags) } void SPNamedView::set(SPAttr key, const gchar* value) { - // Send page attributes to the page manager. if (this->_page_manager && this->_page_manager->subset(key, value)) { this->requestModified(SP_OBJECT_MODIFIED_FLAG); diff --git a/src/rubberband.cpp b/src/rubberband.cpp index f31aecc98aad9b2619a7861bc08017e5e50d74c2..2d03bf0701c3d0493be06652b539e5b6c04b68fe 100644 --- a/src/rubberband.cpp +++ b/src/rubberband.cpp @@ -57,7 +57,6 @@ void Inkscape::Rubberband::start(SPDesktop *d, Geom::Point const &p) _points.push_back(_desktop->d2w(p)); delete_canvas_items(); - _desktop->getCanvas()->forced_redraws_start(5); } void Inkscape::Rubberband::stop() @@ -69,10 +68,6 @@ void Inkscape::Rubberband::stop() _touchpath_curve->reset(); delete_canvas_items(); - - if (_desktop && _desktop->getCanvas()) { - _desktop->getCanvas()->forced_redraws_stop(); - } } void Inkscape::Rubberband::move(Geom::Point const &p) diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 07e80ce439b22f352afdb42b170e376158bd3d36..ec777713f71862bd2bbe376cb8fc12d4a7422746 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -190,6 +190,7 @@ set(ui_SRC widget/font-variants.cpp widget/font-variations.cpp widget/frame.cpp + widget/framecheck.cpp widget/gradient-image.cpp widget/gradient-editor.cpp widget/gradient-selector.cpp @@ -445,6 +446,7 @@ set(ui_SRC widget/font-variants.h widget/font-variations.h widget/frame.h + widget/framecheck.h widget/gradient-image.h widget/gradient-editor.h widget/gradient-selector.h diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 808adef394e2627426e72fe19dc76c6bc77af3de..b273b6f0c504de6e9e1e6ce53f00d8870ed1e7c9 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2639,11 +2639,9 @@ void InkscapePreferences::initPageBehavior() void InkscapePreferences::initPageRendering() { - /* threaded blur */ //related comments/widgets/functions should be renamed and option should be moved elsewhere when inkscape is fully multi-threaded _filter_multi_threaded.init("/options/threading/numthreads", 1.0, 8.0, 1.0, 2.0, 4.0, true, false); - _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, _("(requires restart)"), - _("Configure number of processors/threads to use when rendering filters"), false); + _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, _("(requires restart)"), _("Configure number of processors/threads to use when rendering filters"), false); // rendering cache _rendering_cache_size.init("/options/renderingcache/size", 0.0, 4096.0, 1.0, 32.0, 64.0, true, false); @@ -2651,32 +2649,22 @@ void InkscapePreferences::initPageRendering() // rendering tile multiplier _rendering_tile_multiplier.init("/options/rendering/tile-multiplier", 1.0, 512.0, 1.0, 16.0, 16.0, true, false); - _page_rendering.add_line( false, _("Rendering tile multiplier:"), _rendering_tile_multiplier, "", - _("On modern hardware, increasing this value (default is 16) can help to get a better performance when there are large areas with filtered objects (this includes blur and blend modes) in your drawing. Decrease the value to make zooming and panning in relevant areas faster on low-end hardware in drawings with few or no filters."), false); - // rendering xray radius + _page_rendering.add_line( false, _("Rendering tile multiplier:"), _rendering_tile_multiplier, "", _("On modern hardware, increasing this value (default is 16) can help to get a better performance when there are large areas with filtered objects (this includes blur and blend modes) in your drawing. Decrease the value to make zooming and panning in relevant areas faster on low-end hardware in drawings with few or no filters."), false); + + // rendering x-ray radius _rendering_xray_radius.init("/options/rendering/xray-radius", 1.0, 1500.0, 1.0, 100.0, 100.0, true, false); - _page_rendering.add_line( false, _("X-ray radius:"), _rendering_xray_radius, "", - _("Radius of the circular area around the mouse cursor in X-ray mode"), false); + _page_rendering.add_line( false, _("X-ray radius:"), _rendering_xray_radius, "", _("Radius of the circular area around the mouse cursor in X-ray mode"), false); - // rendering outline overlay opcaity + // rendering outline overlay opacity _rendering_outline_overlay_opacity.init("/options/rendering/outline-overlay-opacity", 1.0, 100.0, 1.0, 5.0, 50.0, true, false); _rendering_outline_overlay_opacity.signal_focus_out_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_outline_overlay_changed)); - _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), - _("Opacity of the color in outline overlay view mode"), false); - - { - // if these GTK constants ever change, consider adding a compatibility shim to SPCanvas::addIdle() - static_assert(G_PRIORITY_HIGH_IDLE == 100, "G_PRIORITY_HIGH_IDLE must be 100 to match preferences.xml"); - static_assert(G_PRIORITY_DEFAULT_IDLE == 200, "G_PRIORITY_DEFAULT_IDLE must be 200 to match preferences.xml"); - - Glib::ustring redrawPriorityLabels[] = {_("Responsive"), _("Conservative")}; - int redrawPriorityValues[] = {G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE}; + _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), _("Opacity of the color in outline overlay view mode"), false); - // redraw priority - _rendering_redraw_priority.init("/options/redrawpriority/value", redrawPriorityLabels, redrawPriorityValues, G_N_ELEMENTS(redrawPriorityLabels), 0); - _page_rendering.add_line(false, _("Redraw while editing:"), _rendering_redraw_priority, "", - _("Set how quickly the canvas display is updated while editing objects"), false); - } + // update strategy + int values[] = {1, 2, 3}; + Glib::ustring labels[] = {_("Responsive"), _("Full redraw"), _("Multiscale")}; + _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3); + _page_rendering.add_line(false, _("Update strategy:"), _canvas_update_strategy, "", _("How to update continually changing content when it can't be redrawn fast enough"), false); /* blur quality */ _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value", @@ -2731,6 +2719,93 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line(false, "", _cairo_dithering, "", _("Makes gradients smoother. This can significantly impact the size of generated PNG files. To update the display after changing this option, just zoom out/in.")); #endif + auto grid = Gtk::make_managed(); + grid->set_border_width(12); + grid->set_orientation(Gtk::ORIENTATION_VERTICAL); + grid->set_column_spacing(12); + grid->set_row_spacing(6); + auto revealer = Gtk::make_managed(); + revealer->add(*grid); + revealer->set_reveal_child(Inkscape::Preferences::get()->getBool("/options/rendering/devmode")); + _canvas_developer_mode_enabled.init(_("Enable developer mode"), "/options/rendering/devmode", false); + _canvas_developer_mode_enabled.signal_toggled().connect([revealer, this] {revealer->set_reveal_child(_canvas_developer_mode_enabled.get_active());}); + _page_rendering.add_group_header(_("Developer mode")); + _page_rendering.add_line(true, "", _canvas_developer_mode_enabled, "", _("Enable additional debugging options"), false); + _page_rendering.add(*revealer); + + auto add_devmode_line = [&] (Glib::ustring const &label, Gtk::Widget &widget, Glib::ustring const &suffix, const Glib::ustring &tip) { + widget.set_tooltip_text(tip); + + auto hb = Gtk::manage(new Gtk::Box()); + hb->set_spacing(12); + hb->set_hexpand(true); + hb->pack_start(widget, false, false); + hb->set_valign(Gtk::ALIGN_CENTER); + + auto label_widget = Gtk::make_managed(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + label_widget->set_mnemonic_widget(widget); + label_widget->set_markup(label_widget->get_text()); + label_widget->set_margin_start(12); + + label_widget->set_valign(Gtk::ALIGN_CENTER); + grid->add(*label_widget); + grid->attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1); + + if (suffix != "") { + auto suffix_widget = Gtk::make_managed(suffix, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + suffix_widget->set_markup(suffix_widget->get_text()); + hb->pack_start(*suffix_widget, false, false); + } + }; + + auto add_devmode_group_header = [&] (Glib::ustring name) { + auto label_widget = Gtk::make_managed(Glib::ustring(/*"*/"") + name + Glib::ustring(""/*"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true); + label_widget->set_use_markup(true); + label_widget->set_valign(Gtk::ALIGN_CENTER); + grid->add(*label_widget); + }; + + add_devmode_group_header(_("Low-level tuning options")); + _canvas_render_time_limit.init("/options/rendering/render_time_limit", 100.0, 1000000.0, 1.0, 0.0, 1000.0, true, false); + add_devmode_line(_("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice")); + _canvas_use_new_bisector.init("", "/options/rendering/use_new_bisector", true); + add_devmode_line(_("Use new bisector"), _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop in half along the larger dimension until small enough")); + _canvas_new_bisector_size.init("/options/rendering/new_bisector_size", 1.0, 10000.0, 1.0, 0.0, 500.0, true, false); + add_devmode_line(_("New bisector tile size"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Chop rectangles until largest dimension is this small")); + _rendering_tile_size.init("/options/rendering/tile-size", 1.0, 10000.0, 1.0, 0.0, 16.0, true, false); + add_devmode_line(_("Tile size:"), _rendering_tile_size, "", _("The \"tile size\" parameter previously hard-coded into Inkscape's original tile bisector.")); + _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false); + add_devmode_line(_("Max affine diff"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again")); + _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); + add_devmode_line(_("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount")); + _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); + add_devmode_line(_("Coarsener min size"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Only coarsen rectangles smaller/thinner than this.")); + _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false); + add_devmode_line(_("Coarsener glue size"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Absorb nearby rectangles within this distance.")); + _canvas_coarsener_min_fullness.init("/options/rendering/coarsener_min_fullness", 0.0, 1.0, 0.0, 0.0, 0.3, false, false); + add_devmode_line(_("Coarsener min fullness"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening attempt if result would be more empty than this.")); + + add_devmode_group_header(_("Debugging, profiling, and experiments")); + _canvas_debug_framecheck.init("", "/options/rendering/debug_framecheck", false); + add_devmode_line(_("Framecheck"), _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); + _canvas_debug_logging.init("", "/options/rendering/debug_logging", false); + add_devmode_line(_("Logging"), _canvas_debug_logging, "", _("Log certain events to the console")); + _canvas_debug_slow_redraw.init("", "/options/rendering/debug_slow_redraw", false); + add_devmode_line(_("Slow redraw"), _canvas_debug_slow_redraw, "", _("Introduce a fixed delay for each tile")); + _canvas_debug_slow_redraw_time.init("/options/rendering/debug_slow_redraw_time", 0.0, 1000000.0, 1.0, 0.0, 50.0, true, false); + add_devmode_line(_("Slow redraw time"), _canvas_debug_slow_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile")); + _canvas_debug_show_redraw.init("", "/options/rendering/debug_show_redraw", false); + add_devmode_line(_("Show redraw"), _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile")); + _canvas_debug_show_unclean.init("", "/options/rendering/debug_show_unclean", false); + add_devmode_line(_("Show unclean region"), _canvas_debug_show_unclean, "", _("Show the unclean region in red")); + _canvas_debug_show_snapshot.init("", "/options/rendering/debug_show_snapshot", false); + add_devmode_line(_("Show snapshot"), _canvas_debug_show_snapshot, "", _("Show the snapshot region in blue")); + _canvas_debug_show_clean.init("", "/options/rendering/debug_show_clean", false); + add_devmode_line(_("Show clean fragmentation"), _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the clean region in green")); + _canvas_debug_disable_redraw.init("", "/options/rendering/debug_disable_redraw", false); + add_devmode_line(_("Disable redraw"), _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely")); + _canvas_debug_sticky_decoupled.init("", "/options/rendering/debug_sticky_decoupled", false); + add_devmode_line(_("Sticky decoupled mode"), _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); } @@ -3035,7 +3110,7 @@ void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel event.keyval = accel_key; event.state = accel_mods; event.hardware_keycode = hardware_keycode; - Gtk::AccelKey const new_shortcut_key = shortcuts.get_from_event(&event, true); + Gtk::AccelKey const new_shortcut_key = shortcuts.get_from_event(&event, true); if (!new_shortcut_key.is_null() && (new_shortcut_key.get_key() != current_shortcut_key.get_key() || diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 79f3c3f72ac94b573ab8cb9b06e05c261bc2e10c..c9d6bb22afe301de73bd5db135af2228c1925ae0 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -331,6 +331,12 @@ protected: UI::Widget::PrefRadioButton _mask_grouping_all; UI::Widget::PrefCheckButton _mask_ungrouping; + UI::Widget::PrefSpinButton _filter_multi_threaded; + UI::Widget::PrefSpinButton _rendering_cache_size; + UI::Widget::PrefSpinButton _rendering_tile_multiplier; + UI::Widget::PrefSpinButton _rendering_xray_radius; + UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity; + UI::Widget::PrefCombo _canvas_update_strategy; UI::Widget::PrefRadioButton _blur_quality_best; UI::Widget::PrefRadioButton _blur_quality_better; UI::Widget::PrefRadioButton _blur_quality_normal; @@ -341,18 +347,31 @@ protected: UI::Widget::PrefRadioButton _filter_quality_normal; UI::Widget::PrefRadioButton _filter_quality_worse; UI::Widget::PrefRadioButton _filter_quality_worst; - UI::Widget::PrefCheckButton _show_filters_info_box; - UI::Widget::PrefCheckButton _rendering_image_outline; - UI::Widget::PrefSpinButton _rendering_cache_size; - UI::Widget::PrefSpinButton _rendering_tile_multiplier; - UI::Widget::PrefSpinButton _rendering_xray_radius; - UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity; - UI::Widget::PrefCombo _rendering_redraw_priority; - UI::Widget::PrefSpinButton _filter_multi_threaded; #ifdef CAIRO_HAS_DITHER UI::Widget::PrefCheckButton _cairo_dithering; #endif + UI::Widget::PrefCheckButton _canvas_developer_mode_enabled; + UI::Widget::PrefSpinButton _canvas_render_time_limit; + UI::Widget::PrefCheckButton _canvas_use_new_bisector; + UI::Widget::PrefSpinButton _canvas_new_bisector_size; + UI::Widget::PrefSpinButton _rendering_tile_size; + UI::Widget::PrefSpinButton _canvas_max_affine_diff; + UI::Widget::PrefSpinButton _canvas_pad; + UI::Widget::PrefSpinButton _canvas_coarsener_min_size; + UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; + UI::Widget::PrefSpinButton _canvas_coarsener_min_fullness; + UI::Widget::PrefCheckButton _canvas_debug_framecheck; + UI::Widget::PrefCheckButton _canvas_debug_logging; + UI::Widget::PrefCheckButton _canvas_debug_slow_redraw; + UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; + UI::Widget::PrefCheckButton _canvas_debug_show_redraw; + UI::Widget::PrefCheckButton _canvas_debug_show_unclean; + UI::Widget::PrefCheckButton _canvas_debug_show_snapshot; + UI::Widget::PrefCheckButton _canvas_debug_show_clean; + UI::Widget::PrefCheckButton _canvas_debug_disable_redraw; + UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled; + UI::Widget::PrefCheckButton _trans_scale_stroke; UI::Widget::PrefCheckButton _trans_scale_corner; UI::Widget::PrefCheckButton _trans_gradient; @@ -383,6 +402,7 @@ protected: UI::Widget::PrefSpinButton _importexport_export_res; UI::Widget::PrefSpinButton _importexport_import_res; UI::Widget::PrefCheckButton _importexport_import_res_override; + UI::Widget::PrefCheckButton _rendering_image_outline; UI::Widget::PrefSlider _snap_delay; UI::Widget::PrefSlider _snap_weight; UI::Widget::PrefSlider _snap_persistence; @@ -435,6 +455,7 @@ protected: UI::Widget::PrefCheckButton _ui_realworldzoom; UI::Widget::PrefCheckButton _ui_partialdynamic; UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction; + UI::Widget::PrefCheckButton _show_filters_info_box; UI::Widget::PrefCheckButton _ui_yaxisdown; UI::Widget::PrefCheckButton _ui_rotationlock; UI::Widget::PrefCheckButton _ui_cursorscaling; diff --git a/src/ui/knot/knot.cpp b/src/ui/knot/knot.cpp index 79371f32a81ed30be24c7a796f3395d624b2e577..f20bbcd931ae43f22fe2e30dbdb75d70c915812e 100644 --- a/src/ui/knot/knot.cpp +++ b/src/ui/knot/knot.cpp @@ -30,6 +30,7 @@ #include "display/control/canvas-item-ctrl.h" #include "ui/tools/tool-base.h" #include "ui/tools/node-tool.h" +#include "ui/widget/canvas.h" using Inkscape::DocumentUndo; @@ -349,7 +350,7 @@ void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot) { knot->desktop->set_coordinate_status(knot->pos); // display the coordinate of knot, not cursor - they may be different! if (event->motion.state & GDK_BUTTON1_MASK) { - Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); + knot->desktop->canvas->gobble_motion_events(GDK_BUTTON1_MASK); } } diff --git a/src/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp index 2ee5d617d99ad2ff5bdb6d2396c26f40f4e30f94..3073b6bec8a26976849f5b931134eabfcfcd878a 100644 --- a/src/ui/tool/control-point-selection.cpp +++ b/src/ui/tool/control-point-selection.cpp @@ -19,6 +19,7 @@ #include "ui/tool/transform-handle-set.h" #include "ui/tool/node.h" #include "display/control/snap-indicator.h" +#include "ui/widget/canvas.h" @@ -515,7 +516,7 @@ void ControlPointSelection::_updateTransformHandles(bool preserve_center) bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) { if (held_control(event)) return false; - unsigned num = 1 + combine_key_events(shortcut_key(event), 0); + unsigned num = 1 + _desktop->canvas->gobble_key_events(shortcut_key(event), 0); Geom::Point delta = dir * num; if (held_shift(event)) delta *= 10; diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp index 8a1763c446b3f7e0e2992fb1d668982463d12978..da6bdaf3a98f14814945b62c9db79c659d929d04 100644 --- a/src/ui/tool/control-point.cpp +++ b/src/ui/tool/control-point.cpp @@ -274,11 +274,7 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G _drag_origin = _position; transferred = grabbed(&event->motion); // _drag_initiated might change during the above virtual call - if (!_drag_initiated) { - // this guarantees smooth redraws while dragging - _desktop->getCanvas()->forced_redraws_start(5); - _drag_initiated = true; - } + _drag_initiated = true; } if (!transferred) { @@ -317,7 +313,6 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G if (_drag_initiated) { // it is the end of a drag - _desktop->getCanvas()->forced_redraws_stop(); _drag_initiated = false; ungrabbed(&event->button); return true; @@ -344,9 +339,6 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G if (_event_grab && !event->grab_broken.keyboard) { { ungrabbed(nullptr); - if (_drag_initiated) { - _desktop->getCanvas()->forced_redraws_stop(); - } } _setState(STATE_NORMAL); _event_grab = false; @@ -393,7 +385,6 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G _canvas_item_ctrl->ungrab(); _clearMouseover(); // this will also reset state to normal - _desktop->getCanvas()->forced_redraws_stop(); _event_grab = false; _drag_initiated = false; @@ -510,10 +501,7 @@ void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event) prev_point->_canvas_item_ctrl->ungrab(); _canvas_item_ctrl->grab(_grab_event_mask, nullptr); // cursor is null - if (!_drag_initiated) { - _desktop->getCanvas()->forced_redraws_start(5); - _drag_initiated = true; - } + _drag_initiated = true; prev_point->_setState(STATE_NORMAL); _setMouseover(this, event->state); diff --git a/src/ui/tool/event-utils.cpp b/src/ui/tool/event-utils.cpp index 5008c127f148f53b2e694f418901addbf10cd5b6..f131d4f57f751028bcdb7f1f949afb203db9e3c7 100644 --- a/src/ui/tool/event-utils.cpp +++ b/src/ui/tool/event-utils.cpp @@ -31,29 +31,6 @@ guint shortcut_key(GdkEventKey const &event) return shortcut_key; } -unsigned combine_key_events(guint keyval, gint mask) -{ - GdkEvent *event_next; - gint i = 0; - - event_next = gdk_event_get(); - // while the next event is also a key notify with the same keyval and mask, - while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type == GDK_KEY_RELEASE) - && event_next->key.keyval == keyval - && (!mask || event_next->key.state & mask)) { - if (event_next->type == GDK_KEY_PRESS) - i ++; - // kill it - gdk_event_free(event_next); - // get next - event_next = gdk_event_get(); - } - // otherwise, put it back onto the queue - if (event_next) gdk_event_put(event_next); - - return i; -} - /** Returns the modifier state valid after this event. Use this when you process events * that change the modifier state. Currently handles only Shift, Ctrl, Alt. */ unsigned state_after_event(GdkEvent *event) diff --git a/src/ui/tool/event-utils.h b/src/ui/tool/event-utils.h index 6b303c95595bcbec1be651f9f24f8ea682c2b139..37961e39e22a32eab3339da6fa4bff5b90ea5d1d 100644 --- a/src/ui/tool/event-utils.h +++ b/src/ui/tool/event-utils.h @@ -110,7 +110,6 @@ inline bool held_button(E const &event) { } guint shortcut_key(GdkEventKey const &event); -unsigned combine_key_events(guint keyval, gint mask); unsigned state_after_event(GdkEvent *event); } // namespace UI diff --git a/src/ui/toolbar/select-toolbar.cpp b/src/ui/toolbar/select-toolbar.cpp index 75a0e5f81dbd52a721391c36989b2948f2768edd..f086f77bce552f22a656bd26bed7ff93d3e65312 100644 --- a/src/ui/toolbar/select-toolbar.cpp +++ b/src/ui/toolbar/select-toolbar.cpp @@ -430,9 +430,6 @@ SelectToolbar::any_value_changed(Glib::RefPtr& adj) if (actionkey != nullptr) { - // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed - desktop->getCanvas()->forced_redraws_start(0); - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); bool transform_stroke = prefs->getBool("/options/transform/stroke", true); bool preserve = prefs->getBool("/options/preservetransform/value", false); @@ -450,9 +447,6 @@ SelectToolbar::any_value_changed(Glib::RefPtr& adj) selection->applyAffine(scaler); DocumentUndo::maybeDone(document, actionkey, _("Transform by toolbar"), INKSCAPE_ICON("tool-pointer")); - - // resume interruptibility - desktop->getCanvas()->forced_redraws_stop(); } _update = false; diff --git a/src/ui/tools/arc-tool.cpp b/src/ui/tools/arc-tool.cpp index f7c53e3668efa7b7ed9bf20360a09f01aa16d664..12143bffee53d76b0163161585239a3cc5f1a070 100644 --- a/src/ui/tools/arc-tool.cpp +++ b/src/ui/tools/arc-tool.cpp @@ -317,8 +317,6 @@ void ArcTool::drag(Geom::Point pt, guint state) { Inkscape::GC::release(repr); this->arc->transform = layer->i2doc_affine().inverse(); this->arc->updateRepr(); - - forced_redraws_start(5); } auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state); @@ -415,8 +413,6 @@ void ArcTool::finishItem() { this->arc->updateRepr(); this->arc->doWriteTransform(this->arc->transform, nullptr, true); - forced_redraws_stop(); - _desktop->getSelection()->set(this->arc); DocumentUndo::done(_desktop->getDocument(), _("Create ellipse"), INKSCAPE_ICON("draw-ellipse")); @@ -439,8 +435,6 @@ void ArcTool::cancel() { this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(_desktop->getDocument()); } diff --git a/src/ui/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp index ea28d70635d5def204c3c56a273b5bc3d27bf8cd..b9e25750b518a2322ebf04365a20f0416164c33e 100644 --- a/src/ui/tools/box3d-tool.cpp +++ b/src/ui/tools/box3d-tool.cpp @@ -40,7 +40,6 @@ #include "ui/icon-names.h" #include "ui/shape-editor.h" #include "ui/tools/box3d-tool.h" -#include "ui/widget/canvas.h" // Forced redraw #include "xml/node-event-vector.h" @@ -508,8 +507,6 @@ void Box3dTool::drag(guint /*state*/) { // TODO: It would be nice to show the VPs during dragging, but since there is no selection // at this point (only after finishing the box), we must do this "manually" /* this._vpdrag->updateDraggers(); */ - - forced_redraws_start(5); } g_assert(this->box3d); @@ -548,8 +545,6 @@ void Box3dTool::finishItem() { this->box3d->relabel_corners(); - forced_redraws_stop(); - _desktop->getSelection()->set(this->box3d); DocumentUndo::done(_desktop->getDocument(), _("Create 3D box"), INKSCAPE_ICON("draw-cuboid")); diff --git a/src/ui/tools/calligraphic-tool.cpp b/src/ui/tools/calligraphic-tool.cpp index 19fe5d4f4d885e8c1e3d7b10e4a94378c2273fdc..aced95d5e7c051f5681a8b8927ab917e194522a4 100644 --- a/src/ui/tools/calligraphic-tool.cpp +++ b/src/ui/tools/calligraphic-tool.cpp @@ -452,7 +452,6 @@ bool CalligraphicTool::root_handler(GdkEvent* event) { ret = TRUE; - forced_redraws_start(3); set_high_motion_precision(); this->is_drawing = true; this->just_started_drawing = true; @@ -731,7 +730,6 @@ bool CalligraphicTool::root_handler(GdkEvent* event) { ungrabCanvasEvents(); - forced_redraws_stop(); set_high_motion_precision(false); this->is_drawing = false; diff --git a/src/ui/tools/eraser-tool.cpp b/src/ui/tools/eraser-tool.cpp index c8438b41615e5578928c429c9ea2677486da3496..9c3298083ddc72dd99bfbe37f56f381de92b4a23 100644 --- a/src/ui/tools/eraser-tool.cpp +++ b/src/ui/tools/eraser-tool.cpp @@ -409,7 +409,6 @@ bool EraserTool::root_handler(GdkEvent* event) { ret = TRUE; - forced_redraws_start(3); this->is_drawing = true; } break; @@ -456,7 +455,6 @@ bool EraserTool::root_handler(GdkEvent* event) { ungrabCanvasEvents(); - forced_redraws_stop(); this->is_drawing = false; if (this->dragging && event->button.button == 1) { diff --git a/src/ui/tools/node-tool.cpp b/src/ui/tools/node-tool.cpp index 0a3e7835fd22462ab2c72cb65e8eeff3e87a77c7..549c05e33ecccdb1401d95ed3b8783e6ac179009 100644 --- a/src/ui/tools/node-tool.cpp +++ b/src/ui/tools/node-tool.cpp @@ -240,8 +240,6 @@ NodeTool::~NodeTool() delete data.outline_group; delete data.dragpoint_group; delete _transform_handle_group; - - forced_redraws_stop(); } void NodeTool::deleteSelected() @@ -434,8 +432,6 @@ bool NodeTool::root_handler(GdkEvent* event) { */ using namespace Inkscape::UI; // pull in event helpers - forced_redraws_start(5); - Inkscape::Selection *selection = _desktop->selection; static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); diff --git a/src/ui/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp index 2053ec2413e644b69b9c5df9917f318d454439ed..c7ced898acb0a0d2a5362be8e66ad3fa5361216e 100644 --- a/src/ui/tools/pen-tool.cpp +++ b/src/ui/tools/pen-tool.cpp @@ -156,8 +156,6 @@ void PenTool::_cancel() { cl1->hide(); this->message_context->clear(); this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); - - forced_redraws_stop(); } /** @@ -1193,8 +1191,6 @@ void PenTool::_setInitialPoint(Geom::Point const p) { this->p[1] = p; this->npoints = 2; this->red_bpath->set_bpath(nullptr); - - forced_redraws_start(5); } /** @@ -1941,8 +1937,6 @@ void PenTool::_finish(gboolean const closed) { this->green_anchor.reset(); - forced_redraws_stop(); - this->_enableEvents(); } diff --git a/src/ui/tools/rect-tool.cpp b/src/ui/tools/rect-tool.cpp index b6dda44d995e609b051b2f81a2146c396839bacf..cbbbbab1bd5222938c0e6669d24df207b70b2aa7 100644 --- a/src/ui/tools/rect-tool.cpp +++ b/src/ui/tools/rect-tool.cpp @@ -344,8 +344,6 @@ void RectTool::drag(Geom::Point const pt, guint state) { this->rect->transform = currentLayer()->i2doc_affine().inverse(); this->rect->updateRepr(); - - forced_redraws_start(5); } Geom::Rect const r = Inkscape::snap_rectangular_box(_desktop, this->rect, pt, this->center, state); @@ -426,8 +424,6 @@ void RectTool::finishItem() { this->rect->updateRepr(); this->rect->doWriteTransform(this->rect->transform, nullptr, true); - forced_redraws_stop(); - _desktop->getSelection()->set(this->rect); DocumentUndo::done(_desktop->getDocument(), _("Create rectangle"), INKSCAPE_ICON("draw-rectangle")); @@ -450,8 +446,6 @@ void RectTool::cancel(){ this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(_desktop->getDocument()); } diff --git a/src/ui/tools/select-tool.cpp b/src/ui/tools/select-tool.cpp index 989ca6752193d91151e99461115f0af82a036bbe..857ba74db5a1f9c20e45397d8e17081f47b80e71 100644 --- a/src/ui/tools/select-tool.cpp +++ b/src/ui/tools/select-tool.cpp @@ -65,10 +65,6 @@ namespace Tools { static gint rb_escaped = 0; // if non-zero, rubberband was canceled by esc, so the next button release should not deselect static gint drag_escaped = 0; // if non-zero, drag was canceled by esc - -// static gint xp = 0, yp = 0; // where drag started -// static gint tolerance = 0; -// static bool within_tolerance = false; static bool is_cycling = false; SelectTool::SelectTool(SPDesktop *desktop) @@ -131,8 +127,6 @@ SelectTool::~SelectTool() sp_object_unref(item); item = nullptr; } - - forced_redraws_stop(); } void SelectTool::set(const Inkscape::Preferences::Entry& val) { @@ -434,7 +428,6 @@ bool SelectTool::root_handler(GdkEvent* event) { if (this->item && this->item->document == nullptr) { this->sp_select_context_abort(); } - forced_redraws_start(5); switch (event->type) { case GDK_2BUTTON_PRESS: diff --git a/src/ui/tools/select-tool.h b/src/ui/tools/select-tool.h index 709946febb3d3502ce2546022a3d5a2794016332..51ed41aaa232df22db8eb1765443df8c65236089 100644 --- a/src/ui/tools/select-tool.h +++ b/src/ui/tools/select-tool.h @@ -32,28 +32,28 @@ public: SelectTool(SPDesktop *desktop); ~SelectTool() override; - bool dragging; - bool moved; - guint button_press_state; + bool dragging; + bool moved; + guint button_press_state; std::vector cycling_items; std::vector cycling_items_cmp; SPItem *cycling_cur_item; - bool cycling_wrap; + bool cycling_wrap; - SPItem *item; + SPItem *item; Inkscape::CanvasItem *grabbed = nullptr; - Inkscape::SelTrans *_seltrans; - Inkscape::SelectionDescriber *_describer; - gchar *no_selection_msg = nullptr; + Inkscape::SelTrans *_seltrans; + Inkscape::SelectionDescriber *_describer; + gchar *no_selection_msg = nullptr; - void set(const Inkscape::Preferences::Entry& val) override; - bool root_handler(GdkEvent* event) override; - bool item_handler(SPItem* item, GdkEvent* event) override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; private: - bool sp_select_context_abort(); - void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event); - void sp_select_context_reset_opacities(); + bool sp_select_context_abort(); + void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event); + void sp_select_context_reset_opacities(); bool _alt_on; bool _force_dragging; @@ -61,9 +61,9 @@ private: std::string _default_cursor; }; -} -} -} +} // namespace Tools +} // namespace UI +} // namespace Inkscape #endif diff --git a/src/ui/tools/spiral-tool.cpp b/src/ui/tools/spiral-tool.cpp index 531bb36180146b1f0f9b649967b24768aa21e71b..bcc9c8c116b96732091c76dce633682a7554831c 100644 --- a/src/ui/tools/spiral-tool.cpp +++ b/src/ui/tools/spiral-tool.cpp @@ -318,8 +318,6 @@ void SpiralTool::drag(Geom::Point const &p, guint state) { Inkscape::GC::release(repr); this->spiral->transform = currentLayer()->i2doc_affine().inverse(); this->spiral->updateRepr(); - - forced_redraws_start(5); } SnapManager &m = _desktop->namedview->snap_manager; @@ -372,7 +370,6 @@ void SpiralTool::finishItem() { double const expansion = spiral->transform.descrim(); spiral->doWriteTransform(spiral->transform, nullptr, true); spiral->adjust_stroke_width_recursive(expansion); - forced_redraws_stop(); _desktop->getSelection()->set(this->spiral); DocumentUndo::done(_desktop->getDocument(), _("Create spiral"), INKSCAPE_ICON("draw-spiral")); @@ -395,8 +392,6 @@ void SpiralTool::cancel() { this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(_desktop->getDocument()); } diff --git a/src/ui/tools/spray-tool.cpp b/src/ui/tools/spray-tool.cpp index c359ad31a7c8452d6ee8a07b5520aaf5d51c705f..837c82bff679bc3eb517f5ca6b35b34e79b1340b 100644 --- a/src/ui/tools/spray-tool.cpp +++ b/src/ui/tools/spray-tool.cpp @@ -1245,7 +1245,6 @@ bool SprayTool::root_handler(GdkEvent* event) { sp_spray_extinput(this, event); - forced_redraws_start(3); set_high_motion_precision(); this->is_drawing = true; this->is_dilating = true; @@ -1319,7 +1318,6 @@ bool SprayTool::root_handler(GdkEvent* event) { } this->last_push = _desktop->dt2doc(scroll_dt); sp_spray_extinput(this, event); - forced_redraws_start(3); this->is_drawing = true; this->is_dilating = true; this->has_dilated = false; @@ -1347,7 +1345,6 @@ bool SprayTool::root_handler(GdkEvent* event) { Geom::Point const motion_w(event->button.x, event->button.y); Geom::Point const motion_dt(_desktop->w2d(motion_w)); - forced_redraws_stop(); set_high_motion_precision(false); this->is_drawing = false; diff --git a/src/ui/tools/star-tool.cpp b/src/ui/tools/star-tool.cpp index ce31221aa36e28b803368e29618237dcb388a02a..625cdfec1318b6437d80d5fa5cdb3601009f308b 100644 --- a/src/ui/tools/star-tool.cpp +++ b/src/ui/tools/star-tool.cpp @@ -332,8 +332,6 @@ void StarTool::drag(Geom::Point p, guint state) Inkscape::GC::release(repr); this->star->transform = currentLayer()->i2doc_affine().inverse(); this->star->updateRepr(); - - forced_redraws_start(5); } /* Snap corner point with no constraints */ @@ -391,7 +389,6 @@ void StarTool::finishItem() { double const expansion = this->star->transform.descrim(); this->star->doWriteTransform(this->star->transform, nullptr, true); this->star->adjust_stroke_width_recursive(expansion); - forced_redraws_stop(); _desktop->getSelection()->set(this->star); DocumentUndo::done(_desktop->getDocument(), _("Create star"), INKSCAPE_ICON("draw-polygon-star")); @@ -414,8 +411,6 @@ void StarTool::cancel() { this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(_desktop->getDocument()); } diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index 741238e13edfb78d8ed0479c2bacbbc44e5460a8..93a046cb6e0def1b851bacbbaf49fcc90c53d62f 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -112,7 +112,6 @@ ToolBase::ToolBase(SPDesktop *desktop, std::string prefs_path, std::string curso } ToolBase::~ToolBase() { - _desktop->getCanvas()->forced_redraws_stop(); this->enableSelectionCue(false); if (this->pref_observer) { @@ -188,56 +187,6 @@ void ToolBase::use_cursor(Glib::RefPtr cursor) } } -/** - * Gobbles next key events on the queue with the same keyval and mask. Returns the number of events consumed. - */ -gint gobble_key_events(guint keyval, gint mask) { - GdkEvent *event_next; - gint i = 0; - - event_next = gdk_event_get(); - // while the next event is also a key notify with the same keyval and mask, - while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type - == GDK_KEY_RELEASE) && event_next->key.keyval == keyval && (!mask - || (event_next->key.state & mask))) { - if (event_next->type == GDK_KEY_PRESS) - i++; - // kill it - gdk_event_free(event_next); - // get next - event_next = gdk_event_get(); - } - // otherwise, put it back onto the queue - if (event_next) - gdk_event_put(event_next); - - return i; -} - -/** - * Gobbles next motion notify events on the queue with the same mask. Returns the number of events consumed. - */ -gint gobble_motion_events(gint mask) { - GdkEvent *event_next; - gint i = 0; - - event_next = gdk_event_get(); - // while the next event is also a key notify with the same keyval and mask, - while (event_next && event_next->type == GDK_MOTION_NOTIFY - && (event_next->motion.state & mask)) { - // kill it - gdk_event_free(event_next); - // get next - event_next = gdk_event_get(); - i++; - } - // otherwise, put it back onto the queue - if (event_next) - gdk_event_put(event_next); - - return i; -} - /** * Toggles current tool between active tool and selector tool. * Subroutine of sp_event_context_private_root_handler(). @@ -308,7 +257,7 @@ static gdouble accelerate_scroll(GdkEvent *event, gdouble acceleration) bool ToolBase::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) { if (held_control(event)) return false; - unsigned num = 1 + combine_key_events(shortcut_key(event), 0); + unsigned num = 1 + gobble_key_events(shortcut_key(event), 0); Geom::Point delta = dir * num; if (held_shift(event)) { @@ -571,8 +520,6 @@ bool ToolBase::root_handler(GdkEvent* event) { "/options/zoomincrement/value", M_SQRT2, 1.01, 10); _desktop->zoom_relative(event_dt, (event->button.state & GDK_SHIFT_MASK) ? 1 / zoom_inc : zoom_inc); - - _desktop->updateNow(); ret = TRUE; } else if (panning == event->button.button) { panning = PANNING_NONE; @@ -586,7 +533,6 @@ bool ToolBase::root_handler(GdkEvent* event) { Geom::Point const moved_w(motion_w - button_w); _desktop->scroll_relative(moved_w); - _desktop->updateNow(); ret = TRUE; } else if (zoom_rb == event->button.button) { zoom_rb = 0; @@ -744,8 +690,6 @@ bool ToolBase::root_handler(GdkEvent* event) { xp = yp = 0; ungrabCanvasEvents(); - - _desktop->updateNow(); } if (panning_cursor == 1) { @@ -1121,36 +1065,33 @@ void ToolBase::set_high_motion_precision(bool high_precision) { } } -/** - * Force canvas to fully update after interruptions. - * Convenience function that just passes request to canvas. - */ -void -ToolBase::forced_redraws_start(int count, bool reset) +Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev) { - _desktop->canvas->forced_redraws_start(count, reset); -} + this->xp = static_cast(ev->button.x); + this->yp = static_cast(ev->button.y); + this->within_tolerance = true; + Geom::Point const p(ev->button.x, ev->button.y); + item_to_select = Inkscape::UI::Tools::sp_event_context_find_item(_desktop, p, ev->button.state & GDK_MOD1_MASK, TRUE); + return _desktop->w2d(p); +} /** - * End force canvas full updates. + * Discard and count matching key events from top of event bucket. * Convenience function that just passes request to canvas. */ -void -ToolBase::forced_redraws_stop() +int ToolBase::gobble_key_events(guint keyval, guint mask) const { - _desktop->canvas->forced_redraws_stop(); + return _desktop->canvas->gobble_key_events(keyval, mask); } -Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev) +/** + * Discard matching motion events from top of event bucket. + * Convenience function that just passes request to canvas. + */ +void ToolBase::gobble_motion_events(guint mask) const { - this->xp = static_cast(ev->button.x); - this->yp = static_cast(ev->button.y); - this->within_tolerance = true; - - Geom::Point const p(ev->button.x, ev->button.y); - item_to_select = Inkscape::UI::Tools::sp_event_context_find_item(_desktop, p, ev->button.state & GDK_MOD1_MASK, TRUE); - return _desktop->w2d(p); + _desktop->canvas->gobble_motion_events(mask); } /** diff --git a/src/ui/tools/tool-base.h b/src/ui/tools/tool-base.h index e148ea31f91dace1ecbf8b95d0582919e70124ea..b5786ccf55e3414d9a77b296095153f1d699e1de 100644 --- a/src/ui/tools/tool-base.h +++ b/src/ui/tools/tool-base.h @@ -260,8 +260,8 @@ protected: void set_high_motion_precision(bool high_precision = true); - void forced_redraws_start(int count, bool reset = false); - void forced_redraws_stop(); + int gobble_key_events(guint keyval, guint mask) const; + void gobble_motion_events(guint mask) const; SPDesktop *_desktop = nullptr; @@ -275,9 +275,6 @@ void sp_event_context_read(ToolBase *ec, gchar const *key); void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event); -gint gobble_key_events(guint keyval, gint mask); -gint gobble_motion_events(gint mask); - void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event, gchar const *ctrl_tip, gchar const *shift_tip, gchar const *alt_tip); diff --git a/src/ui/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp index 508228b3298f6ad7fc8d1ed0062fd323f74b200e..d87d3f28c4c20e6cb1c8a858996340dbe33da7df 100644 --- a/src/ui/tools/tweak-tool.cpp +++ b/src/ui/tools/tweak-tool.cpp @@ -1146,7 +1146,6 @@ bool TweakTool::root_handler(GdkEvent* event) { sp_tweak_extinput(this, event); - forced_redraws_start(3); this->is_drawing = true; this->is_dilating = true; this->has_dilated = false; @@ -1195,7 +1194,6 @@ bool TweakTool::root_handler(GdkEvent* event) { Geom::Point const motion_w(event->button.x, event->button.y); Geom::Point const motion_dt(_desktop->w2d(motion_w)); - forced_redraws_stop(); this->is_drawing = false; if (this->is_dilating && event->button.button == 1) { diff --git a/src/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp index a5ebe82c2fa336835de6e8345b6a729b4fd4d330..cf3f819b1e3fb19afccecea670f4f6e169bf37b0 100644 --- a/src/ui/widget/canvas-grid.cpp +++ b/src/ui/widget/canvas-grid.cpp @@ -153,9 +153,9 @@ CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) void CanvasGrid::UpdateRulers() { - Geom::Rect viewbox = _dtw->desktop->get_display_area(true).bounds(); - // "true" means: Use integer values of the canvas for calculating the display area, similar - // to the integer values used for positioning the grid lines. (see SPCanvas::scrollTo(), + Geom::Rect viewbox = _dtw->desktop->get_display_area().bounds(); + // Use integer values of the canvas for calculating the display area, similar + // to the integer values used for positioning the grid lines. (see Canvas::scrollTo(), // where ix and iy are rounded integer values; these values are stored in CanvasItemBuffer->rect, // and used for drawing the grid). By using the integer values here too, the ruler ticks // will be perfectly aligned to the grid diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 68396039fada414f916122d932b3842c3517f602..4d3ee1a0daf9e386a44c97e8219e5ee4a7c9a417 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -1,17 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Author: + * Authors: * Tavmjong Bah + * PBS * - * Rewrite of code originally in sp-canvas.cpp. - * - * Copyright (C) 2020 Tavmjong Bah + * Copyright (C) 2022 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include +#include // Logging +#include // Sort +#include // Coarsener #include @@ -25,13 +26,16 @@ #include "desktop.h" #include "preferences.h" -#include "display/cairo-utils.h" // Checkerboard background. +#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: @@ -39,40 +43,28 @@ * * * 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 effect + * * 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 CanvasItem's. Use if a + * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a * CanvasItem's geometry or size has changed. * - * * redraw_now() Redraw immediately, skipping the "idle" stage. - * * 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 calls: - * - * * do_update() Which makes a few checks and then calls: + * * 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() Which calls for each area of the canvas that has been marked unclean: + * * 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: * - * * paint_rect() Which determines the maximum area to draw at once and where the cursor is, then calls: - * - * * paint_rect_internal() Which recursively divides the area into smaller pieces until a piece is small - * enough to render. It renders the pieces closest to the cursor first. The pieces - * are rendered onto a Cairo surface "backing_store". After a piece is rendered - * there is a call to: - * - * * queue_draw_area() A Gtk function for drawing into a widget which when the time is right calls: + * * 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. * - * One thing to note is that on_draw() must be called twice to render anything to the screen, as the - * first time through it sets up the backing store which must then be drawn to. The second call then - * blits the backing store to the screen. It might be better to setup the backing store on a call - * to on_allocate() but it is what works now. - * * 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 @@ -83,517 +75,520 @@ * them "externally" (e.g. gradient CanvasItemCurves). */ -struct PaintRectSetup { - Geom::IntRect canvas_rect; - gint64 start_time; - int max_pixels; - Geom::Point mouse_loc; -}; - - namespace Inkscape { namespace UI { namespace Widget { +/* + * Preferences system + */ -Canvas::Canvas() - : _size_observer(this, "/options/grabsize/value") -{ - 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 ); - - // Give _pick_event an initial definition. - _pick_event.type = GDK_LEAVE_NOTIFY; - _pick_event.crossing.x = 0; - _pick_event.crossing.y = 0; - - // Drawing - _clean_region = Cairo::Region::create(); - - _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); +template +struct Pref {}; - _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); - _canvas_item_root->set_name("CanvasItemGroup:Root"); - _canvas_item_root->set_canvas(this); -} +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) {enable();} + 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();} +}; -Canvas::~Canvas() +template<> +struct Pref : PrefBase { - assert(!_desktop); + Pref(const char *path, bool def = false) : PrefBase(path, def) {} + bool read() {return Inkscape::Preferences::get()->getBool(path, def);} + bool changed(const Preferences::Entry &e) {return e.getBool(def);} +}; - _drawing = nullptr; - _in_destruction = true; +template<> +struct Pref : PrefBase +{ + int min, max; + Pref(const char *path, int def, int min, int max) : min(min), max(max), PrefBase(path, def) {} + int read() {return Inkscape::Preferences::get()->getIntLimited(path, def, min, max);} + int changed(const Preferences::Entry &e) {return e.getIntLimited(def, min, max);} +}; - remove_idle(); +template<> +struct Pref : PrefBase +{ + double min, max; + Pref(const char *path, double def, double min, double max) : min(min), max(max), PrefBase(path, def) {} + double read() {return Inkscape::Preferences::get()->getDoubleLimited(path, def, min, max);} + double changed(const Preferences::Entry &e) {return e.getDoubleLimited(def, min, max);} +}; - // Remove entire CanvasItem tree. - delete _canvas_item_root; -} +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); +}; -/** - * Is world point inside of canvas area? - */ -bool -Canvas::world_point_inside_canvas(Geom::Point const &world) +void Prefs::set_devmode(bool on) { - Gtk::Allocation allocation = get_allocation(); - return ( (_x0 <= world.x()) && (world.x() < _x0 + allocation.get_width()) && - (_y0 <= world.y()) && (world.y() < _y0 + allocation.get_height()) ); + 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); } -/** - * Translate point in canvas to world coordinates. +/* + * Conversion functions */ -Geom::Point -Canvas::canvas_to_world(Geom::Point const &point) + +auto geom_to_cairo(Geom::IntRect rect) { - return Geom::Point(point[Geom::X]+ _x0, point[Geom::Y] + _y0); + return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()}; } -/** - * Return the area shown in the canvas in world coordinates. - */ -Geom::Rect -Canvas::get_area_world() +auto cairo_to_geom(Cairo::RectangleInt rect) { - return Geom::Rect::from_xywh(_x0, _y0, _width, _height); + return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); } -/** - * Return the area shown the canvas in world coordinates, rounded to integer values. - */ -Geom::IntRect -Canvas::get_area_world_int() +auto geom_to_cairo(Geom::Affine affine) { - Gtk::Allocation allocation = get_allocation(); - return Geom::IntRect::from_xywh(_x0, _y0, allocation.get_width(), allocation.get_height()); + return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]); } - -/** - * Set the affine for the canvas and flag need for geometry update. - */ -void -Canvas::set_affine(Geom::Affine const &affine) +auto geom_act(Geom::Affine a, Geom::IntPoint p) { - if (_affine != affine) { - _affine = affine; - _need_update = true; - } + Geom::Point p2 = p; + p2 *= a; + return Geom::IntPoint(std::round(p2.x()), std::round(p2.y())); } -/** - * Invalidate drawing and redraw during idle. - */ -void -Canvas::redraw_all() +void region_to_path(const Cairo::RefPtr &cr, const Cairo::RefPtr ®) { - if (_in_destruction) { - // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed. - // We need to ignore their requests! - return; + 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); } - _in_full_redraw = true; - _clean_region->intersect(Cairo::Region::create()); // Empty region (i.e. everything is dirty). - add_idle(); } -/** - * Redraw the given area during idle. +/* + * Update strategy */ -void -Canvas::redraw_area(int x0, int y0, int x1, int y1) -{ - // std::cout << "Canvas::redraw_area: " - // << " x0: " << x0 - // << " y0: " << y0 - // << " x1: " << x1 - // << " y1: " << y1 << std::endl; - if (_in_destruction) { - // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed. - // We need to ignore their requests! - return; - } - if (x0 >= x1 || y0 >= y1) { - return; - } +// A class hierarchy for controlling what order to update invalidated regions. +class Updater +{ +public: + // The subgregion of the store with up-to-date content. + Cairo::RefPtr clean_region; - // 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. + Updater(Cairo::RefPtr clean_region) : clean_region(std::move(clean_region)) {} - constexpr int min_coord = std::numeric_limits::min() / 2; - constexpr int max_coord = std::numeric_limits::max() / 2; + 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 everthing 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. - 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); + 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. +}; - Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; - _clean_region->subtract(crect); - add_idle(); -} +// Responsive updater: As soon as a region is invalidated, redraw it. +using ResponsiveUpdater = Updater; -void -Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1) +// Full redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed. +class FullredrawUpdater : public Updater { - // Handle overflow during conversion gracefully. - // Round outward to make sure integral coordinates cover the entire area. + // Whether we are currently in the middle of a redraw. + bool inprogress = false; - constexpr Geom::Coord min_int = static_cast(std::numeric_limits::min()); - constexpr Geom::Coord max_int = static_cast(std::numeric_limits::max()); + // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null. + Cairo::RefPtr old_clean_region; - redraw_area( - static_cast(std::floor(std::clamp(x0, min_int, max_int))), - static_cast(std::floor(std::clamp(y0, min_int, max_int))), - static_cast(std::ceil(std::clamp(x1, min_int, max_int))), - static_cast(std::ceil(std::clamp(y1, min_int, max_int)))); -} +public: -void -Canvas::redraw_area(Geom::Rect& area) -{ - redraw_area(area.left(), area.top(), area.right(), area.bottom()); -} + FullredrawUpdater(Cairo::RefPtr clean_region) : Updater(std::move(clean_region)) {} -/** - * Immediate redraw of areas needing redrawing (don't wait for idle handler). - */ -void -Canvas::redraw_now() -{ - if (!_drawing) { - g_warning("Canvas::%s _drawing is NULL", __func__); - return; + void reset() override + { + Updater::reset(); + inprogress = false; + old_clean_region.clear(); } - do_update(); -} + void intersect(const Geom::IntRect &rect) override + { + Updater::intersect(rect); + if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect)); + } -/** - * Redraw after changing canvas item geometry. - */ -void -Canvas::request_update() -{ - _need_update = true; - add_idle(); // Geometry changed, need to redraw. -} + void mark_dirty(const Geom::IntRect &rect) override + { + if (inprogress && !old_clean_region) old_clean_region = clean_region->copy(); + Updater::mark_dirty(rect); + } -/** - * This is the first function called (after constructor) for Inkscape (not Inkview). - * Scroll window so drawing point 'c' is at upper left corner of canvas. - * Complete redraw if 'clear' is true. - */ -void -Canvas::scroll_to(Geom::Point const &c, bool clear) -{ - int old_x0 = _x0; - int old_y0 = _y0; + 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)); + } - // This is the only place the _x0 and _y0 are set! - _x0 = (int) round(c[Geom::X]); // cx might be negative, so (int)(cx + 0.5) will not do! - _y0 = (int) round(c[Geom::Y]); - _window_origin = c; // Double value + Cairo::RefPtr get_next_clean_region() override + { + inprogress = true; + if (!old_clean_region) { + return clean_region; + } else { + return old_clean_region; + } + } - if (!_backing_store) { - // We haven't drawn anything yet! - return; + 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: - int dx = _x0 - old_x0; - int dy = _y0 - old_y0; + MultiscaleUpdater(Cairo::RefPtr clean_region) : Updater(std::move(clean_region)) {} - if (dx == 0 && dy == 0) { - return; // No scroll... do nothing. + void reset() override + { + Updater::reset(); + inprogress = activated = false; } - // See if there is any overlap between canvas before and after scrolling. - Geom::IntRect old_area = Geom::IntRect::from_xywh(old_x0, old_y0, _allocation.get_width(), _allocation.get_height()); - Geom::IntRect new_area = old_area + Geom::IntPoint(dx, dy); - bool overlap = new_area.intersects(old_area); + void intersect(const Geom::IntRect &rect) override + { + Updater::intersect(rect); + if (activated) { + for (auto ® : blocked) { + reg->intersect(geom_to_cairo(rect)); + } + } + } - if (_drawing) { - Geom::IntRect expanded = new_area; - Geom::IntPoint expansion(new_area.width()/2, new_area.height()/2); - expanded.expandBy(expansion); - _drawing->setCacheLimit(expanded, false); + 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)); } - if (clear || !overlap) { - redraw_all(); - return; // Check if this is OK + 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; + } } - // Copy backing store - shift_content(Geom::IntPoint(dx, dy), _backing_store); - if (_split_mode != Inkscape::SplitMode::NORMAL || _drawing->outlineOverlay()) { - shift_content(Geom::IntPoint(dx, dy), _outline_store); + 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; + } } - // Mark surface to redraw (everything outside clean region). - Cairo::RectangleInt crect = { _x0, _y0, _allocation.get_width(), _allocation.get_height() }; - _clean_region->intersect(crect); // Shouldn't the clean region be reset and then this added? + 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(); + } - // Scroll without zoom: redraw only newly exposed areas. - if (get_realized()) { - auto window = get_window(); - window->scroll(-dx, -dy); // Triggers of newly exposed region. + // 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]); + } } +}; - auto grid = dynamic_cast(get_parent()); - if (grid) { - grid->UpdateRulers(); +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)); } } -/** - * Set canvas background color (display only). +/* + * Implementation class */ -void -Canvas::set_background_color(guint32 rgba) + +class CanvasPrivate { - double r = SP_RGBA32_R_F(rgba); - double g = SP_RGBA32_G_F(rgba); - double b = SP_RGBA32_B_F(rgba); +public: + + friend class Canvas; + Canvas *q; + CanvasPrivate(Canvas *q) : q(q) {} + + // Preferences system + Prefs prefs; + + // The updater; tracks the unclean region and decides how to redraw it. + std::unique_ptr updater; + + // Events system. Events that interact with the Canvas are placed into the event bucket until the start of the next frame. + std::vector> bucket; + bool pending_draw = false; + sigc::connection bucket_emptier; + int bucket_pos; + GdkEvent *ignore = nullptr; + + bool add_to_bucket(GdkEvent*); + void empty_bucket(); + bool process_bucketed_event(const GdkEvent&); + bool pick_current_item(const GdkEvent&); + bool emit_event(const GdkEvent&); + + // 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; + 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;} - _background = Cairo::SolidPattern::create_rgb(r, g, b); - _background_is_checkerboard = false; + // 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); + std::optional old_bisector(const Geom::IntRect &rect); + std::optional new_bisector(const Geom::IntRect &rect); - redraw_all(); -} + // Trivial overload of GtkWidget function. + void queue_draw_area(Geom::IntRect &rect); -/** - * Set canvas background to a checkerboard pattern. + // 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; +}; + +/* + * Events system */ -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)); - _background_is_checkerboard = true; - redraw_all(); -} -void -Canvas::set_render_mode(Inkscape::RenderMode mode) -{ - if (_render_mode != mode) { - _render_mode = mode; - redraw_all(); - } - if (_desktop) { - _desktop->setWindowTitle(); // Mode is listed in title. - } -} +// 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. -void -Canvas::set_color_mode(Inkscape::ColorMode mode) +bool +Canvas::on_scroll_event(GdkEventScroll *scroll_event) { - if (_color_mode != mode) { - _color_mode = mode; - redraw_all(); - } - if (_desktop) { - _desktop->setWindowTitle(); // Mode is listed in title. - } + return d->add_to_bucket(reinterpret_cast(scroll_event)); } -void -Canvas::set_split_mode(Inkscape::SplitMode mode) +bool +Canvas::on_button_press_event(GdkEventButton *button_event) { - if (_split_mode != mode) { - _split_mode = mode; - redraw_all(); - } + return on_button_event(button_event); } -void -Canvas::set_split_direction(Inkscape::SplitDirection dir) +bool +Canvas::on_button_release_event(GdkEventButton *button_event) { - if (_split_direction != dir) { - _split_direction = dir; - redraw_all(); - } + return on_button_event(button_event); } -void -Canvas::forced_redraws_start(int count, bool reset) +// Unified handler for press and release events. +bool +Canvas::on_button_event(GdkEventButton *button_event) { - _forced_redraw_limit = count; - if (reset) { - _forced_redraw_count = 0; + // 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; } -} -/** - * Clear current and grabbed items. - */ -void -Canvas::canvas_item_clear(Inkscape::CanvasItem* item) -{ - if (item == _current_canvas_item) { - _current_canvas_item = nullptr; - _need_repick = true; - } - - if (item == _current_canvas_item_new) { - _current_canvas_item_new = nullptr; - _need_repick = true; - } - - 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(); - } -} - -// ============== Protected Functions ============== - -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; -} - -// ******* Event handlers ****** -bool -Canvas::on_scroll_event(GdkEventScroll *scroll_event) -{ - // Scroll canvas and in Select Tool, cycle selection through objects under cursor. - return emit_event(reinterpret_cast(scroll_event)); -} - -// Our own function that combines press and release. -bool -Canvas::on_button_event(GdkEventButton *button_event) -{ - // Dispatch normally regardless of the event's window if an item - // has a pointer grab in effect. - auto window = get_window(); - if (!_grabbed_canvas_item && window->gobj() != button_event->window) { - return false; - } - - int mask = 0; - switch (button_event->button) { - case 1: mask = GDK_BUTTON1_MASK; break; - case 2: mask = GDK_BUTTON1_MASK; break; - case 3: mask = GDK_BUTTON1_MASK; break; - case 4: mask = GDK_BUTTON1_MASK; break; - case 5: mask = GDK_BUTTON1_MASK; break; - default: mask = 0; // Buttons can range at least to 9 but mask defined only to 5. - } - - - bool retval = false; + // Drag the split view controller. switch (button_event->type) { case GDK_BUTTON_PRESS: - if (_hover_direction != Inkscape::SplitDirection::NONE) { - // We're hovering over Split controller. _split_dragging = true; _split_drag_start = Geom::Point(button_event->x, button_event->y); - break; + return true; } - // Fallthrough - + break; case GDK_2BUTTON_PRESS: - if (_hover_direction != Inkscape::SplitDirection::NONE) { _split_direction = _hover_direction; _split_dragging = false; queue_draw(); - break; + return true; } - // Fallthrough - - case GDK_3BUTTON_PRESS: - // Pick the current item as if the button were not press and then process event. - - _state = button_event->state; - pick_current_item(reinterpret_cast(button_event)); - _state ^= mask; - retval = emit_event(reinterpret_cast(button_event)); break; - case GDK_BUTTON_RELEASE: - // Process the event as if the button were pressed, then repick after the button has - // been released. _split_dragging = false; - - _state = button_event->state; - retval = emit_event(reinterpret_cast(button_event)); - button_event->state ^= mask; - _state = button_event->state; - pick_current_item(reinterpret_cast(button_event)); - button_event->state ^= mask; break; - - default: - std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl; } - return retval; -} - -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); + // Otherwise, handle as a delayed event. + return d->add_to_bucket(reinterpret_cast(button_event)); } bool Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) { - auto window = get_window(); - if (window->gobj() != crossing_event->window) { + if (crossing_event->window != get_window()->gobj()) { std::cout << " WHOOPS... this does really happen" << std::endl; return false; } - _state = crossing_event->state; - return pick_current_item(reinterpret_cast(crossing_event)); + return d->add_to_bucket(reinterpret_cast(crossing_event)); } bool Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) { - auto window = get_window(); - if (window->gobj() != crossing_event->window) { + if (crossing_event->window != get_window()->gobj()) { std::cout << " WHOOPS... this does really happen" << std::endl; return false; } - _state = crossing_event->state; - // this is needed to remove alignment or distribution snap indicators - if (_desktop) - _desktop->snapindicator->remove_snaptarget(); - return pick_current_item(reinterpret_cast(crossing_event)); + d->last_mouse = {}; + return d->add_to_bucket(reinterpret_cast(crossing_event)); } bool @@ -603,793 +598,1250 @@ Canvas::on_focus_in_event(GdkEventFocus *focus_event) return false; } -// TODO See if we still need this (canvas->focused_item removed between 0.48 and 0.91). -bool -Canvas::on_focus_out_event(GdkEventFocus *focus_event) -{ - return false; -} - -// Actually, key events never reach here. bool Canvas::on_key_press_event(GdkEventKey *key_event) { - return emit_event(reinterpret_cast(key_event)); + return d->add_to_bucket(reinterpret_cast(key_event)); } -// Actually, key events never reach here. bool Canvas::on_key_release_event(GdkEventKey *key_event) { - return emit_event(reinterpret_cast(key_event)); + return d->add_to_bucket(reinterpret_cast(key_event)); } bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) { - Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y); + // Record the last mouse position. + d->last_mouse = Geom::Point(motion_event->x, motion_event->y); + // Handle interactions with the split view controller. if (_desktop) { - // 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() - _allocation.get_width() > -5 || - cursor_position.y() - _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(); + 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; - } + // 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 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; - } + 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); + saction->change_state((int)Inkscape::SplitMode::NORMAL); - return true; + return true; + } } - } - - if (_split_mode == Inkscape::SplitMode::XRAY) { - _split_position = cursor_position; - queue_draw(); // Re-blit - } - 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; + if (_split_mode == Inkscape::SplitMode::XRAY) { + _split_position = cursor_position; queue_draw(); - return true; } - if (Geom::distance(cursor_position, _split_position) < 20 * _device_scale) { + 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; + } - // 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; + 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 { - hover_direction = Inkscape::SplitDirection::WEST; + 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 (difference.y() + difference.x() > 0) { - hover_direction = Inkscape::SplitDirection::EAST; - } else { - hover_direction = Inkscape::SplitDirection::NORTH; + if (std::abs(difference.x()) < 3 * d->_device_scale) { + // We're hovering over vertical line + hover_direction = Inkscape::SplitDirection::VERTICAL; } } - } else if (_split_direction == Inkscape::SplitDirection::NORTH || - _split_direction == Inkscape::SplitDirection::SOUTH) { - if (std::abs(difference.y()) < 3 * _device_scale) { - // We're hovering over horizontal line - hover_direction = Inkscape::SplitDirection::HORIZONTAL; - } - } else { - if (std::abs(difference.x()) < 3 * _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 != 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; + if (_hover_direction != Inkscape::SplitDirection::NONE) { + // We're hovering, don't pick or emit event. + return true; + } } - } - } // End if(desktop) + } // if (_desktop) - _state = motion_event->state; - pick_current_item(reinterpret_cast(motion_event)); - bool status = emit_event(reinterpret_cast(motion_event)); - return status; + // Otherwise, handle as a delayed event. + return d->add_to_bucket(reinterpret_cast(motion_event)); } -/** - * Resize handler - */ -void Canvas::on_size_allocate(Gtk::Allocation &allocation) -{ - parent_type::on_size_allocate(allocation); - - assert(allocation == get_allocation()); - _width = allocation.get_width(); - _height = allocation.get_height(); -} +// 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. -/* - * The on_draw() function is called whenever Gtk wants to update the window. This function: - * - * 1. Sets up the backing and outline stores (images). These stores are drawn to elsewhere during idles. - * The backing store is always uses, rendering in which ever "render mode" the user has selected. - * The outline store is only used when the "split mode" is set to 'split' or 'x-ray'. - * (Changing either the render mode or split mode results in a complete redrawing the store(s).) - * - * 2. Calls shift_content() if the drawing area has changed. - * - * 3. Blits the store(s) onto the canvas, clipping the outline store as required. - * - * 4. Draws the "controller" in the 'split' split mode. - * - * 5. Calls add_idle() to update the drawing if necessary. - */ +// Add an event to the bucket and ensure it will be emptied in the near future. bool -Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) +CanvasPrivate::add_to_bucket(GdkEvent *event) { - // sp_canvas_item_recursive_print_tree(0, _root); - // canvas_item_print_tree(_canvas_item_root); - - // This function should be the only place _allocation is redefined (except in on_realize())! - Gtk::Allocation allocation = get_allocation(); - int device_scale = get_scale_factor(); - - // This is the only place we should initialize _backing_store! (Elsewhere, it's recreated.) - if (!(_allocation == allocation) || _device_scale != device_scale) { // "!=" for allocation not defined! - _allocation = allocation; - _device_scale = device_scale; + framecheck_whole_function(this) - // Create new stores and copy/shift contents. - shift_content(Geom::IntPoint(0, 0), _backing_store); - shift_content(Geom::IntPoint(0, 0), _outline_store); + // Prevent re-fired events from going through again. + if (event == ignore) { + return false; + } - // Clip the clean region to the new allocation - Cairo::RectangleInt clip = { _x0, _y0, _allocation.get_width(), _allocation.get_height() }; - _clean_region->intersect(clip); + // If this is the first event, make sure the bucket will be emptied in the near future. + if (bucket.empty() && !pending_draw) { + q->add_tick_callback([this] (const Glib::RefPtr&) { + empty_bucket(); + return false; + }); } - assert(_backing_store && _outline_store); - assert(_drawing); + // Add a copy to the queue. + bucket.emplace_back(std::make_unique(*event)); - // This is the only place the widget content is drawn! + // Tell GTK the event was handled. + return true; +} - // Blit background (e.g. checkerboard). - cr->save(); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->set_source(_background); - cr->paint(); - cr->restore(); +// The following functions run at the start of the next frame. - // Blit from the backing store, without regard for the clean region. - cr->set_source(_backing_store, 0, 0); - cr->paint(); +// Process bucketed events. +void +CanvasPrivate::empty_bucket() +{ + framecheck_whole_function(this) - // Draw overlay if required. - if (_drawing->outlineOverlay()) { + // Initialise iteration index; may be incremented externally by gobblers. + bucket_pos = 0; - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - double outline_overlay_opacity = 1 - (prefs->getIntLimited("/options/rendering/outline-overlay-opacity", 50, 1, 100) / 100.0); + while (bucket_pos < bucket.size()) { + // Extract next event. + auto event = std::move(bucket[bucket_pos]); + bucket_pos++; - // Partially obscure drawing by painting semi-transparent white. - cr->set_source_rgb(255,255,255); - cr->paint_with_alpha(outline_overlay_opacity); + // Process the event and see if it was handled. + bool handled = process_bucketed_event(*event); - // Overlay outline - cr->set_source(_outline_store, 0, 0); - cr->paint(); + if (!handled) { + // Re-fire the event at the window, and ignore it when it comes back here again. + ignore = event.get(); + q->get_toplevel()->event(event.get()); + ignore = nullptr; + } } - // Draw split if required. - if (_split_mode != Inkscape::SplitMode::NORMAL) { + bucket.clear(); +} - // Move split position to center if not in canvas. - auto const rect = Geom::Rect(0, 0, _width, _height); - if (!rect.contains(_split_position)) { - _split_position = rect.midpoint(); +// Called during 'empty_bucket' 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) const +{ + int count = 0; + + while (d->bucket_pos < d->bucket.size()) { + auto &event = d->bucket[d->bucket_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++; + d->bucket_pos++; + } + else { + // Stop discarding. + break; } + } - // 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(); + if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " key press(es)" << std::endl; - // Add clipping path and blit outline store. - cr->save(); - cr->set_source(_outline_store, 0, 0); - add_clippath(cr); - cr->paint(); - cr->restore(); + return count; +} + +// Called during 'empty_bucket' by some tools to ignore backlogs of motion events that may have built up after a freeze. +void +Canvas::gobble_motion_events(guint mask) const +{ + int count = 0; + + while (d->bucket_pos < d->bucket.size()) { + auto &event = d->bucket[d->bucket_pos]; + if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) { + // Discard event and continue. + count++; + d->bucket_pos++; + } + else { + // Stop discarding. + break; + } } - if (_split_mode == Inkscape::SplitMode::SPLIT) { + if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " motion event(s)" << std::endl; +} - // 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, _allocation.get_height()); - cr->stroke(); - } else { - cr->move_to( 0, (int)_split_position.y() + 0.5); - cr->line_to(_allocation.get_width(), (int)_split_position.y() + 0.5); - cr->stroke(); - } - cr->restore(); +// 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. - // 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 * _device_scale, 0, 2 * M_PI); - cr->fill(); - cr->restore(); +bool +CanvasPrivate::process_bucketed_event(const GdkEvent &event) +{ + auto calc_button_mask = [&] () -> int { + switch (event.button.button) { + case 1: return GDK_BUTTON1_MASK; break; // Fixme: These all used to be GDK_BUTTON1_MASK! Was that intentional? I changed it just in case. Revert on breakage. + 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. + } + }; - cr->save(); - for (int i = 0; i < 4; ++i) { - // The four direction triangles. - cr->save(); + // Do event-specific processing. + switch (event.type) { - // Position triangle. - cr->translate(_split_position.x(), _split_position.y()); - cr->rotate((i+2)*M_PI/2.0); + case GDK_SCROLL: + return emit_event(event); - // Draw triangle. - cr->move_to(-5 * _device_scale, 8 * _device_scale); - cr->line_to( 0, 18 * _device_scale); - cr->line_to( 5 * _device_scale, 8 * _device_scale); - cr->close_path(); + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + { + // Pick the current item as if the button were not pressed... + q->_state = event.button.state; + pick_current_item(event); - double b = (int)_hover_direction == (i+1) ? 0.9 : 0.7; - cr->set_source_rgba(b, b, b, a); - cr->fill(); + // ...then process the event. + q->_state ^= calc_button_mask(); + bool retval = emit_event(event); - cr->restore(); + return retval; } - cr->restore(); - } - // static int i = 0; - // ++i; - // std::string file = "on_draw_" + std::to_string(i) + ".png"; - // _backing_store->write_to_png(file); - - // This whole section is just to determine if we call add_idle! - auto dirty_region = Cairo::Region::create(); + case GDK_BUTTON_RELEASE: + { + // Process the event as if the button were pressed... + q->_state = event.button.state; + bool retval = emit_event(event); - std::vector clip_rectangles; - cr->copy_clip_rectangle_list(clip_rectangles); - for (auto & rectangle : clip_rectangles) { - Geom::Rect dr = Geom::Rect::from_xywh(rectangle.x + _x0, - rectangle.y + _y0, - rectangle.width, - rectangle.height); - // "rectangle" is floating point, we must convert to integer. We round outward as it's - // better to have a larger dirty region to avoid artifacts. - Geom::IntRect ir = dr.roundOutwards(); - Cairo::RectangleInt irect = { ir.left(), ir.top(), ir.width(), ir.height() }; - dirty_region->do_union(irect); - } + // ...then repick after the button has been released. + auto event_copy = event; + event_copy.button.state ^= calc_button_mask(); + q->_state = event_copy.button.state; + pick_current_item(event_copy); - dirty_region->subtract(_clean_region); - - if (!dirty_region->empty()) { - add_idle(); - } + return retval; + } - return true; -} + case GDK_ENTER_NOTIFY: + q->_state = event.crossing.state; + return pick_current_item(event); -void -Canvas::update_canvas_item_ctrl_sizes(int size_index) -{ - _canvas_item_root->update_canvas_item_ctrl_sizes(size_index); -} + case GDK_LEAVE_NOTIFY: + 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); -void -Canvas::add_idle() -{ - if (_in_destruction) { - std::cerr << "Canvas::add_idle: Called after canvas destroyed!" << std::endl; - return; - } + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + return emit_event(event); - if (get_realized() && !_idle_connection.connected()) { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - guint redrawPriority = prefs->getIntLimited("/options/redrawpriority/value", G_PRIORITY_HIGH_IDLE, G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE); - if (_in_full_redraw) { - _in_full_redraw = false; - redrawPriority = G_PRIORITY_DEFAULT_IDLE; - } - // G_PRIORITY_HIGH_IDLE = 100, G_PRIORITY_DEFAULT_IDLE = 200: Higher number => lower priority. + case GDK_MOTION_NOTIFY: + q->_state = event.motion.state; + pick_current_item(event); + return emit_event(event); - _idle_connection = Glib::signal_idle().connect(sigc::mem_fun(*this, &Canvas::on_idle), redrawPriority); + default: + return false; } } -// Probably not needed. -void -Canvas::remove_idle() -{ - _idle_connection.disconnect(); -} - +// 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 -Canvas::on_idle() +CanvasPrivate::pick_current_item(const GdkEvent &event) { - if (_in_destruction) { - std::cerr << "Canvas::on_idle: Called after canvas destroyed!" << std::endl; - } - - if (!_drawing) { - return false; // Disconnect + // Ensure geometry is correct. + 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; } - bool done = do_update(); - int n_rects = _clean_region->get_num_rectangles(); + int button_down = 0; + if (!q->_all_enter_events) { + // Only set true in connector-tool.cpp. - if (n_rects > 1) { - done = false; + // 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; } - return !done; -} + // 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_BUTTON_RELEASE) { + // Convert to GDK_ENTER_NOTIFY -/* - * Paints if drawable (widget mapped and visible). - * Otherwise picks (which makes no sense). - * Return true if done. - */ -bool -Canvas::do_update() -{ - assert(_canvas_item_root); - assert(_drawing); + // These fields have the same offsets in both 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; + q->_pick_event.crossing.state = event.motion.state; - if (_drawing_disabled) { - return true; - } + // These fields don't have the same offsets in both types of events. + if (event.type == GDK_MOTION_NOTIFY) { + q->_pick_event.crossing.x_root = event.motion.x_root; + q->_pick_event.crossing.y_root = event.motion.y_root; + } else { + q->_pick_event.crossing.x_root = event.button.x_root; + q->_pick_event.crossing.y_root = event.button.y_root; + } - if (get_is_drawable()) { - // We're mapped and visible. - if (_need_update) { - _canvas_item_root->update(_affine); - _need_update = false; + } else { + q->_pick_event = event; } - return paint(); } - // TODO: This makes no sense as normally we wouldn't reach here. - // Pick current item: - while (_need_repick) { - _need_repick = false; - pick_current_item(&_pick_event); + if (q->_in_repick) { + // Don't do anything else if this is a recursive call. + return false; } - return true; // FIXME?? -} + // 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; -/* - * Paint the "dirty" areas of the canvas, usually multiple rectangles. - */ -bool -Canvas::paint() -{ - if (_need_update) { - std::cerr << "Canvas::Paint: called while needing update!" << std::endl; - } + 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; + } - Cairo::RectangleInt crect = { _x0, _y0, _allocation.get_width(), _allocation.get_height() }; - auto draw_region = Cairo::Region::create(crect); - draw_region->subtract(_clean_region); + // If in split mode, look at where cursor is to see if one should pick with outline mode. + q->_drawing->setRenderMode(q->_render_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(); + } - int n_rects = draw_region->get_num_rectangles(); - for (int i = 0; i < n_rects; ++i) { - auto rect = draw_region->get_rectangle(i); - if (!paint_rect(rect)) { - // Aborted - return false; - }; + 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; + // } } - return true; -} + if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) { + // Current item did not change! + return false; + } -/* - * Paint a rectangular area. - * rect: The rectangle to paint (in widget coordinates). - */ -bool -Canvas::paint_rect(Cairo::RectangleInt& rect) -{ - // Find window rectangle in 'world coordinates'. - Geom::IntRect canvas_rect = Geom::IntRect::from_xywh(_x0, _y0, _allocation.get_width(), _allocation.get_height()); - Geom::IntRect paint_rect = Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); - Geom::OptIntRect area = paint_rect & canvas_rect; + // 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 ) { - // Don't stop idle process if empty. - if (!area || area->hasZeroArea()) { - return true; + 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; } - // Get cursor position - auto const display = Gdk::Display::get_default(); - auto const seat = display->get_default_seat(); - auto const device = seat->get_pointer(); - - int x = 0; - int y = 0; - Gdk::ModifierType mask; - auto window = get_window(); - if (window) { - window->get_device_position(device, x, y, mask); + 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; + } } - PaintRectSetup setup; - setup.canvas_rect = canvas_rect; - setup.mouse_loc = Geom::Point(_x0 + x, _y0 + y); - setup.start_time = g_get_monotonic_time(); + // Handle the rest of cases + q->_left_grabbed_item = false; + q->_current_canvas_item = q->_current_canvas_item_new; - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - unsigned tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512); - if (_render_mode != Inkscape::RenderMode::OUTLINE) { - // Can't be too small or large gradient will be rerendered too many times! - setup.max_pixels = 65536 * tile_multiplier; - } else { - // Paths only. 1M is catched buffer and we need four channels. - setup.max_pixels = 262144; + 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 paint_rect_internal(&setup, paint_rect); + return retval; } - -/* - * Returns true on successful rendering of rectangle (unless error). - * Returns false if rectangle has no area or if timed out. - * Queues Gtk redraw of widget. - */ +// Fires an event at the canvas, after a little pre-processing. Returns true if handled. bool -Canvas::paint_rect_internal(PaintRectSetup const *setup, Geom::IntRect const &this_rect) +CanvasPrivate::emit_event(const GdkEvent &event) { - if (!_drawing) { - std::cerr << "Canvas::paint_rect_internal: no CanvasItemDrawing!" << std::endl; - return false; - } + // Handle grabbed items. + if (q->_grabbed_canvas_item) { + auto mask = (Gdk::EventMask)0; - gint64 now = g_get_monotonic_time(); - gint64 elapsed = now - setup->start_time; - - // Allow only very fast buffers to be run together; - // as soon as the total redraw time exceeds 1ms, cancel; - // this returns control to the idle loop and allows Inkscape to process user input - // (potentially interrupting the redraw); as soon as Inkscape has some more idle time, - if (elapsed > 1000) { - // Interrupting redraw isn't always good. - // For example, when you drag one node of a big path, only the buffer containing - // the mouse cursor will be redrawn again and again, and the rest of the path - // will remain stale because Inkscape never has enough idle time to redraw all - // of the screen. To work around this, such operations set a forced_redraw_limit > 0. - // If this limit is set, and if we have aborted redraw more times than is allowed, - // interrupting is blocked and we're forced to redraw full screen once - // (after which we can again interrupt forced_redraw_limit times). - if (_forced_redraw_limit < 0 || - _forced_redraw_count < _forced_redraw_limit) { - - if (_forced_redraw_limit != -1) { - _forced_redraw_count++; - } + 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; } - _forced_redraw_count = 0; } - // Find optimal buffer dimension - int bw = this_rect.width(); - int bh = this_rect.height(); + // 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(); + }; - if (bw < 1 || bh < 1) { - // Nothing to draw! - return false; // Don't idle stop process if area is empty. + auto event_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; } - if (bw * bh < setup->max_pixels) { - // We are small enough! - - _drawing->setRenderMode(_render_mode); - _drawing->setColorMode(_color_mode); - - paint_single_buffer(this_rect, setup->canvas_rect, _backing_store); - bool outline_overlay = _drawing->outlineOverlay(); - if (_split_mode != Inkscape::SplitMode::NORMAL || outline_overlay) { - _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); - paint_single_buffer(this_rect, setup->canvas_rect, _outline_store); - if (outline_overlay) { - _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE_OVERLAY); - } - } - - Cairo::RectangleInt crect = { this_rect.left(), this_rect.top(), this_rect.width(), this_rect.height() }; - _clean_region->do_union( crect ); - - queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); - - return true; + // 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; } - /* - * 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 psycologically faster. - * - * Default is for strips mode. - */ - - static int TILE_SIZE = 16; - Geom::IntRect lo, hi; - - if (bw < bh || bh < 2 * TILE_SIZE) { - int mid = this_rect[Geom::X].middle(); - - lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); - hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); + if (q->_current_canvas_item) { + // Choose where to send event. + auto item = q->_current_canvas_item; - if (setup->mouse_loc[Geom::X] < mid) { - // Always paint towards the mouse first - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); + if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { + item = q->_grabbed_canvas_item; } - } else { - int mid = this_rect[Geom::Y].middle(); - lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); - hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); - - if (setup->mouse_loc[Geom::Y] < mid) { - // Always paint towards the mouse first - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); + // Propagate the event up the canvas item hierarchy until handled. + while (item) { + if (item->handle_event(&event_copy)) return true; + item = item->get_parent(); } } + + return false; } /* - * Paint a single buffer. - * paint_rect: buffer rectangle. - * canvas_rect: canvas rectangle. - * store: Cairo surface to draw on. + * Canvas */ -void -Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, - Cairo::RefPtr &store) -{ - if (!store) { - std::cerr << "Canvas::paint_single_buffer: store not created!" << std::endl; - return; - // Maybe store not created! - } - - Inkscape::CanvasItemBuffer buf(paint_rect, canvas_rect, _device_scale); - - // Make sure the following code does not go outside of store's data - assert(store->get_format() == Cairo::FORMAT_ARGB32); - assert(paint_rect.left() - _x0 >= 0); - assert(paint_rect.top() - _y0 >= 0); - assert(paint_rect.right() - _x0 <= store->get_width()); - assert(paint_rect.bottom() - _y0 <= store->get_height()); - - // Create temporary surface that draws directly to store. - store->flush(); - - // std::cout << " Writing store to png" << std::endl; - // static int i = 0; - // ++i; - // if (i < 5) { - // std::string file = "paint_single_buffer0_" + std::to_string(i) + ".png"; - // store->write_to_png(file); - // } - // Create temporary surface that draws directly to store. - 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); +Canvas::Canvas() + : d(std::make_unique(this)) +{ + set_name("InkscapeCanvas"); - // Move to the correct row. - data += stride * (paint_rect.top() - _y0) * (int)y_scale; - // Move to the correct column. - data += 4 * (paint_rect.left() - _x0) * (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! + // 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 ); + + // 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(); - auto cr = Cairo::Context::create(imgs); + // Give _pick_event an initial definition. + _pick_event.type = GDK_LEAVE_NOTIFY; + _pick_event.crossing.x = 0; + _pick_event.crossing.y = 0; - // Clear background - cr->save(); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->set_source_rgba(0,0,0,0); - cr->paint(); - cr->restore(); + // Drawing + d->updater = make_updater(d->prefs.update_strategy); - buf.cr = cr; + _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); + d->solid_background = true; - // Render drawing on top of background. - if (_canvas_item_root->is_visible()) { - _canvas_item_root->render(&buf); - } + _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); + _canvas_item_root->set_name("CanvasItemGroup:Root"); + _canvas_item_root->set_canvas(this); +} - if (_cms_active) { - cmsHTRANSFORM transf = nullptr; - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); - if ( fromDisplay ) { - transf = Inkscape::CMSSystem::getDisplayPer(_cms_key); - } else { - transf = Inkscape::CMSSystem::getDisplayTransform(); - } +Canvas::~Canvas() +{ + assert(!_desktop); - if (transf) { - imgs->flush(); - unsigned char *px = imgs->get_data(); - int stride = imgs->get_stride(); - for (int i=0; imark_dirty(); - } - } + _drawing = nullptr; + _in_destruction = true; - store->mark_dirty(); + // Disconnect signals. Otherwise called after destructor and crashes. + d->hipri_idle.disconnect(); + d->lopri_idle.disconnect(); - // if (i < 5) { - // std::cout << " Writing store to png" << std::endl; - // std::string file = "paint_single_buffer1_" + std::to_string(i) + ".png"; - // store->write_to_png(file); - // } + // Remove entire CanvasItem tree. + delete _canvas_item_root; +} - // Uncomment to see how Inkscape paints to rectangles on canvas. - // cr->save(); - // cr->move_to (0.5, 0.5); - // cr->line_to (0.5, paint_rect.height()-0.5); - // cr->line_to (paint_rect.width()-0.5, paint_rect.height()-0.5); - // cr->line_to (paint_rect.width()-0.5, 0.5); - // cr->close_path(); - // cr->set_source_rgba(0.0, 0.0, 0.5, 1.0); - // cr->stroke(); - // cr->restore(); +Geom::IntPoint +Canvas::get_dimensions() const +{ + Gtk::Allocation allocation = get_allocation(); + return {allocation.get_width(), allocation.get_height()}; +} - // TODO Check... the rest duplicates a call after this function returns. - Cairo::RectangleInt crect = { paint_rect.left(), paint_rect.top(), paint_rect.width(), paint_rect.height() }; - _clean_region->do_union( crect ); +/** + * Is world point inside canvas area? + */ +bool +Canvas::world_point_inside_canvas(Geom::Point const &world) const +{ + return get_area_world().contains(world.floor()); +} - queue_draw_area(paint_rect.left() - _x0, paint_rect.top() - _y0, paint_rect.width(), paint_rect.height()); +/** + * 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()); +} -// Shift backing store (when canvas scrolled or size changed). +/** + * Set the affine for the canvas. + */ void -Canvas::shift_content(Geom::IntPoint shift, Cairo::RefPtr &store) +Canvas::set_affine(Geom::Affine const &affine) { - Cairo::RefPtr<::Cairo::ImageSurface> new_store = - Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, - _allocation.get_width() * _device_scale, - _allocation.get_height() * _device_scale); - - cairo_surface_set_device_scale(new_store->cobj(), _device_scale, _device_scale); // No C++ API! - - // Copy the old store contents to new backing store. - auto cr = Cairo::Context::create(new_store); - - // Paint background - cr->set_operator(Cairo::Operator::OPERATOR_SOURCE); - cr->set_source(_background); - cr->paint(); - - if (store) { - // Copy old background unshifted (reduces sensation of flicker while waiting for rendering newly exposed area). - cr->set_source(store, 0, 0); - cr->paint(); - - // Copy old background - cr->rectangle(-shift.x(), -shift.y(), _allocation.get_width(), _allocation.get_height()); - cr->clip(); - cr->translate(-shift.x(), -shift.y()); - cr->set_source(store, 0, 0); - cr->paint(); + if (_affine == affine) { + return; } - store = new_store; + _affine = affine; - // static int i = 0; - // ++i; - // std::string file = "shift_content_" + std::to_string(i) + ".png"; - // _store->write_to_png(file); + 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()); +} -// Sets clip path for Split and X-Ray modes. +/** + * Invalidate drawing and redraw during idle. + */ void -Canvas::add_clippath(const Cairo::RefPtr& cr) { - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - double radius = prefs->getIntLimited("/options/rendering/xray-radius", 100, 1, 1500); - - double width = _allocation.get_width(); - double height = _allocation.get_height(); - double sx = _split_position.x(); +Canvas::redraw_all() +{ + if (_in_destruction) { + // 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 (_in_destruction) { + // 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(); +} + +/** + * This is the first function called (after constructor) for Inkscape (not Inkview). + * Scroll window so drawing point 'c' is at upper left corner of canvas. + */ +void +Canvas::scroll_to(Geom::Point const &c) +{ + auto newpos = c.round(); + + if (newpos == _pos) { + return; + } + + _pos = newpos; + + 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; + 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(); + } +} + +void +Canvas::set_split_direction(Inkscape::SplitDirection dir) +{ + if (_split_direction != dir) { + _split_direction = dir; + redraw_all(); + } +} + +Cairo::RefPtr +Canvas::get_backing_store() const +{ + return d->_backing_store; +} + +/** + * Clear current and grabbed items. + */ +void +Canvas::canvas_item_clear(Inkscape::CanvasItem* item) +{ + if (item == _current_canvas_item) { + _current_canvas_item = nullptr; + _need_repick = true; + } + + if (item == _current_canvas_item_new) { + _current_canvas_item_new = nullptr; + _need_repick = true; + } + + 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(); + } +} + +// 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. +} + +void +Canvas::on_realize() +{ + parent_type::on_realize(); + d->add_idle(); +} + +/* + * 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(); + + // 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) { + if (!d->decoupled_mode) { + // Blit store to screen. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw"); + cr->save(); + cr->set_operator(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 background to complement of both clean regions, if solid (and therefore not already drawn). + if (d->solid_background) { + 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->_store_affine.inverse())); + region_to_path(cr, d->updater->clean_region); + 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(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + 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(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(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); + + // 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); + } + + // 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); + 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 that runs as soon as this function is completed. + if (!d->bucket.empty()) { + Glib::signal_idle().connect([this] { + d->empty_bucket(); + return false; + }, G_PRIORITY_DEFAULT_IDLE - 5); // before lopri_idle + } + + // 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(); + + 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) { @@ -1412,311 +1864,708 @@ Canvas::add_clippath(const Cairo::RefPtr& cr) { break; } } else { - cr->arc(sx, sy, radius, 0, 2 * M_PI); + cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI); + } + + cr->clip(); +} + +void +CanvasPrivate::add_idle() +{ + framecheck_whole_function(this) + + if (q->_in_destruction) { + std::cerr << "Canvas::add_idle: Called after canvas destroyed!" << std::endl; + return; + } + + if (!q->get_realized()) { + // We can safely discard events until realized, because we will run add_idle in the on_realize handler 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; } - cr->clip(); -} - -// Change cursor -void -Canvas::set_cursor() { - - if (!_desktop) { - return; + if (bw * bh > max_pixels) { + if (bw < bh || bh < 2 * prefs.tile_size) { + return Geom::X; + } else { + return Geom::Y; + } } - auto display = Gdk::Display::get_default(); - - switch (_hover_direction) { + return {}; +} - case Inkscape::SplitDirection::NONE: - _desktop->event_context->use_tool_cursor(); - break; +std::optional +CanvasPrivate::new_bisector(const Geom::IntRect &rect) +{ + int bw = rect.width(); + int bh = rect.height(); - 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; + // 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; } - - case Inkscape::SplitDirection::HORIZONTAL: - { - auto cursor = Gdk::Cursor::create(display, "ns-resize"); - get_window()->set_cursor(cursor); - break; + } else { + if (bh > prefs.new_bisector_size) { + return Geom::Y; } + } - case Inkscape::SplitDirection::VERTICAL: - { - auto cursor = Gdk::Cursor::create(display, "ew-resize"); - get_window()->set_cursor(cursor); - break; - } + return {}; +} - default: - // Shouldn't reach. - std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl; +bool +CanvasPrivate::on_hipri_idle() +{ + if (idle_running) { + idle_running = on_idle(); } + return false; } +bool +CanvasPrivate::on_lopri_idle() +{ + if (idle_running) { + idle_running = on_idle(); + } + return idle_running; +} -// 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 -Canvas::pick_current_item(GdkEvent *event) +CanvasPrivate::on_idle() { - // Ensure geometry is correct. - if (_need_update) { - _canvas_item_root->update(_affine); - _need_update = false; + framecheck_whole_function(this) + + assert(q->_canvas_item_root); + + if (q->_in_destruction) { + std::cerr << "Canvas::on_idle: Called after canvas destroyed!" << std::endl; + return false; } - int button_down = 0; - if (_all_enter_events == false) { - // Only set true in connector-tool.cpp. + // Quit idle process if not supposed to be drawing. + if (!q->_drawing || q->_drawing_disabled) { + return false; + } - // 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 = _state & (GDK_BUTTON1_MASK | - GDK_BUTTON2_MASK | - GDK_BUTTON3_MASK | - GDK_BUTTON4_MASK | - GDK_BUTTON5_MASK); - if (!button_down) _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 != &_pick_event) { - if (event->type == GDK_MOTION_NOTIFY || event->type == GDK_BUTTON_RELEASE) { - // Convert to GDK_ENTER_NOTIFY + 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); + _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); + 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(); + } - // These fields have the same offsets in both types of events. - _pick_event.crossing.type = GDK_ENTER_NOTIFY; - _pick_event.crossing.window = event->motion.window; - _pick_event.crossing.send_event = event->motion.send_event; - _pick_event.crossing.subwindow = nullptr; - _pick_event.crossing.x = event->motion.x; - _pick_event.crossing.y = event->motion.y; - _pick_event.crossing.mode = GDK_CROSSING_NORMAL; - _pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; - _pick_event.crossing.focus = false; - _pick_event.crossing.state = event->motion.state; + // 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(); - // These fields don't have the same offsets in both types of events. - if (event->type == GDK_MOTION_NOTIFY) { - _pick_event.crossing.x_root = event->motion.x_root; - _pick_event.crossing.y_root = event->motion.y_root; + // 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 { - _pick_event.crossing.x_root = event->button.x_root; - _pick_event.crossing.y_root = event->button.y_root; + 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; + } } - } else { - _pick_event = *event; } } - if (_in_repick) { - // Don't do anything else if this is a recursive call. + // 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; } - // Find new item - _current_canvas_item_new = nullptr; + // 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(); - if (_pick_event.type != GDK_LEAVE_NOTIFY && _canvas_item_root->is_visible()) { - // Leave notify means there is no current item. - // Find closest item. - double x = 0.0; - double y = 0.0; + // 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); + } - if (_pick_event.type == GDK_ENTER_NOTIFY) { - x = _pick_event.crossing.x; - y = _pick_event.crossing.y; + // 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 { - x = _pick_event.motion.x; - y = _pick_event.motion.y; + paint_region = Cairo::Region::create(); } - // If in split mode, look at where cursor is to see if one should pick with outline mode. - _drawing->setRenderMode(_render_mode); - if (_split_mode == Inkscape::SplitMode::SPLIT && !_drawing->outlineOverlay()) { - if ((_split_direction == Inkscape::SplitDirection::NORTH && y > _split_position.y()) || - (_split_direction == Inkscape::SplitDirection::SOUTH && y < _split_position.y()) || - (_split_direction == Inkscape::SplitDirection::WEST && x > _split_position.x()) || - (_split_direction == Inkscape::SplitDirection::EAST && x < _split_position.x()) ) { - _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); - } + // 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); + + // 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 - // Convert to world coordinates. - x += _x0; - y += _y0; - Geom::Point p(x, y); + // 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. + 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) { + continue; + } - _current_canvas_item_new = _canvas_item_root->pick_item(p); - // if (_current_canvas_item_new) { - // std::cout << " PICKING: FOUND ITEM: " << _current_canvas_item_new->get_name() << std::endl; - // } else { - // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl; - // } - } + // 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) { + continue; + } - if (_current_canvas_item_new == _current_canvas_item && - !_left_grabbed_item ) { - // Current item did not change! - return false; - } + // 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) { + 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); + continue; + } - // Synthesize events for old and new current items. - bool retval = false; - if (_current_canvas_item_new != _current_canvas_item && - _current_canvas_item != nullptr && - !_left_grabbed_item ) { + // Paint the rectangle. + paint_rect_internal(rect); - GdkEvent new_event; - new_event = _pick_event; - new_event.type = GDK_LEAVE_NOTIFY; - new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; - new_event.crossing.subwindow = nullptr; - _in_repick = true; - retval = emit_event(&new_event); - _in_repick = false; + // Check for timeout. + auto now = g_get_monotonic_time(); + auto elapsed = now - start_time; + if (elapsed > 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; } - if (_all_enter_events == false) { - // new_current_item may have been set to nullptr during the call to emitEvent() above. - if (_current_canvas_item_new != _current_canvas_item && - button_down ) { - _left_grabbed_item = true; - return retval; + // 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; } +} - // Handle the rest of cases - _left_grabbed_item = false; - _current_canvas_item = _current_canvas_item_new; +void +CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) +{ + // Paint the rectangle. + q->_drawing->setColorMode(q->_color_mode); + q->_drawing->setRenderMode(q->_render_mode); + paint_single_buffer(rect, _backing_store); - if (_current_canvas_item != nullptr ) { - GdkEvent new_event; - new_event = _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); + if (_outline_store) { + q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); + paint_single_buffer(rect, _outline_store); } - return retval; -} + // Introduce an artificial delay for each rectangle. + if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time); -bool -Canvas::emit_event(GdkEvent *event) -{ - Gdk::EventMask mask = (Gdk::EventMask)0; - if (_grabbed_canvas_item) { - 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; - } + // Mark the rectangle as clean. + updater->mark_clean(rect); - if (!(mask & _grabbed_event_mask)) { - return false; + // 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. + bucket_emptier.disconnect(); + 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); + bucket_emptier.disconnect(); + pending_draw = true; } } +} - // Convert to world coordinates. We have two different cases due to - // different event structures. - GdkEvent *event_copy = gdk_event_copy(event); - switch (event_copy->type) { - case GDK_ENTER_NOTIFY: - case GDK_LEAVE_NOTIFY: - event_copy->crossing.x += _x0; - event_copy->crossing.y += _y0; - break; - case GDK_MOTION_NOTIFY: - case GDK_BUTTON_PRESS: - case GDK_2BUTTON_PRESS: - case GDK_3BUTTON_PRESS: - case GDK_BUTTON_RELEASE: - event_copy->motion.x += _x0; - event_copy->motion.y += _y0; - break; - default: - break; +void +CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo::RefPtr &store) +{ + // 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 (solid_background) { + cr->set_source(q->_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + } else { + cr->set_operator(Cairo::OPERATOR_CLEAR); } + cr->paint(); + cr->restore(); - // Block undo/redo while anything is dragged. - if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) { - _is_dragging = true; - } else if (event->type == GDK_BUTTON_RELEASE) { - _is_dragging = false; + // Render drawing on top of background. + if (q->_canvas_item_root->is_visible()) { + auto buf = Inkscape::CanvasItemBuffer{ paint_rect, _device_scale, cr }; + q->_canvas_item_root->render(&buf); } - gint finished = false; // Can't be bool or "stack smashing detected"! - - if (_current_canvas_item) { - // Choose where to send event; - CanvasItem *item = _current_canvas_item; + // 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 (_grabbed_canvas_item && !_current_canvas_item->is_descendant_of(_grabbed_canvas_item)) { - item = _grabbed_canvas_item; - } + if (q->_cms_active) { + auto transf = prefs.from_display + ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key) + : Inkscape::CMSSystem::getDisplayTransform(); - // Propagate the event up the canvas item hierarchy until handled. - while (item) { - finished = item->handle_event(event_copy); - if (finished) break; - item = item->get_parent(); + 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(); } } - gdk_event_free(event_copy); - - return finished; + store->mark_dirty(); } - } // namespace Widget } // namespace UI } // namespace Inkscape diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 1887c056552cce5c00b2c5a782a977ee3857c3aa..412099e5c2f5d2fb6644256f7826b5737bf3ef3c 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -3,10 +3,11 @@ #ifndef INKSCAPE_UI_WIDGET_CANVAS_H #define INKSCAPE_UI_WIDGET_CANVAS_H /* - * Author: + * Authors: * Tavmjong Bah + * PBS * - * Copyright (C) 2020 Tavmjong Bah + * Copyright (C) 2022 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ @@ -15,20 +16,14 @@ #include "config.h" #endif +#include #include - #include <2geom/rect.h> #include <2geom/int-rect.h> - -#include "preferences.h" // Update canvas_item_ctrl sizes. - #include "display/rendermode.h" class SPDesktop; -struct PaintRectSetup; - - namespace Inkscape { class CanvasItem; @@ -38,6 +33,8 @@ class Drawing; namespace UI { namespace Widget { +class CanvasPrivate; + /** * A Gtk::DrawingArea widget for Inkscape's canvas. */ @@ -52,153 +49,113 @@ public: // Structure TODO: Remove desktop dependency. void set_desktop(SPDesktop *desktop) { _desktop = desktop; } - SPDesktop *get_desktop() { return _desktop; } + SPDesktop *get_desktop() const { return _desktop; } // Geometry - bool world_point_inside_canvas(Geom::Point const &world); // desktop-events.cpp - Geom::Point canvas_to_world(Geom::Point const &window); - Geom::Rect get_area_world(); - Geom::IntRect get_area_world_int(); // Shouldn't really need this, only used for rulers. + Geom::IntPoint get_dimensions() const; + bool world_point_inside_canvas(Geom::Point const &world) const; // desktop-events.cpp + Geom::Point canvas_to_world(Geom::Point const &window) const; + Geom::IntRect get_area_world() const; + void set_affine(Geom::Affine const &affine); - Geom::Affine get_affine() { return _affine; } + Geom::Affine get_affine() const { return _affine; } // Drawing void set_drawing(Inkscape::Drawing *drawing) { _drawing = drawing; } - void redraw_all(); // Draw entire surface during idle. - void redraw_area(Geom::Rect& area); // Draw specified area during idle. - void redraw_now(); // Draw areas needing update immediately. - void request_update(); // Draw after updating canvas items. - void scroll_to(Geom::Point const &c, bool clear); + void redraw_all(); // Mark everything as having changed. + void redraw_area(Geom::Rect& area); // Mark a rectangle of world space as having changed. + void redraw_area(int x0, int y0, int x1, int y1); + void redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1); + void request_update(); // Mark geometry as needing recalculation. + void scroll_to(Geom::Point const &c); void set_background_color(guint32 rgba); void set_background_checkerboard(guint32 rgba = 0xC4C4C4FF, bool use_alpha = false); - void set_drawing_disabled(bool disable) { _drawing_disabled = disable; } // Disable during path ops, etc. - bool is_dragging() {return _is_dragging; } // selection-chemistry.cpp + void set_drawing_disabled(bool disable); // Disable during path ops, etc. + bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp // Rendering modes void set_render_mode(Inkscape::RenderMode mode); - void set_color_mode(Inkscape::ColorMode mode); - void set_split_mode(Inkscape::SplitMode mode); + void set_color_mode( Inkscape::ColorMode mode); + void set_split_mode( Inkscape::SplitMode mode); void set_split_direction(Inkscape::SplitDirection dir); - Inkscape::RenderMode get_render_mode() { return _render_mode; } - Inkscape::ColorMode get_color_mode() { return _color_mode; } - Inkscape::SplitMode get_split_mode() { return _split_mode; } + Inkscape::RenderMode get_render_mode() const { return _render_mode; } + Inkscape::ColorMode get_color_mode() const { return _color_mode; } + Inkscape::SplitMode get_split_mode() const { return _split_mode; } void set_cms_key(std::string key) { _cms_key = key; _cms_active = !key.empty(); } - std::string get_cms_key() { return _cms_key; } + std::string get_cms_key() const { return _cms_key; } void set_cms_active(bool active) { _cms_active = active; } - bool get_cms_active() { return _cms_active; } + bool get_cms_active() const { return _cms_active; } - Cairo::RefPtr get_backing_store() { return _backing_store; } // Background rotation preview - Cairo::RefPtr get_background_store() { return _background; } + Cairo::RefPtr get_backing_store() const; // Background rotation preview + Cairo::RefPtr get_background_pattern() const { return _background; } - // For a GTK bug (see SelectedStyle::on_opacity_changed()). - void forced_redraws_start(int count, bool reset = true); - void forced_redraws_stop() { _forced_redraw_limit = -1; } + int gobble_key_events(guint keyval, guint mask) const; // tool-base.cpp + void gobble_motion_events(guint mask) const; // tool-base.cpp // Canvas Items - CanvasItemGroup *get_canvas_item_root() { return _canvas_item_root; } + CanvasItemGroup *get_canvas_item_root() const { return _canvas_item_root; } - Inkscape::CanvasItem *get_current_canvas_item() { return _current_canvas_item; } + Inkscape::CanvasItem *get_current_canvas_item() const { return _current_canvas_item; } void set_current_canvas_item(Inkscape::CanvasItem *item) { - _current_canvas_item = item; } - Inkscape::CanvasItem *get_grabbed_canvas_item() { return _grabbed_canvas_item; } + _current_canvas_item = item; + } + Inkscape::CanvasItem *get_grabbed_canvas_item() const { return _grabbed_canvas_item; } void set_grabbed_canvas_item(Inkscape::CanvasItem *item, Gdk::EventMask mask) { _grabbed_canvas_item = item; _grabbed_event_mask = mask; } - void set_need_repick(bool repick = true) { _need_repick = repick; } - void canvas_item_clear(Inkscape::CanvasItem *item); + void set_need_repick(bool repick = true) { _need_repick = repick; } + void canvas_item_clear(Inkscape::CanvasItem *item); // Events - void set_all_enter_events(bool on) { _all_enter_events = on; } + void set_all_enter_events(bool on) { _all_enter_events = on; } protected: - - void get_preferred_width_vfunc( int& minimum_width, int& natural_width ) const override; - void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + void get_preferred_width_vfunc (int &minimum_width, int &natural_width ) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; // Event handlers - bool on_scroll_event( GdkEventScroll *scroll_event) override; - bool on_button_event( GdkEventButton *button_event); - bool on_button_press_event( GdkEventButton *button_event) override; - bool on_button_release_event(GdkEventButton *button_event) override; - bool on_enter_notify_event( GdkEventCrossing *crossing_event) override; - bool on_leave_notify_event( GdkEventCrossing *crossing_event) override; - bool on_focus_in_event( GdkEventFocus *focus_event ) override; - bool on_focus_out_event( GdkEventFocus *focus_event ) override; - bool on_key_press_event( GdkEventKey *key_event ) override; - bool on_key_release_event( GdkEventKey *key_event ) override; - bool on_motion_notify_event( GdkEventMotion *motion_event) override; - void on_size_allocate(Gtk::Allocation &) override; - - // Painting - bool on_draw(const Cairo::RefPtr& cr) override; - - void update_canvas_item_ctrl_sizes(int size_index); + bool on_scroll_event (GdkEventScroll* ) override; + bool on_button_event (GdkEventButton* ); + bool on_button_press_event (GdkEventButton* ) override; + bool on_button_release_event(GdkEventButton* ) override; + bool on_enter_notify_event (GdkEventCrossing*) override; + bool on_leave_notify_event (GdkEventCrossing*) override; + bool on_focus_in_event (GdkEventFocus* ) override; + bool on_key_press_event (GdkEventKey* ) override; + bool on_key_release_event (GdkEventKey* ) override; + bool on_motion_notify_event (GdkEventMotion* ) override; + + void on_realize() override; + void on_size_allocate(Gtk::Allocation&) override; + bool on_draw(const Cairo::RefPtr&) override; private: - - // ======== Functions ======= - void add_idle(); - void remove_idle(); // Not needed? - bool on_idle(); - - // Drawing (internal overloads) - void redraw_area(int x0, int y0, int x1, int y1); - void redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1); - - // Painting - - // In order they are called in painting. - bool do_update(); - bool paint(); - bool paint_rect(Cairo::RectangleInt& rect); - bool paint_rect_internal(PaintRectSetup const *setup, Geom::IntRect const &this_rect); - void paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, - Cairo::RefPtr &store); - - void shift_content(Geom::IntPoint shift, Cairo::RefPtr &store); void add_clippath(const Cairo::RefPtr& cr); void set_cursor(); - // Events - bool pick_current_item(GdkEvent *event); - bool emit_event(GdkEvent *event); - - // ==== Signal callbacks ==== - sigc::connection _idle_connection; // Probably not needed (automatically disconnects). - - // ====== Data members ======= - // Structure - SPDesktop * _desktop = nullptr; + SPDesktop *_desktop = nullptr; // Geometry - int _x0 = 0; ///< World coordinate of the leftmost pixels of window. - int _y0 = 0; ///< World coordinate of the topmost pixels of window. - Geom::Point _window_origin; ///< World coordinate of the upper-leftmost pixel of window. - Geom::Affine _affine; // Only used for canvas items at moment. - bool _in_full_redraw = false; - int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. - Gtk::Allocation _allocation; ///< Canvas allocation, save so we know when it changes. - - int _width = 0; ///< Canvas width, tracked by on_size_allocate - int _height = 0; ///< Canvas height, tracked by on_size_allocate + Geom::IntPoint _pos; ///< Coordinates of top-left pixel of canvas view within canvas. + Geom::Affine _affine; ///< The affine that we have been requested to draw at. // Event handling/item picking - GdkEvent _pick_event; ///< Event used to find currently selected item. - bool _need_repick = true; ///< ? - bool _in_repick = false; ///< Used internally by pick_current_item(). - bool _left_grabbed_item = false; ///< ? - bool _all_enter_events = false; ///< Keep all enter events. Only set true in connector-tool.cpp. - bool _is_dragging = false; ///< Used in selection-chemistry to block undo/redo. - int _state = 0; ///< Last know modifier state (SHIFT, CTRL, etc.). + GdkEvent _pick_event; ///< Event used to find currently selected item. + bool _need_repick = true; ///< ? + bool _in_repick = false; ///< Used internally by pick_current_item(). + bool _left_grabbed_item = false; ///< ? + bool _all_enter_events = false; ///< Keep all enter events. Only set true in connector-tool.cpp. + bool _is_dragging = false; ///< Used in selection-chemistry to block undo/redo. + int _state = 0; ///< Last know modifier state (SHIFT, CTRL, etc.). Inkscape::CanvasItem *_current_canvas_item = nullptr; ///< Item containing cursor, nullptr if none. Inkscape::CanvasItem *_current_canvas_item_new = nullptr; ///< Item to become _current_item, nullptr if none. @@ -214,67 +171,32 @@ private: Inkscape::RenderMode _render_mode = Inkscape::RenderMode::NORMAL; Inkscape::SplitMode _split_mode = Inkscape::SplitMode::NORMAL; + Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL; Geom::Point _split_position{-1, -1}; // initialize with off-canvas coordinates - Inkscape::SplitDirection _split_direction = Inkscape::SplitDirection::EAST; - Inkscape::SplitDirection _hover_direction = Inkscape::SplitDirection::NONE; + Inkscape::SplitDirection _split_direction = Inkscape::SplitDirection::EAST; + Inkscape::SplitDirection _hover_direction = Inkscape::SplitDirection::NONE; bool _split_dragging = false; Geom::Point _split_drag_start; - Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL; std::string _cms_key; bool _cms_active = false; - // For a GTK bug (see SelectedStyle::on_opacity_changed()). - int _forced_redraw_limit = -1; - int _forced_redraw_count = 0; - // Some objects (e.g. grids) when destroyed will request redraws. We need to block them when canvas // is destructed. (Windows are destroyed before documents as a document may have several windows. // Changes to documents should not be triggering changes to closed windows. This fix is a hack.) bool _in_destruction = false; - // ======= CAIRO ======= ... Keep in one place + Cairo::RefPtr _background; ///< The background of the widget. - /// Image surface storing the content of the widget. - Cairo::RefPtr _backing_store; ///< The canvas image content. We draw to this then blit. - Cairo::RefPtr _outline_store; ///< The outline image if we are in split/x-ray mode. - - Cairo::RefPtr _background; ///< The background of the image. - bool _background_is_checkerboard = false; - - Cairo::RefPtr _clean_region; ///< Area of widget that has up-to-date content. - - - // Used to update CanvasItemCtrl's when size changed. - class CanvasPrefObserver : public Inkscape::Preferences::Observer { - public: - CanvasPrefObserver(Inkscape::UI::Widget::Canvas *canvas, Glib::ustring const &path) - : Inkscape::Preferences::Observer(path) - , _canvas(canvas) - { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->addObserver(*this); - } - ~CanvasPrefObserver() override = default; - private: - void notify(Inkscape::Preferences::Entry const &entry) override - { - if (entry.getEntryName() == "value") { - int size = entry.getIntLimited(3, 1, 15); - _canvas->update_canvas_item_ctrl_sizes(size); - } - } - Inkscape::UI::Widget::Canvas *_canvas = nullptr; - }; - - CanvasPrefObserver _size_observer; + // Opaque pointer to implementation + friend class CanvasPrivate; + std::unique_ptr d; }; } // namespace Widget } // namespace UI } // namespace Inkscape - #endif // INKSCAPE_UI_WIDGET_CANVAS_H /* diff --git a/src/ui/widget/fill-style.cpp b/src/ui/widget/fill-style.cpp index fd68bcb298b6844205c25ff6f7b70f01580af1a8..b5b9d9abfa60306e10d71c571c6a1a7cc05f8bd7 100644 --- a/src/ui/widget/fill-style.cpp +++ b/src/ui/widget/fill-style.cpp @@ -40,7 +40,6 @@ #include "style.h" #include "ui/icon-names.h" -#include "ui/widget/canvas.h" // Forced redraws // These can be deleted once we sort out the libart dependence. @@ -411,23 +410,12 @@ void FillNStroke::updateFromPaint(bool switch_style) } case UI::Widget::PaintSelector::MODE_SOLID_COLOR: { - if (kind == FILL) { - // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in - // losing release events - _desktop->getCanvas()->forced_redraws_start(0); - } - _psel->setFlatColor(_desktop, (kind == FILL) ? "fill" : "stroke", (kind == FILL) ? "fill-opacity" : "stroke-opacity"); DocumentUndo::maybeDone(_desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, (kind == FILL) ? _("Set fill color") : _("Set stroke color"), INKSCAPE_ICON("dialog-fill-and-stroke")); - if (kind == FILL) { - // resume interruptibility - _desktop->getCanvas()->forced_redraws_stop(); - } - // on release, toggle undo_label so that the next drag will not be lumped with this one if (undo_F_label == undo_F_label_1) { undo_F_label = undo_F_label_2; diff --git a/src/ui/widget/framecheck.cpp b/src/ui/widget/framecheck.cpp new file mode 100644 index 0000000000000000000000000000000000000000..27b3d5bf36e38eb3029661c36ee58fabbe3e50fa --- /dev/null +++ b/src/ui/widget/framecheck.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include // Using boost::filesystem instead of std::filesystem due to broken C++17 on MacOS. +#include "framecheck.h" +namespace fs = boost::filesystem; + +namespace Inkscape { +namespace FrameCheck { + +std::ostream &logfile() +{ + static std::ofstream f; + + if (!f.is_open()) { + try { + auto path = fs::temp_directory_path() / "framecheck.txt"; + auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary; + f.open(path.string(), mode); + } catch (...) { + std::cerr << "failed to create framecheck logfile" << std::endl; + } + } + + return f; +} + +} // namespace FrameCheck +} // namespace Inkscape diff --git a/src/ui/widget/framecheck.h b/src/ui/widget/framecheck.h new file mode 100644 index 0000000000000000000000000000000000000000..36eeea16efcbbe1d27d5982a918b072cbf557617 --- /dev/null +++ b/src/ui/widget/framecheck.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Functions for logging timing events. + * Copyright (C) 2022 PBS + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef FRAMECHECK_H +#define FRAMECHECK_H + +#include +#include + +namespace Inkscape { +namespace FrameCheck { + +extern std::ostream &logfile(); + +// RAII object that logs a timing event for the duration of its lifetime. +struct Event +{ + gint64 start; + const char *name; + int subtype; + + Event() : start(-1) {} + + Event(const char *name, int subtype = 0) : start(g_get_monotonic_time()), name(name), subtype(subtype) {} + + Event(Event &&p) + { + movefrom(p); + } + + ~Event() + { + finish(); + } + + Event &operator=(Event &&p) + { + finish(); + movefrom(p); + return *this; + } + + void movefrom(Event &p) + { + start = p.start; + name = p.name; + subtype = p.subtype; + p.start = -1; + } + + void finish() + { + if (start != -1) { + logfile() << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << '\n'; + } + } +}; + +} // namespace FrameCheck +} // namespace Inkscape + +#endif // FRAMECHECK_H diff --git a/src/ui/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp index 639f8d1a86b1da51e7a506ee61d858aac1a1d5c1..7f32823c7ef7277590efcb0bacc29f39fe3b022a 100644 --- a/src/ui/widget/rotateable.cpp +++ b/src/ui/widget/rotateable.cpp @@ -104,7 +104,6 @@ bool Rotateable::on_motion(GdkEventMotion *event) { do_motion(force, modifier); } } - Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); return true; } return false; diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp index 1a4be9dfda48d6c6edb70f68bb7eb5bf0b57434e..433112c7992ee1e4c6474d110e44923014a293f1 100644 --- a/src/ui/widget/selected-style.cpp +++ b/src/ui/widget/selected-style.cpp @@ -1147,21 +1147,9 @@ void SelectedStyle::on_opacity_changed () Inkscape::CSSOStringStream os; os << CLAMP ((_opacity_adjustment->get_value() / 100), 0.0, 1.0); sp_repr_css_set_property (css, "opacity", os.str().c_str()); - - // FIXME: workaround for GTK breakage: display interruptibility sometimes results in GTK - // sending multiple value-changed events. As if when Inkscape interrupts redraw for main loop - // iterations, GTK discovers that this callback hasn't finished yet, and for some weird reason - // decides to add yet another value-changed event to the queue. Totally braindead if you ask - // me. As a result, scrolling the spinbutton once results in runaway change until it hits 1.0 - // or 0.0. (And no, this is not a race with ::update, I checked that.) - // Sigh. So we disable interruptibility while we're setting the new value. - _desktop->getCanvas()->forced_redraws_start(0); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::maybeDone(_desktop->getDocument(), "fillstroke:opacity", _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke")); - - // resume interruptibility - _desktop->getCanvas()->forced_redraws_stop(); _opacity_blocked = false; } diff --git a/src/widgets/desktop-widget.cpp b/src/widgets/desktop-widget.cpp index 4d6e299090301c7f4eadb5ae8298ae4b6f812af0..c821ebb405b04e8ef2796b175190de8dcfe4d051 100644 --- a/src/widgets/desktop-widget.cpp +++ b/src/widgets/desktop-widget.cpp @@ -1754,8 +1754,8 @@ SPDesktopWidget::update_scrollbars(double scale) /* Canvas region we always show unconditionally */ double const y_dir = desktop->yaxisdir(); - Geom::Rect carea( Geom::Point(deskarea->left() * scale - 64, (deskarea->top() * scale + 64) * y_dir), - Geom::Point(deskarea->right() * scale + 64, (deskarea->bottom() * scale - 64) * y_dir) ); + Geom::Rect carea( Geom::Point(deskarea->left() * scale - 64, (deskarea->top() * scale + 64) * y_dir), + Geom::Point(deskarea->right() * scale + 64, (deskarea->bottom() * scale - 64) * y_dir) ); Geom::Rect viewbox = _canvas->get_area_world();