Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
canvas.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
5/*
6 * Authors:
7 * Tavmjong Bah
8 * PBS <pbs3141@gmail.com>
9 *
10 * Copyright (C) 2022 Authors
11 *
12 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
13 */
14
15#include "canvas.h"
16
17#include <algorithm> // Sort
18#include <array>
19#include <cassert>
20#include <iostream> // Logging
21#include <mutex>
22#include <set> // Coarsener
23#include <thread>
24#include <utility>
25#include <vector>
26#include <boost/asio/thread_pool.hpp>
27#include <boost/asio/post.hpp>
28#include <gtkmm/eventcontrollerfocus.h>
29#include <gtkmm/eventcontrollerkey.h>
30#include <gtkmm/eventcontrollermotion.h>
31#include <gtkmm/eventcontrollerscroll.h>
32#include <gdkmm/frameclock.h>
33#include <gdkmm/glcontext.h>
34#include <gtkmm/applicationwindow.h>
35#include <gtkmm/gestureclick.h>
36#include <sigc++/functors/mem_fun.h>
37
38#include "canvas/fragment.h"
39#include "canvas/graphics.h"
40#include "canvas/prefs.h"
41#include "canvas/stores.h"
42#include "canvas/synchronizer.h"
43#include "canvas/util.h"
45#include "colors/cms/system.h"
46#include "desktop.h"
47#include "desktop-events.h"
51#include "display/drawing.h"
53#include "document.h"
54#include "events/canvas-event.h"
55#include "helper/geom.h"
56#include "preferences.h"
57#include "ui/controller.h"
58#include "ui/tools/tool-base.h" // Default cursor
59#include "ui/util.h"
60
61#include "canvas/updaters.h" // Update strategies
62#include "canvas/framecheck.h" // For frame profiling
63#define framecheck_whole_function(D) \
64 auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event();
65
66/*
67 * The canvas is responsible for rendering the SVG drawing with various "control"
68 * items below and on top of the drawing. Rendering is triggered by a call to one of:
69 *
70 *
71 * * redraw_all() Redraws the entire canvas by calling redraw_area() with the canvas area.
72 *
73 * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't affect
74 * a CanvasItem's geometry or size.
75 *
76 * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a
77 * CanvasItem's geometry or size has changed.
78 *
79 * The first three functions add a request to the Gtk's "idle" list via
80 *
81 * * add_idle() Which causes Gtk to call when resources are available:
82 *
83 * * on_idle() Which sets up the backing stores, divides the area of the canvas that has been marked
84 * unclean into rectangles that are small enough to render quickly, and renders them outwards
85 * from the mouse with a call to:
86 *
87 * * paint_rect_internal() Which paints the rectangle using paint_single_buffer(). It renders onto a Cairo
88 * surface "backing_store". After a piece is rendered there is a call to:
89 *
90 * * queue_draw_area() A Gtk function for marking areas of the window as needing a repaint, which when
91 * the time is right calls:
92 *
93 * * on_draw() Which blits the Cairo surface to the screen.
94 *
95 * The other responsibility of the canvas is to determine where to send GUI events. It does this
96 * by determining which CanvasItem is "picked" and then forwards the events to that item. Not all
97 * items can be picked. As a last resort, the "CatchAll" CanvasItem will be picked as it is the
98 * lowest CanvasItem in the stack (except for the "root" CanvasItem). With a small be of work, it
99 * should be possible to make the "root" CanvasItem a "CatchAll" eliminating the need for a
100 * dedicated "CatchAll" CanvasItem. There probably could be efficiency improvements as some
101 * items that are not pickable probably should be which would save having to effectively pick
102 * them "externally" (e.g. gradient CanvasItemCurves).
103 */
104
105namespace Inkscape::UI::Widget {
106namespace {
107
108/*
109 * Utilities
110 */
111
112// Convert an integer received from preferences into an Updater enum.
113auto pref_to_updater(int index)
114{
115 constexpr auto arr = std::array{Updater::Strategy::Responsive,
118 assert(1 <= index && index <= arr.size());
119 return arr[index - 1];
120}
121
122std::optional<Antialiasing> get_antialiasing_override(bool enabled)
123{
124 if (enabled) {
125 // Default antialiasing, controlled by SVG elements.
126 return {};
127 } else {
128 // Force antialiasing off.
129 return Antialiasing::None;
130 }
131}
132
133// Represents the raster data and location of an in-flight tile (one that is drawn, but not yet pasted into the stores).
134struct Tile
135{
136 Fragment fragment;
137 Cairo::RefPtr<Cairo::ImageSurface> surface;
138 Cairo::RefPtr<Cairo::ImageSurface> outline_surface;
139};
140
141// The urgency with which the async redraw process should exit.
142enum class AbortFlags : int
143{
144 None = 0,
145 Soft = 1, // exit if reached prerender phase
146 Hard = 2 // exit in any phase
147};
148
149// A copy of all the data the async redraw process needs access to, along with its internal state.
150struct RedrawData
151{
152 // Data on what/how to draw.
155 Fragment store;
157 Cairo::RefPtr<Cairo::Region> snapshot_drawn;
158 std::shared_ptr<Colors::CMS::Transform> cms_transform;
159
160 // Saved prefs
167 std::optional<int> redraw_delay;
171 uint64_t page, desk;
174
175 // State
176 std::mutex mutex;
179 int phase;
181
183 Cairo::RefPtr<Cairo::Region> clean;
186 std::vector<Geom::IntRect> rects;
188
189 // Results
190 std::mutex tiles_mutex;
191 std::vector<Tile> tiles;
193
194 // Return comparison object for sorting rectangles by distance from mouse point.
195 auto getcmp() const
196 {
197 return [mouse_loc = mouse_loc] (Geom::IntRect const &a, Geom::IntRect const &b) {
198 return a.distanceSq(mouse_loc) > b.distanceSq(mouse_loc);
199 };
200 }
201};
202
203} // namespace
204
205/*
206 * Implementation class
207 */
208
209class CanvasPrivate
210{
211public:
212 friend class Canvas;
213 Canvas *q;
214 CanvasPrivate(Canvas *q)
215 : q(q)
216 , stores(prefs) {}
217
218 // Lifecycle
219 bool active = false;
220 void activate();
221 void deactivate();
222
223 // CanvasItem tree
224 std::optional<CanvasItemContext> canvasitem_ctx;
225
226 // Preferences
227 Prefs prefs;
228
229 // Stores
230 Stores stores;
231 void handle_stores_action(Stores::Action action);
232
233 // Invalidation
234 std::unique_ptr<Updater> updater; // Tracks the unclean region and decides how to redraw it.
235 Cairo::RefPtr<Cairo::Region> invalidated; // Buffers invalidations while the updater is in use by the background process.
236
237 // Graphics state; holds all the graphics resources, including the drawn content.
238 std::unique_ptr<Graphics> graphics;
239 void activate_graphics();
240 void deactivate_graphics();
241
242 // Redraw process management.
243 bool redraw_active = false;
244 bool redraw_requested = false;
245 sigc::connection schedule_redraw_conn;
246 void schedule_redraw(bool instant = false);
247 void launch_redraw();
248 void after_redraw();
249 void commit_tiles();
250
251 // Event handling.
252 bool process_event(CanvasEvent &event);
253 CanvasItem *find_item_at(Geom::Point pt);
254 bool repick();
255 bool emit_event(CanvasEvent &event);
256 void ensure_geometry_uptodate();
257 CanvasItem *pre_scroll_grabbed_item;
258 unsigned unreleased_presses = 0;
259 bool delayed_leave_event = false;
260
261 // Various state affecting what is drawn.
262 uint32_t desk = 0xffffffff; // The background colour, with the alpha channel used to control checkerboard.
263 uint32_t border = 0x00000000; // The border colour, used only to control shadow colour.
264 uint32_t page = 0xffffffff; // The page colour, also with alpha channel used to control checkerboard.
265
266 bool clip_to_page = false; // Whether to enable clip-to-page mode.
267 PageInfo pi; // The list of page rectangles.
268 std::optional<Geom::PathVector> calc_page_clip() const; // Union of the page rectangles if in clip-to-page mode, otherwise no clip.
269
270 int scale_factor = 1; // The device scale the stores are drawn at.
271
272 RenderMode render_mode = RenderMode::NORMAL;
273 SplitMode split_mode = SplitMode::NORMAL;
274
275 bool outlines_enabled = false; // Whether to enable the outline layer.
276 bool outlines_required() const { return split_mode != SplitMode::NORMAL || render_mode == RenderMode::OUTLINE_OVERLAY; }
277
278 bool background_in_stores_enabled = false; // Whether the page and desk should be drawn into the stores/tiles; if not then transparency is used instead.
279 bool background_in_stores_required() const { return !q->get_opengl_enabled() && SP_RGBA32_A_U(page) == 255 && SP_RGBA32_A_U(desk) == 255; } // Enable solid colour optimisation if both page and desk are solid (as opposed to checkerboard).
280
281 // Async redraw process.
282 std::optional<boost::asio::thread_pool> pool;
283 int numthreads;
284 int get_numthreads() const;
285
286 Synchronizer sync;
287 RedrawData rd;
288 std::atomic<int> abort_flags;
289
290 void init_tiler();
291 bool init_redraw();
292 bool end_redraw(); // returns true to indicate further redraw cycles required
293 void process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible = true, bool preemptible = true);
294 void render_tile(int debug_id);
295 void paint_rect(Geom::IntRect const &rect);
296 void paint_single_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface, const Geom::IntRect &rect, bool need_background, bool outline_pass);
297 void paint_error_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface);
298
299 // Trivial overload of GtkWidget function.
300 void queue_draw_area(Geom::IntRect const &rect);
301
302 // 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.)
303 std::optional<Geom::Point> last_mouse;
304
305 // For tracking the old size in size_allocate_vfunc(). As of GTK4, we only have access to the new size.
306 Geom::IntPoint old_dimensions;
307
308 // Auto-scrolling.
309 std::optional<guint> tick_callback;
310 std::optional<gint64> last_time;
311 Geom::Point strain;
312 Geom::Point displacement, velocity;
313 void autoscroll_begin(Geom::Point const &to);
314 void autoscroll_end();
315};
316
317/*
318 * Lifecycle
319 */
320
322 : d(std::make_unique<CanvasPrivate>(this))
323{
324 set_name("InkscapeCanvas");
325
326 // Events
327 auto const scroll = Gtk::EventControllerScroll::create();
328 scroll->set_flags(Gtk::EventControllerScroll::Flags::BOTH_AXES);
329 scroll->signal_scroll().connect([this, &scroll = *scroll](auto &&...args) { return on_scroll(scroll, args...); }, true);
330 add_controller(scroll);
331
332 auto const click = Gtk::GestureClick::create();
333 click->set_button(0);
334 click->signal_pressed().connect(Controller::use_state([this](auto &&...args) { return on_button_pressed(args...); }, *click));
335 click->signal_released().connect(Controller::use_state([this](auto &&...args) { return on_button_released(args...); }, *click));
336 add_controller(click);
337
338 auto const key = Gtk::EventControllerKey::create();
339 key->signal_key_pressed().connect([this, &key = *key](auto &&...args) { return on_key_pressed(key, args...); }, true);
340 key->signal_key_released().connect([this, &key = *key](auto &&...args) { on_key_released(key, args...); });
341 add_controller(key);
342
343 auto const motion = Gtk::EventControllerMotion::create();
344 motion->signal_enter().connect([this, &motion = *motion](auto &&...args) { on_enter(motion, args...); });
345 motion->signal_motion().connect([this, &motion = *motion](auto &&...args) { on_motion(motion, args...); });
346 motion->signal_leave().connect([this, &motion = *motion](auto &&...args) { on_leave(motion, args...); });
347 add_controller(motion);
348
349 auto const focus = Gtk::EventControllerFocus::create();
350 focus->set_propagation_phase(Gtk::PropagationPhase::BUBBLE);
351 focus->signal_enter().connect(sigc::mem_fun(*this, &Canvas::on_focus_in));
352 focus->signal_leave().connect(sigc::mem_fun(*this, &Canvas::on_focus_out));
353 add_controller(focus);
354
355 // Updater
356 d->updater = Updater::create(pref_to_updater(d->prefs.update_strategy));
357 d->updater->reset();
358 d->invalidated = Cairo::Region::create();
359
360 // Preferences
361 d->prefs.grabsize.action = [this] { d->canvasitem_ctx->root()->update_canvas_item_ctrl_sizes(d->prefs.grabsize); };
362 d->prefs.debug_show_unclean.action = [this] { queue_draw(); };
363 d->prefs.debug_show_clean.action = [this] { queue_draw(); };
364 d->prefs.debug_disable_redraw.action = [this] { d->schedule_redraw(); };
365 d->prefs.debug_sticky_decoupled.action = [this] { d->schedule_redraw(); };
366 d->prefs.debug_animate.action = [this] { queue_draw(); };
367 d->prefs.outline_overlay_opacity.action = [this] { queue_draw(); };
368 d->prefs.softproof.action = [this] { set_cms_transform(); redraw_all(); };
369 d->prefs.displayprofile.action = [this] { set_cms_transform(); redraw_all(); };
370 d->prefs.request_opengl.action = [this] {
371 if (get_realized()) {
372 d->deactivate();
373 d->deactivate_graphics();
374 set_opengl_enabled(d->prefs.request_opengl);
375 d->updater->reset();
376 d->activate_graphics();
377 d->activate();
378 }
379 };
380 d->prefs.pixelstreamer_method.action = [this] {
381 if (get_realized() && get_opengl_enabled()) {
382 d->deactivate();
383 d->deactivate_graphics();
384 d->activate_graphics();
385 d->activate();
386 }
387 };
388 d->prefs.numthreads.action = [this] {
389 if (!d->active) return;
390 int const new_numthreads = d->get_numthreads();
391 if (d->numthreads == new_numthreads) return;
392 d->numthreads = new_numthreads;
393 d->deactivate();
394 d->deactivate_graphics();
395 d->pool.emplace(d->numthreads);
396 d->activate_graphics();
397 d->activate();
398 };
399
400 // Canvas item tree
401 d->canvasitem_ctx.emplace(this);
402
403 // Split view.
405 _split_frac = {0.5, 0.5};
406
407 // CMS Set initial CMS transform.
409 // If we have monitor dependence: signal_map().connect([this]() { this->set_cms_transform(); });
410
411 // Recreate stores on HiDPI change.
412 property_scale_factor().signal_changed().connect([this] { d->schedule_redraw(); });
413
414 // OpenGL switch.
415 set_opengl_enabled(d->prefs.request_opengl);
416
417 // Async redraw process.
418 d->numthreads = d->get_numthreads();
419 d->pool.emplace(d->numthreads);
420
421 d->sync.connectExit([this] { d->after_redraw(); });
422}
423
424int CanvasPrivate::get_numthreads() const
425{
426 if (int n = prefs.numthreads; n > 0) {
427 // First choice is the value set in preferences.
428 return n;
429 } else if (int n = std::thread::hardware_concurrency(); n > 0) {
430 // If not set, use the number of processors minus one. (Using all of them causes stuttering.)
431 return n == 1 ? 1 : n - 1;
432 } else {
433 // If not reported, use a sensible fallback.
434 return 4;
435 }
436}
437
438// Graphics becomes active when the widget is realized.
439void CanvasPrivate::activate_graphics()
440{
441 if (q->get_opengl_enabled()) {
442 q->make_current();
443 graphics = Graphics::create_gl(prefs, stores, pi);
444 } else {
445 graphics = Graphics::create_cairo(prefs, stores, pi);
446 }
447 stores.set_graphics(graphics.get());
448 stores.reset();
449}
450
451// After graphics becomes active, the canvas becomes active when additionally a drawing is set.
452void CanvasPrivate::activate()
453{
454 q->_left_grabbed_item = false;
455 q->_all_enter_events = false;
456 q->_is_dragging = false;
457 q->_state = 0;
458
459 q->_current_canvas_item = nullptr;
460 q->_current_canvas_item_new = nullptr;
461 q->_grabbed_canvas_item = nullptr;
462 q->_grabbed_event_mask = {};
463 pre_scroll_grabbed_item = nullptr;
464
465 // Drawing
466 q->_need_update = true;
467
468 // Split view
469 q->_split_dragging = false;
470
471 active = true;
472
473 schedule_redraw(true);
474}
475
476void CanvasPrivate::deactivate()
477{
478 active = false;
479
480 if (redraw_active) {
481 if (schedule_redraw_conn) {
482 // In first link in chain, from schedule_redraw() to launch_redraw(). Break the link and exit.
483 schedule_redraw_conn.disconnect();
484 } else {
485 // Otherwise, the background process is running. Interrupt the signal chain at exit.
486 abort_flags.store((int)AbortFlags::Hard, std::memory_order_relaxed);
487 if (prefs.debug_logging) std::cout << "Hard exit request" << std::endl;
488 sync.waitForExit();
489
490 // Unsnapshot the CanvasItems and DrawingItems.
491 canvasitem_ctx->unsnapshot();
492 q->_drawing->unsnapshot();
493 }
494
495 redraw_active = false;
496 redraw_requested = false;
497 assert(!schedule_redraw_conn);
498 }
499}
500
501void CanvasPrivate::deactivate_graphics()
502{
503 if (q->get_opengl_enabled()) q->make_current();
504 commit_tiles();
505 stores.set_graphics(nullptr);
506 graphics.reset();
507}
508
510{
511 // Handle missed unrealisation due to new GTK4 lifetimes allowing C object to persist.
512 if (d->active) d->deactivate();
513 if (d->graphics) d->deactivate_graphics();
514
515 // Remove entire CanvasItem tree.
516 d->canvasitem_ctx.reset();
517}
518
520{
521 if (d->active && !drawing) d->deactivate();
522 _drawing = drawing;
523 if (_drawing) {
526 _drawing->setOutlineOverlay(d->outlines_required());
527 _drawing->setAntialiasingOverride(get_antialiasing_override(_antialiasing_enabled));
528 }
529 if (!d->active && get_realized() && drawing) d->activate();
530}
531
533{
534 return d->canvasitem_ctx->root();
535}
536
538{
540 d->activate_graphics();
541 if (_drawing) d->activate();
542}
543
545{
546 if (_drawing) d->deactivate();
547 d->deactivate_graphics();
549}
550
551/*
552 * Redraw process managment
553 */
554
555// Schedule another redraw iteration to take place, waiting for the current one to finish if necessary.
556void CanvasPrivate::schedule_redraw(bool instant)
557{
558 if (!active) {
559 // We can safely discard calls until active, because we will call this again later in activate().
560 return;
561 }
562
563 if (q->get_width() == 0 || q->get_height() == 0) {
564 // Similarly, we can safely discard calls until we are assigned a valid size,
565 // because we will call this again when that happens in size_allocate_vfunc().
566 return;
567 }
568
569 // Ensure another iteration is performed if one is in progress.
570 redraw_requested = true;
571
572 if (redraw_active) {
573 if (schedule_redraw_conn && instant) {
574 // skip a scheduled redraw and launch it instantly
575 } else {
576 return;
577 }
578 }
579
580 redraw_active = true;
581
582 auto callback = [this] {
583 if (q->get_opengl_enabled()) {
584 q->make_current();
585 }
586 if (prefs.debug_logging) std::cout << "Redraw start" << std::endl;
587 launch_redraw();
588 };
589
590 if (instant) {
591 schedule_redraw_conn.disconnect();
592 callback();
593 } else {
594 assert(!schedule_redraw_conn);
595 schedule_redraw_conn = Glib::signal_idle().connect([=] { callback(); return false; });
596 // Note: Any higher priority than default results in competition with other idle callbacks,
597 // causing flickering snap indicators - https://gitlab.com/inkscape/inkscape/-/issues/4242
598 }
599}
600
601// Update state and launch redraw process in background. Requires a current OpenGL context.
602void CanvasPrivate::launch_redraw()
603{
604 assert(redraw_active);
605
606 if (q->_render_mode != render_mode) {
607 if ((render_mode == RenderMode::OUTLINE_OVERLAY) != (q->_render_mode == RenderMode::OUTLINE_OVERLAY) && !q->get_opengl_enabled()) {
608 q->queue_draw(); // Clear the whitewash effect, an artifact of cairo mode.
609 }
610 render_mode = q->_render_mode;
611 q->_drawing->setRenderMode(render_mode == RenderMode::OUTLINE_OVERLAY ? RenderMode::NORMAL : render_mode);
612 q->_drawing->setOutlineOverlay(outlines_required());
613 }
614
615 if (q->_split_mode != split_mode) {
616 q->queue_draw(); // Clear the splitter overlay.
617 split_mode = q->_split_mode;
618 q->_drawing->setOutlineOverlay(outlines_required());
619 }
620
621 // Determine whether the rendering parameters have changed, and trigger full store recreation if so.
622 if ((outlines_required() && !outlines_enabled) || scale_factor != q->get_scale_factor()) {
623 stores.reset();
624 }
625
626 outlines_enabled = outlines_required();
627 scale_factor = q->get_scale_factor();
628
629 graphics->set_outlines_enabled(outlines_enabled);
630 graphics->set_scale_factor(scale_factor);
631
632 /*
633 * Update state.
634 */
635
636 // Page information.
637 pi.pages.clear();
638 canvasitem_ctx->root()->visit_page_rects([this] (auto &rect) {
639 pi.pages.emplace_back(rect);
640 });
641
642 graphics->set_colours(page, desk, border);
643 graphics->set_background_in_stores(background_in_stores_required());
644
645 q->_drawing->setClip(calc_page_clip());
646
647 // Stores.
648 handle_stores_action(stores.update(Fragment{ q->_affine, q->get_area_world() }));
649
650 // Geometry.
651 bool const affine_changed = canvasitem_ctx->affine() != stores.store().affine;
652 if (q->_need_update || affine_changed) {
653 FrameCheck::Event fc;
654 if (prefs.debug_framecheck) fc = FrameCheck::Event("update");
655 q->_need_update = false;
656 canvasitem_ctx->setAffine(stores.store().affine);
657 canvasitem_ctx->root()->update(affine_changed);
658 }
659
660 // Update strategy.
661 auto const strategy = pref_to_updater(prefs.update_strategy);
662 if (updater->get_strategy() != strategy) {
663 auto new_updater = Updater::create(strategy);
664 new_updater->clean_region = std::move(updater->clean_region);
665 updater = std::move(new_updater);
666 }
667
668 updater->mark_dirty(invalidated);
669 invalidated = Cairo::Region::create();
670
671 updater->next_frame();
672
673 /*
674 * Launch redraw process in background.
675 */
676
677 // If asked to, don't paint anything and instead halt the redraw process.
678 if (prefs.debug_disable_redraw) {
679 redraw_active = false;
680 return;
681 }
682
683 redraw_requested = false;
684
685 // Snapshot the CanvasItems and DrawingItems.
686 canvasitem_ctx->snapshot();
687 q->_drawing->snapshot();
688
689 // Get the mouse position in screen space.
690 rd.mouse_loc = last_mouse.value_or(Geom::Point(q->get_dimensions()) / 2).round();
691
692 // Map the mouse to canvas space.
693 rd.mouse_loc += q->_pos;
694 if (stores.mode() == Stores::Mode::Decoupled) {
695 rd.mouse_loc = (Geom::Point(rd.mouse_loc) * q->_affine.inverse() * stores.store().affine).round();
696 }
697
698 // Get the visible rect.
699 rd.visible = q->get_area_world();
700 if (stores.mode() == Stores::Mode::Decoupled) {
701 rd.visible = (Geom::Parallelogram(rd.visible) * q->_affine.inverse() * stores.store().affine).bounds().roundOutwards();
702 }
703
704 // Get other misc data.
705 rd.store = Fragment{ stores.store().affine, stores.store().rect };
706 rd.decoupled_mode = stores.mode() == Stores::Mode::Decoupled;
707 rd.coarsener_min_size = prefs.coarsener_min_size;
708 rd.coarsener_glue_size = prefs.coarsener_glue_size;
709 rd.coarsener_min_fullness = prefs.coarsener_min_fullness;
710 rd.tile_size = prefs.tile_size;
711 rd.preempt = prefs.preempt;
712 rd.margin = prefs.prerender;
713 rd.redraw_delay = prefs.debug_delay_redraw ? std::make_optional<int>(prefs.debug_delay_redraw_time) : std::nullopt;
714 rd.render_time_limit = prefs.render_time_limit;
715 rd.numthreads = get_numthreads();
716 rd.background_in_stores_required = background_in_stores_required();
717 rd.page = page;
718 rd.desk = desk;
719 rd.debug_framecheck = prefs.debug_framecheck;
720 rd.debug_show_redraw = prefs.debug_show_redraw;
721
722 rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr<Cairo::Region>();
723 rd.cms_transform = q->_cms_active ? q->_cms_transform : nullptr;
724
725 abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed);
726
727 boost::asio::post(*pool, [this] { init_tiler(); });
728}
729
730void CanvasPrivate::after_redraw()
731{
732 assert(redraw_active);
733
734 // Unsnapshot the CanvasItems and DrawingItems.
735 canvasitem_ctx->unsnapshot();
736 q->_drawing->unsnapshot();
737
738 // OpenGL context needed for commit_tiles(), stores.finished_draw(), and launch_redraw().
739 if (q->get_opengl_enabled()) {
740 q->make_current();
741 }
742
743 // Commit tiles before stores.finished_draw() to avoid changing stores while tiles are still pending.
744 commit_tiles();
745
746 // Handle any pending stores action.
747 bool stores_changed = false;
748 if (!rd.timeoutflag) {
749 auto const ret = stores.finished_draw(Fragment{ q->_affine, q->get_area_world() });
750 handle_stores_action(ret);
751 if (ret != Stores::Action::None) {
752 stores_changed = true;
753 }
754 }
755
756 // Relaunch or stop as necessary.
757 if (rd.timeoutflag || redraw_requested || stores_changed) {
758 if (prefs.debug_logging) std::cout << "Continuing redrawing" << std::endl;
759 redraw_requested = false;
760 launch_redraw();
761 } else {
762 if (prefs.debug_logging) std::cout << "Redraw exit" << std::endl;
763 redraw_active = false;
764 }
765}
766
767void CanvasPrivate::handle_stores_action(Stores::Action action)
768{
769 switch (action) {
771 // Set everything as needing redraw.
772 invalidated->do_union(geom_to_cairo(stores.store().rect));
773 updater->reset();
774
775 if (prefs.debug_show_unclean) q->queue_draw();
776 break;
777
779 invalidated->intersect(geom_to_cairo(stores.store().rect));
780 updater->intersect(stores.store().rect);
781
782 if (prefs.debug_show_unclean) q->queue_draw();
783 break;
784
785 default:
786 break;
787 }
788
789 if (action != Stores::Action::None) {
790 q->_drawing->setCacheLimit(stores.store().rect);
791 }
792}
793
794// Commit all in-flight tiles to the stores. Requires a current OpenGL context (for graphics->draw_tile).
795void CanvasPrivate::commit_tiles()
796{
797 framecheck_whole_function(this)
798
799 decltype(rd.tiles) tiles;
800
801 {
802 auto lock = std::lock_guard(rd.tiles_mutex);
803 tiles = std::move(rd.tiles);
804 }
805
806 for (auto &tile : tiles) {
807 // Paste tile content onto stores.
808 graphics->draw_tile(tile.fragment, std::move(tile.surface), std::move(tile.outline_surface));
809
810 // Add to drawn region.
811 assert(stores.store().rect.contains(tile.fragment.rect));
812 stores.mark_drawn(tile.fragment.rect);
813
814 // Get the rectangle of screen-space needing repaint.
815 Geom::IntRect repaint_rect;
816 if (stores.mode() == Stores::Mode::Normal) {
817 // Simply translate to get back to screen space.
818 repaint_rect = tile.fragment.rect - q->_pos;
819 } else {
820 // Transform into screen space, take bounding box, and round outwards.
821 auto pl = Geom::Parallelogram(tile.fragment.rect);
822 pl *= stores.store().affine.inverse() * q->_affine;
823 pl *= Geom::Translate(-q->_pos);
824 repaint_rect = pl.bounds().roundOutwards();
825 }
826
827 // Check if repaint is necessary - some rectangles could be entirely off-screen.
828 auto screen_rect = Geom::IntRect({0, 0}, q->get_dimensions());
829 if ((repaint_rect & screen_rect).regularized()) {
830 // Schedule repaint.
831 queue_draw_area(repaint_rect);
832 }
833 }
834}
835
836/*
837 * Auto-scrolling
838 */
839
840static Geom::Point cap_length(Geom::Point const &pt, double max)
841{
842 auto const r = pt.length();
843 return r <= max ? pt : pt * (max / r);
844}
845
846static double profile(double r)
847{
848 constexpr double max_speed = 30.0;
849 constexpr double max_distance = 25.0;
850 return std::clamp(Geom::sqr(r / max_distance) * max_speed, 1.0, max_speed);
851}
852
854{
855 auto const r = pt.length();
856 if (r <= Geom::EPSILON) return {};
857 return pt * profile(r) / r;
858}
859
860void CanvasPrivate::autoscroll_begin(Geom::Point const &to)
861{
862 if (!q->_desktop) {
863 return;
864 }
865
866 auto const rect = expandedBy(Geom::Rect({}, q->get_dimensions()), -(int)prefs.autoscrolldistance);
867 strain = to - rect.clamp(to);
868
869 if (strain == Geom::Point(0, 0) || tick_callback) {
870 return;
871 }
872
873 tick_callback = q->add_tick_callback([this] (Glib::RefPtr<Gdk::FrameClock> const &clock) {
874 auto timings = clock->get_current_timings();
875 auto const t = timings->get_frame_time();
876 double dt;
877 if (last_time) {
878 dt = t - *last_time;
879 } else {
880 dt = timings->get_refresh_interval();
881 }
882 last_time = t;
883 dt *= 60.0 / 1e6 * prefs.autoscrollspeed;
884
885 bool const strain_zero = strain == Geom::Point(0, 0);
886
887 if (strain.x() * velocity.x() < 0) velocity.x() = 0;
888 if (strain.y() * velocity.y() < 0) velocity.y() = 0;
889 auto const tgtvel = apply_profile(strain);
890 auto const max_accel = strain_zero ? 3 : 2;
891 velocity += cap_length(tgtvel - velocity, max_accel * dt);
892 displacement += velocity * dt;
893 auto const dpos = displacement.round();
894 q->_desktop->scroll_relative(-dpos);
895 displacement -= dpos;
896
897 if (last_mouse) {
898 ensure_geometry_uptodate();
899 auto event = MotionEvent();
900 event.modifiers = q->_state;
901 event.pos = *last_mouse;
902 emit_event(event);
903 }
904
905 if (strain_zero && velocity.length() <= 0.1) {
906 tick_callback = {};
907 last_time = {};
908 displacement = velocity = {};
909 return false;
910 }
911
912 return true;
913 });
914}
915
916void CanvasPrivate::autoscroll_end()
917{
918 strain = {};
919}
920
921// Allow auto-scrolling to take place if the mouse reaches the edge.
922// The effect wears off when the mouse is next released.
924{
925 if (d->last_mouse) {
926 d->autoscroll_begin(*d->last_mouse);
927 } else {
928 d->autoscroll_end();
929 }
930}
931
932/*
933 * Event handling
934 */
935
936bool Canvas::on_scroll(Gtk::EventControllerScroll const &controller, double dx, double dy)
937{
938 auto gdkevent = controller.get_current_event();
939 _state = (int)controller.get_current_event_state();
940
941 auto event = ScrollEvent();
942 event.modifiers = _state;
943 event.device = controller.get_current_event_device();
944 event.delta = { dx, dy };
945 event.unit = controller.get_unit();
946 event.extinput = extinput_from_gdkevent(*gdkevent);
947
948 return d->process_event(event);
949}
950
951Gtk::EventSequenceState Canvas::on_button_pressed(Gtk::GestureClick const &controller,
952 int n_press, double x, double y)
953{
954 _state = (int)controller.get_current_event_state();
955 d->last_mouse = Geom::Point(x, y);
956 d->unreleased_presses |= 1 << controller.get_current_button();
957
958 grab_focus();
959
960 if (controller.get_current_button() == 3) {
961 _drawing->getCanvasItemDrawing()->set_sticky(_state & GDK_SHIFT_MASK);
962 }
963
964 // Drag the split view controller.
966 if (n_press == 1) {
967 _split_dragging = true;
969 return Gtk::EventSequenceState::CLAIMED;
970 } else if (n_press == 2) {
972 _split_dragging = false;
973 queue_draw();
974 return Gtk::EventSequenceState::CLAIMED;
975 }
976 }
977
978 auto event = ButtonPressEvent();
979 event.modifiers = _state;
980 event.device = controller.get_current_event_device();
981 event.pos = *d->last_mouse;
982 event.button = controller.get_current_button();
983 event.time = controller.get_current_event_time();
984 event.num_press = 1;
985 event.extinput = extinput_from_gdkevent(*controller.get_current_event());
986
987 bool result = d->process_event(event);
988
989 if (n_press > 1) {
990 event.num_press = n_press;
991 result = d->process_event(event);
992 }
993
994 return result ? Gtk::EventSequenceState::CLAIMED : Gtk::EventSequenceState::NONE;
995}
996
997Gtk::EventSequenceState Canvas::on_button_released(Gtk::GestureClick const &controller,
998 int /*n_press*/, double x, double y)
999{
1000 _state = (int)controller.get_current_event_state();
1001 d->last_mouse = Geom::Point(x, y);
1002 d->unreleased_presses &= ~(1 << controller.get_current_button());
1003
1004 // Drag the split view controller.
1006 _split_dragging = false;
1007
1008 // Check if we are near the edge. If so, revert to normal mode.
1009 if (x < 5 ||
1010 y < 5 ||
1011 x > get_allocation().get_width() - 5 ||
1012 y > get_allocation().get_height() - 5)
1013 {
1014 // Reset everything.
1015 update_cursor();
1017
1018 // Update action (turn into utility function?).
1019 auto window = dynamic_cast<Gtk::ApplicationWindow*>(get_root());
1020 if (!window) {
1021 std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl;
1022 return Gtk::EventSequenceState::CLAIMED;
1023 }
1024
1025 auto action = window->lookup_action("canvas-split-mode");
1026 if (!action) {
1027 std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl;
1028 return Gtk::EventSequenceState::CLAIMED;
1029 }
1030
1031 auto saction = std::dynamic_pointer_cast<Gio::SimpleAction>(action);
1032 if (!saction) {
1033 std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl;
1034 return Gtk::EventSequenceState::CLAIMED;
1035 }
1036
1037 saction->change_state(static_cast<int>(SplitMode::NORMAL));
1038 }
1039 }
1040
1041 auto const button = controller.get_current_button();
1042 if (button == 1) {
1043 d->autoscroll_end();
1044 }
1045
1046 auto event = ButtonReleaseEvent();
1047 event.modifiers = _state;
1048 event.device = controller.get_current_event_device();
1049 event.pos = *d->last_mouse;
1050 event.button = controller.get_current_button();
1051 event.time = controller.get_current_event_time();
1052
1053 auto result = d->process_event(event) ? Gtk::EventSequenceState::CLAIMED : Gtk::EventSequenceState::NONE;
1054
1055 if (d->unreleased_presses == 0 && d->delayed_leave_event) {
1056 d->last_mouse = {};
1057 d->delayed_leave_event = false;
1058
1059 auto event = LeaveEvent();
1060 event.modifiers = _state;
1061
1062 d->process_event(event);
1063 }
1064
1065 return result;
1066}
1067
1068void Canvas::on_enter(Gtk::EventControllerMotion const &controller, double x, double y)
1069{
1070 if (d->delayed_leave_event) {
1071 d->delayed_leave_event = false;
1072 return;
1073 }
1074
1075 _state = (int)controller.get_current_event_state();
1076 d->last_mouse = Geom::Point(x, y);
1077
1078 auto event = EnterEvent();
1079 event.modifiers = _state;
1080 event.pos = *d->last_mouse;
1081
1082 d->process_event(event);
1083}
1084
1085void Canvas::on_leave(Gtk::EventControllerMotion const &controller)
1086{
1087 if (d->unreleased_presses != 0) {
1088 d->delayed_leave_event = true;
1089 return;
1090 }
1091
1092 _state = (int)controller.get_current_event_state();
1093 d->last_mouse = {};
1094
1095 auto event = LeaveEvent();
1096 event.modifiers = _state;
1097
1098 d->process_event(event);
1099}
1100
1102{
1103 grab_focus(); // Why? Is this even needed anymore?
1104 _signal_focus_in.emit();
1105}
1106
1108{
1109 _signal_focus_out.emit();
1110}
1111
1112bool Canvas::on_key_pressed(Gtk::EventControllerKey const &controller,
1113 unsigned keyval, unsigned keycode, Gdk::ModifierType state)
1114{
1115 _state = static_cast<int>(state);
1116
1117 auto event = KeyPressEvent();
1118 event.modifiers = _state;
1119 event.device = controller.get_current_event_device();
1120 event.keyval = keyval;
1121 event.keycode = keycode;
1122 event.group = controller.get_group();
1123 event.time = controller.get_current_event_time();
1124 event.pos = d->last_mouse;
1125
1126 return d->process_event(event);
1127}
1128
1129void Canvas::on_key_released(Gtk::EventControllerKey const &controller,
1130 unsigned keyval, unsigned keycode, Gdk::ModifierType state)
1131{
1132 _state = static_cast<int>(state);
1133
1134 auto event = KeyReleaseEvent();
1135 event.modifiers = _state;
1136 event.device = controller.get_current_event_device();
1137 event.keyval = keyval;
1138 event.keycode = keycode;
1139 event.group = controller.get_group();
1140 event.time = controller.get_current_event_time();
1141 event.pos = d->last_mouse;
1142
1143 d->process_event(event);
1144}
1145
1146void Canvas::on_motion(Gtk::EventControllerMotion const &controller, double x, double y)
1147{
1148 auto const mouse = Geom::Point{x, y};
1149 if (mouse == d->last_mouse) {
1150 return; // Scrolling produces spurious motion events; discard them.
1151 }
1152 d->last_mouse = mouse;
1153
1154 _state = (int)controller.get_current_event_state();
1155
1156 // Handle interactions with the split view controller.
1158 queue_draw();
1159 } else if (_split_mode == SplitMode::SPLIT) {
1160 auto cursor_position = mouse.floor();
1161
1162 // Move controller.
1163 if (_split_dragging) {
1164 auto delta = cursor_position - _split_drag_start;
1166 delta.x() = 0;
1168 delta.y() = 0;
1169 }
1171 _split_drag_start = cursor_position;
1172 queue_draw();
1173 return;
1174 }
1175
1176 auto split_position = (_split_frac * get_dimensions()).round();
1177 auto diff = cursor_position - split_position;
1178 auto hover_direction = SplitDirection::NONE;
1179 if (Geom::Point(diff).length() < 20.0) {
1180 // We're hovering over circle, figure out which direction we are in.
1181 if (diff.y() - diff.x() > 0) {
1182 if (diff.y() + diff.x() > 0) {
1183 hover_direction = SplitDirection::SOUTH;
1184 } else {
1185 hover_direction = SplitDirection::WEST;
1186 }
1187 } else {
1188 if (diff.y() + diff.x() > 0) {
1189 hover_direction = SplitDirection::EAST;
1190 } else {
1191 hover_direction = SplitDirection::NORTH;
1192 }
1193 }
1196 {
1197 if (std::abs(diff.y()) < 3) {
1198 // We're hovering over the horizontal line.
1199 hover_direction = SplitDirection::HORIZONTAL;
1200 }
1201 } else {
1202 if (std::abs(diff.x()) < 3) {
1203 // We're hovering over the vertical line.
1204 hover_direction = SplitDirection::VERTICAL;
1205 }
1206 }
1207
1208 if (_hover_direction != hover_direction) {
1209 _hover_direction = hover_direction;
1210 update_cursor();
1211 queue_draw();
1212 }
1213
1215 // We're hovering, don't pick or emit event.
1216 return;
1217 }
1218 }
1219
1220 // Avoid embarrassing neverending autoscroll in case the button-released handler somehow doesn't fire.
1221 if (!(_state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) {
1222 d->autoscroll_end();
1223 }
1224
1225 auto event = MotionEvent();
1226 event.modifiers = _state;
1227 event.device = controller.get_current_event_device();
1228 event.pos = *d->last_mouse;
1229 event.time = controller.get_current_event_time();
1230 event.extinput = extinput_from_gdkevent(*controller.get_current_event());
1231
1232 d->process_event(event);
1233}
1234
1240bool CanvasPrivate::process_event(CanvasEvent &event)
1241{
1242 framecheck_whole_function(this)
1243
1244 if (!active) {
1245 std::cerr << "Canvas::process_event: Called while not active!" << std::endl;
1246 return false;
1247 }
1248
1249 // Do event-specific processing.
1250 switch (event.type()) {
1251 case EventType::SCROLL: {
1252 // Save the current event-receiving item just before scrolling starts. It will continue to receive scroll events until the mouse is moved.
1253 if (!pre_scroll_grabbed_item) {
1254 pre_scroll_grabbed_item = q->_current_canvas_item;
1255 if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
1256 pre_scroll_grabbed_item = q->_grabbed_canvas_item;
1257 }
1258 }
1259
1260 // Process the scroll event...
1261 bool retval = emit_event(event);
1262
1263 // ...then repick.
1264 repick();
1265
1266 return retval;
1267 }
1268
1270 pre_scroll_grabbed_item = nullptr;
1271
1272 // Pick the current item as if the button were not pressed...
1273 repick();
1274
1275 // ...then process the event after the button has been pressed.
1276 q->_state = event.modifiersAfter();
1277 return emit_event(event);
1278 }
1279
1281 pre_scroll_grabbed_item = nullptr;
1282
1283 // Process the event as if the button were pressed...
1284 bool retval = emit_event(event);
1285
1286 // ...then repick after the button has been released.
1287 q->_state = event.modifiersAfter();
1288 repick();
1289
1290 return retval;
1291 }
1292
1293 case EventType::ENTER:
1294 pre_scroll_grabbed_item = nullptr;
1295 return repick();
1296
1297 case EventType::LEAVE:
1298 pre_scroll_grabbed_item = nullptr;
1299 // This is needed to remove alignment or distribution snap indicators.
1300 if (q->_desktop) {
1301 q->_desktop->getSnapIndicator()->remove_snaptarget();
1302 }
1303 return repick();
1304
1307 return emit_event(event);
1308
1309 case EventType::MOTION:
1310 pre_scroll_grabbed_item = nullptr;
1311 repick();
1312 return emit_event(event);
1313
1314 default:
1315 return false;
1316 }
1317}
1318
1327CanvasItem *CanvasPrivate::find_item_at(Geom::Point pt)
1328{
1329 // Look at where the cursor is to see if one should pick with outline mode.
1330 bool outline = q->canvas_point_in_outline_zone(pt);
1331
1332 // Convert to world coordinates.
1333 pt += q->_pos;
1334 if (stores.mode() == Stores::Mode::Decoupled) {
1335 pt *= q->_affine.inverse() * canvasitem_ctx->affine();
1336 }
1337
1338 q->_drawing->getCanvasItemDrawing()->set_pick_outline(outline);
1339 return canvasitem_ctx->root()->pick_item(pt);
1340}
1341
1357bool CanvasPrivate::repick()
1358{
1359 // Ensure requested geometry updates are performed first.
1360 ensure_geometry_uptodate();
1361
1362 bool button_down = false;
1363 if (!q->_all_enter_events) {
1364 // Only set true in connector-tool.cpp.
1365
1366 // If a button is down, we'll perform enter and leave events on the
1367 // current item, but not enter on any other item. This is more or
1368 // less like X pointer grabbing for canvas items.
1369 button_down = q->_state & (GDK_BUTTON1_MASK |
1370 GDK_BUTTON2_MASK |
1371 GDK_BUTTON3_MASK |
1372 GDK_BUTTON4_MASK |
1373 GDK_BUTTON5_MASK);
1374 if (!button_down) {
1375 q->_left_grabbed_item = false;
1376 }
1377 }
1378
1379 // Find new item
1380 q->_current_canvas_item_new = nullptr;
1381
1382 // An empty last_mouse means there is no new item (i.e. cursor has left the canvas).
1383 if (last_mouse && canvasitem_ctx->root()->is_visible()) {
1384 q->_current_canvas_item_new = find_item_at(*last_mouse);
1385 // if (q->_current_canvas_item_new) {
1386 // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl;
1387 // } else {
1388 // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl;
1389 // }
1390 }
1391
1392 if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) {
1393 // Current item did not change!
1394 return false;
1395 }
1396
1397 // Synthesize events for old and new current items.
1398 bool retval = false;
1399 if (q->_current_canvas_item_new != q->_current_canvas_item &&
1400 q->_current_canvas_item &&
1401 !q->_left_grabbed_item)
1402 {
1403 auto event = LeaveEvent();
1404 event.modifiers = q->_state;
1405 retval = emit_event(event);
1406 }
1407
1408 if (!q->_all_enter_events) {
1409 // new_current_item may have been set to nullptr during the call to emitEvent() above.
1410 if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) {
1411 q->_left_grabbed_item = true;
1412 return retval;
1413 }
1414 }
1415
1416 // Handle the rest of cases
1417 q->_left_grabbed_item = false;
1418 q->_current_canvas_item = q->_current_canvas_item_new;
1419
1420 if (q->_current_canvas_item) {
1421 auto event = EnterEvent();
1422 event.modifiers = q->_state;
1423 event.pos = *last_mouse; // nonempty by if condition
1424 retval = emit_event(event);
1425 }
1426
1427 return retval;
1428}
1429
1445bool CanvasPrivate::emit_event(CanvasEvent &event)
1446{
1447 ensure_geometry_uptodate();
1448
1449 // Handle grabbed items.
1450 if (q->_grabbed_canvas_item && !(event.type() & q->_grabbed_event_mask)) {
1451 return false;
1452 }
1453
1454 // Convert to world coordinates.
1455 auto conv = [&, this](Geom::Point &p, Geom::Point *orig_pos = nullptr) {
1456 // Store orig point in case anyone needs it for widget-relative positioning.
1457 if (orig_pos) {
1458 *orig_pos = p;
1459 }
1460
1461 p += q->_pos;
1462 if (stores.mode() == Stores::Mode::Decoupled) {
1463 p = p * q->_affine.inverse() * canvasitem_ctx->affine();
1464 }
1465 };
1466
1467 inspect_event(event,
1468 [&] (EnterEvent &event) { conv(event.pos); },
1469 [&] (MotionEvent &event) { conv(event.pos); },
1470 [&] (ButtonPressEvent &event) {
1471 // on_button_pressed() will call process_event() and therefore also emit_event() twice, on the same event
1472 if (event.num_press == 1) { // Only convert the coordinates once, on the first call
1473 conv(event.pos, &event.orig_pos);
1474 }
1475 },
1476 [&] (ButtonReleaseEvent &event) { conv(event.pos); },
1477 [&] (KeyEvent &event) {
1478 if (event.pos) {
1479 event.orig_pos.emplace();
1480 conv(*event.pos, &*event.orig_pos);
1481 }
1482 },
1483 [&] (CanvasEvent &event) {}
1484 );
1485
1486 // Block undo/redo while anything is dragged.
1487 inspect_event(event,
1488 [&] (ButtonPressEvent &event) {
1489 if (event.button == 1) {
1490 q->_is_dragging = true;
1491 }
1492 },
1493 [&] (ButtonReleaseEvent &event) { q->_is_dragging = false; },
1494 [&] (CanvasEvent &event) {}
1495 );
1496
1497 if (q->_current_canvas_item) {
1498 // Choose where to send event.
1499 auto item = q->_current_canvas_item;
1500
1501 if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
1502 item = q->_grabbed_canvas_item;
1503 }
1504
1505 if (pre_scroll_grabbed_item && event.type() == EventType::SCROLL) {
1506 item = pre_scroll_grabbed_item;
1507 }
1508
1509 // Propagate the event up the canvas item hierarchy until handled.
1510 while (item) {
1511 if (item->handle_event(event)) return true;
1512 item = item->get_parent();
1513 }
1514 } else if (q->_desktop && (event.type() == EventType::KEY_PRESS || event.type() == EventType::KEY_RELEASE)) {
1515 // Pass keyboard events back to the desktop root handler so TextTool can work
1516 return sp_desktop_root_handler(event, q->_desktop);
1517 }
1518
1519 return false;
1520}
1521
1522void CanvasPrivate::ensure_geometry_uptodate()
1523{
1524 if (q->_need_update && !q->_drawing->snapshotted() && !canvasitem_ctx->snapshotted()) {
1525 FrameCheck::Event fc;
1526 if (prefs.debug_framecheck) fc = FrameCheck::Event("update", 1);
1527 q->_need_update = false;
1528 canvasitem_ctx->root()->update(false);
1529 }
1530}
1531
1532/*
1533 * Protected functions
1534 */
1535
1537{
1538 return {get_width(), get_height()};
1539}
1540
1545{
1546 return get_area_world().contains(world.floor());
1547}
1548
1553{
1554 return point + _pos;
1555}
1556
1564
1570{
1572 return true;
1573 } else if (_split_mode == SplitMode::SPLIT) {
1574 auto split_position = _split_frac * get_dimensions();
1575 switch (_split_direction) {
1576 case SplitDirection::NORTH: return p.y() > split_position.y();
1577 case SplitDirection::SOUTH: return p.y() < split_position.y();
1578 case SplitDirection::WEST: return p.x() > split_position.x();
1579 case SplitDirection::EAST: return p.x() < split_position.x();
1580 default: return false;
1581 }
1582 } else {
1583 return false;
1584 }
1585}
1586
1590std::optional<Geom::Point> Canvas::get_last_mouse() const
1591{
1592 return d->last_mouse;
1593}
1594
1596{
1597 return d->canvasitem_ctx->affine();
1598}
1599
1600void CanvasPrivate::queue_draw_area(const Geom::IntRect &rect)
1601{
1602 q->queue_draw();
1603 // Todo: Use the following if/when gtk supports partial invalidations again.
1604 // q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height());
1605}
1606
1611{
1612 if (!d->active) {
1613 // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
1614 // We need to ignore their requests!
1615 return;
1616 }
1617 d->invalidated->do_union(geom_to_cairo(d->stores.store().rect));
1618 d->schedule_redraw();
1619 if (d->prefs.debug_show_unclean) queue_draw();
1620}
1621
1625void Canvas::redraw_area(int x0, int y0, int x1, int y1)
1626{
1627 if (!d->active) {
1628 // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
1629 // We need to ignore their requests!
1630 return;
1631 }
1632
1633 // Clamp area to Cairo's technically supported max size (-2^30..+2^30-1).
1634 // This ensures that the rectangle dimensions don't overflow and wrap around.
1635 constexpr int min_coord = -(1 << 30);
1636 constexpr int max_coord = (1 << 30) - 1;
1637
1638 x0 = std::clamp(x0, min_coord, max_coord);
1639 y0 = std::clamp(y0, min_coord, max_coord);
1640 x1 = std::clamp(x1, min_coord, max_coord);
1641 y1 = std::clamp(y1, min_coord, max_coord);
1642
1643 if (x0 >= x1 || y0 >= y1) {
1644 return;
1645 }
1646
1647 if (d->redraw_active && d->invalidated->empty()) {
1648 d->abort_flags.store((int)AbortFlags::Soft, std::memory_order_relaxed); // responding to partial invalidations takes priority over prerendering
1649 if (d->prefs.debug_logging) std::cout << "Soft exit request" << std::endl;
1650 }
1651
1652 auto const rect = Geom::IntRect(x0, y0, x1, y1);
1653 d->invalidated->do_union(geom_to_cairo(rect));
1654 d->schedule_redraw();
1655 if (d->prefs.debug_show_unclean) queue_draw();
1656}
1657
1659{
1660 // Handle overflow during conversion gracefully.
1661 // Round outward to make sure integral coordinates cover the entire area.
1662 constexpr Geom::Coord min_int = std::numeric_limits<int>::min();
1663 constexpr Geom::Coord max_int = std::numeric_limits<int>::max();
1664
1666 (int)std::floor(std::clamp(x0, min_int, max_int)),
1667 (int)std::floor(std::clamp(y0, min_int, max_int)),
1668 (int)std::ceil (std::clamp(x1, min_int, max_int)),
1669 (int)std::ceil (std::clamp(y1, min_int, max_int))
1670 );
1671}
1672
1674{
1675 redraw_area(area.left(), area.top(), area.right(), area.bottom());
1676}
1677
1682{
1683 // Flag geometry as needing update.
1684 _need_update = true;
1685
1686 // Trigger the redraw process to perform the update.
1687 d->schedule_redraw();
1688}
1689
1694{
1695 if (pos == _pos) {
1696 return;
1697 }
1698
1699 _pos = pos;
1700
1701 d->schedule_redraw();
1702 queue_draw();
1703}
1704
1709{
1710 if (_affine == affine) {
1711 return;
1712 }
1713
1714 _affine = affine;
1715
1716 d->schedule_redraw();
1717 queue_draw();
1718}
1719
1723void Canvas::set_desk(uint32_t rgba)
1724{
1725 if (d->desk == rgba) return;
1726 bool invalidated = d->background_in_stores_enabled;
1727 d->desk = rgba;
1728 invalidated |= d->background_in_stores_enabled = d->background_in_stores_required();
1729 if (get_realized() && invalidated) redraw_all();
1730 queue_draw();
1731}
1732
1736void Canvas::set_border(uint32_t rgba)
1737{
1738 if (d->border == rgba) return;
1739 d->border = rgba;
1740 if (get_realized() && get_opengl_enabled()) queue_draw();
1741}
1742
1746void Canvas::set_page(uint32_t rgba)
1747{
1748 if (d->page == rgba) return;
1749 bool invalidated = d->background_in_stores_enabled;
1750 d->page = rgba;
1751 invalidated |= d->background_in_stores_enabled = d->background_in_stores_required();
1752 if (get_realized() && invalidated) redraw_all();
1753 queue_draw();
1754}
1755
1757{
1758 if (mode == _render_mode) return;
1760 d->schedule_redraw();
1761}
1762
1770
1772{
1773 if (mode == _split_mode) return;
1774 _split_mode = mode;
1775 d->schedule_redraw();
1778 _split_frac = {0.5, 0.5};
1779 }
1780}
1781
1783{
1784 if (enabled != _antialiasing_enabled) {
1785 _antialiasing_enabled = enabled;
1786 _drawing->setAntialiasingOverride(get_antialiasing_override(_antialiasing_enabled));
1787 }
1788}
1789
1791{
1792 if (clip != d->clip_to_page) {
1793 d->clip_to_page = clip;
1794 d->schedule_redraw();
1795 }
1796}
1797
1802{
1803 if (!d->active) {
1804 return;
1805 }
1806
1807 if (item == _current_canvas_item) {
1808 _current_canvas_item = nullptr;
1809 }
1810
1812 _current_canvas_item_new = nullptr;
1813 }
1814
1815 if (item == _grabbed_canvas_item) {
1816 item->ungrab(); // Calls gtk_grab_remove(canvas).
1817 }
1818
1819 if (item == d->pre_scroll_grabbed_item) {
1820 d->pre_scroll_grabbed_item = nullptr;
1821 }
1822}
1823
1824std::optional<Geom::PathVector> CanvasPrivate::calc_page_clip() const
1825{
1826 if (!clip_to_page) {
1827 return {};
1828 }
1829
1831 for (auto &rect : pi.pages) {
1832 pv.push_back(Geom::Path(rect));
1833 }
1834 return pv;
1835}
1836
1837// Set the cms transform
1839{
1840 // TO DO: Select per monitor. Note Gtk has a bug where the monitor is not correctly reported on start-up.
1841 // auto display = get_display();
1842 // auto monitor = display->get_monitor_at_window(get_window());
1843 // std::cout << " " << monitor->get_manufacturer() << ", " << monitor->get_model() << std::endl;
1844
1845 // gtk4
1846 // auto surface = get_surface();
1847 // auto the_monitor = display->get_monitor_at_surface(surface);
1848
1850}
1851
1852// Change cursor
1854{
1855 if (!_desktop) {
1856 return;
1857 }
1858
1859 switch (_hover_direction) {
1862 break;
1863
1868 {
1869 set_cursor("pointer");
1870 break;
1871 }
1872
1874 {
1875 set_cursor("ns-resize");
1876 break;
1877 }
1878
1880 {
1881 set_cursor("ew-resize");
1882 break;
1883 }
1884
1885 default:
1886 // Shouldn't reach.
1887 std::cerr << "Canvas::update_cursor: Unknown hover direction!" << std::endl;
1888 }
1889}
1890
1891void Canvas::size_allocate_vfunc(int const width, int const height, int const baseline)
1892{
1893 parent_type::size_allocate_vfunc(width, height, baseline);
1894
1895 if (width == 0 || height == 0) {
1896 // Widget is being hidden, don't count it.
1897 return;
1898 }
1899
1900 auto const new_dimensions = Geom::IntPoint{width, height};
1901
1902 // Keep canvas centered and optionally zoomed in.
1903 if (_desktop && new_dimensions != d->old_dimensions && d->old_dimensions != Geom::IntPoint{0, 0}) {
1904 auto const midpoint = _desktop->w2d(_pos + Geom::Point(d->old_dimensions) * 0.5);
1905 double zoom = _desktop->current_zoom();
1906
1907 auto prefs = Preferences::get();
1908 if (prefs->getBool("/options/stickyzoom/value", false)) {
1909 // Calculate adjusted zoom.
1910 auto const old_minextent = min(d->old_dimensions);
1911 auto const new_minextent = min(new_dimensions);
1912 if (old_minextent != 0) {
1913 zoom *= (double)new_minextent / old_minextent;
1914 }
1915 }
1916
1917 _desktop->zoom_absolute(midpoint, zoom, false);
1918 }
1919
1920 d->old_dimensions = new_dimensions;
1921
1922 _signal_resize.emit();
1923
1924 d->schedule_redraw(true);
1925}
1926
1927Glib::RefPtr<Gdk::GLContext> Canvas::create_context()
1928{
1929 Glib::RefPtr<Gdk::GLContext> result;
1930
1931 try {
1932 result = dynamic_cast<Gtk::Window &>(*get_root()).get_surface()->create_gl_context();
1933 } catch (const Gdk::GLError &e) {
1934 std::cerr << "Failed to create OpenGL context: " << e.what() << std::endl;
1935 return {};
1936 }
1937
1938 result->set_allowed_apis(Gdk::GLApi::GL);
1939
1940 try {
1941 result->realize();
1942 } catch (const Glib::Error &e) {
1943 std::cerr << "Failed to realize OpenGL context: " << e.what() << std::endl;
1944 return {};
1945 }
1946
1947 return result;
1948}
1949
1950void Canvas::paint_widget(Cairo::RefPtr<Cairo::Context> const &cr)
1951{
1952 framecheck_whole_function(d)
1953
1954 if (!d->active) {
1955 std::cerr << "Canvas::paint_widget: Called while not active!" << std::endl;
1956 return;
1957 }
1958
1959 if constexpr (false) d->canvasitem_ctx->root()->canvas_item_print_tree();
1960
1961 // On activation, launch_redraw() is scheduled at a priority much higher than draw, so it
1962 // should have been called at least one before this point to perform vital initialisation
1963 // (needed not to crash). However, we don't want to rely on that, hence the following check.
1964 if (d->stores.mode() == Stores::Mode::None) {
1965 std::cerr << "Canvas::paint_widget: Called while active but uninitialised!" << std::endl;
1966 return;
1967 }
1968
1969 // If launch_redraw() has been scheduled but not yet called, call it now so there's no
1970 // possibility of it getting blocked indefinitely by a busy idle loop.
1971 if (d->schedule_redraw_conn) {
1972 // Note: This also works around the bug https://gitlab.com/inkscape/inkscape/-/issues/4696.
1973 d->launch_redraw();
1974 d->schedule_redraw_conn.disconnect();
1975 }
1976
1977 // Commit pending tiles in case GTK called on_draw even though after_redraw() is scheduled at higher priority.
1978 d->commit_tiles();
1979
1980 if (get_opengl_enabled()) {
1982 }
1983
1985 args.mouse = d->last_mouse;
1986 args.render_mode = d->render_mode;
1987 args.splitmode = d->split_mode;
1988 args.splitfrac = _split_frac;
1991 args.yaxisdir = _desktop ? _desktop->yaxisdir() : 1.0;
1992
1993 d->graphics->paint_widget(Fragment{ _affine, get_area_world() }, args, cr);
1994
1995 // If asked, run an animation loop.
1996 if (d->prefs.debug_animate) {
1997 auto t = g_get_monotonic_time() / 1700000.0;
1998 auto affine = Geom::Rotate(t * 5) * Geom::Scale(1.0 + 0.6 * cos(t * 2));
1999 set_affine(affine);
2000 auto dim = _desktop && _desktop->doc() ? _desktop->doc()->getDimensions() : Geom::Point();
2001 set_pos(Geom::Point((0.5 + 0.3 * cos(t * 2)) * dim.x(), (0.5 + 0.3 * sin(t * 3)) * dim.y()) * affine - Geom::Point(get_dimensions()) * 0.5);
2002 }
2003}
2004
2005/*
2006 * Async redrawing process
2007 */
2008
2009// Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.)
2010auto coarsen(const Cairo::RefPtr<Cairo::Region> &region, int min_size, int glue_size, double min_fullness)
2011{
2012 // Sort the rects by minExtent.
2013 struct Compare
2014 {
2015 bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const {
2016 return a.minExtent() < b.minExtent();
2017 }
2018 };
2019 std::multiset<Geom::IntRect, Compare> rects;
2020 int nrects = region->get_num_rectangles();
2021 for (int i = 0; i < nrects; i++) {
2022 rects.emplace(cairo_to_geom(region->get_rectangle(i)));
2023 }
2024
2025 // List of processed rectangles.
2026 std::vector<Geom::IntRect> processed;
2027 processed.reserve(nrects);
2028
2029 // Removal lists.
2030 std::vector<decltype(rects)::iterator> remove_rects;
2031 std::vector<int> remove_processed;
2032
2033 // Repeatedly expand small rectangles by absorbing their nearby small rectangles.
2034 while (!rects.empty() && rects.begin()->minExtent() < min_size) {
2035 // Extract the smallest unprocessed rectangle.
2036 auto rect = *rects.begin();
2037 rects.erase(rects.begin());
2038
2039 // Initialise the effective glue size.
2040 int effective_glue_size = glue_size;
2041
2042 while (true) {
2043 // Find the glue zone.
2044 auto glue_zone = rect;
2045 glue_zone.expandBy(effective_glue_size);
2046
2047 // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast.
2048 auto newrect = rect;
2049 int absorbed_area = 0;
2050
2051 remove_rects.clear();
2052 for (auto it = rects.begin(); it != rects.end(); ++it) {
2053 if (glue_zone.contains(*it)) {
2054 newrect.unionWith(*it);
2055 absorbed_area += it->area();
2056 remove_rects.emplace_back(it);
2057 }
2058 }
2059
2060 remove_processed.clear();
2061 for (int i = 0; i < processed.size(); i++) {
2062 auto &r = processed[i];
2063 if (glue_zone.contains(r)) {
2064 newrect.unionWith(r);
2065 absorbed_area += r.area();
2066 remove_processed.emplace_back(i);
2067 }
2068 }
2069
2070 // If the result was too empty, try again with a smaller glue size.
2071 double fullness = (double)(rect.area() + absorbed_area) / newrect.area();
2072 if (fullness < min_fullness) {
2073 effective_glue_size /= 2;
2074 continue;
2075 }
2076
2077 // Commit the change.
2078 rect = newrect;
2079
2080 for (auto &it : remove_rects) {
2081 rects.erase(it);
2082 }
2083
2084 for (int j = (int)remove_processed.size() - 1; j >= 0; j--) {
2085 int i = remove_processed[j];
2086 processed[i] = processed.back();
2087 processed.pop_back();
2088 }
2089
2090 // Stop growing if not changed or now big enough.
2091 bool finished = absorbed_area == 0 || rect.minExtent() >= min_size;
2092 if (finished) {
2093 break;
2094 }
2095
2096 // Otherwise, continue normally.
2097 effective_glue_size = glue_size;
2098 }
2099
2100 // Put the finished rectangle in processed.
2101 processed.emplace_back(rect);
2102 }
2103
2104 // Put any remaining rectangles in processed.
2105 for (auto &rect : rects) {
2106 processed.emplace_back(rect);
2107 }
2108
2109 return processed;
2110}
2111
2112static std::optional<Geom::Dim2> bisect(Geom::IntRect const &rect, int tile_size)
2113{
2114 int bw = rect.width();
2115 int bh = rect.height();
2116
2117 // Chop in half along the bigger dimension if the bigger dimension is too big.
2118 if (bw > bh) {
2119 if (bw > tile_size) {
2120 return Geom::X;
2121 }
2122 } else {
2123 if (bh > tile_size) {
2124 return Geom::Y;
2125 }
2126 }
2127
2128 return {};
2129}
2130
2131void CanvasPrivate::init_tiler()
2132{
2133 // Begin processing redraws.
2134 rd.start_time = g_get_monotonic_time();
2135 rd.phase = 0;
2136 rd.vis_store = (rd.visible & rd.store.rect).regularized();
2137
2138 if (!init_redraw()) {
2139 sync.signalExit();
2140 return;
2141 }
2142
2143 // Launch render threads to process tiles.
2144 rd.timeoutflag = false;
2145
2146 rd.numactive = rd.numthreads;
2147
2148 for (int i = 0; i < rd.numthreads - 1; i++) {
2149 boost::asio::post(*pool, [=, this] { render_tile(i); });
2150 }
2151
2152 render_tile(rd.numthreads - 1);
2153}
2154
2155bool CanvasPrivate::init_redraw()
2156{
2157 assert(rd.rects.empty());
2158
2159 switch (rd.phase) {
2160 case 0:
2161 if (rd.vis_store && rd.decoupled_mode) {
2162 // The highest priority to redraw is the region that is visible but not covered by either clean or snapshot content, if in decoupled mode.
2163 // If this is not rendered immediately, it will be perceived as edge flicker, most noticeably on zooming out, but also on rotation too.
2164 process_redraw(*rd.vis_store, unioned(updater->clean_region->copy(), rd.snapshot_drawn));
2165 return true;
2166 } else {
2167 rd.phase++;
2168 // fallthrough
2169 }
2170
2171 case 1:
2172 if (rd.vis_store) {
2173 // The main priority to redraw, and the bread and butter of Inkscape's painting, is the visible content that is not clean.
2174 // This may be done over several cycles, at the direction of the Updater, each outwards from the mouse.
2175 process_redraw(*rd.vis_store, updater->get_next_clean_region());
2176 return true;
2177 } else {
2178 rd.phase++;
2179 // fallthrough
2180 }
2181
2182 case 2: {
2183 // The lowest priority to redraw is the prerender margin around the visible rectangle.
2184 // (This is in addition to any opportunistic prerendering that may have already occurred in the above steps.)
2185 auto prerender = expandedBy(rd.visible, rd.margin);
2186 auto prerender_store = (prerender & rd.store.rect).regularized();
2187 if (prerender_store) {
2188 process_redraw(*prerender_store, updater->clean_region);
2189 return true;
2190 } else {
2191 return false;
2192 }
2193 }
2194
2195 default:
2196 assert(false);
2197 return false;
2198 }
2199}
2200
2201// Paint a given subrectangle of the store given by 'bounds', but avoid painting the part of it within 'clean' if possible.
2202// Some parts both outside the bounds and inside the clean region may also be painted if it helps reduce fragmentation.
2203void CanvasPrivate::process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible, bool preemptible)
2204{
2205 rd.bounds = bounds;
2206 rd.clean = std::move(clean);
2207 rd.interruptible = interruptible;
2208 rd.preemptible = preemptible;
2209
2210 // Assert that we do not render outside of store.
2211 assert(rd.store.rect.contains(rd.bounds));
2212
2213 // Get the region we are asked to paint.
2214 auto region = Cairo::Region::create(geom_to_cairo(rd.bounds));
2215 region->subtract(rd.clean);
2216
2217 // Get the list of rectangles to paint, coarsened to avoid fragmentation.
2218 rd.rects = coarsen(region,
2219 std::min<int>(rd.coarsener_min_size, rd.tile_size / 2),
2220 std::min<int>(rd.coarsener_glue_size, rd.tile_size / 2),
2221 rd.coarsener_min_fullness);
2222
2223 // Put the rectangles into a heap sorted by distance from mouse.
2224 std::make_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
2225
2226 // Adjust the effective tile size proportional to the painting area.
2227 double adjust = (double)cairo_to_geom(region->get_extents()).maxExtent() / rd.visible.maxExtent();
2228 adjust = std::clamp(adjust, 0.3, 1.0);
2229 rd.effective_tile_size = rd.tile_size * adjust;
2230}
2231
2232// Process rectangles until none left or timed out.
2233void CanvasPrivate::render_tile(int debug_id)
2234{
2235 rd.mutex.lock();
2236
2237 std::string fc_str;
2238 FrameCheck::Event fc;
2239 if (rd.debug_framecheck) {
2240 fc_str = "render_thread_" + std::to_string(debug_id + 1);
2241 fc = FrameCheck::Event(fc_str.c_str());
2242 }
2243
2244 while (true) {
2245 // If we've run out of rects, try to start a new redraw cycle.
2246 if (rd.rects.empty()) {
2247 if (end_redraw()) {
2248 // More redraw cycles to do.
2249 continue;
2250 } else {
2251 // All finished.
2252 break;
2253 }
2254 }
2255
2256 // Check for cancellation.
2257 auto const flags = abort_flags.load(std::memory_order_relaxed);
2258 bool const soft = flags & (int)AbortFlags::Soft;
2259 bool const hard = flags & (int)AbortFlags::Hard;
2260 if (hard || (rd.phase == 3 && soft)) {
2261 break;
2262 }
2263
2264 // Extract the closest rectangle to the mouse.
2265 std::pop_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
2266 auto rect = rd.rects.back();
2267 rd.rects.pop_back();
2268
2269 // Cull empty rectangles.
2270 if (rect.hasZeroArea()) {
2271 continue;
2272 }
2273
2274 // Cull rectangles that lie entirely inside the clean region.
2275 // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.)
2276 if (rd.clean->contains_rectangle(geom_to_cairo(rect)) == Cairo::Region::Overlap::IN) {
2277 continue;
2278 }
2279
2280 // Lambda to add a rectangle to the heap.
2281 auto add_rect = [&] (Geom::IntRect const &rect) {
2282 rd.rects.emplace_back(rect);
2283 std::push_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
2284 };
2285
2286 // If the rectangle needs bisecting, bisect it and put it back on the heap.
2287 if (auto axis = bisect(rect, rd.effective_tile_size)) {
2288 int mid = rect[*axis].middle();
2289 auto lo = rect; lo[*axis].setMax(mid); add_rect(lo);
2290 auto hi = rect; hi[*axis].setMin(mid); add_rect(hi);
2291 continue;
2292 }
2293
2294 // Extend thin rectangles at the edge of the bounds rect to at least some minimum size, being sure to keep them within the store.
2295 // (This ensures we don't end up rendering one thin rectangle at the edge every frame while the view is moved continuously.)
2296 if (rd.preemptible) {
2297 if (rect.width() < rd.preempt) {
2298 if (rect.left() == rd.bounds.left() ) rect.setLeft (std::max(rect.right() - rd.preempt, rd.store.rect.left() ));
2299 if (rect.right() == rd.bounds.right()) rect.setRight(std::min(rect.left() + rd.preempt, rd.store.rect.right()));
2300 }
2301 if (rect.height() < rd.preempt) {
2302 if (rect.top() == rd.bounds.top() ) rect.setTop (std::max(rect.bottom() - rd.preempt, rd.store.rect.top() ));
2303 if (rect.bottom() == rd.bounds.bottom()) rect.setBottom(std::min(rect.top() + rd.preempt, rd.store.rect.bottom()));
2304 }
2305 }
2306
2307 // Mark the rectangle as clean.
2308 updater->mark_clean(rect);
2309
2310 rd.mutex.unlock();
2311
2312 // Paint the rectangle.
2313 paint_rect(rect);
2314
2315 rd.mutex.lock();
2316
2317 // Check for timeout.
2318 if (rd.interruptible) {
2319 auto now = g_get_monotonic_time();
2320 auto elapsed = now - rd.start_time;
2321 if (elapsed > rd.render_time_limit * 1000) {
2322 // Timed out. Temporarily return to GTK main loop, and come back here when next idle.
2323 rd.timeoutflag = true;
2324 break;
2325 }
2326 }
2327 }
2328
2329 if (rd.debug_framecheck && rd.timeoutflag) {
2330 fc.subtype = 1;
2331 }
2332
2333 rd.numactive--;
2334 bool const done = rd.numactive == 0;
2335
2336 rd.mutex.unlock();
2337
2338 if (done) {
2339 rd.rects.clear();
2340 sync.signalExit();
2341 }
2342}
2343
2344bool CanvasPrivate::end_redraw()
2345{
2346 switch (rd.phase) {
2347 case 0:
2348 rd.phase++;
2349 return init_redraw();
2350
2351 case 1:
2352 if (!updater->report_finished()) {
2353 rd.phase++;
2354 }
2355 return init_redraw();
2356
2357 case 2:
2358 return false;
2359
2360 default:
2361 assert(false);
2362 return false;
2363 }
2364}
2365
2366void CanvasPrivate::paint_rect(Geom::IntRect const &rect)
2367{
2368 // Make sure the paint rectangle lies within the store.
2369 assert(rd.store.rect.contains(rect));
2370
2371 auto paint = [&, this] (bool need_background, bool outline_pass) {
2372
2373 auto surface = graphics->request_tile_surface(rect, true);
2374 if (!surface) {
2375 sync.runInMain([&] {
2376 if (prefs.debug_logging) std::cout << "Blocked - buffer mapping" << std::endl;
2377 if (q->get_opengl_enabled()) q->make_current();
2378 surface = graphics->request_tile_surface(rect, false);
2379 });
2380 }
2381
2382 auto on_error = [&, this] (char const *err) {
2383 std::cerr << "paint_rect: " << err << std::endl;
2384 sync.runInMain([&] {
2385 if (q->get_opengl_enabled()) q->make_current();
2386 graphics->junk_tile_surface(std::move(surface));
2387 surface = graphics->request_tile_surface(rect, false);
2388 paint_error_buffer(surface);
2389 });
2390 };
2391
2392 try {
2393 paint_single_buffer(surface, rect, need_background, outline_pass);
2394 } catch (std::bad_alloc const &e) {
2395 // Note: std::bad_alloc actually indicates a Cairo error that occurs regularly at high zoom, and we must handle it.
2396 // See https://gitlab.com/inkscape/inkscape/-/issues/3975
2397 on_error(e.what());
2398 } catch (Cairo::logic_error const &e) {
2399 on_error(e.what());
2400 }
2401
2402 return surface;
2403 };
2404
2405 // Create and render the tile.
2406 Tile tile;
2407 tile.fragment.affine = rd.store.affine;
2408 tile.fragment.rect = rect;
2409 tile.surface = paint(rd.background_in_stores_required, false);
2410 if (outlines_enabled) {
2411 tile.outline_surface = paint(false, true);
2412 }
2413
2414 // Introduce an artificial delay for each rectangle.
2415 if (rd.redraw_delay) g_usleep(*rd.redraw_delay);
2416
2417 // Stick the tile on the list of tiles to reap.
2418 {
2419 auto g = std::lock_guard(rd.tiles_mutex);
2420 rd.tiles.emplace_back(std::move(tile));
2421 }
2422}
2423
2424void CanvasPrivate::paint_single_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface, Geom::IntRect const &rect, bool need_background, bool outline_pass)
2425{
2426 // Create Cairo context.
2427 auto cr = Cairo::Context::create(surface);
2428
2429 // Clear background.
2430 cr->save();
2431 if (need_background) {
2432 Graphics::paint_background(Fragment{ rd.store.affine, rect }, pi, rd.page, rd.desk, cr);
2433 } else {
2434 cr->set_operator(Cairo::Context::Operator::CLEAR);
2435 cr->paint();
2436 }
2437 cr->restore();
2438
2439 // Render drawing on top of background.
2440 auto buf = CanvasItemBuffer{ rect, scale_factor, cr, outline_pass };
2441 canvasitem_ctx->root()->render(buf);
2442
2443 // Apply CMS transform for the screen. This rarely is used by modern desktops, but sometimes
2444 // the user will apply an RGB transform to color correct their screen. This happens now, so the
2445 // drawing plus all other canvas items (selection boxes, handles, etc) are also color corrected.
2446 if (rd.cms_transform) {
2447 rd.cms_transform->do_transform(surface->cobj(), surface->cobj());
2448 }
2449
2450 // Paint over newly drawn content with a translucent random colour.
2451 if (rd.debug_show_redraw) {
2452 cr->set_source_rgba((rand() % 256) / 255.0, (rand() % 256) / 255.0, (rand() % 256) / 255.0, 0.2);
2453 cr->set_operator(Cairo::Context::Operator::OVER);
2454 cr->paint();
2455 }
2456}
2457
2458void CanvasPrivate::paint_error_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface)
2459{
2460 // Paint something into surface to represent an "error" state for that tile.
2461 // Currently just paints solid black.
2462 auto cr = Cairo::Context::create(surface);
2463 cr->set_source_rgb(0, 0, 0);
2464 cr->paint();
2465}
2466
2467} // namespace Inkscape::UI::Widget
2468
2469/*
2470 Local Variables:
2471 mode:c++
2472 c-file-style:"stroustrup"
2473 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
2474 indent-tabs-mode:nil
2475 fill-column:99
2476 End:
2477*/
2478// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
double coarsener_min_fullness
Definition canvas.cpp:163
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
int effective_tile_size
Definition canvas.cpp:187
std::mutex mutex
Definition canvas.cpp:176
std::vector< Geom::IntRect > rects
Definition canvas.cpp:186
bool debug_framecheck
Definition canvas.cpp:172
uint64_t page
Definition canvas.cpp:171
int coarsener_min_size
Definition canvas.cpp:161
bool decoupled_mode
Definition canvas.cpp:156
Geom::IntRect visible
Definition canvas.cpp:154
std::shared_ptr< Colors::CMS::Transform > cms_transform
Definition canvas.cpp:158
Fragment store
Definition canvas.cpp:155
bool background_in_stores_required
Definition canvas.cpp:170
int numactive
Definition canvas.cpp:178
bool debug_show_redraw
Definition canvas.cpp:173
int tile_size
Definition canvas.cpp:164
int preempt
Definition canvas.cpp:165
std::vector< Tile > tiles
Definition canvas.cpp:191
bool interruptible
Definition canvas.cpp:184
int margin
Definition canvas.cpp:166
bool preemptible
Definition canvas.cpp:185
Cairo::RefPtr< Cairo::Region > snapshot_drawn
Definition canvas.cpp:157
bool timeoutflag
Definition canvas.cpp:192
uint64_t desk
Definition canvas.cpp:171
std::mutex tiles_mutex
Definition canvas.cpp:190
std::optional< int > redraw_delay
Definition canvas.cpp:167
int render_time_limit
Definition canvas.cpp:168
gint64 start_time
Definition canvas.cpp:177
Geom::IntPoint mouse_loc
Definition canvas.cpp:153
int coarsener_glue_size
Definition canvas.cpp:162
Geom::OptIntRect vis_store
Definition canvas.cpp:180
int phase
Definition canvas.cpp:179
Cairo::RefPtr< Cairo::ImageSurface > outline_surface
Definition canvas.cpp:138
Geom::IntRect bounds
Definition canvas.cpp:182
int numthreads
Definition canvas.cpp:169
Cairo::RefPtr< Cairo::Region > clean
Definition canvas.cpp:183
Fragment fragment
Definition canvas.cpp:136
Inkscape canvas widget.
3x3 matrix representing an affine transformation.
Definition affine.h:70
Axis-aligned generic rectangle that can be empty.
Axis aligned, non-empty, generic rectangle.
C right() const
Return rightmost coordinate of the rectangle (+X is to the right).
C area() const
Compute the rectangle's area.
void setMin(CPoint const &p)
Set the upper left point of the rectangle.
bool contains(GenericRect< C > const &r) const
Check whether the rectangle includes all points in the given rectangle.
void setLeft(C val)
Set the minimum X coordinate of the rectangle.
C top() const
Return top coordinate of the rectangle (+Y is downwards).
void setTop(C val)
Set the minimum Y coordinate of the rectangle.
void setMax(CPoint const &p)
Set the lower right point of the rectangle.
void setRight(C val)
Set the maximum X coordinate of the rectangle.
void setBottom(C val)
Set the maximum Y coordinate of the rectangle.
void expandBy(C amount)
Expand the rectangle in both directions by the specified amount.
C left() const
Return leftmost coordinate of the rectangle (+X is to the right).
C height() const
Get the vertical extent of the rectangle.
C minExtent() const
Get the smaller extent (width or height) of the rectangle.
void unionWith(CRect const &b)
Enlarge the rectangle to contain the argument.
C width() const
Get the horizontal extent of the rectangle.
C bottom() const
Return bottom coordinate of the rectangle (+Y is downwards).
C maxExtent() const
Get the larger extent (width or height) of the rectangle.
bool hasZeroArea() const
Check whether the rectangle has zero area.
Two-dimensional point with integer coordinates.
Definition int-point.h:57
constexpr IntCoord x() const noexcept
Definition int-point.h:77
Paralellogram, representing a linear transformation of a rectangle.
Sequence of subpaths.
Definition pathvector.h:122
void push_back(Path const &path)
Append a path at the end.
Definition pathvector.h:172
Sequence of contiguous curves, aka spline.
Definition path.h:353
Two-dimensional point that doubles as a vector.
Definition point.h:66
IntPoint floor() const
Round coordinates downwards.
Definition point.h:206
Coord length() const
Compute the distance from origin.
Definition point.h:118
constexpr Coord y() const noexcept
Definition point.h:106
constexpr Coord x() const noexcept
Definition point.h:104
Axis aligned, non-empty rectangle.
Definition rect.h:92
Rotation around the origin.
Definition transforms.h:187
Scaling from the origin.
Definition transforms.h:150
Translation by a vector.
Definition transforms.h:115
static System & get()
Definition system.h:35
const std::shared_ptr< Transform > & getDisplayTransform()
Get the color managed trasform for the screen.
Definition system.cpp:240
void setRenderMode(RenderMode)
Definition drawing.cpp:74
CanvasItemDrawing * getCanvasItemDrawing()
Definition drawing.h:47
void setColorMode(ColorMode)
Definition drawing.cpp:87
void setAntialiasingOverride(std::optional< Antialiasing > antialiasing_override)
Definition drawing.cpp:219
void setOutlineOverlay(bool)
Definition drawing.cpp:98
static Preferences * get()
Access the singleton Preferences object.
void use_tool_cursor()
Uses the saved cursor, based on the saved filename.
void on_key_released(Gtk::EventControllerKey const &controller, unsigned keyval, unsigned keycode, Gdk::ModifierType state)
Definition canvas.cpp:1129
Inkscape::SplitDirection _split_direction
Definition canvas.h:225
void request_update()
Redraw after changing canvas item geometry.
Definition canvas.cpp:1681
void set_pos(const Geom::IntPoint &pos)
Scroll window so drawing point 'pos' is at upper left corner of canvas.
Definition canvas.cpp:1693
Geom::Affine _affine
The affine that we have been requested to draw at.
Definition canvas.h:195
bool on_scroll(Gtk::EventControllerScroll const &controller, double dx, double dy)
Definition canvas.cpp:936
Geom::IntPoint get_dimensions() const
Definition canvas.cpp:1536
sigc::signal< void()> _signal_focus_in
Definition canvas.h:232
Inkscape::SplitMode _split_mode
Definition canvas.h:199
Inkscape::SplitDirection _hover_direction
Definition canvas.h:227
int _state
Last known modifier state (SHIFT, CTRL, etc.).
Definition canvas.h:214
void paint_widget(Cairo::RefPtr< Cairo::Context > const &) final
Reimplement to render the widget.
Definition canvas.cpp:1950
void set_page(uint32_t rgba)
Set the page colour.
Definition canvas.cpp:1746
Gtk::EventSequenceState on_button_pressed(Gtk::GestureClick const &controller, int n_press, double x, double y)
Definition canvas.cpp:951
Gtk::EventSequenceState on_button_released(Gtk::GestureClick const &controller, int n_press, double x, double y)
Definition canvas.cpp:997
bool on_key_pressed(Gtk::EventControllerKey const &controller, unsigned keyval, unsigned keycode, Gdk::ModifierType state)
Definition canvas.cpp:1112
CanvasItemGroup * get_canvas_item_root() const
Definition canvas.cpp:532
void size_allocate_vfunc(int width, int height, int baseline) final
Definition canvas.cpp:1891
Inkscape::ColorMode _color_mode
Definition canvas.h:200
Inkscape::CanvasItem * _grabbed_canvas_item
Item that holds a pointer grab; nullptr if none.
Definition canvas.h:218
sigc::signal< void()> _signal_focus_out
Definition canvas.h:233
void on_leave(Gtk::EventControllerMotion const &controller)
Definition canvas.cpp:1085
void set_antialiasing_enabled(bool enabled)
Definition canvas.cpp:1782
void canvas_item_destructed(Inkscape::CanvasItem *item)
Clear current and grabbed items.
Definition canvas.cpp:1801
void set_clip_to_page_mode(bool clip)
Definition canvas.cpp:1790
sigc::signal< void()> _signal_resize
Definition canvas.h:231
void redraw_area(Geom::Rect const &area)
Definition canvas.cpp:1673
void set_cms_transform()
Set the lcms transform.
Definition canvas.cpp:1838
Geom::IntPoint _pos
Coordinates of top-left pixel of canvas view within canvas.
Definition canvas.h:194
void set_render_mode(Inkscape::RenderMode mode)
Definition canvas.cpp:1756
void set_drawing(Inkscape::Drawing *drawing)
Definition canvas.cpp:519
Glib::RefPtr< Gdk::GLContext > create_context() final
Reimplement to create the desired OpenGL context.
Definition canvas.cpp:1927
Geom::IntPoint _split_drag_start
Definition canvas.h:229
Geom::IntRect get_area_world() const
Return the area shown in the canvas in world coordinates.
Definition canvas.cpp:1560
std::unique_ptr< CanvasPrivate > d
Definition canvas.h:239
void set_color_mode(Inkscape::ColorMode mode)
Definition canvas.cpp:1763
void on_motion(Gtk::EventControllerMotion const &controller, double x, double y)
Definition canvas.cpp:1146
Inkscape::Drawing * _drawing
Definition canvas.h:191
void redraw_all()
Invalidate drawing and redraw during idle.
Definition canvas.cpp:1610
bool canvas_point_in_outline_zone(Geom::Point const &world) const
Return whether a point in screen space / canvas coordinates is inside the region of the canvas where ...
Definition canvas.cpp:1569
Inkscape::CanvasItem * _current_canvas_item
Item containing cursor, nullptr if none.
Definition canvas.h:216
Inkscape::RenderMode _render_mode
Definition canvas.h:198
void set_border(uint32_t rgba)
Set the page border colour.
Definition canvas.cpp:1736
bool world_point_inside_canvas(Geom::Point const &world) const
Is world point inside canvas area?
Definition canvas.cpp:1544
void set_split_mode(Inkscape::SplitMode mode)
Definition canvas.cpp:1771
std::optional< Geom::Point > get_last_mouse() const
Return the last known mouse position of center if off-canvas.
Definition canvas.cpp:1590
Geom::Point canvas_to_world(Geom::Point const &window) const
Translate point in canvas to world coordinates.
Definition canvas.cpp:1552
Inkscape::CanvasItem * _current_canvas_item_new
Item to become _current_item, nullptr if none.
Definition canvas.h:217
const Geom::Affine & get_geom_affine() const
Definition canvas.cpp:1595
void set_desk(uint32_t rgba)
Set the desk colour.
Definition canvas.cpp:1723
void on_enter(Gtk::EventControllerMotion const &controller, double x, double y)
Definition canvas.cpp:1068
void set_affine(const Geom::Affine &affine)
Set the affine for the canvas.
Definition canvas.cpp:1708
std::shared_ptr< Colors::CMS::Transform > _cms_transform
The lcms transform to apply to canvas.
Definition canvas.h:205
static std::unique_ptr< Graphics > create_gl(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
static std::unique_ptr< Graphics > create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
static void paint_background(Fragment const &fragment, PageInfo const &pi, std::uint32_t page, std::uint32_t desk, Cairo::RefPtr< Cairo::Context > const &cr)
Definition graphics.cpp:54
void set_opengl_enabled(bool)
Set whether OpenGL is enabled.
Definition optglarea.cpp:91
void bind_framebuffer() const
Call before rendering to the widget to bind the widget's framebuffer.
@ Normal
Not initialised or just reset; no stores exist yet.
@ Decoupled
Normal mode consisting of just a backing store.
@ Shifted
The backing store was completely recreated.
@ Recreated
The backing store was not changed.
static std::unique_ptr< Updater > create()
double current_zoom() const
Definition desktop.h:335
Inkscape::UI::Tools::ToolBase * getTool() const
Definition desktop.h:187
double yaxisdir() const
Definition desktop.h:426
SPDocument * doc() const
Definition desktop.h:159
void zoom_absolute(Geom::Point const &c, double zoom, bool keep_point=true)
Zoom to the given absolute zoom level.
Definition desktop.cpp:538
Geom::Affine const & w2d() const
Transformation from window to desktop coordinates (zoom/rotate).
Definition desktop.h:416
Geom::Point getDimensions() const
Definition document.cpp:973
Access operating system wide information about color management.
constexpr uint32_t SP_RGBA32_A_U(uint32_t v)
Definition utils.h:39
RectangularCluster rd
Utilities to more easily use Gtk::EventController & subclasses like Gesture.
Css & result
bool sp_desktop_root_handler(Inkscape::CanvasEvent const &event, SPDesktop *desktop)
Editable view implementation.
Canvas item belonging to an SVG drawing element.
SVG drawing for display.
double Coord
Floating point type used to store coordinates.
Definition coord.h:76
constexpr Coord EPSILON
Default "acceptably small" value.
Definition coord.h:84
@ Y
Definition coord.h:48
@ X
Definition coord.h:48
Specific geometry functions for Inkscape, not provided my lib2geom.
auto expandedBy(Geom::IntRect rect, int amount)
Definition geom.h:66
SPItem * item
GenericRect< IntCoord > IntRect
Definition forward.h:57
T sqr(const T &x)
Definition math-utils.h:57
auto use_state(Slot &&slot)
Definition controller.h:43
Custom widgets.
Definition desktop.h:126
static double profile(double r)
Definition canvas.cpp:846
auto coarsen(const Cairo::RefPtr< Cairo::Region > &region, int min_size, int glue_size, double min_fullness)
Definition canvas.cpp:2010
static constexpr int height
static std::optional< Geom::Dim2 > bisect(Geom::IntRect const &rect, int tile_size)
Definition canvas.cpp:2112
static Geom::Point cap_length(Geom::Point const &pt, double max)
Definition canvas.cpp:840
static Geom::Point apply_profile(Geom::Point const &pt)
Definition canvas.cpp:853
auto unioned(Cairo::RefPtr< Cairo::Region > a, Cairo::RefPtr< Cairo::Region > const &b)
Definition util.h:29
Geom::PathVector outline(Geom::Path const &input, double width, double miter, LineJoinType join, LineCapType butt, double tolerance)
Strokes the path given by input.
void inspect_event(E &&event, Fs... funcs)
Perform pattern-matching on a CanvasEvent.
ExtendedInput extinput_from_gdkevent(Gdk::Event const &event)
Read the extended input data from a Gdk::Event.
time_t now()
parse current time from SOURCE_DATE_EPOCH environment variable
constexpr float pi
Definition ok-color.cpp:37
STL namespace.
static T clip(T const &v, T const &a, T const &b)
static cairo_user_data_key_t key
int mode
int buf
GLsync sync
Singleton class to access the preferences file in a convenient way.
Provides a class that shows a temporary indicator on the canvas of where the snap was,...
Abstraction of the store/snapshot mechanism.
A mouse button (left/right/middle) is pressed.
A mouse button (left/right/middle) is released.
Abstract base class for events.
virtual EventType type() const =0
Return the dynamic type of the CanvasEvent.
The pointer has entered a widget or item.
A key has been pressed.
A key has been released.
The pointer has exited a widget or item.
Movement of the mouse pointer.
Scroll the item or widget by the provided amount.
A "fragment" is a rectangle of drawn content at a specfic place.
Definition fragment.h:12
std::optional< Geom::Point > mouse
Definition graphics.h:79
int delta
int index
double border
double width
static void activate(GApplication *app, gpointer)
Cairo::RectangleInt geom_to_cairo(const Geom::IntRect &rect)
Definition util.cpp:352
Geom::IntRect cairo_to_geom(const Cairo::RectangleInt &rect)
Definition util.cpp:357