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