Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
cursor-utils.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Cursor utilities
4 *
5 * Copyright (C) 2020 Tavmjong Bah
6 *
7 * The contents of this file may be used under the GNU General Public License Version 2 or later.
8 *
9 */
10
11#include "cursor-utils.h"
12
13#include <algorithm>
14#include <cmath>
15#include <memory>
16#include <tuple>
17#include <unordered_map>
18#include <utility>
19#include <boost/compute/detail/lru_cache.hpp>
20#include <glibmm/miscutils.h>
21#include <giomm/file.h>
22#include <gdkmm/cursor.h>
23#include <gdkmm/pixbuf.h>
24#include <gtkmm/icontheme.h>
25#include <gtkmm/settings.h>
26#include <gtkmm/version.h>
27#include <gtkmm/widget.h>
28
29#include "document.h"
30#include "document-update.h"
31#include "preferences.h"
32#include "display/cairo-utils.h"
33#include "helper/pixbuf-ops.h"
34#include "io/file.h"
35#include "io/resource.h"
37#include "object/sp-object.h"
38#include "object/sp-root.h"
39#include "ui/util.h"
40#include "util/statics.h"
41#include "util/units.h"
42
45
46namespace Inkscape {
47namespace {
48
49// SVG cursor unique ID/key
50using Key = std::tuple<std::string, std::string, std::string, std::uint32_t, std::uint32_t, bool, double>;
51
52struct CursorDocCache : public Util::EnableSingleton<CursorDocCache, Util::Depends<FontFactory>> {
53 std::unordered_map<std::string, std::unique_ptr<SPDocument>> map;
54};
55
56struct CursorInputParams
57{
58 Glib::RefPtr<Gtk::IconTheme> icon_theme;
59 std::string file_name;
60 Colors::Color fill;
61 Colors::Color stroke;
62};
63
64struct CursorRenderResult
65{
66 Glib::RefPtr<Gdk::Texture> texture;
69};
70
76CursorRenderResult render_svg_cursor(double scale, CursorInputParams const &in)
77{
78 // GTK puts cursors in a "cursors" subdirectory of icon themes. We'll do the same... but
79 // note that we cannot use the normal GTK method for loading cursors as GTK knows nothing
80 // about scalable SVG cursors. We must locate and load the files ourselves. (Even if
81 // GTK could handle scalable cursors, we would need to load the files ourselves inorder
82 // to modify CSS 'fill' and 'stroke' properties.)
83 auto fill = in.fill;
84 auto stroke = in.stroke;
85
86 // Make list of icon themes, highest priority first.
87 std::vector<std::string> theme_names;
88
89 // Set in preferences
91 Glib::ustring theme_name = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
92 if (!theme_name.empty()) {
93 theme_names.push_back(std::move(theme_name));
94 }
95
96 // System
97 theme_name = Gtk::Settings::get_default()->property_gtk_icon_theme_name();
98 theme_names.push_back(std::move(theme_name));
99
100 // Our default
101 theme_names.emplace_back("hicolor");
102
103 // quantize opacity to limit number of cursor variations we generate
104 fill.setOpacity(std::floor(std::clamp(fill.getOpacity(), 0.0, 1.0) * 100) / 100);
105 stroke.setOpacity(std::floor(std::clamp(stroke.getOpacity(), 0.0, 1.0) * 100) / 100);
106
107 const auto enable_drop_shadow = prefs->getBool("/options/cursor-drop-shadow", true);
108
109 // Find the rendered size of the icon.
110 // cursor scaling? note: true by default - this has to be in sync with inkscape-preferences where it is true
111 bool cursor_scaling = prefs->getBool("/options/cursorscaling", true); // Fractional scaling is broken but we can't detect it.
112 if (!cursor_scaling) {
113 scale = 1;
114 }
115
116 constexpr int max_cached_cursors = 100;
117 static boost::compute::detail::lru_cache<Key, CursorRenderResult> cursor_cache(max_cached_cursors);
118
119 // construct a key
120 Key cursor_key = {theme_names[0], theme_names[1], in.file_name,
122 enable_drop_shadow, scale};
123 if (auto const it = cursor_cache.get(cursor_key)) {
124 return *it;
125 }
126
127 // Find theme paths.
128 auto theme_paths = in.icon_theme->get_search_path();
129
130 // cache cursor SVG documents too, so we can regenerate cursors (with different colors) quickly
131 auto &cursor_docs = CursorDocCache::get().map;
132 SPRoot* root = nullptr;
133
134 if (auto it = cursor_docs.find(in.file_name); it != end(cursor_docs)) {
135 root = it->second->getRoot();
136 }
137
138 if (!root) {
139 // Loop over theme names and paths, looking for file.
140 Glib::RefPtr<Gio::File> file;
141 std::string full_file_path;
142 bool file_exists = false;
143 for (auto const &theme_name : theme_names) {
144 for (auto const &theme_path : theme_paths) {
145 full_file_path = Glib::build_filename(theme_path, theme_name, "cursors", in.file_name);
146 // std::cout << "Checking: " << full_file_path << std::endl;
147 file = Gio::File::create_for_path(full_file_path);
148 file_exists = file->query_exists();
149 if (file_exists) break;
150 }
151 if (file_exists) break;
152 }
153
154 if (!file->query_exists()) {
155 std::cerr << "load_svg_cursor: Cannot locate cursor file: " << in.file_name << std::endl;
156 return {};
157 }
158
159 auto document = ink_file_open(file).first;
160
161 if (!document) {
162 std::cerr << "load_svg_cursor: Could not open document: " << full_file_path << std::endl;
163 return {};
164 }
165
166 root = document->getRoot();
167 if (!root) {
168 std::cerr << "load_svg_cursor: Could not find SVG element: " << full_file_path << std::endl;
169 return {};
170 }
171
172 cursor_docs[in.file_name] = std::move(document);
173 }
174
175 if (!root) {
176 return {};
177 }
178
179 // Set the CSS 'fill' and 'stroke' properties on the SVG element (for cascading).
180 SPCSSAttr *css = sp_repr_css_attr(root->getRepr(), "style");
185 root->changeCSS(css, "style");
187
188 if (!enable_drop_shadow) {
189 // turn off drop shadow, if any
190 Glib::ustring shadow("drop-shadow");
191 auto objects = root->document->getObjectsByClass(shadow);
192 for (auto&& el : objects) {
193 if (auto val = el->getAttribute("class")) {
194 Glib::ustring cls = val;
195 auto pos = cls.find(shadow);
196 if (pos != Glib::ustring::npos) {
197 cls.erase(pos, shadow.length());
198 }
199 el->setAttribute("class", cls);
200 }
201 }
202 }
203
204 // Some cursors are un-versioned, so always attempt to adjust legacy files.
206
207 int w = root->document->getWidth().value("px");
208 int h = root->document->getHeight().value("px");
209
210 Geom::Rect area(0, 0, w, h);
211 int dpi = Inkscape::Util::Quantity::convert(scale, "in", "px");
212
213 // render document into internal bitmap; returns null on failure
214 auto ink_pixbuf = std::unique_ptr<Inkscape::Pixbuf>(sp_generate_internal_bitmap(root->document, area, dpi));
215 if (!ink_pixbuf) {
216 std::cerr << "load_svg_cursor: failed to create pixbuf for: " << in.file_name << std::endl;
217 return {};
218 }
219
220 // Calculate the hotspot
221 auto const root_pos = Geom::Point(-root->root_x.computed, -root->root_y.computed);
222 auto const hotspot = (area.clamp(root_pos) * scale).round();
223
224 auto cursor = CursorRenderResult{
225 .texture = to_texture(ink_pixbuf->getSurface()),
226 .size = {w, h},
227 .hotspot = hotspot
228 };
229
230 cursor_cache.insert(std::move(cursor_key), cursor);
231
232 return cursor;
233}
234
235} // namespace
236
237Glib::RefPtr<Gdk::Cursor>
238load_svg_cursor(Gtk::Widget &widget,
239 std::string const &file_name,
240 std::optional<Colors::Color> maybe_fill,
241 std::optional<Colors::Color> maybe_stroke)
242{
243 auto params = CursorInputParams{
244 .icon_theme = Gtk::IconTheme::get_for_display(widget.get_display()),
245 .file_name = file_name,
246 .fill = maybe_fill.value_or(Colors::Color(0xffffffff)),
247 .stroke = maybe_stroke.value_or(Colors::Color(0x000000ff))
248 };
249
250#if GTKMM_CHECK_VERSION(4, 16, 0) // Use new scaling cursor API if possible.
251 return Gdk::Cursor::create_from_slot([params] (int, double scale, int &width, int &height, int &hotspot_x, int &hotspot_y) {
252 auto res = render_svg_cursor(scale, params);
253 width = res.size.x();
254 height = res.size.y();
255 hotspot_x = res.hotspot.x();
256 hotspot_y = res.hotspot.y();
257 return std::move(res.texture);
258 });
259#else
260 auto res = render_svg_cursor(widget.get_scale_factor(), params);
261
262 if (!res.texture) {
263 return {};
264 }
265
266 return Gdk::Cursor::create(std::move(res.texture), res.hotspot.x(), res.hotspot.y());
267#endif
268}
269
274void
275set_svg_cursor(Gtk::Widget &widget,
276 std::string const &file_name,
277 std::optional<Colors::Color> fill,
278 std::optional<Colors::Color> stroke)
279{
280 auto cursor = load_svg_cursor(widget, file_name, fill, stroke);
281 widget.set_cursor(std::move(cursor));
282}
283
284} // namespace Inkscape
285
286/*
287 Local Variables:
288 mode:c++
289 c-file-style:"stroustrup"
290 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
291 indent-tabs-mode:nil
292 fill-column:99
293 End:
294*/
295// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
double scale
Definition aa.cpp:228
Cairo integration helpers.
Two-dimensional point with integer coordinates.
Definition int-point.h:57
Two-dimensional point that doubles as a vector.
Definition point.h:66
Axis aligned, non-empty rectangle.
Definition rect.h:92
std::string toString(bool opacity=true) const
Format the color as a css string and return it.
Definition color.cpp:106
uint32_t toRGBA(double opacity=1.0) const
Return an sRGB conversion of the color in RGBA int32 format.
Definition color.cpp:117
bool setOpacity(double opacity)
Set the opacity of this color object.
Definition color.cpp:444
double getOpacity() const
Get the opacity in this color, if it's stored.
Definition color.cpp:407
Preference storage class.
Definition preferences.h:61
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
Glib::ustring getString(Glib::ustring const &pref_path, Glib::ustring const &def="")
Retrieve an UTF-8 string.
static Preferences * get()
Access the singleton Preferences object.
static double convert(double from_dist, Unit const *from, Unit const *to)
Convert distances.
Definition units.cpp:588
<svg> element
Definition sp-root.h:31
const double w
Definition conic-4.cpp:19
RootCluster root
std::shared_ptr< Css const > css
std::unordered_map< std::string, std::unique_ptr< SPDocument > > map
Geom::IntPoint size
Geom::IntPoint hotspot
std::string file_name
Glib::RefPtr< Gtk::IconTheme > icon_theme
Colors::Color fill
Colors::Color stroke
Glib::RefPtr< Gdk::Texture > texture
void sp_file_fix_hotspot(SPRoot *o)
TODO: insert short description here.
std::unique_ptr< SPDocument > ink_file_open(std::span< char const > buffer)
Open a document from memory.
Definition file.cpp:60
Geom::Point end
bool file_exists(const std::string &filepath)
Helper class to stream background task notifications as a series of messages.
Glib::RefPtr< Gdk::Cursor > load_svg_cursor(Gtk::Widget &widget, std::string const &file_name, std::optional< Colors::Color > maybe_fill, std::optional< Colors::Color > maybe_stroke)
void set_svg_cursor(Gtk::Widget &widget, std::string const &file_name, std::optional< Colors::Color > fill, std::optional< Colors::Color > stroke)
Loads an SVG cursor from the specified file name, and sets it as the cursor of the given widget.
Inkscape::Pixbuf * sp_generate_internal_bitmap(SPDocument *document, Geom::Rect const &area, double dpi, std::vector< SPItem const * > items, bool opaque, uint32_t const *checkerboard_color, double device_scale, std::optional< Antialiasing > antialias)
Generates a bitmap from given items.
Singleton class to access the preferences file in a convenient way.
void sp_repr_css_set_property_double(SPCSSAttr *css, gchar const *name, double value)
Set a style property to a new float value (e.g.
Definition repr-css.cpp:223
void sp_repr_css_attr_unref(SPCSSAttr *css)
Unreferences an SPCSSAttr (will be garbage collected if no references remain).
Definition repr-css.cpp:76
SPCSSAttr * sp_repr_css_attr(Node const *repr, gchar const *attr)
Creates a new SPCSSAttr with one attribute (i.e.
Definition repr-css.cpp:88
void sp_repr_css_set_property_string(SPCSSAttr *css, char const *name, std::string const &value)
Set a style property to a standard string.
Definition repr-css.cpp:234
Inkscape::IO::Resource - simple resource API.
SPRoot: SVG <svg> implementation.
Static objects with destruction before main() exit.
double height
double width
Glib::RefPtr< Gdk::Texture > to_texture(Cairo::RefPtr< Cairo::Surface > const &surface)
Convert an image surface in ARGB32 format to a texture.
Definition util.cpp:495