diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
commit | c853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch) | |
tree | 7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/widget/canvas | |
parent | Initial commit. (diff) | |
download | inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip |
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
30 files changed, 7266 insertions, 0 deletions
diff --git a/src/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp new file mode 100644 index 0000000..460b606 --- /dev/null +++ b/src/ui/widget/canvas-grid.cpp @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* + * Author: + * Tavmjong Bah + * + * Rewrite of code originally in desktop-widget.cpp. + * + * Copyright (C) 2020 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// The scrollbars, and canvas are tightly coupled so it makes sense to have a dedicated +// widget to handle their interactions. The buttons are along for the ride. I don't see +// how to add the buttons easily via a .ui file (which would allow the user to put any +// buttons they want in their place). + +#include <glibmm/i18n.h> +#include <gtkmm/enums.h> +#include <gtkmm/label.h> + +#include "canvas-grid.h" + +#include "desktop.h" // Hopefully temp. +#include "desktop-events.h" // Hopefully temp. + +#include "display/control/canvas-item-drawing.h" // sticky + +#include "page-manager.h" + +#include "ui/dialog/command-palette.h" +#include "ui/icon-loader.h" +#include "ui/widget/canvas.h" +#include "ui/widget/canvas-notice.h" +#include "ui/widget/ink-ruler.h" +#include "io/resource.h" + +#include "widgets/desktop-widget.h" // Hopefully temp. + +namespace Inkscape { +namespace UI { +namespace Widget { + +CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) +{ + _dtw = dtw; + set_name("CanvasGrid"); + + // Canvas + _canvas = std::make_unique<Inkscape::UI::Widget::Canvas>(); + _canvas->set_hexpand(true); + _canvas->set_vexpand(true); + _canvas->set_can_focus(true); + _canvas->signal_event().connect(sigc::mem_fun(*this, &CanvasGrid::SignalEvent)); // TEMP + + // Command palette + _command_palette = std::make_unique<Inkscape::UI::Dialog::CommandPalette>(); + + // Notice overlay, note using unique_ptr will cause destruction race conditions + _notice = CanvasNotice::create(); + + // Canvas overlay + _canvas_overlay.add(*_canvas); + _canvas_overlay.add_overlay(*_command_palette->get_base_widget()); + _canvas_overlay.add_overlay(*_notice); + + // Horizontal Ruler + _hruler = std::make_unique<Inkscape::UI::Widget::Ruler>(Gtk::ORIENTATION_HORIZONTAL); + _hruler->add_track_widget(*_canvas); + _hruler->set_hexpand(true); + _hruler->show(); + // Tooltip/Unit set elsewhere + + // Vertical Ruler + _vruler = std::make_unique<Inkscape::UI::Widget::Ruler>(Gtk::ORIENTATION_VERTICAL); + _vruler->add_track_widget(*_canvas); + _vruler->set_vexpand(true); + _vruler->show(); + // Tooltip/Unit set elsewhere. + + // Guide Lock + _guide_lock.set_name("LockGuides"); + _guide_lock.add(*Gtk::make_managed<Gtk::Image>("object-locked", Gtk::ICON_SIZE_MENU)); + // To be replaced by Gio::Action: + _guide_lock.signal_toggled().connect(sigc::mem_fun(*_dtw, &SPDesktopWidget::update_guides_lock)); + _guide_lock.set_tooltip_text(_("Toggle lock of all guides in the document")); + // Subgrid + _subgrid.attach(_guide_lock, 0, 0, 1, 1); + _subgrid.attach(*_vruler, 0, 1, 1, 1); + _subgrid.attach(*_hruler, 1, 0, 1, 1); + _subgrid.attach(_canvas_overlay, 1, 1, 1, 1); + + // Horizontal Scrollbar + _hadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + _hadj->signal_value_changed().connect(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_adjustment_value_changed)); + _hscrollbar = Gtk::Scrollbar(_hadj, Gtk::ORIENTATION_HORIZONTAL); + _hscrollbar.set_name("CanvasScrollbar"); + _hscrollbar.set_hexpand(true); + + // Vertical Scrollbar + _vadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + _vadj->signal_value_changed().connect(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_adjustment_value_changed)); + _vscrollbar = Gtk::Scrollbar(_vadj, Gtk::ORIENTATION_VERTICAL); + _vscrollbar.set_name("CanvasScrollbar"); + _vscrollbar.set_vexpand(true); + + // CMS Adjust (To be replaced by Gio::Action) + _cms_adjust.set_name("CMS_Adjust"); + _cms_adjust.add(*Gtk::make_managed<Gtk::Image>("color-management", Gtk::ICON_SIZE_MENU)); + // Can't access via C++ API, fixed in Gtk4. + gtk_actionable_set_action_name( GTK_ACTIONABLE(_cms_adjust.gobj()), "win.canvas-color-manage"); + _cms_adjust.set_tooltip_text(_("Toggle color-managed display for this document window")); + + // popover with some common display mode related options + auto builder = Gtk::Builder::create_from_file(Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "display-popup.glade")); + _display_popup = builder; + Gtk::Popover* popover; + _display_popup->get_widget("popover", popover); + Gtk::CheckButton* sticky_zoom; + _display_popup->get_widget("zoom-resize", sticky_zoom); + // To be replaced by Gio::Action: + sticky_zoom->signal_toggled().connect([=](){ _dtw->sticky_zoom_toggled(); }); + _quick_actions.set_name("QuickActions"); + _quick_actions.set_popover(*popover); + _quick_actions.set_image_from_icon_name("display-symbolic"); + _quick_actions.set_direction(Gtk::ARROW_LEFT); + _quick_actions.set_tooltip_text(_("Display options")); + + // Main grid + attach(_subgrid, 0, 0, 1, 2); + attach(_hscrollbar, 0, 2, 1, 1); + attach(_cms_adjust, 1, 2, 1, 1); + attach(_quick_actions, 1, 0, 1, 1); + attach(_vscrollbar, 1, 1, 1, 1); + + // For creating guides, etc. + _hruler->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _hruler.get(), true)); + _hruler->signal_button_release_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _hruler.get(), true)); + _hruler->signal_motion_notify_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _hruler.get(), true)); + + // For creating guides, etc. + _vruler->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _vruler.get(), false)); + _vruler->signal_button_release_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _vruler.get(), false)); + _vruler->signal_motion_notify_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _vruler.get(), false)); + + show_all(); +} + +CanvasGrid::~CanvasGrid() +{ + _page_modified_connection.disconnect(); + _page_selected_connection.disconnect(); + _sel_modified_connection.disconnect(); + _sel_changed_connection.disconnect(); + _document = nullptr; + _notice = nullptr; +} + +void CanvasGrid::on_realize() { + // actions should be available now + + if (auto map = _dtw->get_action_map()) { + auto set_display_icon = [=]() { + Glib::ustring id; + auto mode = _canvas->get_render_mode(); + switch (mode) { + case RenderMode::NORMAL: id = "display"; + break; + case RenderMode::OUTLINE: id = "display-outline"; + break; + case RenderMode::OUTLINE_OVERLAY: id = "display-outline-overlay"; + break; + case RenderMode::VISIBLE_HAIRLINES: id = "display-enhance-stroke"; + break; + case RenderMode::NO_FILTERS: id = "display-no-filter"; + break; + default: + g_warning("Unknown display mode in canvas-grid"); + break; + } + + if (!id.empty()) { + // if CMS is ON show alternative icons + if (_canvas->get_cms_active()) { + id += "-alt"; + } + _quick_actions.set_image_from_icon_name(id + "-symbolic"); + } + }; + + set_display_icon(); + + // when display mode state changes, update icon + auto cms_action = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(map->lookup_action("canvas-color-manage")); + auto disp_action = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(map->lookup_action("canvas-display-mode")); + + if (cms_action && disp_action) { + disp_action->signal_activate().connect([=](const Glib::VariantBase& state){ set_display_icon(); }); + cms_action-> signal_activate().connect([=](const Glib::VariantBase& state){ set_display_icon(); }); + } + else { + g_warning("No canvas-display-mode and/or canvas-color-manage action available to canvas-grid"); + } + } + else { + g_warning("No action map available to canvas-grid"); + } + + parent_type::on_realize(); +} + +// TODO: remove when sticky zoom gets replaced by Gio::Action: +Gtk::ToggleButton* CanvasGrid::GetStickyZoom() { + Gtk::CheckButton* sticky_zoom; + _display_popup->get_widget("zoom-resize", sticky_zoom); + return sticky_zoom; +} + +// _dt2r should be a member of _canvas. +// get_display_area should be a member of _canvas. +void +CanvasGrid::UpdateRulers() +{ + auto prefs = Inkscape::Preferences::get(); + auto desktop = _dtw->desktop; + auto document = desktop->getDocument(); + auto &pm = document->getPageManager(); + auto sel = desktop->getSelection(); + + // Our connections to the document are handled with a lazy pattern to avoid + // having to refactor the SPDesktopWidget class. We know UpdateRulers is + // called in all situations when documents are loaded and replaced. + if (document != _document) { + _document = document; + _page_selected_connection = pm.connectPageSelected([=](SPPage *) { UpdateRulers(); }); + _page_modified_connection = pm.connectPageModified([=](SPPage *) { UpdateRulers(); }); + _sel_modified_connection = sel->connectModified([=](Inkscape::Selection *, int) { UpdateRulers(); }); + _sel_changed_connection = sel->connectChanged([=](Inkscape::Selection *) { UpdateRulers(); }); + } + + Geom::Rect viewbox = desktop->get_display_area().bounds(); + Geom::Rect startbox = viewbox; + if (prefs->getBool("/options/origincorrection/page", true)) { + // Move viewbox according to the selected page's position (if any) + startbox *= pm.getSelectedPageAffine().inverse(); + } + + // Scale and offset the ruler coordinates + // Use an integer box to align the ruler to the grid and page. + auto rulerbox = (startbox * Geom::Scale(_dtw->_dt2r)); + _hruler->set_range(rulerbox.left(), rulerbox.right()); + if (_dtw->desktop->is_yaxisdown()) { + _vruler->set_range(rulerbox.top(), rulerbox.bottom()); + } else { + _vruler->set_range(rulerbox.bottom(), rulerbox.top()); + } + + Geom::Point pos(_canvas->get_pos()); + auto scale = _canvas->get_affine(); + auto d2c = Geom::Translate(pos * scale.inverse()).inverse() * scale; + auto pagebox = (pm.getSelectedPageRect() * d2c).roundOutwards(); + _hruler->set_page(pagebox.left(), pagebox.right()); + _vruler->set_page(pagebox.top(), pagebox.bottom()); + + Geom::Rect selbox = Geom::IntRect(0, 0, 0, 0); + if (auto bbox = sel->preferredBounds()) + selbox = (*bbox * d2c).roundOutwards(); + _hruler->set_selection(selbox.left(), selbox.right()); + _vruler->set_selection(selbox.top(), selbox.bottom()); +} + +void +CanvasGrid::ShowScrollbars(bool state) +{ + if (_show_scrollbars == state) return; + _show_scrollbars = state; + + if (_show_scrollbars) { + // Show scrollbars + _hscrollbar.show(); + _vscrollbar.show(); + _cms_adjust.show(); + _cms_adjust.show_all_children(); + _quick_actions.show(); + } else { + // Hide scrollbars + _hscrollbar.hide(); + _vscrollbar.hide(); + _cms_adjust.hide(); + _quick_actions.hide(); + } +} + +void +CanvasGrid::ToggleScrollbars() +{ + _show_scrollbars = !_show_scrollbars; + ShowScrollbars(_show_scrollbars); + + // Will be replaced by actions + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/fullscreen/scrollbars/state", _show_scrollbars); + prefs->setBool("/window/scrollbars/state", _show_scrollbars); +} + +void +CanvasGrid::ShowRulers(bool state) +{ + if (_show_rulers == state) return; + _show_rulers = state; + + if (_show_rulers) { + // Show rulers + _hruler->show(); + _vruler->show(); + _guide_lock.show(); + _guide_lock.show_all_children(); + } else { + // Hide rulers + _hruler->hide(); + _vruler->hide(); + _guide_lock.hide(); + } +} + +void +CanvasGrid::ToggleRulers() +{ + _show_rulers = !_show_rulers; + ShowRulers(_show_rulers); + + // Will be replaced by actions + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/fullscreen/rulers/state", _show_rulers); + prefs->setBool("/window/rulers/state", _show_rulers); +} + +void +CanvasGrid::ToggleCommandPalette() +{ + _command_palette->toggle(); +} + +void +CanvasGrid::showNotice(Glib::ustring const &msg, unsigned timeout) +{ + _notice->show(msg, timeout); +} + +void +CanvasGrid::ShowCommandPalette(bool state) +{ + if (state) { + _command_palette->open(); + } + _command_palette->close(); +} + +// Update rulers on change of widget size, but only if allocation really changed. +void +CanvasGrid::on_size_allocate(Gtk::Allocation& allocation) +{ + Gtk::Grid::on_size_allocate(allocation); + if (!(_allocation == allocation)) { // No != function defined! + _allocation = allocation; + UpdateRulers(); + } +} + +// This belong in Canvas class +bool +CanvasGrid::SignalEvent(GdkEvent *event) +{ + if (event->type == GDK_BUTTON_PRESS) { + _canvas->grab_focus(); + _command_palette->close(); + } + + if (event->type == GDK_BUTTON_PRESS && event->button.button == 3) { + _dtw->desktop->getCanvasDrawing()->set_sticky(event->button.state & GDK_SHIFT_MASK); + } + + // Pass keyboard events back to the desktop root handler so TextTool can work + if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) + && !_canvas->get_current_canvas_item()) + { + return sp_desktop_root_handler(event, _dtw->desktop); + } + + return false; +} + +// TODO Add actions so we can set shortcuts. +// * Sticky Zoom +// * CMS Adjust +// * Guide Lock + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/canvas-grid.h b/src/ui/widget/canvas-grid.h new file mode 100644 index 0000000..5cddbbe --- /dev/null +++ b/src/ui/widget/canvas-grid.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_WIDGET_CANVASGRID_H +#define INKSCAPE_UI_WIDGET_CANVASGRID_H +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include <gtkmm/label.h> +#include <gtkmm/overlay.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/builder.h> + +class SPPage; +class SPDocument; +class SPCanvas; +class SPDesktopWidget; + +namespace Inkscape { +namespace UI { + +namespace Dialog { +class CommandPalette; +} + +namespace Widget { + +class Canvas; +class CanvasNotice; +class Ruler; + +/** + * A Gtk::Grid widget that contains rulers, scrollbars, buttons, and, of course, the canvas. + * Canvas has an overlay to let us put stuff on the canvas. + */ +class CanvasGrid : public Gtk::Grid +{ + using parent_type = Gtk::Grid; +public: + CanvasGrid(SPDesktopWidget *dtw); + ~CanvasGrid() override; + + void ShowScrollbars(bool state = true); + void ToggleScrollbars(); + + void ShowRulers(bool state = true); + void ToggleRulers(); + void UpdateRulers(); + + void ShowCommandPalette(bool state = true); + void ToggleCommandPalette(); + + void showNotice(Glib::ustring const &msg, unsigned timeout = 0); + + Inkscape::UI::Widget::Canvas *GetCanvas() { return _canvas.get(); }; + + // Hopefully temp. + Inkscape::UI::Widget::Ruler *GetHRuler() { return _vruler.get(); }; + Inkscape::UI::Widget::Ruler *GetVRuler() { return _hruler.get(); }; + Gtk::Adjustment *GetHAdj() { return _hadj.get(); }; + Gtk::Adjustment *GetVAdj() { return _vadj.get(); }; + Gtk::ToggleButton *GetGuideLock() { return &_guide_lock; } + Gtk::ToggleButton *GetCmsAdjust() { return &_cms_adjust; } + Gtk::ToggleButton *GetStickyZoom(); + +private: + // Signal callbacks + void on_size_allocate(Gtk::Allocation& allocation) override; + bool SignalEvent(GdkEvent *event); + void on_realize() override; + + // The widgets + std::unique_ptr<Inkscape::UI::Widget::Canvas> _canvas; + std::unique_ptr<Dialog::CommandPalette> _command_palette; + CanvasNotice *_notice; + Gtk::Overlay _canvas_overlay; + Gtk::Grid _subgrid; + + Glib::RefPtr<Gtk::Adjustment> _hadj; + Glib::RefPtr<Gtk::Adjustment> _vadj; + Gtk::Scrollbar _hscrollbar; + Gtk::Scrollbar _vscrollbar; + + std::unique_ptr<Inkscape::UI::Widget::Ruler> _hruler; + std::unique_ptr<Inkscape::UI::Widget::Ruler> _vruler; + + Gtk::ToggleButton _guide_lock; + Gtk::ToggleButton _cms_adjust; + Gtk::MenuButton _quick_actions; + Glib::RefPtr<Gtk::Builder> _display_popup; + + // To be replaced by stateful Gio::Actions + bool _show_scrollbars = true; + bool _show_rulers = true; + + // Hopefully temp + SPDesktopWidget *_dtw; + SPDocument *_document = nullptr; + + // Store allocation so we don't redraw too often. + Gtk::Allocation _allocation; + + // Connections for page and selection tracking + sigc::connection _page_selected_connection; + sigc::connection _page_modified_connection; + sigc::connection _sel_changed_connection; + sigc::connection _sel_modified_connection; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVASGRID_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/canvas-notice.cpp b/src/ui/widget/canvas-notice.cpp new file mode 100644 index 0000000..0337bf9 --- /dev/null +++ b/src/ui/widget/canvas-notice.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "canvas-notice.h" + +#include <utility> +#include <glibmm/main.h> + +#include "ui/builder-utils.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + +CanvasNotice::CanvasNotice(BaseObjectType *cobject, Glib::RefPtr<Gtk::Builder> builder) + : Gtk::Revealer(cobject) + , _builder(std::move(builder)) + , _icon(get_widget<Gtk::Image>(_builder, "notice-icon")) + , _label(get_widget<Gtk::Label>(_builder, "notice-label")) +{ + auto &close = get_widget<Gtk::Button>(_builder, "notice-close"); + close.signal_clicked().connect([=]() { + hide(); + }); +} + +void CanvasNotice::show(Glib::ustring const &msg, unsigned timeout) +{ + _label.set_text(msg); + set_reveal_child(true); + if (timeout != 0) { + _timeout = Glib::signal_timeout().connect([=]() { + hide(); + return false; + }, timeout); + } +} + +void CanvasNotice::hide() +{ + set_reveal_child(false); +} + +CanvasNotice *CanvasNotice::create() +{ + CanvasNotice *widget = nullptr; + auto builder = create_builder("canvas-notice.glade"); + builder->get_widget_derived("canvas-notice", widget); + return widget; +} + +}}} // namespaces diff --git a/src/ui/widget/canvas-notice.h b/src/ui/widget/canvas-notice.h new file mode 100644 index 0000000..88c7bed --- /dev/null +++ b/src/ui/widget/canvas-notice.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H +#define INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H + +#include <glibmm/refptr.h> +#include <gtkmm/builder.h> + +#include <gtkmm/revealer.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> +#include <gtkmm/button.h> + +#include "helper/auto-connection.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class CanvasNotice : public Gtk::Revealer { +public: + static CanvasNotice *create(); + + CanvasNotice(BaseObjectType *cobject, Glib::RefPtr<Gtk::Builder> refGlade); + void show(Glib::ustring const &msg, unsigned timeout = 0); + void hide(); +private: + Glib::RefPtr<Gtk::Builder> _builder; + + Gtk::Image& _icon; + Gtk::Label& _label; + + Inkscape::auto_connection _timeout; +}; + +}}} // namespaces + +#endif // INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp new file mode 100644 index 0000000..cfdb966 --- /dev/null +++ b/src/ui/widget/canvas.cpp @@ -0,0 +1,2426 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Tavmjong Bah + * PBS <pbs3141@gmail.com> + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> // Logging +#include <algorithm> // Sort +#include <set> // Coarsener +#include <thread> +#include <mutex> +#include <array> +#include <cassert> +#include <boost/asio/thread_pool.hpp> +#include <boost/asio/post.hpp> +#include <2geom/convex-hull.h> + +#include "canvas.h" +#include "canvas-grid.h" + +#include "color.h" // Background color +#include "cms-system.h" // Color correction +#include "desktop.h" +#include "document.h" +#include "preferences.h" +#include "ui/util.h" +#include "helper/geom.h" + +#include "canvas/prefs.h" +#include "canvas/fragment.h" +#include "canvas/util.h" +#include "canvas/stores.h" +#include "canvas/graphics.h" +#include "canvas/synchronizer.h" +#include "display/drawing.h" +#include "display/control/canvas-item-drawing.h" +#include "display/control/canvas-item-group.h" +#include "display/control/snap-indicator.h" + +#include "ui/tools/tool-base.h" // Default cursor + +#include "canvas/updaters.h" // Update strategies +#include "canvas/framecheck.h" // For frame profiling +#define framecheck_whole_function(D) \ + auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event(); + +/* + * The canvas is responsible for rendering the SVG drawing with various "control" + * items below and on top of the drawing. Rendering is triggered by a call to one of: + * + * + * * redraw_all() Redraws the entire canvas by calling redraw_area() with the canvas area. + * + * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't affect + * a CanvasItem's geometry or size. + * + * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a + * CanvasItem's geometry or size has changed. + * + * The first three functions add a request to the Gtk's "idle" list via + * + * * add_idle() Which causes Gtk to call when resources are available: + * + * * on_idle() Which sets up the backing stores, divides the area of the canvas that has been marked + * unclean into rectangles that are small enough to render quickly, and renders them outwards + * from the mouse with a call to: + * + * * paint_rect_internal() Which paints the rectangle using paint_single_buffer(). It renders onto a Cairo + * surface "backing_store". After a piece is rendered there is a call to: + * + * * queue_draw_area() A Gtk function for marking areas of the window as needing a repaint, which when + * the time is right calls: + * + * * on_draw() Which blits the Cairo surface to the screen. + * + * The other responsibility of the canvas is to determine where to send GUI events. It does this + * by determining which CanvasItem is "picked" and then forwards the events to that item. Not all + * items can be picked. As a last resort, the "CatchAll" CanvasItem will be picked as it is the + * lowest CanvasItem in the stack (except for the "root" CanvasItem). With a small be of work, it + * should be possible to make the "root" CanvasItem a "CatchAll" eliminating the need for a + * dedicated "CatchAll" CanvasItem. There probably could be efficiency improvements as some + * items that are not pickable probably should be which would save having to effectively pick + * them "externally" (e.g. gradient CanvasItemCurves). + */ + +namespace Inkscape::UI::Widget { +namespace { + +/* + * Utilities + */ + +// GdkEvents can only be safely copied using gdk_event_copy. Since this function allocates, we need the following smart pointer to wrap the result. +struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; +using GdkEventUniqPtr = std::unique_ptr<GdkEvent, GdkEventFreer>; + +// Copies a GdkEvent, returning the result as a smart pointer. +auto make_unique_copy(GdkEvent const *ev) { return GdkEventUniqPtr(gdk_event_copy(ev)); } + +// Convert an integer received from preferences into an Updater enum. +auto pref_to_updater(int index) +{ + constexpr auto arr = std::array{Updater::Strategy::Responsive, + Updater::Strategy::FullRedraw, + Updater::Strategy::Multiscale}; + assert(1 <= index && index <= arr.size()); + return arr[index - 1]; +} + +// Represents the raster data and location of an in-flight tile (one that is drawn, but not yet pasted into the stores). +struct Tile +{ + Fragment fragment; + Cairo::RefPtr<Cairo::ImageSurface> surface; + Cairo::RefPtr<Cairo::ImageSurface> outline_surface; +}; + +// The urgency with which the async redraw process should exit. +enum class AbortFlags : int +{ + None = 0, + Soft = 1, // exit if reached prerender phase + Hard = 2 // exit in any phase +}; + +// A copy of all the data the async redraw process needs access to, along with its internal state. +struct RedrawData +{ + // Data on what/how to draw. + Geom::IntPoint mouse_loc; + Geom::IntRect visible; + Fragment store; + bool decoupled_mode; + Cairo::RefPtr<Cairo::Region> snapshot_drawn; + Geom::OptIntRect grabbed; + + // Saved prefs + int coarsener_min_size; + int coarsener_glue_size; + double coarsener_min_fullness; + int tile_size; + int preempt; + int margin; + std::optional<int> redraw_delay; + int render_time_limit; + int numthreads; + bool background_in_stores_required; + uint64_t page, desk; + bool debug_framecheck; + bool debug_show_redraw; + + // State + std::mutex mutex; + gint64 start_time; + int numactive; + int phase; + Geom::OptIntRect vis_store; + + Geom::IntRect bounds; + Cairo::RefPtr<Cairo::Region> clean; + bool interruptible; + bool preemptible; + std::vector<Geom::IntRect> rects; + int effective_tile_size; + + // Results + std::mutex tiles_mutex; + std::vector<Tile> tiles; + bool timeoutflag; + + // Return comparison object for sorting rectangles by distance from mouse point. + auto getcmp() const + { + return [mouse_loc = mouse_loc] (Geom::IntRect const &a, Geom::IntRect const &b) { + return a.distanceSq(mouse_loc) > b.distanceSq(mouse_loc); + }; + } +}; + +} // namespace + +/* + * Implementation class + */ + +class CanvasPrivate +{ +public: + friend class Canvas; + Canvas *q; + CanvasPrivate(Canvas *q) + : q(q) + , stores(prefs) {} + + // Lifecycle + bool active = false; + void activate(); + void deactivate(); + + // CanvasItem tree + std::optional<CanvasItemContext> canvasitem_ctx; + + // Preferences + Prefs prefs; + + // Stores + Stores stores; + void handle_stores_action(Stores::Action action); + + // Invalidation + std::unique_ptr<Updater> updater; // Tracks the unclean region and decides how to redraw it. + Cairo::RefPtr<Cairo::Region> invalidated; // Buffers invalidations while the updater is in use by the background process. + + // Graphics state; holds all the graphics resources, including the drawn content. + std::unique_ptr<Graphics> graphics; + void activate_graphics(); + void deactivate_graphics(); + + // Redraw process management. + bool redraw_active = false; + bool redraw_requested = false; + sigc::connection schedule_redraw_conn; + void schedule_redraw(); + void launch_redraw(); + void after_redraw(); + void commit_tiles(); + + // Event handling. + bool process_event(const GdkEvent*); + bool pick_current_item(const GdkEvent*); + bool emit_event(const GdkEvent*); + Inkscape::CanvasItem *pre_scroll_grabbed_item; + + // Various state affecting what is drawn. + uint32_t desk = 0xffffffff; // The background colour, with the alpha channel used to control checkerboard. + uint32_t border = 0x000000ff; // The border colour, used only to control shadow colour. + uint32_t page = 0xffffffff; // The page colour, also with alpha channel used to control checkerboard. + + bool clip_to_page = false; // Whether to enable clip-to-page mode. + PageInfo pi; // The list of page rectangles. + std::optional<Geom::PathVector> calc_page_clip() const; // Union of the page rectangles if in clip-to-page mode, otherwise no clip. + + int scale_factor = 1; // The device scale the stores are drawn at. + + bool outlines_enabled = false; // Whether to enable the outline layer. + bool outlines_required() const { return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY; } + + 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. + 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). + + // Async redraw process. + std::optional<boost::asio::thread_pool> pool; + int numthreads; + int get_numthreads() const; + + Synchronizer sync; + RedrawData rd; + std::atomic<int> abort_flags; + + void init_tiler(); + bool init_redraw(); + bool end_redraw(); // returns true to indicate further redraw cycles required + void process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible = true, bool preemptible = true); + void render_tile(int debug_id); + void paint_rect(Geom::IntRect const &rect); + void paint_single_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface, const Geom::IntRect &rect, bool need_background, bool outline_pass); + void paint_error_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface); + + // Trivial overload of GtkWidget function. + void queue_draw_area(Geom::IntRect const &rect); + + // For tracking the last known mouse position. (The function Gdk::Window::get_device_position cannot be used because of slow X11 round-trips. Remove this workaround when X11 dies.) + std::optional<Geom::IntPoint> last_mouse; + + // Auto-scrolling. + std::optional<guint> tick_callback; + std::optional<gint64> last_time; + Geom::IntPoint strain; + Geom::Point displacement, velocity; + void autoscroll_begin(Geom::IntPoint const &to); + void autoscroll_end(); +}; + +/* + * Lifecycle + */ + +Canvas::Canvas() + : d(std::make_unique<CanvasPrivate>(this)) +{ + set_name("InkscapeCanvas"); + + // Events + add_events(Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::FOCUS_CHANGE_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::SCROLL_MASK | + Gdk::SMOOTH_SCROLL_MASK ); + + // Updater + d->updater = Updater::create(pref_to_updater(d->prefs.update_strategy)); + d->updater->reset(); + d->invalidated = Cairo::Region::create(); + + // Preferences + d->prefs.grabsize.action = [=] { d->canvasitem_ctx->root()->update_canvas_item_ctrl_sizes(d->prefs.grabsize); }; + d->prefs.debug_show_unclean.action = [=] { queue_draw(); }; + d->prefs.debug_show_clean.action = [=] { queue_draw(); }; + d->prefs.debug_disable_redraw.action = [=] { d->schedule_redraw(); }; + d->prefs.debug_sticky_decoupled.action = [=] { d->schedule_redraw(); }; + d->prefs.debug_animate.action = [=] { queue_draw(); }; + d->prefs.outline_overlay_opacity.action = [=] { queue_draw(); }; + d->prefs.softproof.action = [=] { redraw_all(); }; + d->prefs.displayprofile.action = [=] { redraw_all(); }; + d->prefs.request_opengl.action = [=] { + if (get_realized()) { + d->deactivate(); + d->deactivate_graphics(); + set_opengl_enabled(d->prefs.request_opengl); + d->updater->reset(); + d->activate_graphics(); + d->activate(); + } + }; + d->prefs.pixelstreamer_method.action = [=] { + if (get_realized() && get_opengl_enabled()) { + d->deactivate(); + d->deactivate_graphics(); + d->activate_graphics(); + d->activate(); + } + }; + d->prefs.numthreads.action = [=] { + if (!d->active) return; + int const new_numthreads = d->get_numthreads(); + if (d->numthreads == new_numthreads) return; + d->numthreads = new_numthreads; + d->deactivate(); + d->deactivate_graphics(); + d->pool.emplace(d->numthreads); + d->activate_graphics(); + d->activate(); + }; + + // Canvas item tree + d->canvasitem_ctx.emplace(this); + + // Split view. + _split_direction = Inkscape::SplitDirection::EAST; + _split_frac = {0.5, 0.5}; + + // Recreate stores on HiDPI change. + property_scale_factor().signal_changed().connect([this] { d->schedule_redraw(); }); + + // OpenGL switch. + set_opengl_enabled(d->prefs.request_opengl); + + // Async redraw process. + d->numthreads = d->get_numthreads(); + d->pool.emplace(d->numthreads); + + d->sync.connectExit([this] { d->after_redraw(); }); +} + +int CanvasPrivate::get_numthreads() const +{ + if (int n = prefs.numthreads; n > 0) { + // First choice is the value set in preferences. + return n; + } else if (int n = std::thread::hardware_concurrency(); n > 0) { + // If not set, use the number of processors minus one. (Using all of them causes stuttering.) + return n == 1 ? 1 : n - 1; + } else { + // If not reported, use a sensible fallback. + return 4; + } +} + +// Graphics becomes active when the widget is realized. +void CanvasPrivate::activate_graphics() +{ + if (q->get_opengl_enabled()) { + q->make_current(); + graphics = Graphics::create_gl(prefs, stores, pi); + } else { + graphics = Graphics::create_cairo(prefs, stores, pi); + } + stores.set_graphics(graphics.get()); + stores.reset(); +} + +// After graphics becomes active, the canvas becomes active when additionally a drawing is set. +void CanvasPrivate::activate() +{ + // Event handling/item picking + q->_pick_event.type = GDK_LEAVE_NOTIFY; + q->_pick_event.crossing.x = 0; + q->_pick_event.crossing.y = 0; + + q->_in_repick = false; + q->_left_grabbed_item = false; + q->_all_enter_events = false; + q->_is_dragging = false; + q->_state = 0; + + q->_current_canvas_item = nullptr; + q->_current_canvas_item_new = nullptr; + q->_grabbed_canvas_item = nullptr; + q->_grabbed_event_mask = (Gdk::EventMask)0; + pre_scroll_grabbed_item = nullptr; + + // Drawing + q->_need_update = true; + + // Split view + q->_split_dragging = false; + + // Todo: Disable GTK event compression again when doing so is no longer buggy. + // Note: ToolBase::set_high_motion_precision() will keep turning it back on. + // q->get_window()->set_event_compression(false); + + active = true; + + schedule_redraw(); +} + +void CanvasPrivate::deactivate() +{ + active = false; + + if (redraw_active) { + if (schedule_redraw_conn.connected()) { + // In first link in chain, from schedule_redraw() to launch_redraw(). Break the link and exit. + schedule_redraw_conn.disconnect(); + } else { + // Otherwise, the background process is running. Interrupt the signal chain at exit. + abort_flags.store((int)AbortFlags::Hard, std::memory_order_relaxed); + if (prefs.debug_logging) std::cout << "Hard exit request" << std::endl; + sync.waitForExit(); + + // Unsnapshot the CanvasItems and DrawingItems. + canvasitem_ctx->unsnapshot(); + q->_drawing->unsnapshot(); + } + + redraw_active = false; + redraw_requested = false; + assert(!schedule_redraw_conn.connected()); + } +} + +void CanvasPrivate::deactivate_graphics() +{ + if (q->get_opengl_enabled()) q->make_current(); + commit_tiles(); + stores.set_graphics(nullptr); + graphics.reset(); +} + +Canvas::~Canvas() +{ + // Remove entire CanvasItem tree. + d->canvasitem_ctx.reset(); +} + +void Canvas::set_drawing(Drawing *drawing) +{ + if (d->active && !drawing) d->deactivate(); + _drawing = drawing; + if (_drawing) { + _drawing->setRenderMode(_render_mode == RenderMode::OUTLINE_OVERLAY ? RenderMode::NORMAL : _render_mode); + _drawing->setColorMode(_color_mode); + _drawing->setOutlineOverlay(d->outlines_required()); + } + if (!d->active && get_realized() && drawing) d->activate(); +} + +CanvasItemGroup *Canvas::get_canvas_item_root() const +{ + return d->canvasitem_ctx->root(); +} + +void Canvas::on_realize() +{ + parent_type::on_realize(); + d->activate_graphics(); + if (_drawing) d->activate(); +} + +void Canvas::on_unrealize() +{ + if (_drawing) d->deactivate(); + d->deactivate_graphics(); + parent_type::on_unrealize(); +} + +/* + * Redraw process managment + */ + +// Schedule another redraw iteration to take place, waiting for the current one to finish if necessary. +void CanvasPrivate::schedule_redraw() +{ + if (!active) { + // We can safely discard calls until active, because we will run an iteration on activation later in initialisation. + return; + } + + // Ensure another iteration is performed if one is in progress. + redraw_requested = true; + + if (redraw_active) { + return; + } + + redraw_active = true; + + // Call run_redraw() as soon as possible on the main loop. (Cannot run now since CanvasItem tree could be in an invalid intermediate state.) + assert(!schedule_redraw_conn.connected()); + schedule_redraw_conn = Glib::signal_idle().connect([this] { + if (q->get_opengl_enabled()) { + q->make_current(); + } + if (prefs.debug_logging) std::cout << "Redraw start" << std::endl; + launch_redraw(); + return false; + }, Glib::PRIORITY_HIGH); +} + +// Update state and launch redraw process in background. Requires a current OpenGL context. +void CanvasPrivate::launch_redraw() +{ + assert(redraw_active); + + // Determine whether the rendering parameters have changed, and trigger full store recreation if so. + if ((outlines_required() && !outlines_enabled) || scale_factor != q->get_scale_factor()) { + stores.reset(); + } + + outlines_enabled = outlines_required(); + scale_factor = q->get_scale_factor(); + + graphics->set_outlines_enabled(outlines_enabled); + graphics->set_scale_factor(scale_factor); + + /* + * Update state. + */ + + // Page information. + pi.pages.clear(); + canvasitem_ctx->root()->visit_page_rects([this] (auto &rect) { + pi.pages.emplace_back(rect); + }); + + graphics->set_colours(page, desk, border); + graphics->set_background_in_stores(background_in_stores_required()); + + q->_drawing->setClip(calc_page_clip()); + + // Stores. + handle_stores_action(stores.update(Fragment{ q->_affine, q->get_area_world() })); + + // Geometry. + bool const affine_changed = canvasitem_ctx->affine() != stores.store().affine; + if (q->_need_update || affine_changed) { + FrameCheck::Event fc; + if (prefs.debug_framecheck) fc = FrameCheck::Event("update"); + q->_need_update = false; + canvasitem_ctx->setAffine(stores.store().affine); + canvasitem_ctx->root()->update(affine_changed); + } + + // Update strategy. + auto const strategy = pref_to_updater(prefs.update_strategy); + if (updater->get_strategy() != strategy) { + auto new_updater = Updater::create(strategy); + new_updater->clean_region = std::move(updater->clean_region); + updater = std::move(new_updater); + } + + updater->mark_dirty(invalidated); + invalidated = Cairo::Region::create(); + + updater->next_frame(); + + /* + * Launch redraw process in background. + */ + + // If asked to, don't paint anything and instead halt the redraw process. + if (prefs.debug_disable_redraw) { + redraw_active = false; + return; + } + + // Snapshot the CanvasItems and DrawingItems. + canvasitem_ctx->snapshot(); + q->_drawing->snapshot(); + + // Get the mouse position in screen space. + rd.mouse_loc = last_mouse.value_or((Geom::Point(q->get_dimensions()) / 2).round()); + + // Map the mouse to canvas space. + rd.mouse_loc += q->_pos; + if (stores.mode() == Stores::Mode::Decoupled) { + rd.mouse_loc = (Geom::Point(rd.mouse_loc) * q->_affine.inverse() * stores.store().affine).round(); + } + + // Get the visible rect. + rd.visible = q->get_area_world(); + if (stores.mode() == Stores::Mode::Decoupled) { + rd.visible = (Geom::Parallelogram(rd.visible) * q->_affine.inverse() * stores.store().affine).bounds().roundOutwards(); + } + + // Get other misc data. + rd.store = Fragment{ stores.store().affine, stores.store().rect }; + rd.decoupled_mode = stores.mode() == Stores::Mode::Decoupled; + rd.coarsener_min_size = prefs.coarsener_min_size; + rd.coarsener_glue_size = prefs.coarsener_glue_size; + rd.coarsener_min_fullness = prefs.coarsener_min_fullness; + rd.tile_size = prefs.tile_size; + rd.preempt = prefs.preempt; + rd.margin = prefs.prerender; + rd.redraw_delay = prefs.debug_delay_redraw ? std::make_optional<int>(prefs.debug_delay_redraw_time) : std::nullopt; + rd.render_time_limit = prefs.render_time_limit; + rd.numthreads = get_numthreads(); + rd.background_in_stores_required = background_in_stores_required(); + rd.page = page; + rd.desk = desk; + rd.debug_framecheck = prefs.debug_framecheck; + rd.debug_show_redraw = prefs.debug_show_redraw; + + rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr<Cairo::Region>(); + rd.grabbed = q->_grabbed_canvas_item && prefs.block_updates ? (roundedOutwards(q->_grabbed_canvas_item->get_bounds()) & rd.visible & rd.store.rect).regularized() : Geom::OptIntRect(); + + abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed); + + boost::asio::post(*pool, [this] { init_tiler(); }); +} + +void CanvasPrivate::after_redraw() +{ + assert(redraw_active); + + // Unsnapshot the CanvasItems and DrawingItems. + canvasitem_ctx->unsnapshot(); + q->_drawing->unsnapshot(); + + // OpenGL context needed for commit_tiles(), stores.finished_draw(), and launch_redraw(). + if (q->get_opengl_enabled()) { + q->make_current(); + } + + // Commit tiles before stores.finished_draw() to avoid changing stores while tiles are still pending. + commit_tiles(); + + // Handle any pending stores action. + bool stores_changed = false; + if (!rd.timeoutflag) { + auto const ret = stores.finished_draw(Fragment{ q->_affine, q->get_area_world() }); + handle_stores_action(ret); + if (ret != Stores::Action::None) { + stores_changed = true; + } + } + + // Relaunch or stop as necessary. + if (rd.timeoutflag || redraw_requested || stores_changed) { + if (prefs.debug_logging) std::cout << "Continuing redrawing" << std::endl; + redraw_requested = false; + launch_redraw(); + } else { + if (prefs.debug_logging) std::cout << "Redraw exit" << std::endl; + redraw_active = false; + } +} + +void CanvasPrivate::handle_stores_action(Stores::Action action) +{ + switch (action) { + case Stores::Action::Recreated: + // Set everything as needing redraw. + invalidated->do_union(geom_to_cairo(stores.store().rect)); + updater->reset(); + + if (prefs.debug_show_unclean) q->queue_draw(); + break; + + case Stores::Action::Shifted: + invalidated->intersect(geom_to_cairo(stores.store().rect)); + updater->intersect(stores.store().rect); + + if (prefs.debug_show_unclean) q->queue_draw(); + break; + + default: + break; + } + + if (action != Stores::Action::None) { + q->_drawing->setCacheLimit(stores.store().rect); + } +} + +// Commit all in-flight tiles to the stores. Requires a current OpenGL context (for graphics->draw_tile). +void CanvasPrivate::commit_tiles() +{ + framecheck_whole_function(this) + + decltype(rd.tiles) tiles; + + { + auto lock = std::lock_guard(rd.tiles_mutex); + tiles = std::move(rd.tiles); + } + + for (auto &tile : tiles) { + // Todo: Make CMS system thread-safe, then move this to render thread too. + if (q->_cms_active) { + auto transf = prefs.from_display + ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key) + : Inkscape::CMSSystem::getDisplayTransform(); + if (transf) { + tile.surface->flush(); + auto px = tile.surface->get_data(); + int stride = tile.surface->get_stride(); + for (int i = 0; i < tile.surface->get_height(); i++) { + auto row = px + i * stride; + Inkscape::CMSSystem::doTransform(transf, row, row, tile.surface->get_width()); + } + tile.surface->mark_dirty(); + } + } + + // Paste tile content onto stores. + graphics->draw_tile(tile.fragment, std::move(tile.surface), std::move(tile.outline_surface)); + + // Add to drawn region. + assert(stores.store().rect.contains(tile.fragment.rect)); + stores.mark_drawn(tile.fragment.rect); + + // Get the rectangle of screen-space needing repaint. + Geom::IntRect repaint_rect; + if (stores.mode() == Stores::Mode::Normal) { + // Simply translate to get back to screen space. + repaint_rect = tile.fragment.rect - q->_pos; + } else { + // Transform into screen space, take bounding box, and round outwards. + auto pl = Geom::Parallelogram(tile.fragment.rect); + pl *= stores.store().affine.inverse() * q->_affine; + pl *= Geom::Translate(-q->_pos); + repaint_rect = pl.bounds().roundOutwards(); + } + + // Check if repaint is necessary - some rectangles could be entirely off-screen. + auto screen_rect = Geom::IntRect({0, 0}, q->get_dimensions()); + if ((repaint_rect & screen_rect).regularized()) { + // Schedule repaint. + queue_draw_area(repaint_rect); + } + } +} + +/* + * Auto-scrolling + */ + +static Geom::Point cap_length(Geom::Point const &pt, double max) +{ + auto const r = pt.length(); + return r <= max ? pt : pt * max / r; +} + +static double profile(double r) +{ + constexpr double max_speed = 30.0; + constexpr double max_distance = 25.0; + return std::clamp(Geom::sqr(r / max_distance) * max_speed, 1.0, max_speed); +} + +static Geom::Point apply_profile(Geom::Point const &pt) +{ + auto const r = pt.length(); + if (r <= Geom::EPSILON) return {}; + return pt * profile(r) / r; +} + +void CanvasPrivate::autoscroll_begin(Geom::IntPoint const &to) +{ + if (!q->_desktop) { + return; + } + + auto const rect = expandedBy(Geom::IntRect({}, q->get_dimensions()), -(int)prefs.autoscrolldistance); + strain = to - rect.clamp(to); + + if (strain == Geom::IntPoint(0, 0) || tick_callback) { + return; + } + + tick_callback = q->add_tick_callback([this] (Glib::RefPtr<Gdk::FrameClock> const &clock) { + auto timings = clock->get_current_timings(); + auto const t = timings->get_frame_time(); + double dt; + if (last_time) { + dt = t - *last_time; + } else { + dt = timings->get_refresh_interval(); + } + last_time = t; + dt *= 60.0 / 1e6 * prefs.autoscrollspeed; + + bool const strain_zero = strain == Geom::IntPoint(0, 0); + + if (strain.x() * velocity.x() < 0) velocity.x() = 0; + if (strain.y() * velocity.y() < 0) velocity.y() = 0; + auto const tgtvel = apply_profile(strain); + auto const max_accel = strain_zero ? 3 : 2; + velocity += cap_length(tgtvel - velocity, max_accel * dt); + displacement += velocity * dt; + auto const dpos = displacement.round(); + q->_desktop->scroll_relative(-dpos); + displacement -= dpos; + + if (last_mouse) { + GdkEventMotion event; + memset(&event, 0, sizeof(GdkEventMotion)); + event.type = GDK_MOTION_NOTIFY; + event.x = last_mouse->x(); + event.y = last_mouse->y(); + event.state = q->_state; + emit_event(reinterpret_cast<GdkEvent*>(&event)); + } + + if (strain_zero && velocity.length() <= 0.1) { + tick_callback = {}; + last_time = {}; + displacement = velocity = {}; + return false; + } + + q->queue_draw(); + + return true; + }); +} + +void CanvasPrivate::autoscroll_end() +{ + strain = {}; +} + +// Allow auto-scrolling to take place if the mouse reaches the edge. +// The effect wears off when the mouse is next released. +void Canvas::enable_autoscroll() +{ + if (d->last_mouse) { + d->autoscroll_begin(*d->last_mouse); + } else { + d->autoscroll_end(); + } +} + +/* + * Event handling + */ + +bool Canvas::on_scroll_event(GdkEventScroll *scroll_event) +{ + return d->process_event(reinterpret_cast<GdkEvent*>(scroll_event)); +} + +bool Canvas::on_button_press_event(GdkEventButton *button_event) +{ + return on_button_event(button_event); +} + +bool Canvas::on_button_release_event(GdkEventButton *button_event) +{ + if (button_event->button == 1) { + d->autoscroll_end(); + } + + return on_button_event(button_event); +} + +// Unified handler for press and release events. +bool Canvas::on_button_event(GdkEventButton *button_event) +{ + // Sanity-check event type. + switch (button_event->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + break; // Good + default: + std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl; + return false; + } + + // Drag the split view controller. + if (_split_mode == Inkscape::SplitMode::SPLIT) { + auto cursor_position = Geom::IntPoint(button_event->x, button_event->y); + switch (button_event->type) { + case GDK_BUTTON_PRESS: + if (_hover_direction != Inkscape::SplitDirection::NONE) { + _split_dragging = true; + _split_drag_start = cursor_position; + return true; + } + break; + case GDK_2BUTTON_PRESS: + if (_hover_direction != Inkscape::SplitDirection::NONE) { + _split_direction = _hover_direction; + _split_dragging = false; + queue_draw(); + return true; + } + break; + case GDK_BUTTON_RELEASE: + if (!_split_dragging) break; + _split_dragging = false; + + // Check if we are near the edge. If so, revert to normal mode. + if (cursor_position.x() < 5 || + cursor_position.y() < 5 || + cursor_position.x() > get_allocation().get_width() - 5 || + cursor_position.y() > get_allocation().get_height() - 5) + { + // Reset everything. + _split_frac = {0.5, 0.5}; + set_cursor(); + set_split_mode(Inkscape::SplitMode::NORMAL); + + // Update action (turn into utility function?). + auto window = dynamic_cast<Gtk::ApplicationWindow*>(get_toplevel()); + if (!window) { + std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl; + return true; + } + + auto action = window->lookup_action("canvas-split-mode"); + if (!action) { + std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl; + return true; + } + + auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(action); + if (!saction) { + std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl; + return true; + } + + saction->change_state(static_cast<int>(Inkscape::SplitMode::NORMAL)); + } + + break; + + default: + break; + } + } + + return d->process_event(reinterpret_cast<GdkEvent*>(button_event)); +} + +bool Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) +{ + if (crossing_event->window != get_window()->gobj()) { + return false; + } + return d->process_event(reinterpret_cast<GdkEvent*>(crossing_event)); +} + +bool Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) +{ + if (crossing_event->window != get_window()->gobj()) { + return false; + } + d->last_mouse = {}; + return d->process_event(reinterpret_cast<GdkEvent*>(crossing_event)); +} + +bool Canvas::on_focus_in_event(GdkEventFocus *focus_event) +{ + grab_focus(); + return false; +} + +bool Canvas::on_key_press_event(GdkEventKey *key_event) +{ + return d->process_event(reinterpret_cast<GdkEvent*>(key_event)); +} + +bool Canvas::on_key_release_event(GdkEventKey *key_event) +{ + return d->process_event(reinterpret_cast<GdkEvent*>(key_event)); +} + +bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) +{ + // Record the last mouse position. + d->last_mouse = Geom::IntPoint(motion_event->x, motion_event->y); + + // Handle interactions with the split view controller. + if (_split_mode == Inkscape::SplitMode::XRAY) { + queue_draw(); + } else if (_split_mode == Inkscape::SplitMode::SPLIT) { + auto cursor_position = Geom::IntPoint(motion_event->x, motion_event->y); + + // Move controller. + if (_split_dragging) { + auto delta = cursor_position - _split_drag_start; + if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) { + delta.x() = 0; + } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) { + delta.y() = 0; + } + _split_frac += Geom::Point(delta) / get_dimensions(); + _split_drag_start = cursor_position; + queue_draw(); + return true; + } + + auto split_position = (_split_frac * get_dimensions()).round(); + auto diff = cursor_position - split_position; + auto hover_direction = Inkscape::SplitDirection::NONE; + if (Geom::Point(diff).length() < 20.0) { + // We're hovering over circle, figure out which direction we are in. + if (diff.y() - diff.x() > 0) { + if (diff.y() + diff.x() > 0) { + hover_direction = Inkscape::SplitDirection::SOUTH; + } else { + hover_direction = Inkscape::SplitDirection::WEST; + } + } else { + if (diff.y() + diff.x() > 0) { + hover_direction = Inkscape::SplitDirection::EAST; + } else { + hover_direction = Inkscape::SplitDirection::NORTH; + } + } + } else if (_split_direction == Inkscape::SplitDirection::NORTH || + _split_direction == Inkscape::SplitDirection::SOUTH) + { + if (std::abs(diff.y()) < 3) { + // We're hovering over the horizontal line. + hover_direction = Inkscape::SplitDirection::HORIZONTAL; + } + } else { + if (std::abs(diff.x()) < 3) { + // We're hovering over the vertical line. + hover_direction = Inkscape::SplitDirection::VERTICAL; + } + } + + if (_hover_direction != hover_direction) { + _hover_direction = hover_direction; + set_cursor(); + queue_draw(); + } + + if (_hover_direction != Inkscape::SplitDirection::NONE) { + // We're hovering, don't pick or emit event. + return true; + } + } + + // Avoid embarrassing neverending autoscroll in case the button-released handler somehow doesn't fire. + if (!(motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) { + d->autoscroll_end(); + } + + return d->process_event(reinterpret_cast<GdkEvent*>(motion_event)); +} + +// Unified handler for all events. +bool CanvasPrivate::process_event(const GdkEvent *event) +{ + framecheck_whole_function(this) + + if (!active) { + std::cerr << "Canvas::process_event: Called while not active!" << std::endl; + return false; + } + + auto calc_button_mask = [&] () -> int { + switch (event->button.button) { + case 1: return GDK_BUTTON1_MASK; break; + case 2: return GDK_BUTTON2_MASK; break; + case 3: return GDK_BUTTON3_MASK; break; + case 4: return GDK_BUTTON4_MASK; break; + case 5: return GDK_BUTTON5_MASK; break; + default: return 0; // Buttons can range at least to 9 but mask defined only to 5. + } + }; + + // Do event-specific processing. + switch (event->type) { + case GDK_SCROLL: + { + // Save the current event-receiving item just before scrolling starts. It will continue to receive scroll events until the mouse is moved. + if (!pre_scroll_grabbed_item) { + pre_scroll_grabbed_item = q->_current_canvas_item; + if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { + pre_scroll_grabbed_item = q->_grabbed_canvas_item; + } + } + + // Process the scroll event... + bool retval = emit_event(event); + + // ...then repick. + q->_state = event->scroll.state; + pick_current_item(event); + + return retval; + } + + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + { + pre_scroll_grabbed_item = nullptr; + + // Pick the current item as if the button were not pressed... + q->_state = event->button.state; + pick_current_item(event); + + // ...then process the event. + q->_state ^= calc_button_mask(); + return emit_event(event); + } + + case GDK_BUTTON_RELEASE: + { + pre_scroll_grabbed_item = nullptr; + + // Process the event as if the button were pressed... + q->_state = event->button.state; + bool retval = emit_event(event); + + // ...then repick after the button has been released. + auto event_copy = make_unique_copy(event); + event_copy->button.state ^= calc_button_mask(); + q->_state = event_copy->button.state; + pick_current_item(event_copy.get()); + + return retval; + } + + case GDK_ENTER_NOTIFY: + pre_scroll_grabbed_item = nullptr; + q->_state = event->crossing.state; + return pick_current_item(event); + + case GDK_LEAVE_NOTIFY: + pre_scroll_grabbed_item = nullptr; + q->_state = event->crossing.state; + // This is needed to remove alignment or distribution snap indicators. + if (q->_desktop) { + q->_desktop->snapindicator->remove_snaptarget(); + } + return pick_current_item(event); + + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + return emit_event(event); + + case GDK_MOTION_NOTIFY: + pre_scroll_grabbed_item = nullptr; + q->_state = event->motion.state; + pick_current_item(event); + return emit_event(event); + + default: + return false; + } +} + +// This function is called by 'process_event' to manipulate the state variables relating +// to the current object under the mouse, for example, to generate enter and leave events. +// +// This routine reacts to events from the canvas. Its main purpose is to find the canvas item +// closest to the cursor where the event occurred and then send the event (sometimes modified) to +// that item. The event then bubbles up the canvas item tree until an object handles it. If the +// widget is redrawn, this routine may be called again for the same event. +// +// Canvas items register their interest by connecting to the "event" signal. +// Example in desktop.cpp: +// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); +bool CanvasPrivate::pick_current_item(const GdkEvent *event) +{ + // Ensure requested geometry updates are performed first. + if (q->_need_update && !q->_drawing->snapshotted() && !canvasitem_ctx->snapshotted()) { + FrameCheck::Event fc; + if (prefs.debug_framecheck) fc = FrameCheck::Event("update", 1); + q->_need_update = false; + canvasitem_ctx->root()->update(false); + } + + int button_down = 0; + if (!q->_all_enter_events) { + // Only set true in connector-tool.cpp. + + // If a button is down, we'll perform enter and leave events on the + // current item, but not enter on any other item. This is more or + // less like X pointer grabbing for canvas items. + button_down = q->_state & (GDK_BUTTON1_MASK | + GDK_BUTTON2_MASK | + GDK_BUTTON3_MASK | + GDK_BUTTON4_MASK | + GDK_BUTTON5_MASK); + if (!button_down) q->_left_grabbed_item = false; + } + + // Save the event in the canvas. This is used to synthesize enter and + // leave events in case the current item changes. It is also used to + // re-pick the current item if the current one gets deleted. Also, + // synthesize an enter event. + if (event != &q->_pick_event) { + if (event->type == GDK_MOTION_NOTIFY || event->type == GDK_SCROLL || event->type == GDK_BUTTON_RELEASE) { + // Convert to GDK_ENTER_NOTIFY + + // These fields have the same offsets in all types of events. + q->_pick_event.crossing.type = GDK_ENTER_NOTIFY; + q->_pick_event.crossing.window = event->motion.window; + q->_pick_event.crossing.send_event = event->motion.send_event; + q->_pick_event.crossing.subwindow = nullptr; + q->_pick_event.crossing.x = event->motion.x; + q->_pick_event.crossing.y = event->motion.y; + q->_pick_event.crossing.mode = GDK_CROSSING_NORMAL; + q->_pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; + q->_pick_event.crossing.focus = false; + + // These fields don't have the same offsets in all types of events. + switch (event->type) + { + case GDK_MOTION_NOTIFY: + q->_pick_event.crossing.state = event->motion.state; + q->_pick_event.crossing.x_root = event->motion.x_root; + q->_pick_event.crossing.y_root = event->motion.y_root; + break; + case GDK_SCROLL: + q->_pick_event.crossing.state = event->scroll.state; + q->_pick_event.crossing.x_root = event->scroll.x_root; + q->_pick_event.crossing.y_root = event->scroll.y_root; + break; + case GDK_BUTTON_RELEASE: + q->_pick_event.crossing.state = event->button.state; + q->_pick_event.crossing.x_root = event->button.x_root; + q->_pick_event.crossing.y_root = event->button.y_root; + break; + default: + assert(false); + } + + } else { + q->_pick_event = *event; + } + } + + if (q->_in_repick) { + // Don't do anything else if this is a recursive call. + return false; + } + + // Find new item + q->_current_canvas_item_new = nullptr; + + if (q->_pick_event.type != GDK_LEAVE_NOTIFY && canvasitem_ctx->root()->is_visible()) { + // Leave notify means there is no current item. + // Find closest item. + double x = 0.0; + double y = 0.0; + + if (q->_pick_event.type == GDK_ENTER_NOTIFY) { + x = q->_pick_event.crossing.x; + y = q->_pick_event.crossing.y; + } else { + x = q->_pick_event.motion.x; + y = q->_pick_event.motion.y; + } + + // Look at where the cursor is to see if one should pick with outline mode. + bool outline = q->canvas_point_in_outline_zone({ x, y }); + + // Convert to world coordinates. + auto p = Geom::Point(x, y) + q->_pos; + if (stores.mode() == Stores::Mode::Decoupled) { + p *= q->_affine.inverse() * canvasitem_ctx->affine(); + } + + q->_drawing->getCanvasItemDrawing()->set_pick_outline(outline); + q->_current_canvas_item_new = canvasitem_ctx->root()->pick_item(p); + // if (q->_current_canvas_item_new) { + // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl; + // } else { + // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl; + // } + } + + if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) { + // Current item did not change! + return false; + } + + // Synthesize events for old and new current items. + bool retval = false; + if (q->_current_canvas_item_new != q->_current_canvas_item && + q->_current_canvas_item != nullptr && + !q->_left_grabbed_item ) { + + GdkEvent new_event; + new_event = q->_pick_event; + new_event.type = GDK_LEAVE_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + q->_in_repick = true; + retval = emit_event(&new_event); + q->_in_repick = false; + } + + if (q->_all_enter_events == false) { + // new_current_item may have been set to nullptr during the call to emitEvent() above. + if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) { + q->_left_grabbed_item = true; + return retval; + } + } + + // Handle the rest of cases + q->_left_grabbed_item = false; + q->_current_canvas_item = q->_current_canvas_item_new; + + if (q->_current_canvas_item != nullptr) { + GdkEvent new_event; + new_event = q->_pick_event; + new_event.type = GDK_ENTER_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + retval = emit_event(&new_event); + } + + return retval; +} + +// Fires an event at the canvas, after a little pre-processing. Returns true if handled. +bool CanvasPrivate::emit_event(const GdkEvent *event) +{ + // Handle grabbed items. + if (q->_grabbed_canvas_item) { + auto mask = (Gdk::EventMask)0; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + mask = Gdk::ENTER_NOTIFY_MASK; + break; + case GDK_LEAVE_NOTIFY: + mask = Gdk::LEAVE_NOTIFY_MASK; + break; + case GDK_MOTION_NOTIFY: + mask = Gdk::POINTER_MOTION_MASK; + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + mask = Gdk::BUTTON_PRESS_MASK; + break; + case GDK_BUTTON_RELEASE: + mask = Gdk::BUTTON_RELEASE_MASK; + break; + case GDK_KEY_PRESS: + mask = Gdk::KEY_PRESS_MASK; + break; + case GDK_KEY_RELEASE: + mask = Gdk::KEY_RELEASE_MASK; + break; + case GDK_SCROLL: + mask = Gdk::SCROLL_MASK; + mask |= Gdk::SMOOTH_SCROLL_MASK; + break; + default: + break; + } + + if (!(mask & q->_grabbed_event_mask)) { + return false; + } + } + + // Convert to world coordinates. We have two different cases due to different event structures. + auto conv = [&, this] (double &x, double &y) { + auto p = Geom::Point(x, y) + q->_pos; + if (stores.mode() == Stores::Mode::Decoupled) { + p *= q->_affine.inverse() * canvasitem_ctx->affine(); + } + x = p.x(); + y = p.y(); + }; + + auto event_copy = make_unique_copy(event); + + switch (event->type) { + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + conv(event_copy->crossing.x, event_copy->crossing.y); + break; + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + conv(event_copy->motion.x, event_copy->motion.y); + break; + default: + break; + } + + // Block undo/redo while anything is dragged. + if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) { + q->_is_dragging = true; + } else if (event->type == GDK_BUTTON_RELEASE) { + q->_is_dragging = false; + } + + if (q->_current_canvas_item) { + // Choose where to send event. + auto item = q->_current_canvas_item; + + if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { + item = q->_grabbed_canvas_item; + } + + if (pre_scroll_grabbed_item && event->type == GDK_SCROLL) { + item = pre_scroll_grabbed_item; + } + + // Propagate the event up the canvas item hierarchy until handled. + while (item) { + if (item->handle_event(event_copy.get())) return true; + item = item->get_parent(); + } + } + + return false; +} + +/* + * Protected functions + */ + +Geom::IntPoint Canvas::get_dimensions() const +{ + return dimensions(get_allocation()); +} + +/** + * Is world point inside canvas area? + */ +bool Canvas::world_point_inside_canvas(Geom::Point const &world) const +{ + return get_area_world().contains(world.floor()); +} + +/** + * Translate point in canvas to world coordinates. + */ +Geom::Point Canvas::canvas_to_world(Geom::Point const &point) const +{ + return point + _pos; +} + +/** + * Return the area shown in the canvas in world coordinates. + */ +Geom::IntRect Canvas::get_area_world() const +{ + return Geom::IntRect(_pos, _pos + get_dimensions()); +} + +/** + * Return whether a point in screen space / canvas coordinates is inside the region + * of the canvas where things respond to mouse clicks as if they are in outline mode. + */ +bool Canvas::canvas_point_in_outline_zone(Geom::Point const &p) const +{ + if (_render_mode == RenderMode::OUTLINE || _render_mode == RenderMode::OUTLINE_OVERLAY) { + return true; + } else if (_split_mode == SplitMode::SPLIT) { + auto split_position = _split_frac * get_dimensions(); + switch (_split_direction) { + case SplitDirection::NORTH: return p.y() > split_position.y(); + case SplitDirection::SOUTH: return p.y() < split_position.y(); + case SplitDirection::WEST: return p.x() > split_position.x(); + case SplitDirection::EAST: return p.x() < split_position.x(); + default: return false; + } + } else { + return false; + } +} + +/** + * Return the last known mouse position of center if off-canvas. + */ +std::optional<Geom::Point> Canvas::get_last_mouse() const +{ + return d->last_mouse; +} + +const Geom::Affine &Canvas::get_geom_affine() const +{ + return d->canvasitem_ctx->affine(); +} + +void CanvasPrivate::queue_draw_area(const Geom::IntRect &rect) +{ + if (q->get_opengl_enabled()) { + // Note: GTK glitches out when you use queue_draw_area in OpenGL mode. + // It's also pointless, because it seems to just call queue_draw anyway. + q->queue_draw(); + } else { + q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); + } +} + +/** + * Invalidate drawing and redraw during idle. + */ +void Canvas::redraw_all() +{ + if (!d->active) { + // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed. + // We need to ignore their requests! + return; + } + d->invalidated->do_union(geom_to_cairo(d->stores.store().rect)); + d->schedule_redraw(); + if (d->prefs.debug_show_unclean) queue_draw(); +} + +/** + * Redraw the given area during idle. + */ +void Canvas::redraw_area(int x0, int y0, int x1, int y1) +{ + if (!d->active) { + // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed. + // We need to ignore their requests! + return; + } + + // Clamp area to Cairo's technically supported max size (-2^30..+2^30-1). + // This ensures that the rectangle dimensions don't overflow and wrap around. + constexpr int min_coord = -(1 << 30); + constexpr int max_coord = (1 << 30) - 1; + + x0 = std::clamp(x0, min_coord, max_coord); + y0 = std::clamp(y0, min_coord, max_coord); + x1 = std::clamp(x1, min_coord, max_coord); + y1 = std::clamp(y1, min_coord, max_coord); + + if (x0 >= x1 || y0 >= y1) { + return; + } + + if (d->redraw_active && d->invalidated->empty()) { + d->abort_flags.store((int)AbortFlags::Soft, std::memory_order_relaxed); // responding to partial invalidations takes priority over prerendering + if (d->prefs.debug_logging) std::cout << "Soft exit request" << std::endl; + } + + auto const rect = Geom::IntRect(x0, y0, x1, y1); + d->invalidated->do_union(geom_to_cairo(rect)); + d->schedule_redraw(); + if (d->prefs.debug_show_unclean) queue_draw(); +} + +void Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1) +{ + // Handle overflow during conversion gracefully. + // Round outward to make sure integral coordinates cover the entire area. + constexpr Geom::Coord min_int = std::numeric_limits<int>::min(); + constexpr Geom::Coord max_int = std::numeric_limits<int>::max(); + + redraw_area( + (int)std::floor(std::clamp(x0, min_int, max_int)), + (int)std::floor(std::clamp(y0, min_int, max_int)), + (int)std::ceil (std::clamp(x1, min_int, max_int)), + (int)std::ceil (std::clamp(y1, min_int, max_int)) + ); +} + +void Canvas::redraw_area(Geom::Rect const &area) +{ + redraw_area(area.left(), area.top(), area.right(), area.bottom()); +} + +/** + * Redraw after changing canvas item geometry. + */ +void Canvas::request_update() +{ + // Flag geometry as needing update. + _need_update = true; + + // Trigger the redraw process to perform the update. + d->schedule_redraw(); +} + +/** + * Scroll window so drawing point 'pos' is at upper left corner of canvas. + */ +void Canvas::set_pos(Geom::IntPoint const &pos) +{ + if (pos == _pos) { + return; + } + + _pos = pos; + + d->schedule_redraw(); + queue_draw(); +} + +/** + * Set the affine for the canvas. + */ +void Canvas::set_affine(Geom::Affine const &affine) +{ + if (_affine == affine) { + return; + } + + _affine = affine; + + d->schedule_redraw(); + queue_draw(); +} + +/** + * Set the desk colour. Transparency is interpreted as amount of checkerboard. + */ +void Canvas::set_desk(uint32_t rgba) +{ + if (d->desk == rgba) return; + bool invalidated = d->background_in_stores_enabled; + d->desk = rgba; + invalidated |= d->background_in_stores_enabled = d->background_in_stores_required(); + if (get_realized() && invalidated) redraw_all(); + queue_draw(); +} + +/** + * Set the page border colour. Although we don't draw the borders, this colour affects the shadows which we do draw (in OpenGL mode). + */ +void Canvas::set_border(uint32_t rgba) +{ + if (d->border == rgba) return; + d->border = rgba; + if (get_realized() && get_opengl_enabled()) queue_draw(); +} + +/** + * Set the page colour. Like the desk colour, transparency is interpreted as checkerboard. + */ +void Canvas::set_page(uint32_t rgba) +{ + if (d->page == rgba) return; + bool invalidated = d->background_in_stores_enabled; + d->page = rgba; + invalidated |= d->background_in_stores_enabled = d->background_in_stores_required(); + if (get_realized() && invalidated) redraw_all(); + queue_draw(); +} + +uint32_t Canvas::get_effective_background() const +{ + auto arr = checkerboard_darken(rgb_to_array(d->desk), 1.0f - 0.5f * SP_RGBA32_A_U(d->desk) / 255.0f); + return SP_RGBA32_F_COMPOSE(arr[0], arr[1], arr[2], 1.0); +} + +void Canvas::set_render_mode(Inkscape::RenderMode mode) +{ + if ((_render_mode == RenderMode::OUTLINE_OVERLAY) != (mode == RenderMode::OUTLINE_OVERLAY) && !get_opengl_enabled()) { + queue_draw(); + } + _render_mode = mode; + if (_drawing) { + _drawing->setRenderMode(_render_mode == RenderMode::OUTLINE_OVERLAY ? RenderMode::NORMAL : _render_mode); + _drawing->setOutlineOverlay(d->outlines_required()); + } + if (_desktop) { + _desktop->setWindowTitle(); // Mode is listed in title. + } +} + +void Canvas::set_color_mode(Inkscape::ColorMode mode) +{ + _color_mode = mode; + if (_drawing) { + _drawing->setColorMode(_color_mode); + } + if (_desktop) { + _desktop->setWindowTitle(); // Mode is listed in title. + } +} + +void Canvas::set_split_mode(Inkscape::SplitMode mode) +{ + if (_split_mode != mode) { + _split_mode = mode; + if (_split_mode == Inkscape::SplitMode::SPLIT) { + _hover_direction = Inkscape::SplitDirection::NONE; + } + if (_drawing) { + _drawing->setOutlineOverlay(d->outlines_required()); + } + redraw_all(); + } +} + +void Canvas::set_clip_to_page_mode(bool clip) +{ + if (clip != d->clip_to_page) { + d->clip_to_page = clip; + d->schedule_redraw(); + } +} + +void Canvas::set_cms_key(std::string key) +{ + _cms_key = std::move(key); + _cms_active = !_cms_key.empty(); + redraw_all(); +} + +/** + * Clear current and grabbed items. + */ +void Canvas::canvas_item_destructed(Inkscape::CanvasItem *item) +{ + if (!d->active) { + return; + } + + if (item == _current_canvas_item) { + _current_canvas_item = nullptr; + } + + if (item == _current_canvas_item_new) { + _current_canvas_item_new = nullptr; + } + + if (item == _grabbed_canvas_item) { + _grabbed_canvas_item = nullptr; + auto const display = Gdk::Display::get_default(); + auto const seat = display->get_default_seat(); + seat->ungrab(); + } + + if (item == d->pre_scroll_grabbed_item) { + d->pre_scroll_grabbed_item = nullptr; + } +} + +std::optional<Geom::PathVector> CanvasPrivate::calc_page_clip() const +{ + if (!clip_to_page) { + return {}; + } + + Geom::PathVector pv; + for (auto &rect : pi.pages) { + pv.push_back(Geom::Path(rect)); + } + return pv; +} + +// Change cursor +void Canvas::set_cursor() +{ + if (!_desktop) { + return; + } + + auto display = Gdk::Display::get_default(); + + switch (_hover_direction) { + case Inkscape::SplitDirection::NONE: + _desktop->event_context->use_tool_cursor(); + break; + + case Inkscape::SplitDirection::NORTH: + case Inkscape::SplitDirection::EAST: + case Inkscape::SplitDirection::SOUTH: + case Inkscape::SplitDirection::WEST: + { + auto cursor = Gdk::Cursor::create(display, "pointer"); + get_window()->set_cursor(cursor); + break; + } + + case Inkscape::SplitDirection::HORIZONTAL: + { + auto cursor = Gdk::Cursor::create(display, "ns-resize"); + get_window()->set_cursor(cursor); + break; + } + + case Inkscape::SplitDirection::VERTICAL: + { + auto cursor = Gdk::Cursor::create(display, "ew-resize"); + get_window()->set_cursor(cursor); + break; + } + + default: + // Shouldn't reach. + std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl; + } +} + +void Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const +{ + minimum_width = natural_width = 256; +} + +void Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const +{ + minimum_height = natural_height = 256; +} + +void Canvas::on_size_allocate(Gtk::Allocation &allocation) +{ + auto const old_dimensions = get_dimensions(); + parent_type::on_size_allocate(allocation); + auto const new_dimensions = get_dimensions(); + + // Necessary as GTK seems to somehow invalidate the current pipeline state upon resize. + if (d->active) { + d->graphics->invalidated_glstate(); + } + + // Trigger the size update to be applied to the stores before the next redraw of the window. + d->schedule_redraw(); + + // Keep canvas centered and optionally zoomed in. + if (_desktop && new_dimensions != old_dimensions) { + auto const midpoint = _desktop->w2d(_pos + Geom::Point(old_dimensions) * 0.5); + double zoom = _desktop->current_zoom(); + + auto prefs = Preferences::get(); + if (prefs->getBool("/options/stickyzoom/value", false)) { + // Calculate adjusted zoom. + auto const old_minextent = min(old_dimensions); + auto const new_minextent = min(new_dimensions); + if (old_minextent != 0) { + zoom *= (double)new_minextent / old_minextent; + } + } + + _desktop->zoom_absolute(midpoint, zoom, false); + } +} + +Glib::RefPtr<Gdk::GLContext> Canvas::create_context() +{ + Glib::RefPtr<Gdk::GLContext> result; + + try { + result = get_window()->create_gl_context(); + } catch (const Gdk::GLError &e) { + std::cerr << "Failed to create OpenGL context: " << e.what().raw() << std::endl; + return {}; + } + + try { + result->realize(); + } catch (const Glib::Error &e) { + std::cerr << "Failed to realize OpenGL context: " << e.what().raw() << std::endl; + return {}; + } + + return result; +} + +void Canvas::paint_widget(Cairo::RefPtr<Cairo::Context> const &cr) +{ + framecheck_whole_function(d) + + if (!d->active) { + std::cerr << "Canvas::paint_widget: Called while not active!" << std::endl; + return; + } + + if constexpr (false) d->canvasitem_ctx->root()->canvas_item_print_tree(); + + // Although launch_redraw() is scheduled at a priority higher than draw, and should therefore always be called first if + // asked, there are times when GTK simply decides to call on_draw anyway. Since launch_redraw() is required to have been + // called at least once to perform vital initalisation, if it has not been called, we have to exit. + if (d->stores.mode() == Stores::Mode::None) { + return; + } + + // Commit pending tiles in case GTK called on_draw even though after_redraw() is scheduled at higher priority. + if (!d->redraw_active) { + d->commit_tiles(); + } + + if (get_opengl_enabled()) { + bind_framebuffer(); + } + + Graphics::PaintArgs args; + args.mouse = d->last_mouse; + args.render_mode = _render_mode; + args.splitmode = _split_mode; + args.splitfrac = _split_frac; + args.splitdir = _split_direction; + args.hoverdir = _hover_direction; + args.yaxisdir = _desktop ? _desktop->yaxisdir() : 1.0; + + d->graphics->paint_widget(Fragment{ _affine, get_area_world() }, args, cr); + + // If asked, run an animation loop. + if (d->prefs.debug_animate) { + auto t = g_get_monotonic_time() / 1700000.0; + auto affine = Geom::Rotate(t * 5) * Geom::Scale(1.0 + 0.6 * cos(t * 2)); + set_affine(affine); + auto dim = _desktop && _desktop->doc() ? _desktop->doc()->getDimensions() : Geom::Point(); + 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); + } +} + +/* + * Async redrawing process + */ + +// Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.) +auto coarsen(const Cairo::RefPtr<Cairo::Region> ®ion, int min_size, int glue_size, double min_fullness) +{ + // Sort the rects by minExtent. + struct Compare + { + bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const { + return a.minExtent() < b.minExtent(); + } + }; + std::multiset<Geom::IntRect, Compare> rects; + int nrects = region->get_num_rectangles(); + for (int i = 0; i < nrects; i++) { + rects.emplace(cairo_to_geom(region->get_rectangle(i))); + } + + // List of processed rectangles. + std::vector<Geom::IntRect> processed; + processed.reserve(nrects); + + // Removal lists. + std::vector<decltype(rects)::iterator> remove_rects; + std::vector<int> remove_processed; + + // Repeatedly expand small rectangles by absorbing their nearby small rectangles. + while (!rects.empty() && rects.begin()->minExtent() < min_size) { + // Extract the smallest unprocessed rectangle. + auto rect = *rects.begin(); + rects.erase(rects.begin()); + + // Initialise the effective glue size. + int effective_glue_size = glue_size; + + while (true) { + // Find the glue zone. + auto glue_zone = rect; + glue_zone.expandBy(effective_glue_size); + + // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast. + auto newrect = rect; + int absorbed_area = 0; + + remove_rects.clear(); + for (auto it = rects.begin(); it != rects.end(); ++it) { + if (glue_zone.contains(*it)) { + newrect.unionWith(*it); + absorbed_area += it->area(); + remove_rects.emplace_back(it); + } + } + + remove_processed.clear(); + for (int i = 0; i < processed.size(); i++) { + auto &r = processed[i]; + if (glue_zone.contains(r)) { + newrect.unionWith(r); + absorbed_area += r.area(); + remove_processed.emplace_back(i); + } + } + + // If the result was too empty, try again with a smaller glue size. + double fullness = (double)(rect.area() + absorbed_area) / newrect.area(); + if (fullness < min_fullness) { + effective_glue_size /= 2; + continue; + } + + // Commit the change. + rect = newrect; + + for (auto &it : remove_rects) { + rects.erase(it); + } + + for (int j = (int)remove_processed.size() - 1; j >= 0; j--) { + int i = remove_processed[j]; + processed[i] = processed.back(); + processed.pop_back(); + } + + // Stop growing if not changed or now big enough. + bool finished = absorbed_area == 0 || rect.minExtent() >= min_size; + if (finished) { + break; + } + + // Otherwise, continue normally. + effective_glue_size = glue_size; + } + + // Put the finished rectangle in processed. + processed.emplace_back(rect); + } + + // Put any remaining rectangles in processed. + for (auto &rect : rects) { + processed.emplace_back(rect); + } + + return processed; +} + +static std::optional<Geom::Dim2> bisect(Geom::IntRect const &rect, int tile_size) +{ + int bw = rect.width(); + int bh = rect.height(); + + // Chop in half along the bigger dimension if the bigger dimension is too big. + if (bw > bh) { + if (bw > tile_size) { + return Geom::X; + } + } else { + if (bh > tile_size) { + return Geom::Y; + } + } + + return {}; +} + +void CanvasPrivate::init_tiler() +{ + // Begin processing redraws. + rd.start_time = g_get_monotonic_time(); + rd.phase = 0; + rd.vis_store = (rd.visible & rd.store.rect).regularized(); + + if (!init_redraw()) { + sync.signalExit(); + return; + } + + // Launch render threads to process tiles. + rd.timeoutflag = false; + + rd.numactive = rd.numthreads; + + for (int i = 0; i < rd.numthreads - 1; i++) { + boost::asio::post(*pool, [=] { render_tile(i); }); + } + + render_tile(rd.numthreads - 1); +} + +bool CanvasPrivate::init_redraw() +{ + assert(rd.rects.empty()); + + switch (rd.phase) { + case 0: + if (rd.vis_store && rd.decoupled_mode) { + // The highest priority to redraw is the region that is visible but not covered by either clean or snapshot content, if in decoupled mode. + // If this is not rendered immediately, it will be perceived as edge flicker, most noticeably on zooming out, but also on rotation too. + process_redraw(*rd.vis_store, unioned(updater->clean_region->copy(), rd.snapshot_drawn)); + return true; + } else { + rd.phase++; + // fallthrough + } + + case 1: + // Another high priority to redraw is the grabbed canvas item, if the user has requested block updates. + if (rd.grabbed) { + process_redraw(*rd.grabbed, updater->clean_region, false, false); // non-interruptible, non-preemptible + return true; + } else { + rd.phase++; + // fallthrough + } + + case 2: + if (rd.vis_store) { + // The main priority to redraw, and the bread and butter of Inkscape's painting, is the visible content that is not clean. + // This may be done over several cycles, at the direction of the Updater, each outwards from the mouse. + process_redraw(*rd.vis_store, updater->get_next_clean_region()); + return true; + } else { + rd.phase++; + // fallthrough + } + + case 3: { + // The lowest priority to redraw is the prerender margin around the visible rectangle. + // (This is in addition to any opportunistic prerendering that may have already occurred in the above steps.) + auto prerender = expandedBy(rd.visible, rd.margin); + auto prerender_store = (prerender & rd.store.rect).regularized(); + if (prerender_store) { + process_redraw(*prerender_store, updater->clean_region); + return true; + } else { + return false; + } + } + + default: + assert(false); + return false; + } +} + +// Paint a given subrectangle of the store given by 'bounds', but avoid painting the part of it within 'clean' if possible. +// Some parts both outside the bounds and inside the clean region may also be painted if it helps reduce fragmentation. +void CanvasPrivate::process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible, bool preemptible) +{ + rd.bounds = bounds; + rd.clean = std::move(clean); + rd.interruptible = interruptible; + rd.preemptible = preemptible; + + // Assert that we do not render outside of store. + assert(rd.store.rect.contains(rd.bounds)); + + // Get the region we are asked to paint. + auto region = Cairo::Region::create(geom_to_cairo(rd.bounds)); + region->subtract(rd.clean); + + // Get the list of rectangles to paint, coarsened to avoid fragmentation. + rd.rects = coarsen(region, + std::min<int>(rd.coarsener_min_size, rd.tile_size / 2), + std::min<int>(rd.coarsener_glue_size, rd.tile_size / 2), + rd.coarsener_min_fullness); + + // Put the rectangles into a heap sorted by distance from mouse. + std::make_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp()); + + // Adjust the effective tile size proportional to the painting area. + double adjust = (double)cairo_to_geom(region->get_extents()).maxExtent() / rd.visible.maxExtent(); + adjust = std::clamp(adjust, 0.3, 1.0); + rd.effective_tile_size = rd.tile_size * adjust; +} + +// Process rectangles until none left or timed out. +void CanvasPrivate::render_tile(int debug_id) +{ + rd.mutex.lock(); + + std::string fc_str; + FrameCheck::Event fc; + if (rd.debug_framecheck) { + fc_str = "render_thread_" + std::to_string(debug_id + 1); + fc = FrameCheck::Event(fc_str.c_str()); + } + + while (true) { + // If we've run out of rects, try to start a new redraw cycle. + if (rd.rects.empty()) { + if (end_redraw()) { + // More redraw cycles to do. + continue; + } else { + // All finished. + break; + } + } + + // Check for cancellation. + auto const flags = abort_flags.load(std::memory_order_relaxed); + bool const soft = flags & (int)AbortFlags::Soft; + bool const hard = flags & (int)AbortFlags::Hard; + if (hard || (rd.phase == 3 && soft)) { + break; + } + + // Extract the closest rectangle to the mouse. + std::pop_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp()); + auto rect = rd.rects.back(); + rd.rects.pop_back(); + + // Cull empty rectangles. + if (rect.hasZeroArea()) { + continue; + } + + // Cull rectangles that lie entirely inside the clean region. + // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.) + if (rd.clean->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) { + continue; + } + + // Lambda to add a rectangle to the heap. + auto add_rect = [&] (Geom::IntRect const &rect) { + rd.rects.emplace_back(rect); + std::push_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp()); + }; + + // If the rectangle needs bisecting, bisect it and put it back on the heap. + if (auto axis = bisect(rect, rd.effective_tile_size)) { + int mid = rect[*axis].middle(); + auto lo = rect; lo[*axis].setMax(mid); add_rect(lo); + auto hi = rect; hi[*axis].setMin(mid); add_rect(hi); + continue; + } + + // Extend thin rectangles at the edge of the bounds rect to at least some minimum size, being sure to keep them within the store. + // (This ensures we don't end up rendering one thin rectangle at the edge every frame while the view is moved continuously.) + if (rd.preemptible) { + if (rect.width() < rd.preempt) { + if (rect.left() == rd.bounds.left() ) rect.setLeft (std::max(rect.right() - rd.preempt, rd.store.rect.left() )); + if (rect.right() == rd.bounds.right()) rect.setRight(std::min(rect.left() + rd.preempt, rd.store.rect.right())); + } + if (rect.height() < rd.preempt) { + if (rect.top() == rd.bounds.top() ) rect.setTop (std::max(rect.bottom() - rd.preempt, rd.store.rect.top() )); + if (rect.bottom() == rd.bounds.bottom()) rect.setBottom(std::min(rect.top() + rd.preempt, rd.store.rect.bottom())); + } + } + + // Mark the rectangle as clean. + updater->mark_clean(rect); + + rd.mutex.unlock(); + + // Paint the rectangle. + paint_rect(rect); + + rd.mutex.lock(); + + // Check for timeout. + if (rd.interruptible) { + auto now = g_get_monotonic_time(); + auto elapsed = now - rd.start_time; + if (elapsed > rd.render_time_limit * 1000) { + // Timed out. Temporarily return to GTK main loop, and come back here when next idle. + rd.timeoutflag = true; + break; + } + } + } + + if (rd.debug_framecheck && rd.timeoutflag) { + fc.subtype = 1; + } + + rd.numactive--; + bool const done = rd.numactive == 0; + + rd.mutex.unlock(); + + if (done) { + rd.rects.clear(); + sync.signalExit(); + } +} + +bool CanvasPrivate::end_redraw() +{ + switch (rd.phase) { + case 0: + rd.phase++; + return init_redraw(); + + case 1: + rd.phase++; + // Reset timeout to leave the normal amount of time for clearing up artifacts. + rd.start_time = g_get_monotonic_time(); + return init_redraw(); + + case 2: + if (!updater->report_finished()) { + rd.phase++; + } + return init_redraw(); + + case 3: + return false; + + default: + assert(false); + return false; + } +} + +void CanvasPrivate::paint_rect(Geom::IntRect const &rect) +{ + // Make sure the paint rectangle lies within the store. + assert(rd.store.rect.contains(rect)); + + auto paint = [&, this] (bool need_background, bool outline_pass) { + + auto surface = graphics->request_tile_surface(rect, true); + if (!surface) { + sync.runInMain([&] { + if (prefs.debug_logging) std::cout << "Blocked - buffer mapping" << std::endl; + if (q->get_opengl_enabled()) q->make_current(); + surface = graphics->request_tile_surface(rect, false); + }); + } + + try { + + paint_single_buffer(surface, rect, need_background, outline_pass); + + } catch (std::bad_alloc const &) { + // Note: std::bad_alloc actually indicates a Cairo error that occurs regularly at high zoom, and we must handle it. + // See https://gitlab.com/inkscape/inkscape/-/issues/3975 + sync.runInMain([&] { + std::cerr << "Rendering failure. You probably need to zoom out!" << std::endl; + if (q->get_opengl_enabled()) q->make_current(); + graphics->junk_tile_surface(std::move(surface)); + surface = graphics->request_tile_surface(rect, false); + paint_error_buffer(surface); + }); + } + + return surface; + }; + + // Create and render the tile. + Tile tile; + tile.fragment.affine = rd.store.affine; + tile.fragment.rect = rect; + tile.surface = paint(background_in_stores_required(), false); + if (outlines_enabled) { + tile.outline_surface = paint(false, true); + } + + // Introduce an artificial delay for each rectangle. + if (rd.redraw_delay) g_usleep(*rd.redraw_delay); + + // Stick the tile on the list of tiles to reap. + { + auto g = std::lock_guard(rd.tiles_mutex); + rd.tiles.emplace_back(std::move(tile)); + } +} + +void CanvasPrivate::paint_single_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface, Geom::IntRect const &rect, bool need_background, bool outline_pass) +{ + // Create Cairo context. + auto cr = Cairo::Context::create(surface); + + // Clear background. + cr->save(); + if (need_background) { + Graphics::paint_background(Fragment{ rd.store.affine, rect }, pi, rd.page, rd.desk, cr); + } else { + cr->set_operator(Cairo::OPERATOR_CLEAR); + cr->paint(); + } + cr->restore(); + + // Render drawing on top of background. + auto buf = Inkscape::CanvasItemBuffer{ rect, scale_factor, cr, outline_pass }; + canvasitem_ctx->root()->render(buf); + + // Paint over newly drawn content with a translucent random colour. + if (rd.debug_show_redraw) { + cr->set_source_rgba((rand() % 256) / 255.0, (rand() % 256) / 255.0, (rand() % 256) / 255.0, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->paint(); + } +} + +void CanvasPrivate::paint_error_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface) +{ + // Paint something into surface to represent an "error" state for that tile. + // Currently just paints solid black. + auto cr = Cairo::Context::create(surface); + cr->set_source_rgb(0, 0, 0); + cr->paint(); +} + +} // namespace Inkscape::UI::Widget + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h new file mode 100644 index 0000000..970488e --- /dev/null +++ b/src/ui/widget/canvas.h @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_H +#define INKSCAPE_UI_WIDGET_CANVAS_H +/* + * Authors: + * Tavmjong Bah + * PBS <pbs3141@gmail.com> + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <memory> +#include <gtkmm.h> +#include <2geom/rect.h> +#include <2geom/int-rect.h> +#include "display/rendermode.h" +#include "optglarea.h" + +class SPDesktop; + +namespace Inkscape { + +class CanvasItem; +class CanvasItemGroup; +class Drawing; + +namespace UI { +namespace Widget { + +class CanvasPrivate; + +/** + * A widget for Inkscape's canvas. + */ +class Canvas : public OptGLArea +{ + using parent_type = OptGLArea; + +public: + Canvas(); + ~Canvas() override; + + /* Configuration */ + + // Desktop (Todo: Remove.) + void set_desktop(SPDesktop *desktop) { _desktop = desktop; } + SPDesktop *get_desktop() const { return _desktop; } + + // Drawing + void set_drawing(Inkscape::Drawing *drawing); + + // Canvas item root + CanvasItemGroup *get_canvas_item_root() const; + + // Geometry + void set_pos (const Geom::IntPoint &pos); + void set_pos (const Geom::Point &fpos) { set_pos(fpos.round()); } + void set_affine(const Geom::Affine &affine); + const Geom::IntPoint &get_pos () const { return _pos; } + const Geom::Affine &get_affine() const { return _affine; } + const Geom::Affine &get_geom_affine() const; // tool-base.cpp (todo: remove this dependency) + + // Background + void set_desk (uint32_t rgba); + void set_border(uint32_t rgba); + void set_page (uint32_t rgba); + uint32_t get_effective_background() const; // This function is now wrong. + + // Rendering modes + void set_render_mode(Inkscape::RenderMode mode); + void set_color_mode (Inkscape::ColorMode mode); + void set_split_mode (Inkscape::SplitMode mode); + Inkscape::RenderMode get_render_mode() const { return _render_mode; } + Inkscape::ColorMode get_color_mode() const { return _color_mode; } + Inkscape::SplitMode get_split_mode() const { return _split_mode; } + void set_clip_to_page_mode(bool clip); + + // CMS + void set_cms_key(std::string key); + const std::string &get_cms_key() const { return _cms_key; } + void set_cms_active(bool active) { _cms_active = active; } + bool get_cms_active() const { return _cms_active; } + + /* Observers */ + + // Geometry + Geom::IntPoint get_dimensions() const; + bool world_point_inside_canvas(Geom::Point const &world) const; // desktop-events.cpp + Geom::Point canvas_to_world(Geom::Point const &window) const; + Geom::IntRect get_area_world() const; + bool canvas_point_in_outline_zone(Geom::Point const &world) const; + + // State + bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp + + // Mouse + std::optional<Geom::Point> get_last_mouse() const; // desktop-widget.cpp + + /* Methods */ + + // Invalidation + void redraw_all(); // Mark everything as having changed. + void redraw_area(Geom::Rect const &area); // Mark a rectangle of world space as having changed. + void redraw_area(int x0, int y0, int x1, int y1); + void redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1); + void request_update(); // Mark geometry as needing recalculation. + + // Callback run on destructor of any canvas item + void canvas_item_destructed(Inkscape::CanvasItem *item); + + // State + Inkscape::CanvasItem *get_current_canvas_item() const { return _current_canvas_item; } + void set_current_canvas_item(Inkscape::CanvasItem *item) { + _current_canvas_item = item; + } + Inkscape::CanvasItem *get_grabbed_canvas_item() const { return _grabbed_canvas_item; } + void set_grabbed_canvas_item(Inkscape::CanvasItem *item, Gdk::EventMask mask) { + _grabbed_canvas_item = item; + _grabbed_event_mask = mask; + } + void set_all_enter_events(bool on) { _all_enter_events = on; } + + void enable_autoscroll(); + +protected: + void get_preferred_width_vfunc (int &minimum_width, int &natural_width ) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + + // Event handlers + bool on_scroll_event (GdkEventScroll* ) override; + bool on_button_event (GdkEventButton* ); + bool on_button_press_event (GdkEventButton* ) override; + bool on_button_release_event(GdkEventButton* ) override; + bool on_enter_notify_event (GdkEventCrossing*) override; + bool on_leave_notify_event (GdkEventCrossing*) override; + bool on_focus_in_event (GdkEventFocus* ) override; + bool on_key_press_event (GdkEventKey* ) override; + bool on_key_release_event (GdkEventKey* ) override; + bool on_motion_notify_event (GdkEventMotion* ) override; + + void on_realize() override; + void on_unrealize() override; + void on_size_allocate(Gtk::Allocation&) override; + + Glib::RefPtr<Gdk::GLContext> create_context() override; + void paint_widget(const Cairo::RefPtr<Cairo::Context>&) override; + +private: + /* Configuration */ + + // Desktop + SPDesktop *_desktop = nullptr; + + // Drawing + Inkscape::Drawing *_drawing = nullptr; + + // Geometry + Geom::IntPoint _pos = {0, 0}; ///< Coordinates of top-left pixel of canvas view within canvas. + Geom::Affine _affine; ///< The affine that we have been requested to draw at. + + // Rendering modes + Inkscape::RenderMode _render_mode = Inkscape::RenderMode::NORMAL; + Inkscape::SplitMode _split_mode = Inkscape::SplitMode::NORMAL; + Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL; + + // CMS + std::string _cms_key; + bool _cms_active = false; + + /* Internal state */ + + // Event handling/item picking + GdkEvent _pick_event; ///< Event used to find currently selected item. + bool _in_repick; ///< For tracking recursion of pick_current_item(). + bool _left_grabbed_item; ///< ? + bool _all_enter_events; ///< Keep all enter events. Only set true in connector-tool.cpp. + bool _is_dragging; ///< Used in selection-chemistry to block undo/redo. + int _state; ///< Last known modifier state (SHIFT, CTRL, etc.). + + Inkscape::CanvasItem *_current_canvas_item; ///< Item containing cursor, nullptr if none. + Inkscape::CanvasItem *_current_canvas_item_new; ///< Item to become _current_item, nullptr if none. + Inkscape::CanvasItem *_grabbed_canvas_item; ///< Item that holds a pointer grab; nullptr if none. + Gdk::EventMask _grabbed_event_mask; + + // Drawing + bool _need_update = true; // Set true so setting CanvasItem bounds are calculated at least once. + + // Split view + Inkscape::SplitDirection _split_direction; + Geom::Point _split_frac; + Inkscape::SplitDirection _hover_direction; + bool _split_dragging; + Geom::IntPoint _split_drag_start; + + void set_cursor(); + + // Opaque pointer to implementation + friend class CanvasPrivate; + std::unique_ptr<CanvasPrivate> d; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/canvas/cairographics.cpp b/src/ui/widget/canvas/cairographics.cpp new file mode 100644 index 0000000..42b3353 --- /dev/null +++ b/src/ui/widget/canvas/cairographics.cpp @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <2geom/parallelogram.h> +#include "ui/util.h" +#include "helper/geom.h" +#include "cairographics.h" +#include "stores.h" +#include "prefs.h" +#include "util.h" +#include "framecheck.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +CairoGraphics::CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi) + : prefs(prefs) + , stores(stores) + , pi(pi) {} + +std::unique_ptr<Graphics> Graphics::create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi) +{ + return std::make_unique<CairoGraphics>(prefs, stores, pi); +} + +void CairoGraphics::set_outlines_enabled(bool enabled) +{ + outlines_enabled = enabled; + if (!enabled) { + store.outline_surface.clear(); + snapshot.outline_surface.clear(); + } +} + +void CairoGraphics::recreate_store(Geom::IntPoint const &dims) +{ + auto surface_size = dims * scale_factor; + + auto make_surface = [&, this] { + auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y()); + cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API! + return surface; + }; + + // Recreate the store surface. + bool reuse_surface = store.surface && dimensions(store.surface) == surface_size; + if (!reuse_surface) { + store.surface = make_surface(); + } + + // Ensure the store surface is filled with the correct default background. + if (background_in_stores) { + auto cr = Cairo::Context::create(store.surface); + paint_background(stores.store(), pi, page, desk, cr); + } else if (reuse_surface) { + auto cr = Cairo::Context::create(store.surface); + cr->set_operator(Cairo::OPERATOR_CLEAR); + cr->paint(); + } + + // Do the same for the outline surface (except always clearing it to transparent). + if (outlines_enabled) { + bool reuse_outline_surface = store.outline_surface && dimensions(store.outline_surface) == surface_size; + if (!reuse_outline_surface) { + store.outline_surface = make_surface(); + } else { + auto cr = Cairo::Context::create(store.outline_surface); + cr->set_operator(Cairo::OPERATOR_CLEAR); + cr->paint(); + } + } +} + +void CairoGraphics::shift_store(Fragment const &dest) +{ + auto surface_size = dest.rect.dimensions() * scale_factor; + + // Determine the geometry of the shift. + auto shift = dest.rect.min() - stores.store().rect.min(); + auto reuse_rect = (dest.rect & cairo_to_geom(stores.store().drawn->get_extents())).regularized(); + assert(reuse_rect); // Should not be called if there is no overlap. + + auto make_surface = [&, this] { + auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y()); + cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API! + return surface; + }; + + // Create the new store surface. + bool reuse_surface = snapshot.surface && dimensions(snapshot.surface) == surface_size; + auto new_surface = reuse_surface ? std::move(snapshot.surface) : make_surface(); + + // Paint background into region of store not covered by next operation. + auto cr = Cairo::Context::create(new_surface); + if (background_in_stores || reuse_surface) { + auto reg = Cairo::Region::create(geom_to_cairo(dest.rect)); + reg->subtract(geom_to_cairo(*reuse_rect)); + reg->translate(-dest.rect.left(), -dest.rect.top()); + cr->save(); + region_to_path(cr, reg); + cr->clip(); + if (background_in_stores) { + paint_background(dest, pi, page, desk, cr); + } else { // otherwise, reuse_surface is true + cr->set_operator(Cairo::OPERATOR_CLEAR); + cr->paint(); + } + cr->restore(); + } + + // Copy re-usuable contents of old store into new store, shifted. + cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(store.surface, -shift.x(), -shift.y()); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + + // Set the result as the new store surface. + snapshot.surface = std::move(store.surface); + store.surface = std::move(new_surface); + + // Do the same for the outline store + if (outlines_enabled) { + // Create. + bool reuse_outline_surface = snapshot.outline_surface && dimensions(snapshot.outline_surface) == surface_size; + auto new_outline_surface = reuse_outline_surface ? std::move(snapshot.outline_surface) : make_surface(); + // Background. + auto cr = Cairo::Context::create(new_outline_surface); + if (reuse_outline_surface) { + cr->set_operator(Cairo::OPERATOR_CLEAR); + cr->paint(); + } + // Copy. + cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(store.outline_surface, -shift.x(), -shift.y()); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + // Set. + snapshot.outline_surface = std::move(store.outline_surface); + store.outline_surface = std::move(new_outline_surface); + } +} + +void CairoGraphics::swap_stores() +{ + std::swap(store, snapshot); +} + +void CairoGraphics::fast_snapshot_combine() +{ + auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &from, + Cairo::RefPtr<Cairo::ImageSurface> const &to) { + auto cr = Cairo::Context::create(to); + cr->set_antialias(Cairo::ANTIALIAS_NONE); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->translate(-stores.snapshot().rect.left(), -stores.snapshot().rect.top()); + cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine)); + cr->translate(-1.0, -1.0); + region_to_path(cr, shrink_region(stores.store().drawn, 2)); + cr->translate(1.0, 1.0); + cr->clip(); + cr->set_source(from, stores.store().rect.left(), stores.store().rect.top()); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + }; + + copy(store.surface, snapshot.surface); + if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface); +} + +void CairoGraphics::snapshot_combine(Fragment const &dest) +{ + // Create the new fragment. + auto content_size = dest.rect.dimensions() * scale_factor; + + auto make_surface = [&] { + auto result = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, content_size.x(), content_size.y()); + cairo_surface_set_device_scale(result->cobj(), scale_factor, scale_factor); // No C++ API! + return result; + }; + + CairoFragment fragment; + fragment.surface = make_surface(); + if (outlines_enabled) fragment.outline_surface = make_surface(); + + auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store_from, + Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_from, + Cairo::RefPtr<Cairo::ImageSurface> const &to, bool background) { + auto cr = Cairo::Context::create(to); + cr->set_antialias(Cairo::ANTIALIAS_NONE); + cr->set_operator(Cairo::OPERATOR_SOURCE); + if (background) paint_background(dest, pi, page, desk, cr); + cr->translate(-dest.rect.left(), -dest.rect.top()); + cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * dest.affine)); + cr->rectangle(stores.snapshot().rect.left(), stores.snapshot().rect.top(), stores.snapshot().rect.width(), stores.snapshot().rect.height()); + cr->set_source(snapshot_from, stores.snapshot().rect.left(), stores.snapshot().rect.top()); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->fill(); + cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine)); + cr->translate(-1.0, -1.0); + region_to_path(cr, shrink_region(stores.store().drawn, 2)); + cr->translate(1.0, 1.0); + cr->clip(); + cr->set_source(store_from, stores.store().rect.left(), stores.store().rect.top()); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + }; + + copy(store.surface, snapshot.surface, fragment.surface, background_in_stores); + if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface, fragment.outline_surface, false); + + snapshot = std::move(fragment); +} + +Cairo::RefPtr<Cairo::ImageSurface> CairoGraphics::request_tile_surface(Geom::IntRect const &rect, bool /*nogl*/) +{ + // Create temporary surface, isolated from store. + auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, rect.width() * scale_factor, rect.height() * scale_factor); + cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); + return surface; +} + +void CairoGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) +{ + // Blit from the temporary surface to the store. + auto diff = fragment.rect.min() - stores.store().rect.min(); + + auto cr = Cairo::Context::create(store.surface); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(surface, diff.x(), diff.y()); + cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height()); + cr->fill(); + + if (outlines_enabled) { + auto cr = Cairo::Context::create(store.outline_surface); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(outline_surface, diff.x(), diff.y()); + cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height()); + cr->fill(); + } +} + +void CairoGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const &cr) +{ + auto f = FrameCheck::Event(); + + // Turn off anti-aliasing while compositing the widget for large performance gains. (We can usually + // get away with it without any negative visual impact; when we can't, we turn it back on.) + cr->set_antialias(Cairo::ANTIALIAS_NONE); + + // Due to a Cairo bug, Cairo sometimes draws outside of its clip region. This results in flickering as Canvas content is drawn + // over the bottom scrollbar. This cannot be fixed by setting the correct clip region, as Cairo detects that and turns it into + // a no-op. Hence the following workaround, which recreates the clip region from scratch, is required. + auto rlist = cairo_copy_clip_rectangle_list(cr->cobj()); + cr->reset_clip(); + for (int i = 0; i < rlist->num_rectangles; i++) { + cr->rectangle(rlist->rectangles[i].x, rlist->rectangles[i].y, rlist->rectangles[i].width, rlist->rectangles[i].height); + } + cr->clip(); + cairo_rectangle_list_destroy(rlist); + + // Draw background if solid colour optimisation is not enabled. (If enabled, it is baked into the stores.) + if (!background_in_stores) { + if (prefs.debug_framecheck) f = FrameCheck::Event("background"); + paint_background(view, pi, page, desk, cr); + } + + // Even if in solid colour mode, draw the part of background that is not going to be rendered. + if (background_in_stores) { + auto const &s = stores.mode() == Stores::Mode::Decoupled ? stores.snapshot() : stores.store(); + if (!(Geom::Parallelogram(s.rect) * s.affine.inverse() * view.affine).contains(view.rect)) { + if (prefs.debug_framecheck) f = FrameCheck::Event("background", 2); + cr->save(); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, view.rect.width(), view.rect.height()); + cr->translate(-view.rect.left(), -view.rect.top()); + cr->transform(geom_to_cairo(s.affine.inverse() * view.affine)); + cr->rectangle(s.rect.left(), s.rect.top(), s.rect.width(), s.rect.height()); + cr->clip(); + cr->transform(geom_to_cairo(view.affine.inverse() * s.affine)); + cr->translate(view.rect.left(), view.rect.top()); + paint_background(view, pi, page, desk, cr); + cr->restore(); + } + } + + auto draw_store = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store, Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_store) { + if (stores.mode() == Stores::Mode::Normal) { + // Blit store to view. + if (prefs.debug_framecheck) f = FrameCheck::Event("draw"); + cr->save(); + auto const &r = stores.store().rect; + cr->translate(-view.rect.left(), -view.rect.top()); + cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); // Almost always the identity. + cr->rectangle(r.left(), r.top(), r.width(), r.height()); + cr->set_source(store, r.left(), r.top()); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->fill(); + cr->restore(); + } else { + // Draw transformed snapshot, clipped to the complement of the store's clean region. + if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 1); + + cr->save(); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, view.rect.width(), view.rect.height()); + cr->translate(-view.rect.left(), -view.rect.top()); + cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); + region_to_path(cr, stores.store().drawn); + cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * stores.store().affine)); + cr->clip(); + auto const &r = stores.snapshot().rect; + cr->rectangle(r.left(), r.top(), r.width(), r.height()); + cr->clip(); + cr->set_source(snapshot_store, r.left(), r.top()); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + if (prefs.debug_show_snapshot) { + cr->set_source_rgba(0, 0, 1, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->paint(); + } + cr->restore(); + + // Draw transformed store, clipped to drawn region. + if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 0); + cr->save(); + cr->translate(-view.rect.left(), -view.rect.top()); + cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); + cr->set_source(store, stores.store().rect.left(), stores.store().rect.top()); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + region_to_path(cr, stores.store().drawn); + cr->fill(); + cr->restore(); + } + }; + + auto draw_overlay = [&, this] { + // Get whitewash opacity. + double outline_overlay_opacity = prefs.outline_overlay_opacity / 100.0; + + // Partially obscure drawing by painting semi-transparent white, then paint outline content. + // Note: Unfortunately this also paints over the background, but this is unavoidable. + cr->save(); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->set_source_rgb(1.0, 1.0, 1.0); + cr->paint_with_alpha(outline_overlay_opacity); + draw_store(store.outline_surface, snapshot.outline_surface); + cr->restore(); + }; + + if (a.splitmode == Inkscape::SplitMode::SPLIT) { + + // Calculate the clipping rectangles for split view. + auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir); + + // Draw normal content. + cr->save(); + cr->rectangle(store_clip.left(), store_clip.top(), store_clip.width(), store_clip.height()); + cr->clip(); + cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + draw_store(store.surface, snapshot.surface); + if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay(); + cr->restore(); + + // Draw outline. + if (background_in_stores) { + cr->save(); + cr->translate(outline_clip.left(), outline_clip.top()); + paint_background(Fragment{view.affine, view.rect.min() + outline_clip}, pi, page, desk, cr); + cr->restore(); + } + cr->save(); + cr->rectangle(outline_clip.left(), outline_clip.top(), outline_clip.width(), outline_clip.height()); + cr->clip(); + cr->set_operator(Cairo::OPERATOR_OVER); + draw_store(store.outline_surface, snapshot.outline_surface); + cr->restore(); + + } else { + + // Draw the normal content over the whole view. + cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + draw_store(store.surface, snapshot.surface); + if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay(); + + // Draw outline if in X-ray mode. + if (a.splitmode == Inkscape::SplitMode::XRAY && a.mouse) { + // Clip to circle + cr->set_antialias(Cairo::ANTIALIAS_DEFAULT); + cr->arc(a.mouse->x(), a.mouse->y(), prefs.xray_radius, 0, 2 * M_PI); + cr->clip(); + cr->set_antialias(Cairo::ANTIALIAS_NONE); + // Draw background. + paint_background(view, pi, page, desk, cr); + // Draw outline. + cr->set_operator(Cairo::OPERATOR_OVER); + draw_store(store.outline_surface, snapshot.outline_surface); + } + } + + // The rest can be done with antialiasing. + cr->set_antialias(Cairo::ANTIALIAS_DEFAULT); + + if (a.splitmode == Inkscape::SplitMode::SPLIT) { + paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/cairographics.h b/src/ui/widget/canvas/cairographics.h new file mode 100644 index 0000000..c29eff1 --- /dev/null +++ b/src/ui/widget/canvas/cairographics.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Cairo display backend. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H +#define INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H + +#include "graphics.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +struct CairoFragment +{ + Cairo::RefPtr<Cairo::ImageSurface> surface; + Cairo::RefPtr<Cairo::ImageSurface> outline_surface; +}; + +class CairoGraphics : public Graphics +{ +public: + CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi); + + void set_scale_factor(int scale) override { scale_factor = scale; } + void set_outlines_enabled(bool) override; + void set_background_in_stores(bool enabled) override { background_in_stores = enabled; } + void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; } + + void recreate_store(Geom::IntPoint const &dimensions) override; + void shift_store(Fragment const &dest) override; + void swap_stores() override; + void fast_snapshot_combine() override; + void snapshot_combine(Fragment const &dest) override; + void invalidate_snapshot() override {} + + bool is_opengl() const override { return false; } + void invalidated_glstate() override {} + + Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override; + void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override; + void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override {} + + void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override; + +private: + // Drawn content. + CairoFragment store, snapshot; + + // Dependency objects in canvas. + Prefs const &prefs; + Stores const &stores; + PageInfo const π + + // Backend-agnostic state. + int scale_factor = 1; + bool outlines_enabled = false; + bool background_in_stores = false; + uint32_t page, desk, border; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/fragment.h b/src/ui/widget/canvas/fragment.h new file mode 100644 index 0000000..d3edc74 --- /dev/null +++ b/src/ui/widget/canvas/fragment.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H +#define INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H + +#include <2geom/int-rect.h> +#include <2geom/affine.h> + +namespace Inkscape::UI::Widget { + +/// A "fragment" is a rectangle of drawn content at a specfic place. +struct Fragment +{ + // The matrix the geometry was transformed with when the content was drawn. + Geom::Affine affine; + + // The rectangle of world space where the fragment was drawn. + Geom::IntRect rect; +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/framecheck.cpp b/src/ui/widget/canvas/framecheck.cpp new file mode 100644 index 0000000..c127c8e --- /dev/null +++ b/src/ui/widget/canvas/framecheck.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <fstream> +#include <iostream> +#include <mutex> +#include <boost/filesystem.hpp> // Using boost::filesystem instead of std::filesystem due to broken C++17 on MacOS. +#include "framecheck.h" +namespace fs = boost::filesystem; + +namespace Inkscape::FrameCheck { + +void Event::write() +{ + static std::mutex mutex; + static auto logfile = [] { + auto path = fs::temp_directory_path() / "framecheck.txt"; + auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary; + return std::ofstream(path.string(), mode); + }(); + + auto lock = std::lock_guard(mutex); + logfile << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << std::endl; +} + +} // namespace Inkscape::FrameCheck diff --git a/src/ui/widget/canvas/framecheck.h b/src/ui/widget/canvas/framecheck.h new file mode 100644 index 0000000..8964561 --- /dev/null +++ b/src/ui/widget/canvas/framecheck.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_FRAMECHECK_H +#define INKSCAPE_FRAMECHECK_H + +#include <glib.h> + +namespace Inkscape::FrameCheck { + +/// RAII object that logs a timing event for the duration of its lifetime. +struct Event +{ + gint64 start; + char const *name; + int subtype; + + Event() : start(-1) {} + + Event(char const *name, int subtype = 0) : start(g_get_monotonic_time()), name(name), subtype(subtype) {} + + Event(Event &&p) { movefrom(p); } + + ~Event() { finish(); } + + Event &operator=(Event &&p) + { + finish(); + movefrom(p); + return *this; + } + +private: + void movefrom(Event &p) + { + start = p.start; + name = p.name; + subtype = p.subtype; + p.start = -1; + } + + void finish() { if (start != -1) write(); } + + void write(); +}; + +} // namespace Inkscape::FrameCheck + +#endif // INKSCAPE_FRAMECHECK_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/glgraphics.cpp b/src/ui/widget/canvas/glgraphics.cpp new file mode 100644 index 0000000..b00503c --- /dev/null +++ b/src/ui/widget/canvas/glgraphics.cpp @@ -0,0 +1,873 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <2geom/transforms.h> +#include <2geom/rect.h> +#include "ui/util.h" +#include "helper/geom.h" +#include "glgraphics.h" +#include "stores.h" +#include "prefs.h" +#include "pixelstreamer.h" +#include "util.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +namespace { + +// 2Geom <-> OpenGL + +void geom_to_uniform_mat(Geom::Affine const &affine, GLuint location) +{ + glUniformMatrix2fv(location, 1, GL_FALSE, std::begin({(GLfloat)affine[0], (GLfloat)affine[1], (GLfloat)affine[2], (GLfloat)affine[3]})); +} + +void geom_to_uniform_trans(Geom::Affine const &affine, GLuint location) +{ + glUniform2fv(location, 1, std::begin({(GLfloat)affine[4], (GLfloat)affine[5]})); +} + +void geom_to_uniform(Geom::Affine const &affine, GLuint mat_location, GLuint trans_location) +{ + geom_to_uniform_mat(affine, mat_location); + geom_to_uniform_trans(affine, trans_location); +} + +void geom_to_uniform(Geom::Point const &vec, GLuint location) +{ + glUniform2fv(location, 1, std::begin({(GLfloat)vec.x(), (GLfloat)vec.y()})); +} + +// Get the affine transformation required to paste fragment A onto fragment B, assuming +// coordinates such that A is a texture (0 to 1) and B is a framebuffer (-1 to 1). +static auto calc_paste_transform(Fragment const &a, Fragment const &b) +{ + Geom::Affine result = Geom::Scale(a.rect.dimensions()); + + if (a.affine == b.affine) { + result *= Geom::Translate(a.rect.min() - b.rect.min()); + } else { + result *= Geom::Translate(a.rect.min()) * a.affine.inverse() * b.affine * Geom::Translate(-b.rect.min()); + } + + return result * Geom::Scale(2.0 / b.rect.dimensions()) * Geom::Translate(-1.0, -1.0); +} + +// Given a region, shrink it by 0.5px, and convert the result to a VAO of triangles. +static auto region_shrink_vao(Cairo::RefPtr<Cairo::Region> const ®, Geom::IntRect const &rel) +{ + // Shrink the region by 0.5 (translating it by (0.5, 0.5) in the process). + auto reg2 = shrink_region(reg, 1); + + // Preallocate the vertex buffer. + int nrects = reg2->get_num_rectangles(); + std::vector<GLfloat> verts; + verts.reserve(nrects * 12); + + // Add a vertex to the buffer, transformed to a coordinate system in which the enclosing rectangle 'rel' goes from 0 to 1. + // Also shift them up/left by 0.5px; combined with the width/height increase from earlier, this shrinks the region by 0.5px. + auto emit_vertex = [&] (Geom::IntPoint const &pt) { + verts.emplace_back((pt.x() - 0.5f - rel.left()) / rel.width()); + verts.emplace_back((pt.y() - 0.5f - rel.top() ) / rel.height()); + }; + + // Todo: Use a better triangulation algorithm here that results in 1) less triangles, and 2) no seaming. + for (int i = 0; i < nrects; i++) { + auto rect = cairo_to_geom(reg2->get_rectangle(i)); + for (int j = 0; j < 6; j++) { + int constexpr indices[] = {0, 1, 2, 0, 2, 3}; + emit_vertex(rect.corner(indices[j])); + } + } + + // Package the data in a VAO. + VAO result; + glGenBuffers(1, &result.vbuf); + glBindBuffer(GL_ARRAY_BUFFER, result.vbuf); + glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(GLfloat), verts.data(), GL_STREAM_DRAW); + glGenVertexArrays(1, &result.vao); + glBindVertexArray(result.vao); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0); + + // Return the VAO and the number of rectangles. + return std::make_pair(std::move(result), nrects); +} + +auto pref_to_pixelstreamer(int index) +{ + auto constexpr arr = std::array{PixelStreamer::Method::Auto, + PixelStreamer::Method::Persistent, + PixelStreamer::Method::Asynchronous, + PixelStreamer::Method::Synchronous}; + assert(1 <= index && index <= arr.size()); + return arr[index - 1]; +} + +} // namespace + +GLGraphics::GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi) + : prefs(prefs) + , stores(stores) + , pi(pi) +{ + // Create rectangle geometry. + GLfloat constexpr verts[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f}; + glGenBuffers(1, &rect.vbuf); + glBindBuffer(GL_ARRAY_BUFFER, rect.vbuf); + glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); + glGenVertexArrays(1, &rect.vao); + glBindVertexArray(rect.vao); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0); + + // Create shader programs. + auto vs = VShader(R"( + #version 330 core + + uniform mat2 mat; + uniform vec2 trans; + uniform vec2 subrect; + layout(location = 0) in vec2 pos; + smooth out vec2 uv; + + void main() + { + uv = pos * subrect; + vec2 pos2 = mat * pos + trans; + gl_Position = vec4(pos2.x, pos2.y, 0.0, 1.0); + } + )"); + + auto texcopy_fs = FShader(R"( + #version 330 core + + uniform sampler2D tex; + smooth in vec2 uv; + out vec4 outColour; + + void main() + { + outColour = texture(tex, uv); + } + )"); + + auto texcopydouble_fs = FShader(R"( + #version 330 core + + uniform sampler2D tex; + uniform sampler2D tex_outline; + smooth in vec2 uv; + layout(location = 0) out vec4 outColour; + layout(location = 1) out vec4 outColour_outline; + + void main() + { + outColour = texture(tex, uv); + outColour_outline = texture(tex_outline, uv); + } + )"); + + auto outlineoverlay_fs = FShader(R"( + #version 330 core + + uniform sampler2D tex; + uniform sampler2D tex_outline; + uniform float opacity; + smooth in vec2 uv; + out vec4 outColour; + + void main() + { + vec4 c1 = texture(tex, uv); + vec4 c2 = texture(tex_outline, uv); + vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a); + outColour = c1w * (1.0 - c2.a) + c2; + } + )"); + + auto xray_fs = FShader(R"( + #version 330 core + + uniform sampler2D tex; + uniform sampler2D tex_outline; + uniform vec2 pos; + uniform float radius; + smooth in vec2 uv; + out vec4 outColour; + + void main() + { + vec4 c1 = texture(tex, uv); + vec4 c2 = texture(tex_outline, uv); + + float r = length(gl_FragCoord.xy - pos); + r = clamp((radius - r) / 2.0, 0.0, 1.0); + + outColour = mix(c1, c2, r); + } + )"); + + auto outlineoverlayxray_fs = FShader(R"( + #version 330 core + + uniform sampler2D tex; + uniform sampler2D tex_outline; + uniform float opacity; + uniform vec2 pos; + uniform float radius; + smooth in vec2 uv; + out vec4 outColour; + + void main() + { + vec4 c1 = texture(tex, uv); + vec4 c2 = texture(tex_outline, uv); + vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a); + outColour = c1w * (1.0 - c2.a) + c2; + + float r = length(gl_FragCoord.xy - pos); + r = clamp((radius - r) / 2.0, 0.0, 1.0); + + outColour = mix(outColour, c2, r); + } + )"); + + auto checker_fs = FShader(R"( + #version 330 core + + uniform float size; + uniform vec3 col1, col2; + out vec4 outColour; + + void main() + { + vec2 a = floor(fract(gl_FragCoord.xy / size) * 2.0); + float b = abs(a.x - a.y); + outColour = vec4((1.0 - b) * col1 + b * col2, 1.0); + } + )"); + + auto shadow_gs = GShader(R"( + #version 330 core + + layout(triangles) in; + layout(triangle_strip, max_vertices = 10) out; + + uniform vec2 wh; + uniform float size; + uniform vec2 dir; + + smooth out vec2 uv; + flat out vec2 maxuv; + + void f(vec4 p, vec4 v0, mat2 m) + { + gl_Position = p; + uv = m * (p.xy - v0.xy); + EmitVertex(); + } + + float push(float x) + { + return 0.15 * (1.0 + clamp(x / 0.707, -1.0, 1.0)); + } + + void main() + { + vec4 v0 = gl_in[0].gl_Position; + vec4 v1 = gl_in[1].gl_Position; + vec4 v2 = gl_in[2].gl_Position; + vec4 v3 = gl_in[2].gl_Position - gl_in[1].gl_Position + gl_in[0].gl_Position; + + vec2 a = normalize((v1 - v0).xy * wh); + vec2 b = normalize((v3 - v0).xy * wh); + float det = a.x * b.y - a.y * b.x; + float s = -sign(det); + vec2 c = size / abs(det) / wh; + vec4 d = vec4(a * c, 0.0, 0.0); + vec4 e = vec4(b * c, 0.0, 0.0); + mat2 m = s * mat2(a.y, -b.y, -a.x, b.x) * mat2(wh.x, 0.0, 0.0, wh.y) / size; + + float ap = s * dot(vec2(a.y, -a.x), dir); + float bp = s * dot(vec2(-b.y, b.x), dir); + v0.xy += (b * push( ap) + a * push( bp)) * size / wh; + v1.xy += (b * push( ap) + a * -push(-bp)) * size / wh; + v2.xy += (b * -push(-ap) + a * -push(-bp)) * size / wh; + v3.xy += (b * -push(-ap) + a * push( bp)) * size / wh; + + maxuv = m * (v2.xy - v0.xy); + f(v0, v0, m); + f(v0 - d - e, v0, m); + f(v1, v0, m); + f(v1 + d - e, v0, m); + f(v2, v0, m); + f(v2 + d + e, v0, m); + f(v3, v0, m); + f(v3 - d + e, v0, m); + f(v0, v0, m); + f(v0 - d - e, v0, m); + EndPrimitive(); + } + )"); + + auto shadow_fs = FShader(R"( + #version 330 core + + uniform vec4 shadow_col; + + smooth in vec2 uv; + flat in vec2 maxuv; + + out vec4 outColour; + + void main() + { + float x = max(uv.x - maxuv.x, 0.0) - max(-uv.x, 0.0); + float y = max(uv.y - maxuv.y, 0.0) - max(-uv.y, 0.0); + float s = min(length(vec2(x, y)), 1.0); + + float A = 4.0; // This coefficient changes how steep the curve is and controls shadow drop-off. + s = (exp(A * (1.0 - s)) - 1.0) / (exp(A) - 1.0); // Exponential decay for drop shadow - long tail. + + outColour = shadow_col * s; + } + )"); + + texcopy.create(vs, texcopy_fs); + texcopydouble.create(vs, texcopydouble_fs); + outlineoverlay.create(vs, outlineoverlay_fs); + xray.create(vs, xray_fs); + outlineoverlayxray.create(vs, outlineoverlayxray_fs); + checker.create(vs, checker_fs); + shadow.create(vs, shadow_gs, shadow_fs); + + // Create the framebuffer object for rendering to off-view fragments. + glGenFramebuffers(1, &fbo); + + // Create the texture cache. + texturecache = TextureCache::create(); + + // Create the PixelStreamer. + pixelstreamer = PixelStreamer::create_supported(pref_to_pixelstreamer(prefs.pixelstreamer_method)); + + // Set the last known state as unspecified, forcing a pipeline recreation whatever the next operation is. + state = State::None; +} + +GLGraphics::~GLGraphics() +{ + glDeleteFramebuffers(1, &fbo); +} + +std::unique_ptr<Graphics> Graphics::create_gl(Prefs const &prefs, Stores const &stores, PageInfo const &pi) +{ + return std::make_unique<GLGraphics>(prefs, stores, pi); +} + +void GLGraphics::set_outlines_enabled(bool enabled) +{ + outlines_enabled = enabled; + if (!enabled) { + store.outline_texture.clear(); + snapshot.outline_texture.clear(); + } +} + +void GLGraphics::setup_stores_pipeline() +{ + if (state == State::Stores) return; + state = State::Stores; + + glDisable(GL_BLEND); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; + glDrawBuffers(outlines_enabled ? 2 : 1, attachments); + + auto const &shader = outlines_enabled ? texcopydouble : texcopy; + glUseProgram(shader.id); + mat_loc = shader.loc("mat"); + trans_loc = shader.loc("trans"); + geom_to_uniform({1.0, 1.0}, shader.loc("subrect")); + tex_loc = shader.loc("tex"); + if (outlines_enabled) texoutline_loc = shader.loc("tex_outline"); +} + +void GLGraphics::recreate_store(Geom::IntPoint const &dims) +{ + auto tex_size = dims * scale_factor; + + // Setup the base pipeline. + setup_stores_pipeline(); + + // Recreate the store textures. + auto recreate = [&] (Texture &tex) { + if (tex && tex.size() == tex_size) { + tex.invalidate(); + } else { + tex = Texture(tex_size); + } + }; + + recreate(store.texture); + if (outlines_enabled) { + recreate(store.outline_texture); + } + + // Bind the store to the framebuffer for writing to. + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0); + if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0); + glViewport(0, 0, store.texture.size().x(), store.texture.size().y()); + + // Clear the store to transparent. + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); +} + +void GLGraphics::shift_store(Fragment const &dest) +{ + auto tex_size = dest.rect.dimensions() * scale_factor; + + // Setup the base pipeline. + setup_stores_pipeline(); + + // Create the new fragment. + auto create_or_reuse = [&] (Texture &tex, Texture &from) { + if (from && from.size() == tex_size) { + from.invalidate(); + tex = std::move(from); + } else { + tex = Texture(tex_size); + } + }; + + GLFragment fragment; + create_or_reuse(fragment.texture, snapshot.texture); + if (outlines_enabled) { + create_or_reuse(fragment.outline_texture, snapshot.outline_texture); + } + + // Bind new store to the framebuffer to writing to. + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture .id(), 0); + if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0); + glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y()); + + // Clear new store to transparent. + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + + // Bind the old store to texture units 0 and 1 for reading from. + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, store.texture.id()); + glUniform1i(tex_loc, 0); + if (outlines_enabled) { + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, store.outline_texture.id()); + glUniform1i(texoutline_loc, 1); + } + glBindVertexArray(rect.vao); + + // Copy re-usuable contents of the old store into the new store. + geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + // Set the result as the new store. + snapshot = std::move(store); + store = std::move(fragment); +} + +void GLGraphics::swap_stores() +{ + std::swap(store, snapshot); +} + +void GLGraphics::fast_snapshot_combine() +{ + // Ensure the base pipeline is correctly set up. + setup_stores_pipeline(); + + // Compute the vertex data for the drawn region. + auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect); + + // Bind the snapshot to the framebuffer for writing to. + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, snapshot.texture.id(), 0); + if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, snapshot.outline_texture.id(), 0); + glViewport(0, 0, snapshot.texture.size().x(), snapshot.texture.size().y()); + + // Bind the store to texture unit 0 (and its outline to 1, if necessary). + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, store.texture.id()); + glUniform1i(tex_loc, 0); + if (outlines_enabled) { + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, store.outline_texture.id()); + glUniform1i(texoutline_loc, 1); + } + + // Copy the clean region of the store to the snapshot. + geom_to_uniform(calc_paste_transform(stores.store(), stores.snapshot()), mat_loc, trans_loc); + glBindVertexArray(clean_vao.vao); + glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects); +} + +void GLGraphics::snapshot_combine(Fragment const &dest) +{ + // Create the new fragment. + auto content_size = dest.rect.dimensions() * scale_factor; + + // Ensure the base pipeline is correctly set up. + setup_stores_pipeline(); + + // Compute the vertex data for the clean region. + auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect); + + GLFragment fragment; + fragment.texture = Texture(content_size); + if (outlines_enabled) fragment.outline_texture = Texture(content_size); + + // Bind the new fragment to the framebuffer for writing to. + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture.id(), 0); + if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0); + + // Clear the new fragment to transparent. + glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y()); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + + // Bind the store and snapshot to texture units 0 and 1 (and their outlines to 2 and 3, if necessary). + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, snapshot.texture.id()); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, store.texture.id()); + if (outlines_enabled) { + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id()); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, store.outline_texture.id()); + } + + // Paste the snapshot store onto the new fragment. + glUniform1i(tex_loc, 0); + if (outlines_enabled) glUniform1i(texoutline_loc, 2); + geom_to_uniform(calc_paste_transform(stores.snapshot(), dest), mat_loc, trans_loc); + glBindVertexArray(rect.vao); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + // Paste the backing store onto the new fragment. + glUniform1i(tex_loc, 1); + if (outlines_enabled) glUniform1i(texoutline_loc, 3); + geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc); + glBindVertexArray(clean_vao.vao); + glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects); + + // Set the result as the new snapshot. + snapshot = std::move(fragment); +} + +void GLGraphics::invalidate_snapshot() +{ + if (snapshot.texture) snapshot.texture.invalidate(); + if (snapshot.outline_texture) snapshot.outline_texture.invalidate(); +} + +void GLGraphics::setup_tiles_pipeline() +{ + if (state == State::Tiles) return; + state = State::Tiles; + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; + glDrawBuffers(outlines_enabled ? 2 : 1, attachments); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0); + if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0); + glViewport(0, 0, store.texture.size().x(), store.texture.size().y()); + + auto const &shader = outlines_enabled ? texcopydouble : texcopy; + glUseProgram(shader.id); + mat_loc = shader.loc("mat"); + trans_loc = shader.loc("trans"); + subrect_loc = shader.loc("subrect"); + glUniform1i(shader.loc("tex"), 0); + if (outlines_enabled) glUniform1i(shader.loc("tex_outline"), 1); + + glBindVertexArray(rect.vao); + glDisable(GL_BLEND); +}; + +Cairo::RefPtr<Cairo::ImageSurface> GLGraphics::request_tile_surface(Geom::IntRect const &rect, bool nogl) +{ + Cairo::RefPtr<Cairo::ImageSurface> surface; + + { + auto g = std::lock_guard(ps_mutex); + surface = pixelstreamer->request(rect.dimensions() * scale_factor, nogl); + } + + if (surface) { + cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); + } + + return surface; +} + +void GLGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) +{ + auto g = std::lock_guard(ps_mutex); + auto surface_size = dimensions(surface); + + Texture texture, outline_texture; + + glActiveTexture(GL_TEXTURE0); + texture = texturecache->request(surface_size); // binds + pixelstreamer->finish(std::move(surface)); // uploads content + + if (outlines_enabled) { + glActiveTexture(GL_TEXTURE1); + outline_texture = texturecache->request(surface_size); + pixelstreamer->finish(std::move(outline_surface)); + } + + setup_tiles_pipeline(); + + geom_to_uniform(calc_paste_transform(fragment, stores.store()), mat_loc, trans_loc); + geom_to_uniform(Geom::Point(surface_size) / texture.size(), subrect_loc); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + texturecache->finish(std::move(texture)); + if (outlines_enabled) { + texturecache->finish(std::move(outline_texture)); + } +} + +void GLGraphics::junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) +{ + auto g = std::lock_guard(ps_mutex); + pixelstreamer->finish(std::move(surface), true); +} + +void GLGraphics::setup_widget_pipeline(Fragment const &view) +{ + state = State::Widget; + + glDrawBuffer(GL_COLOR_ATTACHMENT0); + glViewport(0, 0, view.rect.width() * scale_factor, view.rect.height() * scale_factor); + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_NOTEQUAL, 1, 1); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, store.texture.id()); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, snapshot.texture.id()); + if (outlines_enabled) { + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, store.outline_texture.id()); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id()); + } + glBindVertexArray(rect.vao); +}; + +void GLGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const&) +{ + // If in decoupled mode, create the vertex data describing the drawn region of the store. + VAO clean_vao; + int clean_numrects; + if (stores.mode() == Stores::Mode::Decoupled) { + std::tie(clean_vao, clean_numrects) = region_shrink_vao(stores.store().drawn, stores.store().rect); + } + + setup_widget_pipeline(view); + + // Clear the buffers. Since we have to pick a clear colour, we choose the page colour, enabling the single-page optimisation later. + glClearColor(SP_RGBA32_R_U(page) / 255.0f, SP_RGBA32_G_U(page) / 255.0f, SP_RGBA32_B_U(page) / 255.0f, 1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + if (check_single_page(view, pi)) { + // A single page occupies the whole view. + if (SP_RGBA32_A_U(page) == 255) { + // Page is solid - nothing to do, since already cleared to this colour. + } else { + // Page is checkerboard - fill view with page pattern. + glDisable(GL_BLEND); + glUseProgram(checker.id); + glUniform1f(checker.loc("size"), 12.0 * scale_factor); + glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page))); + glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page))); + geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans")); + geom_to_uniform({1.0, 1.0}, checker.loc("subrect")); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + } else { + glDisable(GL_BLEND); + + auto set_page_transform = [&] (Geom::Rect const &rect, Program const &prog) { + geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * calc_paste_transform({{}, Geom::IntRect::from_xywh(0, 0, 1, 1)}, view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans")); + }; + + // Pages + glUseProgram(checker.id); + glUniform1f(checker.loc("size"), 12.0 * scale_factor); + glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page))); + glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page))); + geom_to_uniform({1.0, 1.0}, checker.loc("subrect")); + for (auto &rect : pi.pages) { + set_page_transform(rect, checker); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + } + + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + + // Desk + glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(desk))); + glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(desk))); + geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans")); + geom_to_uniform({1.0, 1.0}, checker.loc("subrect")); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + // Shadows + if (SP_RGBA32_A_U(border) != 0) { + auto dir = (Geom::Point(1.0, a.yaxisdir) * view.affine * Geom::Scale(1.0, -1.0)).normalized(); // Shadow direction rotates with view. + glUseProgram(shadow.id); + geom_to_uniform({1.0, 1.0}, shadow.loc("subrect")); + glUniform2fv(shadow.loc("wh"), 1, std::begin({(GLfloat)view.rect.width(), (GLfloat)view.rect.height()})); + glUniform1f(shadow.loc("size"), 40.0 * std::pow(std::abs(view.affine.det()), 0.25)); + glUniform2fv(shadow.loc("dir"), 1, std::begin({(GLfloat)dir.x(), (GLfloat)dir.y()})); + glUniform4fv(shadow.loc("shadow_col"), 1, std::begin(premultiplied(rgba_to_array(border)))); + for (auto &rect : pi.pages) { + set_page_transform(rect, shadow); + glDrawArrays(GL_TRIANGLES, 0, 3); + } + } + + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + } + + glStencilFunc(GL_NOTEQUAL, 2, 2); + + enum class DrawMode + { + Store, + Outline, + Combine + }; + + auto draw_store = [&, this] (Program const &prog, DrawMode drawmode) { + glUseProgram(prog.id); + geom_to_uniform({1.0, 1.0}, prog.loc("subrect")); + glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 2 : 0); + if (drawmode == DrawMode::Combine) { + glUniform1i(prog.loc("tex_outline"), 2); + glUniform1f(prog.loc("opacity"), prefs.outline_overlay_opacity / 100.0); + } + + if (stores.mode() == Stores::Mode::Normal) { + // Backing store fragment. + geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans")); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + } else { + // Backing store fragment, clipped to its clean region. + geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans")); + glBindVertexArray(clean_vao.vao); + glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects); + + // Snapshot fragment. + glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 3 : 1); + if (drawmode == DrawMode::Combine) glUniform1i(prog.loc("tex_outline"), 3); + geom_to_uniform(calc_paste_transform(stores.snapshot(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans")); + glBindVertexArray(rect.vao); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + } + }; + + if (a.splitmode == Inkscape::SplitMode::NORMAL || (a.splitmode == Inkscape::SplitMode::XRAY && !a.mouse)) { + + // Drawing the backing store over the whole view. + a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY + ? draw_store(outlineoverlay, DrawMode::Combine) + : draw_store(texcopy, DrawMode::Store); + + } else if (a.splitmode == Inkscape::SplitMode::SPLIT) { + + // Calculate the clipping rectangles for split view. + auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir); + + glEnable(GL_SCISSOR_TEST); + + // Draw the backing store. + glScissor(store_clip.left() * scale_factor, (view.rect.height() - store_clip.bottom()) * scale_factor, store_clip.width() * scale_factor, store_clip.height() * scale_factor); + a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY + ? draw_store(outlineoverlay, DrawMode::Combine) + : draw_store(texcopy, DrawMode::Store); + + // Draw the outline store. + glScissor(outline_clip.left() * scale_factor, (view.rect.height() - outline_clip.bottom()) * scale_factor, outline_clip.width() * scale_factor, outline_clip.height() * scale_factor); + draw_store(texcopy, DrawMode::Outline); + + glDisable(GL_SCISSOR_TEST); + glDisable(GL_STENCIL_TEST); + + // Calculate the bounding rectangle of the split view controller. + auto rect = Geom::IntRect({0, 0}, view.rect.dimensions()); + auto dim = a.splitdir == Inkscape::SplitDirection::EAST || a.splitdir == Inkscape::SplitDirection::WEST ? Geom::X : Geom::Y; + rect[dim] = Geom::IntInterval(-21, 21) + std::round(a.splitfrac[dim] * view.rect.dimensions()[dim]); + + // Lease out a PixelStreamer mapping to draw on. + auto surface_size = rect.dimensions() * scale_factor; + auto surface = pixelstreamer->request(surface_size); + cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); + + // Actually draw the content with Cairo. + auto cr = Cairo::Context::create(surface); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source_rgba(0.0, 0.0, 0.0, 0.0); + cr->paint(); + cr->translate(-rect.left(), -rect.top()); + paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr); + + // Convert the surface to a texture. + glActiveTexture(GL_TEXTURE0); + auto texture = texturecache->request(surface_size); + pixelstreamer->finish(std::move(surface)); + + // Paint the texture onto the view. + glUseProgram(texcopy.id); + glUniform1i(texcopy.loc("tex"), 0); + geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * Geom::Scale(2.0 / view.rect.width(), -2.0 / view.rect.height()) * Geom::Translate(-1.0, 1.0), texcopy.loc("mat"), texcopy.loc("trans")); + geom_to_uniform(Geom::Point(surface_size) / texture.size(), texcopy.loc("subrect")); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + // Return the texture back to the texture cache. + texturecache->finish(std::move(texture)); + + } else { // if (_split_mode == Inkscape::SplitMode::XRAY && a.mouse) + + // Draw the backing store over the whole view. + auto const &shader = a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY ? outlineoverlayxray : xray; + glUseProgram(shader.id); + glUniform1f(shader.loc("radius"), prefs.xray_radius * scale_factor); + glUniform2fv(shader.loc("pos"), 1, std::begin({(GLfloat)(a.mouse->x() * scale_factor), (GLfloat)((view.rect.height() - a.mouse->y()) * scale_factor)})); + draw_store(shader, DrawMode::Combine); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/glgraphics.h b/src/ui/widget/canvas/glgraphics.h new file mode 100644 index 0000000..7cb6ecf --- /dev/null +++ b/src/ui/widget/canvas/glgraphics.h @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OpenGL display backend. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H +#define INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H + +#include <mutex> +#include <epoxy/gl.h> +#include "graphics.h" +#include "texturecache.h" + +namespace Inkscape { +namespace UI { +namespace Widget { +class Stores; +class Prefs; +class PixelStreamer; + +template <GLuint type> +struct Shader : boost::noncopyable +{ + GLuint id; + Shader(char const *src) { id = glCreateShader(type); glShaderSource(id, 1, &src, nullptr); glCompileShader(id); } + ~Shader() { glDeleteShader(id); } +}; +using GShader = Shader<GL_GEOMETRY_SHADER>; +using VShader = Shader<GL_VERTEX_SHADER>; +using FShader = Shader<GL_FRAGMENT_SHADER>; + +struct Program : boost::noncopyable +{ + GLuint id = 0; + void create(VShader const &v, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, f.id); glLinkProgram(id); } + void create(VShader const &v, const GShader &g, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, g.id); glAttachShader(id, f.id); glLinkProgram(id); } + auto loc(char const *str) const { return glGetUniformLocation(id, str); } + ~Program() { glDeleteProgram(id); } +}; + +class VAO +{ +public: + GLuint vao = 0; + GLuint vbuf; + + VAO() = default; + VAO(GLuint vao, GLuint vbuf) : vao(vao), vbuf(vbuf) {} + VAO(VAO &&other) noexcept { movefrom(other); } + VAO &operator=(VAO &&other) noexcept { reset(); movefrom(other); return *this; } + ~VAO() { reset(); } + +private: + void reset() noexcept { if (vao) { glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbuf); } } + void movefrom(VAO &other) noexcept { vao = other.vao; vbuf = other.vbuf; other.vao = 0; } +}; + +struct GLFragment +{ + Texture texture; + Texture outline_texture; +}; + +class GLGraphics : public Graphics +{ +public: + GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi); + ~GLGraphics() override; + + void set_scale_factor(int scale) override { scale_factor = scale; } + void set_outlines_enabled(bool) override; + void set_background_in_stores(bool enabled) override { background_in_stores = enabled; } + void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; } + + void recreate_store(Geom::IntPoint const &dimensions) override; + void shift_store(Fragment const &dest) override; + void swap_stores() override; + void fast_snapshot_combine() override; + void snapshot_combine(Fragment const &dest) override; + void invalidate_snapshot() override; + + bool is_opengl() const override { return true; } + void invalidated_glstate() override { state = State::None; } + + Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override; + void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override; + void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override; + + void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override; + +private: + // Drawn content. + GLFragment store, snapshot; + + // OpenGL objects. + VAO rect; // Rectangle vertex data. + Program checker, shadow, texcopy, texcopydouble, outlineoverlay, xray, outlineoverlayxray; // Shaders + GLuint fbo; // Framebuffer object for rendering to stores. + + // Pixel streamer and texture cache for uploading pixel data to GPU. + std::unique_ptr<PixelStreamer> pixelstreamer; + std::unique_ptr<TextureCache> texturecache; + std::mutex ps_mutex; + + // For preventing unnecessary pipeline recreation. + enum class State { None, Widget, Stores, Tiles }; + State state; + void setup_stores_pipeline(); + void setup_tiles_pipeline(); + void setup_widget_pipeline(Fragment const &view); + + // For caching frequently-used uniforms. + GLuint mat_loc, trans_loc, subrect_loc, tex_loc, texoutline_loc; + + // Dependency objects in canvas. + Prefs const &prefs; + Stores const &stores; + PageInfo const π + + // Backend-agnostic state. + int scale_factor = 1; + bool outlines_enabled = false; + bool background_in_stores = false; + uint32_t page, desk, border; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/graphics.cpp b/src/ui/widget/canvas/graphics.cpp new file mode 100644 index 0000000..28972e2 --- /dev/null +++ b/src/ui/widget/canvas/graphics.cpp @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <2geom/parallelogram.h> +#include "ui/util.h" +#include "helper/geom.h" +#include "graphics.h" +#include "util.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +namespace { + +// Convert an rgba into a pattern, turning transparency into checkerboard-ness. +Cairo::RefPtr<Cairo::Pattern> rgba_to_pattern(uint32_t rgba) +{ + if (SP_RGBA32_A_U(rgba) == 255) { + return Cairo::SolidPattern::create_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + } else { + int constexpr w = 6; + int constexpr h = 6; + + auto dark = checkerboard_darken(rgba); + + auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 2 * w, 2 * h); + + auto cr = Cairo::Context::create(surface); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + cr->paint(); + cr->set_source_rgb(dark[0], dark[1], dark[2]); + cr->rectangle(0, 0, w, h); + cr->rectangle(w, h, w, h); + cr->fill(); + + auto pattern = Cairo::SurfacePattern::create(surface); + pattern->set_extend(Cairo::EXTEND_REPEAT); + pattern->set_filter(Cairo::FILTER_NEAREST); + + return pattern; + } +} + +} // namespace + +// Paint the background and pages using Cairo into the given fragment. +void Graphics::paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr) +{ + cr->save(); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height()); + cr->clip(); + + if (desk == page || check_single_page(fragment, pi)) { + // Desk and page are the same, or a single page fills the whole screen; just clear the fragment to page. + cr->set_source(rgba_to_pattern(page)); + cr->paint(); + } else { + // Paint the background to the complement of the pages. (Slightly overpaints when pages overlap.) + cr->save(); + cr->set_source(rgba_to_pattern(desk)); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height()); + cr->translate(-fragment.rect.left(), -fragment.rect.top()); + cr->transform(geom_to_cairo(fragment.affine)); + for (auto &rect : pi.pages) { + cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height()); + } + cr->fill(); + cr->restore(); + + // Paint the pages. + cr->save(); + cr->set_source(rgba_to_pattern(page)); + cr->translate(-fragment.rect.left(), -fragment.rect.top()); + cr->transform(geom_to_cairo(fragment.affine)); + for (auto &rect : pi.pages) { + cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height()); + } + cr->fill(); + cr->restore(); + } + + cr->restore(); +} + +std::pair<Geom::IntRect, Geom::IntRect> Graphics::calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction) +{ + auto window = Geom::IntRect({0, 0}, size); + + auto content = window; + auto outline = window; + auto split = [&] (Geom::Dim2 dim, Geom::IntRect &lo, Geom::IntRect &hi) { + int s = std::round(split_frac[dim] * size[dim]); + lo[dim].setMax(s); + hi[dim].setMin(s); + }; + + switch (split_direction) { + case Inkscape::SplitDirection::NORTH: split(Geom::Y, content, outline); break; + case Inkscape::SplitDirection::EAST: split(Geom::X, outline, content); break; + case Inkscape::SplitDirection::SOUTH: split(Geom::Y, outline, content); break; + case Inkscape::SplitDirection::WEST: split(Geom::X, content, outline); break; + default: assert(false); break; + } + + return std::make_pair(content, outline); +} + +void Graphics::paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction, SplitDirection hover_direction, Cairo::RefPtr<Cairo::Context> const &cr) +{ + auto split_position = (split_frac * size).round(); + + // Add dividing line. + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->set_line_width(1.0); + if (split_direction == Inkscape::SplitDirection::EAST || + split_direction == Inkscape::SplitDirection::WEST) { + cr->move_to(split_position.x() + 0.5, 0.0 ); + cr->line_to(split_position.x() + 0.5, size.y()); + cr->stroke(); + } else { + cr->move_to(0.0 , split_position.y() + 0.5); + cr->line_to(size.x(), split_position.y() + 0.5); + cr->stroke(); + } + + // Add controller image. + double a = hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0; + cr->set_source_rgba(0.2, 0.2, 0.2, a); + cr->arc(split_position.x(), split_position.y(), 20, 0, 2 * M_PI); + cr->fill(); + + for (int i = 0; i < 4; i++) { + // The four direction triangles. + cr->save(); + + // Position triangle. + cr->translate(split_position.x(), split_position.y()); + cr->rotate((i + 2) * M_PI / 2); + + // Draw triangle. + cr->move_to(-5, 8); + cr->line_to( 0, 18); + cr->line_to( 5, 8); + cr->close_path(); + + double b = (int)hover_direction == (i + 1) ? 0.9 : 0.7; + cr->set_source_rgba(b, b, b, a); + cr->fill(); + + cr->restore(); + } +} + +bool Graphics::check_single_page(Fragment const &view, PageInfo const &pi) +{ + auto pl = Geom::Parallelogram(view.rect) * view.affine.inverse(); + return std::any_of(pi.pages.begin(), pi.pages.end(), [&] (auto &rect) { + return Geom::Parallelogram(rect).contains(pl); + }); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape diff --git a/src/ui/widget/canvas/graphics.h b/src/ui/widget/canvas/graphics.h new file mode 100644 index 0000000..0e7767d --- /dev/null +++ b/src/ui/widget/canvas/graphics.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Display backend interface. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H +#define INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H + +#include <memory> +#include <cstdint> +#include <boost/noncopyable.hpp> +#include <2geom/rect.h> +#include <cairomm/cairomm.h> +#include "display/rendermode.h" +#include "fragment.h" + +namespace Inkscape { +namespace UI { +namespace Widget { +class Stores; +class Prefs; + +struct PageInfo +{ + std::vector<Geom::Rect> pages; +}; + +class Graphics +{ +public: + // Creation/destruction. + 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); + virtual ~Graphics() = default; + + // State updating. + virtual void set_scale_factor(int) = 0; ///< Set the HiDPI scale factor. + virtual void set_outlines_enabled(bool) = 0; ///< Whether to maintain a second layer of outline content. + virtual void set_background_in_stores(bool) = 0; ///< Whether to assume the first layer is drawn on top of background or transparency. + virtual void set_colours(uint32_t page, uint32_t desk, uint32_t border) = 0; ///< Set colours for background/page shadow drawing. + + // Store manipulation. + virtual void recreate_store(Geom::IntPoint const &dims) = 0; ///< Set the store to a surface of the given size, of unspecified contents. + virtual void shift_store(Fragment const &dest) = 0; ///< Called when the store fragment shifts position to \a dest. + virtual void swap_stores() = 0; ///< Exchange the store and snapshot surfaces. + virtual void fast_snapshot_combine() = 0; ///< Paste the store onto the snapshot. + virtual void snapshot_combine(Fragment const &dest) = 0; ///< Paste the snapshot followed by the store onto a new snapshot at \a dest. + virtual void invalidate_snapshot() = 0; ///< Indicate that the content in the snapshot store is not going to be used again. + + // Misc. + virtual bool is_opengl() const = 0; ///< Whether this is an OpenGL backend. + virtual void invalidated_glstate() = 0; ///< Tells the Graphics to no longer rely on any OpenGL state it had set up. + + // Tile drawing. + /// Return a surface for drawing on. If nogl is true, no GL commands are issued, as is a requirement off-main-thread. All such surfaces must be + /// returned by passing them either to draw_tile() or junk_tile_surface(). + virtual Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) = 0; + /// Commit the contents of a surface previously issued by request_tile_surface() to the canvas. In outline mode, a second surface must be passed + /// containing the outline content, otherwise it should be null. + virtual void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) = 0; + /// Get rid of a surface previously issued by request_tile_surface() without committing it to the canvas. Usually useful only to dispose of + /// surfaces which have gone into an error state while rendering, which is irreversible, and therefore we can't do anything useful with them. + virtual void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) = 0; + + // Widget painting. + struct PaintArgs + { + std::optional<Geom::IntPoint> mouse; + RenderMode render_mode; + SplitMode splitmode; + Geom::Point splitfrac; + SplitDirection splitdir; + SplitDirection hoverdir; + double yaxisdir; + }; + virtual void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) = 0; + + // Static functions providing common functionality. + static bool check_single_page(Fragment const &view, PageInfo const &pi); + static std::pair<Geom::IntRect, Geom::IntRect> calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir); + static void paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir, SplitDirection hoverdir, Cairo::RefPtr<Cairo::Context> const &cr); + static void paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H diff --git a/src/ui/widget/canvas/pixelstreamer.cpp b/src/ui/widget/canvas/pixelstreamer.cpp new file mode 100644 index 0000000..74d557b --- /dev/null +++ b/src/ui/widget/canvas/pixelstreamer.cpp @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <cassert> +#include <cmath> +#include <vector> +#include <epoxy/gl.h> +#include "pixelstreamer.h" +#include "helper/mathfns.h" + +namespace Inkscape { +namespace UI { +namespace Widget { +namespace { + +cairo_user_data_key_t constexpr key{}; + +class PersistentPixelStreamer : public PixelStreamer +{ + static int constexpr bufsize = 0x1000000; // 16 MiB + + struct Buffer + { + GLuint pbo; // Pixel buffer object. + unsigned char *data; // The pointer to the mapped region. + int off; // Offset of the unused region, in bytes. Always a multiple of 64. + int refs; // How many mappings are currently using this buffer. + GLsync sync; // Sync object for telling us when the GPU has finished reading from this buffer. + bool ready; // Whether this buffer is ready for re-use. + + void create() + { + glGenBuffers(1, &pbo); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glBufferStorage(GL_PIXEL_UNPACK_BUFFER, bufsize, nullptr, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT); + data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, bufsize, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_FLUSH_EXPLICIT_BIT); + off = 0; + refs = 0; + } + + void destroy() + { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + glDeleteBuffers(1, &pbo); + } + + // Advance a buffer in state 3 or 4 as far as possible towards state 5. + void advance() + { + if (!sync) { + sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } else { + auto ret = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 0); + if (ret == GL_CONDITION_SATISFIED || ret == GL_ALREADY_SIGNALED) { + glDeleteSync(sync); + ready = true; + } + } + } + }; + std::vector<Buffer> buffers; + + int current_buffer; + + struct Mapping + { + bool used; // Whether the mapping is in use, or on the freelist. + int buf; // The buffer the mapping is using. + int off; // Offset of the mapped region. + int size; // Size of the mapped region. + int width, height, stride; // Image properties. + }; + std::vector<Mapping> mappings; + + /* + * A Buffer cycles through the following five states: + * + * 1. Current --> We are currently filling this buffer up with allocations. + * 2. Not current, refs > 0 --> Finished the above, but may still be writing into it and issuing GL commands from it. + * 3. Not current, refs == 0, !ready, !sync --> Finished the above, but GL may be reading from it. We have yet to create its sync object. + * 4. Not current, refs == 0, !ready, sync --> We have now created its sync object, but it has not been signalled yet. + * 5. Not current, refs == 0, ready --> The sync object has been signalled and deleted. + * + * Only one Buffer is Current at any given time, and is marked by the current_buffer variable. + */ + +public: + PersistentPixelStreamer() + { + // Create a single initial buffer and make it the current buffer. + buffers.emplace_back(); + buffers.back().create(); + current_buffer = 0; + } + + Method get_method() const override { return Method::Persistent; } + + Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override + { + // Calculate image properties required by cairo. + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x()); + int size = stride * dimensions.y(); + int sizeup = Util::roundup(size, 64); + assert(sizeup < bufsize); + + // Attempt to advance buffers in states 3 or 4 towards 5, if allowed. + if (!nogl) { + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready) { + buffers[i].advance(); + } + } + } + // Continue using the current buffer if possible. + if (buffers[current_buffer].off + sizeup <= bufsize) { + goto chosen_buffer; + } + // Otherwise, the current buffer has filled up. After this point, the current buffer will change. + // Therefore, handle the state change of the current buffer out of the Current state. Usually that + // means doing nothing because the transition to state 2 is automatic. But if refs == 0 already, + // then we need to transition into state 3 by setting ready = false. If we're allowed to use GL, + // then we can additionally transition into state 4 by creating the sync object. + if (buffers[current_buffer].refs == 0) { + buffers[current_buffer].ready = false; + buffers[current_buffer].sync = nogl ? nullptr : glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + // Attempt to re-use a old buffer that has reached state 5. + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && buffers[i].refs == 0 && buffers[i].ready) { + // Found an unused buffer. Re-use it. (Move to state 1.) + buffers[i].off = 0; + current_buffer = i; + goto chosen_buffer; + } + } + // Otherwise, there are no available buffers. Create and use a new one. That requires GL, so fail if not allowed. + if (nogl) { + return {}; + } + buffers.emplace_back(); + buffers.back().create(); + current_buffer = buffers.size() - 1; + chosen_buffer: + // Finished changing the current buffer. + auto &b = buffers[current_buffer]; + + // Choose/create the mapping to use. + auto choose_mapping = [&, this] { + for (int i = 0; i < mappings.size(); i++) { + if (!mappings[i].used) { + // Found unused mapping. + return i; + } + } + // No free mapping; create one. + mappings.emplace_back(); + return (int)mappings.size() - 1; + }; + + auto mapping = choose_mapping(); + auto &m = mappings[mapping]; + + // Set up the mapping bookkeeping. + m = {true, current_buffer, b.off, size, dimensions.x(), dimensions.y(), stride}; + b.off += sizeup; + b.refs++; + + // Create the image surface. + auto surface = Cairo::ImageSurface::create(b.data + m.off, Cairo::FORMAT_ARGB32, dimensions.x(), dimensions.y(), stride); + + // Attach the mapping handle as user data. + cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr); + + return surface; + } + + void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override + { + // Extract the mapping handle from the surface's user data. + auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key); + + // Flush all changes from the image surface to the buffer, and delete it. + surface.clear(); + + auto &m = mappings[mapping]; + auto &b = buffers[m.buf]; + + // Flush the mapped subregion. + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, b.pbo); + glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, m.off, m.size); + + // Tear down the mapping bookkeeping. (if this causes transition 2 --> 3, it is handled below.) + m.used = false; + b.refs--; + + // Upload to the texture from the mapped subregion. + if (!junk) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, (void*)(uintptr_t)m.off); + } + + // If the buffer is due for recycling, issue a sync command so that we can recycle it when it's ready. (Handle transition 2 --> 4.) + if (m.buf != current_buffer && b.refs == 0) { + b.ready = false; + b.sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + + // Check other buffers to see if they're ready for recycling. (Advance from 3/4 towards 5.) + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && i != m.buf && buffers[i].refs == 0 && !buffers[i].ready) { + buffers[i].advance(); + } + } + } + + ~PersistentPixelStreamer() override + { + // Delete any sync objects. (For buffers in state 4.) + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready && buffers[i].sync) { + glDeleteSync(buffers[i].sync); + } + } + + // Wait for GL to finish reading out of all the buffers. + glFinish(); + + // Deallocate the buffers on the GL side. + for (auto &b : buffers) { + b.destroy(); + } + } +}; + +class AsynchronousPixelStreamer : public PixelStreamer +{ + static int constexpr minbufsize = 0x4000; // 16 KiB + static int constexpr expire_timeout = 10000; + + static int constexpr size_to_bucket(int size) { return Util::floorlog2((size - 1) / minbufsize) + 1; } + static int constexpr bucket_maxsize(int b) { return minbufsize * (1 << b); } + + struct Buffer + { + GLuint pbo; + unsigned char *data; + + void create(int size) + { + glGenBuffers(1, &pbo); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW); + data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT); + } + + void destroy() + { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + glDeleteBuffers(1, &pbo); + } + }; + + struct Bucket + { + std::vector<Buffer> spares; + int used = 0; + int high_use_count = 0; + }; + std::vector<Bucket> buckets; + + struct Mapping + { + bool used; + Buffer buf; + int bucket; + int width, height, stride; + }; + std::vector<Mapping> mappings; + + int expire_timer = 0; + +public: + Method get_method() const override { return Method::Asynchronous; } + + Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override + { + // Calculate image properties required by cairo. + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x()); + int size = stride * dimensions.y(); + + // Find the bucket that size falls into. + int bucket = size_to_bucket(size); + if (bucket >= buckets.size()) { + buckets.resize(bucket + 1); + } + auto &b = buckets[bucket]; + + // Find/create a buffer of the appropriate size. + Buffer buf; + if (!b.spares.empty()) { + // If the bucket has any spare mapped buffers, then use one of them. + buf = std::move(b.spares.back()); + b.spares.pop_back(); + } else if (!nogl) { + // Otherwise, we have to use OpenGL to create and map a new buffer. + buf.create(bucket_maxsize(bucket)); + } else { + // If we're not allowed to issue GL commands, then that is a failure. + return {}; + } + + // Record the new use count of the bucket. + b.used++; + if (b.used > b.high_use_count) { + // If the use count has gone above the high-water mark, record it and reset the timer for when to clean up excess spares. + b.high_use_count = b.used; + expire_timer = 0; + } + + auto choose_mapping = [&, this] { + for (int i = 0; i < mappings.size(); i++) { + if (!mappings[i].used) { + return i; + } + } + mappings.emplace_back(); + return (int)mappings.size() - 1; + }; + + auto mapping = choose_mapping(); + auto &m = mappings[mapping]; + + m.used = true; + m.buf = std::move(buf); + m.bucket = bucket; + m.width = dimensions.x(); + m.height = dimensions.y(); + m.stride = stride; + + auto surface = Cairo::ImageSurface::create(m.buf.data, Cairo::FORMAT_ARGB32, m.width, m.height, m.stride); + cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr); + return surface; + } + + void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override + { + auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key); + surface.clear(); + + auto &m = mappings[mapping]; + auto &b = buckets[m.bucket]; + + // Unmap the buffer. + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.buf.pbo); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + + // Upload the buffer to the texture. + if (!junk) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, nullptr); + } + + // Mark the mapping slot as unused. + m.used = false; + + // Orphan and re-map the buffer. + auto size = bucket_maxsize(m.bucket); + glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW); + m.buf.data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT); + + // Put the buffer back in its corresponding bucket's pile of spares. + b.spares.emplace_back(std::move(m.buf)); + b.used--; + + // If the expiration timeout has been reached, get rid of excess spares from all buckets, and reset the high use counts. + expire_timer++; + if (expire_timer >= expire_timeout) { + expire_timer = 0; + + for (auto &b : buckets) { + int max_spares = b.high_use_count - b.used; + assert(max_spares >= 0); + if (b.spares.size() > max_spares) { + for (int i = max_spares; i < b.spares.size(); i++) { + b.spares[i].destroy(); + } + b.spares.resize(max_spares); + } + b.high_use_count = b.used; + } + } + } + + ~AsynchronousPixelStreamer() override + { + // Unmap and delete all spare buffers. (They are not being used.) + for (auto &b : buckets) { + for (auto &buf : b.spares) { + buf.destroy(); + } + } + } +}; + +class SynchronousPixelStreamer : public PixelStreamer +{ + struct Mapping + { + bool used; + std::vector<unsigned char> data; + int size, width, height, stride; + }; + std::vector<Mapping> mappings; + +public: + Method get_method() const override { return Method::Synchronous; } + + Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool) override + { + auto choose_mapping = [&, this] { + for (int i = 0; i < mappings.size(); i++) { + if (!mappings[i].used) { + return i; + } + } + mappings.emplace_back(); + return (int)mappings.size() - 1; + }; + + auto mapping = choose_mapping(); + auto &m = mappings[mapping]; + + m.used = true; + m.width = dimensions.x(); + m.height = dimensions.y(); + m.stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, m.width); + m.size = m.stride * m.height; + m.data.resize(m.size); + + auto surface = Cairo::ImageSurface::create(&m.data[0], Cairo::FORMAT_ARGB32, m.width, m.height, m.stride); + cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr); + return surface; + } + + void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override + { + auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key); + surface.clear(); + + auto &m = mappings[mapping]; + + if (!junk) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, &m.data[0]); + } + + m.used = false; + m.data.clear(); + } +}; + +} // namespace + +std::unique_ptr<PixelStreamer> PixelStreamer::create_supported(Method method) +{ + int ver = epoxy_gl_version(); + + if (method <= Method::Asynchronous) { + if (ver >= 30 || epoxy_has_gl_extension("GL_ARB_map_buffer_range")) { + if (method <= Method::Persistent) { + if (ver >= 44 || (epoxy_has_gl_extension("GL_ARB_buffer_storage") && + epoxy_has_gl_extension("GL_ARB_texture_storage") && + epoxy_has_gl_extension("GL_ARB_SYNC"))) + { + return std::make_unique<PersistentPixelStreamer>(); + } else if (method != Method::Auto) { + std::cerr << "Persistent PixelStreamer not available" << std::endl; + } + } + return std::make_unique<AsynchronousPixelStreamer>(); + } else if (method != Method::Auto) { + std::cerr << "Asynchronous PixelStreamer not available" << std::endl; + } + } + return std::make_unique<SynchronousPixelStreamer>(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/pixelstreamer.h b/src/ui/widget/canvas/pixelstreamer.h new file mode 100644 index 0000000..bcd3684 --- /dev/null +++ b/src/ui/widget/canvas/pixelstreamer.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A class hierarchy implementing various ways of streaming pixel buffers to the GPU. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H +#define INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H + +#include <memory> +#include <2geom/int-point.h> +#include <cairomm/refptr.h> +#include <cairomm/surface.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +// A class for turning Cairo image surfaces into OpenGL textures. +class PixelStreamer +{ +public: + virtual ~PixelStreamer() = default; + + // Method for streaming pixels to the GPU. + enum class Method + { + Auto, // Use the best option available at runtime. + Persistent, // Persistent buffer mapping. (Best, requires OpenGL 4.4.) + Asynchronous, // Ordinary buffer mapping. (Almost as good, requires OpenGL 3.0.) + Synchronous // Synchronous texture uploads. (Worst but still tolerable, requires OpenGL 1.1.) + }; + + // Create a PixelStreamer using a choice of method specified at runtime, falling back if unsupported. + static std::unique_ptr<PixelStreamer> create_supported(Method method); + + // Return the method in use. + virtual Method get_method() const = 0; + + /** + * Request a drawing surface of the given dimensions. If nogl is true, no GL commands will be issued, + * but the request may fail. An effort is made to keep such failures to a minimum. + * + * The surface must be returned to the PixelStreamer by calling finish(), in order to deallocate + * GL resourecs. + */ + virtual Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl = false) = 0; + + /** + * Give back a drawing surface produced by request(), uploading the contents to the currently bound texture. + * The texture must be at least as big as the surface. + * + * If junk is true, then the surface will be junked instead, meaning nothing will be done with the contents, + * and its GL resources will simply be deallocated. + */ + virtual void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk = false) = 0; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/prefs.h b/src/ui/widget/canvas/prefs.h new file mode 100644 index 0000000..363fb6d --- /dev/null +++ b/src/ui/widget/canvas/prefs.h @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_UI_WIDGET_CANVAS_PREFS_H +#define INKSCAPE_UI_WIDGET_CANVAS_PREFS_H + +#include "preferences.h" + +namespace Inkscape::UI::Widget { + +class Prefs +{ +public: + Prefs() + { + devmode.action = [this] { set_devmode(devmode); }; + devmode.action(); + } + + // Main preferences + Pref<int> xray_radius = { "/options/rendering/xray-radius", 100, 1, 1500 }; + Pref<int> outline_overlay_opacity = { "/options/rendering/outline-overlay-opacity", 50, 0, 100 }; + Pref<int> update_strategy = { "/options/rendering/update_strategy", 3, 1, 3 }; + Pref<bool> request_opengl = { "/options/rendering/request_opengl" }; + Pref<int> grabsize = { "/options/grabsize/value", 3, 1, 15 }; + Pref<int> numthreads = { "/options/threading/numthreads", 0, 1, 256 }; + + // Colour management + Pref<bool> from_display = { "/options/displayprofile/from_display" }; + Pref<void> displayprofile = { "/options/displayprofile" }; + Pref<void> softproof = { "/options/softproof" }; + + // Auto-scrolling + Pref<int> autoscrolldistance = { "/options/autoscrolldistance/value", 0, -1000, 10000 }; + Pref<double> autoscrollspeed = { "/options/autoscrollspeed/value", 1.0, 0.0, 10.0 }; + + // Devmode preferences + Pref<int> tile_size = { "/options/rendering/tile_size", 300, 1, 10000 }; + Pref<int> render_time_limit = { "/options/rendering/render_time_limit", 80, 1, 5000 }; + Pref<bool> block_updates = { "/options/rendering/block_updates", true }; + Pref<int> pixelstreamer_method = { "/options/rendering/pixelstreamer_method", 1, 1, 4 }; + Pref<int> padding = { "/options/rendering/padding", 350, 0, 1000 }; + Pref<int> prerender = { "/options/rendering/prerender", 100, 0, 1000 }; + Pref<int> preempt = { "/options/rendering/preempt", 250, 0, 1000 }; + Pref<int> coarsener_min_size = { "/options/rendering/coarsener_min_size", 200, 0, 1000 }; + Pref<int> coarsener_glue_size = { "/options/rendering/coarsener_glue_size", 80, 0, 1000 }; + Pref<double> coarsener_min_fullness = { "/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0 }; + + // Debug switches + Pref<bool> debug_framecheck = { "/options/rendering/debug_framecheck" }; + Pref<bool> debug_logging = { "/options/rendering/debug_logging" }; + Pref<bool> debug_delay_redraw = { "/options/rendering/debug_delay_redraw" }; + Pref<int> debug_delay_redraw_time = { "/options/rendering/debug_delay_redraw_time", 50, 0, 1000000 }; + Pref<bool> debug_show_redraw = { "/options/rendering/debug_show_redraw" }; + Pref<bool> debug_show_unclean = { "/options/rendering/debug_show_unclean" }; // no longer implemented + Pref<bool> debug_show_snapshot = { "/options/rendering/debug_show_snapshot" }; + Pref<bool> debug_show_clean = { "/options/rendering/debug_show_clean" }; // no longer implemented + Pref<bool> debug_disable_redraw = { "/options/rendering/debug_disable_redraw" }; + Pref<bool> debug_sticky_decoupled = { "/options/rendering/debug_sticky_decoupled" }; + Pref<bool> debug_animate = { "/options/rendering/debug_animate" }; + +private: + // Developer mode + Pref<bool> devmode = { "/options/rendering/devmode" }; + + void set_devmode(bool on) + { + tile_size.set_enabled(on); + render_time_limit.set_enabled(on); + pixelstreamer_method.set_enabled(on); + padding.set_enabled(on); + prerender.set_enabled(on); + preempt.set_enabled(on); + coarsener_min_size.set_enabled(on); + coarsener_glue_size.set_enabled(on); + coarsener_min_fullness.set_enabled(on); + debug_framecheck.set_enabled(on); + debug_logging.set_enabled(on); + debug_delay_redraw.set_enabled(on); + debug_delay_redraw_time.set_enabled(on); + debug_show_redraw.set_enabled(on); + debug_show_unclean.set_enabled(on); + debug_show_snapshot.set_enabled(on); + debug_show_clean.set_enabled(on); + debug_disable_redraw.set_enabled(on); + debug_sticky_decoupled.set_enabled(on); + debug_animate.set_enabled(on); + } +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_CANVAS_PREFS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/stores.cpp b/src/ui/widget/canvas/stores.cpp new file mode 100644 index 0000000..70327f5 --- /dev/null +++ b/src/ui/widget/canvas/stores.cpp @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <array> +#include <cmath> +#include <2geom/transforms.h> +#include <2geom/parallelogram.h> +#include <2geom/point.h> +#include "helper/geom.h" +#include "ui/util.h" +#include "stores.h" +#include "prefs.h" +#include "fragment.h" +#include "graphics.h" + +namespace Inkscape { +namespace UI { +namespace Widget { +namespace { + +// Determine whether an affine transformation approximately maps the unit square [0, 1]^2 to itself. +bool preserves_unitsquare(Geom::Affine const &affine) +{ + return approx_dihedral(Geom::Translate(0.5, 0.5) * affine * Geom::Translate(-0.5, -0.5)); +} + +// Apply an affine transformation to a region, then return a strictly smaller region approximating it, made from chunks of size roughly d. +// To reduce computation, only the intersection of the result with bounds will be valid. +auto region_affine_approxinwards(Cairo::RefPtr<Cairo::Region> const ®, Geom::Affine const &affine, Geom::IntRect const &bounds, int d = 200) +{ + // Trivial empty case. + if (reg->empty()) return Cairo::Region::create(); + + // Trivial identity case. + if (affine.isIdentity(0.001)) return reg->copy(); + + // Fast-path for rectilinear transformations. + if (affine.withoutTranslation().isScale(0.001)) { + auto regdst = Cairo::Region::create(); + + auto transform = [&] (const Geom::IntPoint &p) { + return (Geom::Point(p) * affine).round(); + }; + + for (int i = 0; i < reg->get_num_rectangles(); i++) { + auto rect = cairo_to_geom(reg->get_rectangle(i)); + regdst->do_union(geom_to_cairo(Geom::IntRect(transform(rect.min()), transform(rect.max())))); + } + + return regdst; + } + + // General case. + auto ext = cairo_to_geom(reg->get_extents()); + auto rectdst = ((Geom::Parallelogram(ext) * affine).bounds().roundOutwards() & bounds).regularized(); + if (!rectdst) return Cairo::Region::create(); + auto rectsrc = (Geom::Parallelogram(*rectdst) * affine.inverse()).bounds().roundOutwards(); + + auto regdst = Cairo::Region::create(geom_to_cairo(*rectdst)); + auto regsrc = Cairo::Region::create(geom_to_cairo(rectsrc)); + regsrc->subtract(reg); + + double fx = min(absolute(Geom::Point(1.0, 0.0) * affine.withoutTranslation())); + double fy = min(absolute(Geom::Point(0.0, 1.0) * affine.withoutTranslation())); + + for (int i = 0; i < regsrc->get_num_rectangles(); i++) + { + auto rect = cairo_to_geom(regsrc->get_rectangle(i)); + int nx = std::ceil(rect.width() * fx / d); + int ny = std::ceil(rect.height() * fy / d); + auto pt = [&] (int x, int y) { + return rect.min() + (rect.dimensions() * Geom::IntPoint(x, y)) / Geom::IntPoint(nx, ny); + }; + for (int x = 0; x < nx; x++) { + for (int y = 0; y < ny; y++) { + auto r = Geom::IntRect(pt(x, y), pt(x + 1, y + 1)); + auto r2 = (Geom::Parallelogram(r) * affine).bounds().roundOutwards(); + regdst->subtract(geom_to_cairo(r2)); + } + } + } + + return regdst; +} + +} // namespace + +Geom::IntRect Stores::centered(Fragment const &view) const +{ + // Return the visible region of the view, plus the prerender and padding margins. + return expandedBy(view.rect, _prefs.prerender + _prefs.padding); +} + +void Stores::recreate_store(Fragment const &view) +{ + // Recreate the store at the view's affine. + _store.affine = view.affine; + _store.rect = centered(view); + _store.drawn = Cairo::Region::create(); + // Tell the graphics to create a blank new store. + _graphics->recreate_store(_store.rect.dimensions()); +} + +void Stores::shift_store(Fragment const &view) +{ + // Create a new fragment centred on the viewport. + auto rect = centered(view); + // Tell the graphics to copy the drawn part of the old store to the new store. + _graphics->shift_store(Fragment{ _store.affine, rect }); + // Set the shifted store as the new store. + _store.rect = rect; + // Clip the drawn region to the new store. + _store.drawn->intersect(geom_to_cairo(_store.rect)); +}; + +void Stores::take_snapshot(Fragment const &view) +{ + // Copy the store to the snapshot, leaving us temporarily in an invalid state. + _snapshot = std::move(_store); + // Tell the graphics to do the same, except swapping them so we can re-use the old snapshot store. + _graphics->swap_stores(); + // Reset the store. + recreate_store(view); + // Transform the snapshot's drawn region to the new store's affine. + _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, _snapshot.affine.inverse() * _store.affine, _store.rect), 4, -2); +} + +void Stores::snapshot_combine(Fragment const &view) +{ + // Add the drawn region to the snapshot drawn region (they both exist in store space, so this is valid), and save its affine. + _snapshot.drawn->do_union(_store.drawn); + auto old_store_affine = _store.affine; + + // Get the list of corner points in the store's drawn region and the snapshot bounds rect, all at the view's affine. + std::vector<Geom::Point> pts; + auto add_rect = [&, this] (Geom::Parallelogram const &pl) { + for (int i = 0; i < 4; i++) { + pts.emplace_back(Geom::Point(pl.corner(i))); + } + }; + auto add_store = [&, this] (Store const &s) { + int nrects = s.drawn->get_num_rectangles(); + auto affine = s.affine.inverse() * view.affine; + for (int i = 0; i < nrects; i++) { + add_rect(Geom::Parallelogram(cairo_to_geom(s.drawn->get_rectangle(i))) * affine); + } + }; + add_store(_store); + add_rect(Geom::Parallelogram(_snapshot.rect) * _snapshot.affine.inverse() * view.affine); + + // Compute their minimum-area bounding box as a fragment - an (affine, rect) pair. + auto [affine, rect] = min_bounding_box(pts); + affine = view.affine * affine; + + // Check if the paste transform takes the snapshot store exactly onto the new fragment, possibly with a dihedral transformation. + auto paste = Geom::Scale(_snapshot.rect.dimensions()) + * Geom::Translate(_snapshot.rect.min()) + * _snapshot.affine.inverse() + * affine + * Geom::Translate(-rect.min()) + * Geom::Scale(rect.dimensions()).inverse(); + if (preserves_unitsquare(paste)) { + // If so, simply take the new fragment to be exactly the same as the snapshot store. + rect = _snapshot.rect; + affine = _snapshot.affine; + } + + // Compute the scale difference between the backing store and the new fragment, giving the amount of detail that would be lost by pasting. + if ( double scale_ratio = std::sqrt(std::abs(_store.affine.det() / affine.det())); + scale_ratio > 4.0 ) + { + // Zoom the new fragment in to increase its quality. + double grow = scale_ratio / 2.0; + rect *= Geom::Scale(grow); + affine *= Geom::Scale(grow); + } + + // Do not allow the fragment to become more detailed than the window. + if ( double scale_ratio = std::sqrt(std::abs(affine.det() / view.affine.det())); + scale_ratio > 1.0 ) + { + // Zoom the new fragment out to reduce its quality. + double shrink = 1.0 / scale_ratio; + rect *= Geom::Scale(shrink); + affine *= Geom::Scale(shrink); + } + + // Find the bounding rect of the visible region + prerender margin within the new fragment. We do not want to discard this content in the next clipping step. + auto renderable = (Geom::Parallelogram(expandedBy(view.rect, _prefs.prerender)) * view.affine.inverse() * affine).bounds() & rect; + + // Cap the dimensions of the new fragment to slightly larger than the maximum dimension of the window by clipping it towards the screen centre. (Lower in Cairo mode since otherwise too slow to cope.) + double max_dimension = max(view.rect.dimensions()) * (_graphics->is_opengl() ? 1.7 : 0.8); + auto dimens = rect.dimensions(); + dimens.x() = std::min(dimens.x(), max_dimension); + dimens.y() = std::min(dimens.y(), max_dimension); + auto center = Geom::Rect(view.rect).midpoint() * view.affine.inverse() * affine; + center.x() = Util::safeclamp(center.x(), rect.left() + dimens.x() * 0.5, rect.right() - dimens.x() * 0.5); + center.y() = Util::safeclamp(center.y(), rect.top() + dimens.y() * 0.5, rect.bottom() - dimens.y() * 0.5); + rect = Geom::Rect(center - dimens * 0.5, center + dimens * 0.5); + + // Ensure the new fragment contains the renderable rect from earlier, enlarging it and reducing resolution if necessary. + if (!rect.contains(renderable)) { + auto oldrect = rect; + rect.unionWith(renderable); + double shrink = 1.0 / std::max(rect.width() / oldrect.width(), rect.height() / oldrect.height()); + rect *= Geom::Scale(shrink); + affine *= Geom::Scale(shrink); + } + + // Calculate the paste transform from the snapshot store to the new fragment (again). + paste = Geom::Scale(_snapshot.rect.dimensions()) + * Geom::Translate(_snapshot.rect.min()) + * _snapshot.affine.inverse() + * affine + * Geom::Translate(-rect.min()) + * Geom::Scale(rect.dimensions()).inverse(); + + if (_prefs.debug_logging) std::cout << "New fragment dimensions " << rect.width() << ' ' << rect.height() << std::endl; + + if (paste.isIdentity(0.001) && rect.dimensions().round() == _snapshot.rect.dimensions()) { + // Fast path: simply paste the backing store onto the snapshot store. + if (_prefs.debug_logging) std::cout << "Fast snapshot combine" << std::endl; + _graphics->fast_snapshot_combine(); + } else { + // General path: paste the snapshot store and then the backing store onto a new fragment, then set that as the snapshot store. + auto frag_rect = rect.roundOutwards(); + _graphics->snapshot_combine(Fragment{ affine, frag_rect }); + _snapshot.rect = frag_rect; + _snapshot.affine = affine; + } + + // Start drawing again on a new blank store aligned to the screen. + recreate_store(view); + // Transform the snapshot clean region to the new store. + // Todo: Should really clip this to the new snapshot rect, only we can't because it's generally not aligned with the store's affine. + _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, old_store_affine.inverse() * _store.affine, _store.rect), 4, -2); +}; + +void Stores::reset() +{ + _mode = Mode::None; + _store.drawn.clear(); + _snapshot.drawn.clear(); +} + +// Handle transitions and actions in response to viewport changes. +auto Stores::update(Fragment const &view) -> Action +{ + switch (_mode) { + + case Mode::None: { + // Not yet initialised or just reset - create store for first time. + recreate_store(view); + _mode = Mode::Normal; + if (_prefs.debug_logging) std::cout << "Full reset" << std::endl; + return Action::Recreated; + } + + case Mode::Normal: { + auto result = Action::None; + // Enter decoupled mode if the affine has changed from what the store was drawn at. + if (view.affine != _store.affine) { + // Snapshot and reset the store. + take_snapshot(view); + // Enter decoupled mode. + _mode = Mode::Decoupled; + if (_prefs.debug_logging) std::cout << "Enter decoupled mode" << std::endl; + result = Action::Recreated; + } else { + // Determine whether the view has moved sufficiently far that we need to shift the store. + if (!_store.rect.contains(expandedBy(view.rect, _prefs.prerender))) { + // The visible region + prerender margin has reached the edge of the store. + if (!(cairo_to_geom(_store.drawn->get_extents()) & expandedBy(view.rect, _prefs.prerender + _prefs.padding)).regularized()) { + // If the store contains no reusable content at all, recreate it. + recreate_store(view); + if (_prefs.debug_logging) std::cout << "Recreate store" << std::endl; + result = Action::Recreated; + } else { + // Otherwise shift it. + shift_store(view); + if (_prefs.debug_logging) std::cout << "Shift store" << std::endl; + result = Action::Shifted; + } + } + } + // After these operations, the store should now contain the visible region + prerender margin. + assert(_store.rect.contains(expandedBy(view.rect, _prefs.prerender))); + return result; + } + + case Mode::Decoupled: { + // Completely cancel the previous redraw and start again if the viewing parameters have changed too much. + auto check_restart_redraw = [&, this] { + // With this debug feature on, redraws should never be restarted. + if (_prefs.debug_sticky_decoupled) return false; + + // Restart if the store is no longer covering the middle 50% of the screen. (Usually triggered by rotating or zooming out.) + auto pl = Geom::Parallelogram(view.rect); + pl *= Geom::Translate(-pl.midpoint()) * Geom::Scale(0.5) * Geom::Translate(pl.midpoint()); + pl *= view.affine.inverse() * _store.affine; + if (!Geom::Parallelogram(_store.rect).contains(pl)) { + if (_prefs.debug_logging) std::cout << "Restart redraw (store not fully covering screen)" << std::endl; + return true; + } + + // Also restart if zoomed in or out too much. + auto scale_ratio = std::abs(view.affine.det() / _store.affine.det()); + if (scale_ratio > 3.0 || scale_ratio < 0.7) { + // Todo: Un-hard-code these thresholds. + // * The threshold 3.0 is for zooming in. It says that if the quality of what is being redrawn is more than 3x worse than that of the screen, restart. This is necessary to ensure acceptably high resolution is kept as you zoom in. + // * The threshold 0.7 is for zooming out. It says that if the quality of what is being redrawn is too high compared to the screen, restart. This prevents wasting time redrawing the screen slowly, at too high a quality that will probably not ever be seen. + if (_prefs.debug_logging) std::cout << "Restart redraw (zoomed changed too much)" << std::endl; + return true; + } + + // Don't restart. + return false; + }; + + if (check_restart_redraw()) { + // Re-use as much content as possible from the store and the snapshot, and set as the new snapshot. + snapshot_combine(view); + return Action::Recreated; + } + + return Action::None; + } + + default: { + assert(false); + return Action::None; + } + } +} + +auto Stores::finished_draw(Fragment const &view) -> Action +{ + // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to reset the store to the correct affine. + if (_mode == Mode::Decoupled) { + if (_prefs.debug_sticky_decoupled) { + // Debug feature: stop redrawing, but stay in decoupled mode. + } else if (_store.affine == view.affine) { + // Store is at the correct affine - exit decoupled mode. + if (_prefs.debug_logging) std::cout << "Exit decoupled mode" << std::endl; + // Exit decoupled mode. + _mode = Mode::Normal; + _graphics->invalidate_snapshot(); + } else { + // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine. + // Snapshot and reset the backing store. + take_snapshot(view); + if (_prefs.debug_logging) std::cout << "Remain in decoupled mode" << std::endl; + return Action::Recreated; + } + } + + return Action::None; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/stores.h b/src/ui/widget/canvas/stores.h new file mode 100644 index 0000000..70b10cc --- /dev/null +++ b/src/ui/widget/canvas/stores.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Abstraction of the store/snapshot mechanism. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UI_WIDGET_CANVAS_STORES_H +#define INKSCAPE_UI_WIDGET_CANVAS_STORES_H + +#include "fragment.h" +#include "util.h" + +namespace Inkscape { +namespace UI { +namespace Widget { +struct Fragment; +class Prefs; +class Graphics; + +class Stores +{ +public: + enum class Mode + { + None, /// Not initialised or just reset; no stores exist yet. + Normal, /// Normal mode consisting of just a backing store. + Decoupled /// Decoupled mode consisting of both a backing store and a snapshot store. + }; + + enum class Action + { + None, /// The backing store was not changed. + Recreated, /// The backing store was completely recreated. + Shifted /// The backing store was shifted into a new rectangle. + }; + + struct Store : Fragment + { + /** + * The region of space containing drawn content. + * For the snapshot, this region is transformed to store space and approximated inwards. + */ + Cairo::RefPtr<Cairo::Region> drawn; + }; + + /// Construct a blank object with no stores. + Stores(Prefs const &prefs) + : _mode(Mode::None) + , _graphics(nullptr) + , _prefs(prefs) {} + + /// Set the pointer to the graphics object. + void set_graphics(Graphics *g) { _graphics = g; } + + /// Discards all stores. (The actual operation on the graphics is performed on the next update().) + void reset(); + + /// Respond to a viewport change. (Requires a valid graphics.) + Action update(Fragment const &view); + + /// Respond to drawing of the backing store having finished. (Requires a valid graphics.) + Action finished_draw(Fragment const &view); + + /// Record a rectangle as being drawn to the store. + void mark_drawn(Geom::IntRect const &rect) { _store.drawn->do_union(geom_to_cairo(rect)); } + + // Getters. + Store const &store() const { return _store; } + Store const &snapshot() const { return _snapshot; } + Mode mode() const { return _mode; } + +private: + // Internal state. + Mode _mode; + Store _store, _snapshot; + + // The graphics object that executes the operations on the stores. + Graphics *_graphics; + + // The preferences object we read preferences from. + Prefs const &_prefs; + + // Internal actions. + Geom::IntRect centered(Fragment const &view) const; + void recreate_store(Fragment const &view); + void shift_store(Fragment const &view); + void take_snapshot(Fragment const &view); + void snapshot_combine(Fragment const &view); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_STORES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/synchronizer.cpp b/src/ui/widget/canvas/synchronizer.cpp new file mode 100644 index 0000000..331057b --- /dev/null +++ b/src/ui/widget/canvas/synchronizer.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "synchronizer.h" +#include <cassert> + +namespace Inkscape::UI::Widget { + +Synchronizer::Synchronizer() +{ + dispatcher.connect([this] { on_dispatcher(); }); +} + +void Synchronizer::signalExit() const +{ + auto lock = std::unique_lock(mutables); + awaken(); + assert(slots.empty()); + exitposted = true; +} + +void Synchronizer::runInMain(std::function<void()> const &f) const +{ + auto lock = std::unique_lock(mutables); + awaken(); + auto s = Slot{ &f }; + slots.emplace_back(&s); + assert(!exitposted); + slots_cond.wait(lock, [&] { return !s.func; }); +} + +void Synchronizer::waitForExit() const +{ + auto lock = std::unique_lock(mutables); + main_blocked = true; + while (true) { + if (!slots.empty()) { + process_slots(lock); + } else if (exitposted) { + exitposted = false; + break; + } + main_cond.wait(lock); + } + main_blocked = false; +} + +sigc::connection Synchronizer::connectExit(sigc::slot<void()> const &slot) +{ + return signal_exit.connect(slot); +} + +void Synchronizer::awaken() const +{ + if (exitposted || !slots.empty()) { + return; + } + + if (main_blocked) { + main_cond.notify_all(); + } else { + const_cast<Glib::Dispatcher&>(dispatcher).emit(); // Glib::Dispatcher is const-incorrect. + } +} + +void Synchronizer::on_dispatcher() const +{ + auto lock = std::unique_lock(mutables); + if (!slots.empty()) { + process_slots(lock); + } else if (exitposted) { + exitposted = false; + lock.unlock(); + signal_exit.emit(); + } +} + +void Synchronizer::process_slots(std::unique_lock<std::mutex> &lock) const +{ + while (!slots.empty()) { + auto slots_grabbed = std::move(slots); + lock.unlock(); + for (auto &s : slots_grabbed) { + (*s->func)(); + } + lock.lock(); + for (auto &s : slots_grabbed) { + s->func = nullptr; + } + slots_cond.notify_all(); + } +} + +} // namespace Inkscape::UI::Widget + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/synchronizer.h b/src/ui/widget/canvas/synchronizer.h new file mode 100644 index 0000000..45c88d2 --- /dev/null +++ b/src/ui/widget/canvas/synchronizer.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H +#define INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H + +#include <functional> +#include <vector> +#include <mutex> +#include <condition_variable> + +#include <sigc++/sigc++.h> +#include <glibmm/dispatcher.h> + +namespace Inkscape::UI::Widget { + +// Synchronisation primitive suiting the canvas's needs. All synchronisation between the main/render threads goes through here. +class Synchronizer +{ +public: + Synchronizer(); + + // Background side: + + // Indicate that the background process has exited, causing EITHER signal_exit to be emitted OR waitforexit() to unblock. + void signalExit() const; + + // Block until the given function has executed in the main thread, possibly waking it up if it is itself blocked. + // (Note: This is necessary for servicing occasional buffer mapping requests where one can't be pulled from a pool.) + void runInMain(std::function<void()> const &f) const; + + // Main-thread side: + + // Block until the background process has exited, gobbling the emission of signal_exit in the process. + void waitForExit() const; + + // Connect to signal_exit. + sigc::connection connectExit(sigc::slot<void()> const &slot); + +private: + struct Slot + { + std::function<void()> const *func; + }; + + Glib::Dispatcher dispatcher; // Used to wake up main thread if idle in GTK main loop. + sigc::signal<void()> signal_exit; + + mutable std::mutex mutables; + mutable bool exitposted = false; + mutable bool main_blocked = false; // Whether main thread is blocked in waitForExit(). + mutable std::condition_variable main_cond; // Used to wake up main thread if blocked. + mutable std::vector<Slot*> slots; // List of functions from runInMain() waiting to be run. + mutable std::condition_variable slots_cond; // Used to wake up render threads blocked in runInMain(). + + void awaken() const; + void on_dispatcher() const; + void process_slots(std::unique_lock<std::mutex> &lock) const; +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/texture.cpp b/src/ui/widget/canvas/texture.cpp new file mode 100644 index 0000000..420937a --- /dev/null +++ b/src/ui/widget/canvas/texture.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "texture.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +static bool have_gltexstorage() +{ + static bool result = [] { + return epoxy_gl_version() >= 42 || epoxy_has_gl_extension("GL_ARB_texture_storage"); + }(); + return result; +} + +static bool have_glinvalidateteximage() +{ + static bool result = [] { + return epoxy_gl_version() >= 43 || epoxy_has_gl_extension("ARB_invalidate_subdata"); + }(); + return result; +} + +Texture::Texture(Geom::IntPoint const &size) + : _size(size) +{ + glGenTextures(1, &_id); + glBindTexture(GL_TEXTURE_2D, _id); + + // Common flags for all textures used at the moment. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + if (have_gltexstorage()) { + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size.x(), size.y()); + } else { + // Note: This fallback path is always chosen on the Mac due to Apple's crippling of OpenGL. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.x(), size.y(), 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr); + } +} + +void Texture::invalidate() +{ + if (have_glinvalidateteximage()) { + glInvalidateTexImage(_id, 0); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/texture.h b/src/ui/widget/canvas/texture.h new file mode 100644 index 0000000..98aeba2 --- /dev/null +++ b/src/ui/widget/canvas/texture.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H +#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H + +#include <boost/noncopyable.hpp> +#include <2geom/point.h> +#include <epoxy/gl.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Texture +{ +public: + // Create null texture owning no resources. + Texture() = default; + + // Allocate a blank texture of a given size. The texture is bound to GL_TEXTURE_2D. + Texture(Geom::IntPoint const &size); + + // Wrap an existing texture. + Texture(GLuint id, Geom::IntPoint const &size) : _id(id), _size(size) {} + + // Boilerplate constructors/operators + Texture(Texture &&other) noexcept { _movefrom(other); } + Texture &operator=(Texture &&other) noexcept { _reset(); _movefrom(other); return *this; } + ~Texture() { _reset(); } + + // Observers + GLuint id() const { return _id; } + Geom::IntPoint const &size() const { return _size; } + explicit operator bool() const { return _id; } + + // Methods + void clear() { _reset(); _id = 0; } + void invalidate(); + +private: + GLuint _id = 0; + Geom::IntPoint _size; + + void _reset() noexcept { if (_id) glDeleteTextures(1, &_id); } + void _movefrom(Texture &other) noexcept { _id = other._id; _size = other._size; other._id = 0; } +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/texturecache.cpp b/src/ui/widget/canvas/texturecache.cpp new file mode 100644 index 0000000..6215849 --- /dev/null +++ b/src/ui/widget/canvas/texturecache.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <unordered_map> +#include <vector> +#include <cassert> +#include <boost/unordered_map.hpp> // For hash of pair +#include "helper/mathfns.h" +#include "texturecache.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +namespace { + +class BasicTextureCache : public TextureCache +{ + static int constexpr min_dimension = 16; + static int constexpr expiration_timeout = 10000; + + static int constexpr dim_to_ind(int dim) { return Util::floorlog2((dim - 1) / min_dimension) + 1; } + static int constexpr ind_to_maxdim(int index) { return min_dimension * (1 << index); } + + static std::pair<int, int> dims_to_inds(Geom::IntPoint const &dims) { return { dim_to_ind(dims.x()), dim_to_ind(dims.y()) }; } + static Geom::IntPoint inds_to_maxdims(std::pair<int, int> const &inds) { return { ind_to_maxdim(inds.first), ind_to_maxdim(inds.second) }; } + + // A cache of POT textures. + struct Bucket + { + std::vector<Texture> unused; + int used = 0; + int high_use_count = 0; + }; + boost::unordered_map<std::pair<int, int>, Bucket> buckets; + + // Used to periodicially discard excess cached textures. + int expiration_timer = 0; + +public: + Texture request(Geom::IntPoint const &dimensions) override + { + // Find the bucket that the dimensions fall into. + auto indexes = dims_to_inds(dimensions); + auto &b = buckets[indexes]; + + // Reuse or create a texture of the appropriate dimensions. + Texture tex; + if (!b.unused.empty()) { + tex = std::move(b.unused.back()); + b.unused.pop_back(); + glBindTexture(GL_TEXTURE_2D, tex.id()); + } else { + tex = Texture(inds_to_maxdims(indexes)); // binds + } + + // Record the new use count of the bucket. + b.used++; + if (b.used > b.high_use_count) { + // If the use count has gone above the high-water mark, record this, and reset the timer for when to clean up excess unused textures. + b.high_use_count = b.used; + expiration_timer = 0; + } + + return tex; + } + + void finish(Texture tex) override + { + auto indexes = dims_to_inds(tex.size()); + auto &b = buckets[indexes]; + + // Orphan the texture, if possible. + tex.invalidate(); + + // Put the texture back in its corresponding bucket's cache of unused textures. + b.unused.emplace_back(std::move(tex)); + b.used--; + + // If the expiration timeout has been reached, prune the cache of textures down to what was actually used in the last cycle. + expiration_timer++; + if (expiration_timer >= expiration_timeout) { + expiration_timer = 0; + + for (auto &[k, b] : buckets) { + int max_unused = b.high_use_count - b.used; + assert(max_unused >= 0); + if (b.unused.size() > max_unused) { + b.unused.resize(max_unused); + } + b.high_use_count = b.used; + } + } + } +}; + +} // namespace + +std::unique_ptr<TextureCache> TextureCache::create() +{ + return std::make_unique<BasicTextureCache>(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/texturecache.h b/src/ui/widget/canvas/texturecache.h new file mode 100644 index 0000000..ea78a67 --- /dev/null +++ b/src/ui/widget/canvas/texturecache.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Extremely basic gadget for re-using textures, since texture creation turns out to be quite expensive. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H +#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H + +#include <memory> +#include "texture.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class TextureCache +{ +public: + virtual ~TextureCache() = default; + + static std::unique_ptr<TextureCache> create(); + + /** + * Request a texture of at least the given dimensions. + * The texture is bound to GL_TEXTURE_2D. + */ + virtual Texture request(Geom::IntPoint const &dimensions) = 0; + + /** + * Return a no-longer used texture to the pool. + */ + virtual void finish(Texture tex) = 0; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/updaters.cpp b/src/ui/widget/canvas/updaters.cpp new file mode 100644 index 0000000..8441be0 --- /dev/null +++ b/src/ui/widget/canvas/updaters.cpp @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "updaters.h" +#include "ui/util.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ResponsiveUpdater : public Updater +{ +public: + Strategy get_strategy() const override { return Strategy::Responsive; } + + void reset() override { clean_region = Cairo::Region::create(); } + void intersect (Geom::IntRect const &rect) override { clean_region->intersect(geom_to_cairo(rect)); } + void mark_dirty(Geom::IntRect const &rect) override { clean_region->subtract(geom_to_cairo(rect)); } + void mark_dirty(Cairo::RefPtr<Cairo::Region> const ®) override { clean_region->subtract(reg); } + void mark_clean(Geom::IntRect const &rect) override { clean_region->do_union(geom_to_cairo(rect)); } + + Cairo::RefPtr<Cairo::Region> get_next_clean_region() override { return clean_region; } + bool report_finished () override { return false; } + void next_frame () override {} +}; + +class FullRedrawUpdater : public ResponsiveUpdater +{ + // Whether we are currently in the middle of a redraw. + bool inprogress = false; + + // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null. + Cairo::RefPtr<Cairo::Region> old_clean_region; + +public: + Strategy get_strategy() const override { return Strategy::FullRedraw; } + + void reset() override + { + ResponsiveUpdater::reset(); + inprogress = false; + old_clean_region.clear(); + } + + void intersect(const Geom::IntRect &rect) override + { + ResponsiveUpdater::intersect(rect); + if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect)); + } + + void mark_dirty(Geom::IntRect const &rect) override + { + if (inprogress && !old_clean_region) old_clean_region = clean_region->copy(); + ResponsiveUpdater::mark_dirty(rect); + } + + void mark_dirty(const Cairo::RefPtr<Cairo::Region> ®) override + { + if (inprogress && !old_clean_region) old_clean_region = clean_region->copy(); + ResponsiveUpdater::mark_dirty(reg); + } + + void mark_clean(const Geom::IntRect &rect) override + { + ResponsiveUpdater::mark_clean(rect); + if (old_clean_region) old_clean_region->do_union(geom_to_cairo(rect)); + } + + Cairo::RefPtr<Cairo::Region> get_next_clean_region() override + { + inprogress = true; + if (!old_clean_region) { + return clean_region; + } else { + return old_clean_region; + } + } + + bool report_finished() override + { + assert(inprogress); + if (!old_clean_region) { + // Completed redraw without being damaged => finished. + inprogress = false; + return false; + } else { + // Completed redraw but damage events arrived => ask for another redraw, using the up-to-date clean region. + old_clean_region.clear(); + return true; + } + } +}; + +class MultiscaleUpdater : public ResponsiveUpdater +{ + // Whether we are currently in the middle of a redraw. + bool inprogress = false; + + // Whether damage events occurred during the current redraw. + bool activated = false; + + int counter; // A steadily incrementing counter from which the current scale is derived. + int scale; // The current scale to process updates at. + int elapsed; // How much time has been spent at the current scale. + std::vector<Cairo::RefPtr<Cairo::Region>> blocked; // The region blocked from being updated at each scale. + +public: + Strategy get_strategy() const override { return Strategy::Multiscale; } + + void reset() override + { + ResponsiveUpdater::reset(); + inprogress = activated = false; + } + + void intersect(const Geom::IntRect &rect) override + { + ResponsiveUpdater::intersect(rect); + if (activated) { + for (auto ® : blocked) { + reg->intersect(geom_to_cairo(rect)); + } + } + } + + void mark_dirty(Geom::IntRect const &rect) override + { + ResponsiveUpdater::mark_dirty(rect); + post_mark_dirty(); + } + + void mark_dirty(const Cairo::RefPtr<Cairo::Region> ®) override + { + ResponsiveUpdater::mark_dirty(reg); + post_mark_dirty(); + } + + void post_mark_dirty() + { + if (inprogress && !activated) { + counter = scale = elapsed = 0; + blocked = { Cairo::Region::create() }; + activated = true; + } + } + + void mark_clean(const Geom::IntRect &rect) override + { + ResponsiveUpdater::mark_clean(rect); + if (activated) blocked[scale]->do_union(geom_to_cairo(rect)); + } + + Cairo::RefPtr<Cairo::Region> get_next_clean_region() override + { + inprogress = true; + if (!activated) { + return clean_region; + } else { + auto result = clean_region->copy(); + result->do_union(blocked[scale]); + return result; + } + } + + bool report_finished() override + { + assert(inprogress); + if (!activated) { + // Completed redraw without damage => finished. + inprogress = false; + return false; + } else { + // Completed redraw but damage events arrived => begin updating any remaining damaged regions. + activated = false; + blocked.clear(); + return true; + } + } + + void next_frame() override + { + if (!activated) return; + + // Stay at the current scale for 2^scale frames. + elapsed++; + if (elapsed < (1 << scale)) return; + elapsed = 0; + + // Adjust the counter, which causes scale to hop around the values 0, 1, 2... spending half as much time at each subsequent scale. + counter++; + scale = 0; + for (int tmp = counter; tmp % 2 == 1; tmp /= 2) { + scale++; + } + + // Ensure sufficiently many blocked zones exist. + if (scale == blocked.size()) { + blocked.emplace_back(); + } + + // Recreate the current blocked zone as the union of the clean region and lower-scale blocked zones. + blocked[scale] = clean_region->copy(); + for (int i = 0; i < scale; i++) { + blocked[scale]->do_union(blocked[i]); + } + } +}; + +template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Responsive>() {return std::make_unique<ResponsiveUpdater>();} +template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::FullRedraw>() {return std::make_unique<FullRedrawUpdater>();} +template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Multiscale>() {return std::make_unique<MultiscaleUpdater>();} + +std::unique_ptr<Updater> Updater::create(Strategy strategy) +{ + switch (strategy) + { + case Strategy::Responsive: return create<Strategy::Responsive>(); + case Strategy::FullRedraw: return create<Strategy::FullRedraw>(); + case Strategy::Multiscale: return create<Strategy::Multiscale>(); + default: return nullptr; // Never triggered, but GCC errors out on build without. + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/updaters.h b/src/ui/widget/canvas/updaters.h new file mode 100644 index 0000000..d36685a --- /dev/null +++ b/src/ui/widget/canvas/updaters.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Controls the order to update invalidated regions. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H +#define INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H + +#include <vector> +#include <memory> +#include <2geom/int-rect.h> +#include <cairomm/refptr.h> +#include <cairomm/region.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +// A class for tracking invalidation events and producing redraw regions. +class Updater +{ +public: + virtual ~Updater() = default; + + // The subregion of the store with up-to-date content. + Cairo::RefPtr<Cairo::Region> clean_region; + + enum class Strategy + { + Responsive, // As soon as a region is invalidated, redraw it. + FullRedraw, // When a region is invalidated, delay redraw until after the current redraw is completed. + Multiscale, // Updates tiles near the mouse faster. Gives the best of both. + }; + + // Create an Updater using the given strategy. + template <Strategy strategy> + static std::unique_ptr<Updater> create(); + + // Create an Updater using a choice of strategy specified at runtime. + static std::unique_ptr<Updater> create(Strategy strategy); + + // Return the strategy in use. + virtual Strategy get_strategy() const = 0; + + virtual void reset() = 0; // Reset the clean region to empty. + virtual void intersect (Geom::IntRect const &) = 0; // Called when the store changes position; clip everything to the new store rectangle. + virtual void mark_dirty(Geom::IntRect const &) = 0; // Called on every invalidate event. + virtual void mark_dirty(Cairo::RefPtr<Cairo::Region> const &) = 0; // Called on every invalidate event. + virtual void mark_clean(Geom::IntRect const &) = 0; // Called on every rectangle redrawn. + + // Called at the start of a redraw to determine what region to consider clean (i.e. will not be drawn). + virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() = 0; + + // Called after a redraw has finished. Returns true to indicate that further redraws are required with different clean regions. + virtual bool report_finished() = 0; + + // Called at the start of each frame. Some updaters (Multiscale) require this information. + virtual void next_frame() = 0; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/util.cpp b/src/ui/widget/canvas/util.cpp new file mode 100644 index 0000000..3d9d59b --- /dev/null +++ b/src/ui/widget/canvas/util.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "ui/util.h" +#include "helper/geom.h" +#include "util.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const ®) +{ + for (int i = 0; i < reg->get_num_rectangles(); i++) { + auto rect = reg->get_rectangle(i); + cr->rectangle(rect.x, rect.y, rect.width, rect.height); + } +} + +Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const ®, int d, int t) +{ + // Find the bounding rect, expanded by 1 in all directions. + auto rect = geom_to_cairo(expandedBy(cairo_to_geom(reg->get_extents()), 1)); + + // Take the complement of the region within the rect. + auto reg2 = Cairo::Region::create(rect); + reg2->subtract(reg); + + // Increase the width and height of every rectangle by d. + auto reg3 = Cairo::Region::create(); + for (int i = 0; i < reg2->get_num_rectangles(); i++) { + auto rect = reg2->get_rectangle(i); + rect.x += t; + rect.y += t; + rect.width += d; + rect.height += d; + reg3->do_union(rect); + } + + // Take the complement of the region within the rect. + reg2 = Cairo::Region::create(rect); + reg2->subtract(reg3); + + return reg2; +} + +std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount) +{ + std::array<float, 3> hsl; + SPColor::rgb_to_hsl_floatv(&hsl[0], rgb[0], rgb[1], rgb[2]); + hsl[2] += (hsl[2] < 0.08 ? 0.08 : -0.08) * amount; + + std::array<float, 3> rgb2; + SPColor::hsl_to_rgb_floatv(&rgb2[0], hsl[0], hsl[1], hsl[2]); + + return rgb2; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/canvas/util.h b/src/ui/widget/canvas/util.h new file mode 100644 index 0000000..c2c1ad3 --- /dev/null +++ b/src/ui/widget/canvas/util.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_UI_WIDGET_CANVAS_UTIL_H +#define INKSCAPE_UI_WIDGET_CANVAS_UTIL_H + +#include <array> +#include <2geom/int-rect.h> +#include <2geom/affine.h> +#include <cairomm/cairomm.h> +#include "color.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +// Cairo additions + +/** + * Turn a Cairo region into a path on a given Cairo context. + */ +void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const ®); + +/** + * Shrink a region by d/2 in all directions, while also translating it by (d/2 + t, d/2 + t). + */ +Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const ®, int d, int t = 0); + +inline auto unioned(Cairo::RefPtr<Cairo::Region> a, Cairo::RefPtr<Cairo::Region> const &b) +{ + a->do_union(b); + return a; +} + +// Colour operations + +inline auto rgb_to_array(uint32_t rgb) +{ + return std::array{SP_RGBA32_R_U(rgb) / 255.0f, SP_RGBA32_G_U(rgb) / 255.0f, SP_RGBA32_B_U(rgb) / 255.0f}; +} + +inline auto rgba_to_array(uint32_t rgba) +{ + return std::array{SP_RGBA32_R_U(rgba) / 255.0f, SP_RGBA32_G_U(rgba) / 255.0f, SP_RGBA32_B_U(rgba) / 255.0f, SP_RGBA32_A_U(rgba) / 255.0f}; +} + +inline auto premultiplied(std::array<float, 4> arr) +{ + arr[0] *= arr[3]; + arr[1] *= arr[3]; + arr[2] *= arr[3]; + return arr; +} + +std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount = 1.0f); + +inline auto checkerboard_darken(uint32_t rgba) +{ + return checkerboard_darken(rgb_to_array(rgba), 1.0f - SP_RGBA32_A_U(rgba) / 255.0f); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_UTIL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : |