Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
gradient-with-stops.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
5/*
6 * Author:
7 * Michael Kowalski
8 *
9 * Copyright (C) 2020-2021 Michael Kowalski
10 *
11 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12 */
13
14#include "gradient-with-stops.h"
15
16#include <cmath>
17#include <string>
18#include <gdkmm/cursor.h>
19#include <gdkmm/general.h>
20#include <gtkmm/drawingarea.h>
21#include <gtkmm/eventcontrollerkey.h>
22#include <gtkmm/eventcontrollermotion.h>
23#include <gtkmm/gestureclick.h>
24#include <gtkmm/window.h>
25#include <sigc++/functors/mem_fun.h>
26
27#include "display/cairo-utils.h"
28#include "io/resource.h"
29#include "object/sp-gradient.h"
30#include "object/sp-stop.h"
31#include "ui/controller.h"
32#include "ui/cursor-utils.h"
33#include "ui/util.h"
36
37// c.f. share/ui/style.css
38// gradient's image height (multiple of checkerboard tiles, they are 6x6)
39constexpr static int GRADIENT_IMAGE_HEIGHT = 3 * 6;
40
41namespace Inkscape::UI::Widget {
42
43using std::round;
44using namespace Inkscape::IO;
45
46std::string get_stop_template_path(const char* filename) {
47 // "stop handle" template files path
48 return Resource::get_filename(Resource::UIS, filename);
49}
50
52 Glib::ObjectBase{"GradientWithStops"},
54 Gtk::DrawingArea{},
55 _template(get_stop_template_path("gradient-stop.svg").c_str()),
56 _tip_template(get_stop_template_path("gradient-tip.svg").c_str())
57{
58 // default color, it will be updated
59 _background_color.set_grey(0.5);
60
61 // for theming
62 set_name("GradientEdit");
63
64 set_draw_func(sigc::mem_fun(*this, &GradientWithStops::draw_func));
65
66 auto const click = Gtk::GestureClick::create();
67 click->set_button(1); // left
68 click->signal_pressed().connect(sigc::mem_fun(*this, &GradientWithStops::on_click_pressed));
69 click->signal_released().connect(sigc::mem_fun(*this, &GradientWithStops::on_click_released));
70 add_controller(click);
71
72 auto const motion = Gtk::EventControllerMotion::create();
73 motion->signal_motion().connect(sigc::mem_fun(*this, &GradientWithStops::on_motion));
74 add_controller(motion);
75
76 auto const key = Gtk::EventControllerKey::create();
77 key->signal_key_pressed().connect(sigc::mem_fun(*this, &GradientWithStops::on_key_pressed), true);
78 add_controller(key);
79
80 set_focusable(true);
81}
82
84
86 _gradient = gradient;
87
88 // listen to release & changes
89 _release = gradient ? gradient->connectRelease([this](SPObject*){ set_gradient(nullptr); }) : sigc::connection();
90 _modified = gradient ? gradient->connectModified([this](SPObject*, guint){ modified(); }) : sigc::connection();
91
92 // TODO: check selected/focused stop index
93
94 modified();
95
96 set_sensitive(gradient != nullptr);
97}
98
100 // gradient has been modified
101
102 // read all stops
103 _stops.clear();
104
105 if (_gradient) {
106 SPStop* stop = _gradient->getFirstStop();
107 while (stop) {
108 _stops.push_back(stop_t {
109 .offset = stop->offset, .color = stop->getColor()
110 });
111 stop = stop->getNextStop();
112 }
113 }
114
115 update();
116}
117
119 queue_draw();
120}
121
122// capture background color when styles change
123void GradientWithStops::css_changed(GtkCssStyleChange * /*change*/)
124{
125 if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_root())) {
126 _background_color = get_color_with_class(*wnd, "theme_bg_color");
127 }
128
129 // load and cache cursors
130 if (!_cursor_mouseover) {
131 _cursor_mouseover = Gdk::Cursor::create(Glib::ustring("grab"));
132 _cursor_dragging = Gdk::Cursor::create(Glib::ustring("grabbing"));
133 _cursor_insert = Gdk::Cursor::create(Glib::ustring("crosshair"));
134 set_stop_cursor(nullptr);
135 }
136}
137
138// return on-screen position of the UI stop corresponding to the gradient's color stop at 'index'
140 if (!_gradient || index >= _stops.size()) {
141 return stop_pos_t {};
142 }
143
144 // half of the stop template width; round it to avoid half-pixel coordinates
145 const auto dx = round((_template.get_width_px() + 1) / 2);
146
147 auto pos = [&](double offset) { return round(layout.x + layout.width * CLAMP(offset, 0, 1)); };
148 const auto& v = _stops;
149
150 auto offset = pos(v[index].offset);
151 auto left = offset - dx;
152 if (index > 0) {
153 // check previous stop; it may overlap
154 auto prev = pos(v[index - 1].offset) + dx;
155 if (prev > left) {
156 // overlap
157 left = round((left + prev) / 2);
158 }
159 }
160
161 auto right = offset + dx;
162 if (index + 1 < v.size()) {
163 // check next stop for overlap
164 auto next = pos(v[index + 1].offset) - dx;
165 if (right > next) {
166 // overlap
167 right = round((right + next) / 2);
168 }
169 }
170
171 return stop_pos_t {
172 .left = left,
173 .tip = offset,
174 .right = right,
175 .top = layout.height - _template.get_height_px(),
176 .bottom = layout.height
177 };
178}
179
180// widget's layout; mainly location of the gradient's image and stop handles
182 const auto stop_width = _template.get_width_px();
183 const auto half_stop = round((stop_width + 1) / 2);
184 const auto x = half_stop;
185 const double width = get_width() - stop_width;
186 const double height = get_height();
187
188 return layout_t {
189 .x = x,
190 .y = 0,
191 .width = width,
192 .height = height
193 };
194}
195
196// check if stop handle is under (x, y) location, return its index or -1 if not hit
197int GradientWithStops::find_stop_at(double x, double y) const {
198 if (!_gradient) return -1;
199
200 const auto& v = _stops;
201 const auto& layout = get_layout();
202
203 // find stop handle at (x, y) position; note: stops may not be ordered by offsets
204 for (size_t i = 0; i < v.size(); ++i) {
205 auto pos = get_stop_position(i, layout);
206 if (x >= pos.left && x <= pos.right && y >= pos.top && y <= pos.bottom) {
207 return static_cast<int>(i);
208 }
209 }
210
211 return -1;
212}
213
214// this is range of offset adjustment for a given stop
216 if (!_gradient) return limits_t {};
217
218 // let negative index turn into a large out-of-range number
219 auto index = static_cast<size_t>(maybe_index);
220
221 const auto& v = _stops;
222
223 if (index < v.size()) {
224 double min = 0;
225 double max = 1;
226
227 if (v.size() > 1) {
228 std::vector<double> offsets;
229 offsets.reserve(v.size());
230 for (auto& s : _stops) {
231 offsets.push_back(s.offset);
232 }
233 std::sort(offsets.begin(), offsets.end());
234
235 // special cases:
236 if (index == 0) { // first stop
237 max = offsets[index + 1];
238 }
239 else if (index + 1 == v.size()) { // last stop
240 min = offsets[index - 1];
241 }
242 else {
243 // stops "inside" gradient
244 min = offsets[index - 1];
245 max = offsets[index + 1];
246 }
247 }
248 return limits_t { .min_offset = min, .max_offset = max, .offset = v[index].offset };
249 }
250 else {
251 return limits_t {};
252 }
253}
254
255std::optional<bool> GradientWithStops::focus(Gtk::DirectionType const direction)
256{
257 // On arrow key, let ::key-pressed move focused stop (horz) / nothing (vert)
258 if (!(direction == Gtk::DirectionType::TAB_FORWARD || direction == Gtk::DirectionType::TAB_BACKWARD)) {
259 return true;
260 }
261
262 auto const backward = direction == Gtk::DirectionType::TAB_BACKWARD;
263 auto const n_stops = _stops.size();
264
265 if (has_focus()) {
266 auto const new_stop = _focused_stop + (backward ? -1 : +1);
267 // out of range: keep _focused_stop, but give up focus on widget overall
268 if (!(new_stop >= 0 && new_stop < n_stops)) {
269 return false; // let focus go
270 }
271 // in range: next/prev stop
272 set_focused_stop(new_stop);
273 } else {
274 // didnʼt have focus: grab on 1st or last stop, relevant to direction
275 grab_focus();
276 if (n_stops > 0) { // …unless we have no stop, then just focus widget
277 set_focused_stop(backward ? n_stops - 1 : 0);
278 }
279 }
280
281 return true;
282}
283
284bool GradientWithStops::on_key_pressed(unsigned keyval, unsigned /*keycode*/, Gdk::ModifierType state)
285{
286 // currently all keyboard activity involves acting on focused stop handle; bail if nothing's selected
287 if (_focused_stop < 0) return false;
288
290 if (Controller::has_flag(state, Gdk::ModifierType::SHIFT_MASK)) {
291 delta *= 10;
292 }
293
294 switch (keyval) {
295 case GDK_KEY_Left:
296 case GDK_KEY_KP_Left:
298 return true;
299
300 case GDK_KEY_Right:
301 case GDK_KEY_KP_Right:
303 return true;
304
305 case GDK_KEY_BackSpace:
306 case GDK_KEY_Delete:
308 return true;
309 }
310
311 return false;
312}
313
314void GradientWithStops::on_click_pressed(int n_press, double x, double y)
315{
316 if (!_gradient) return;
317
318 if (n_press == 1) {
319 // single button press selects stop and can start dragging it
320
321 if (!has_focus()) {
322 // grab focus, so we can show selection indicator and move selected stop with left/right keys
323 grab_focus();
324 }
325
326 // find stop handle
327 auto const index = find_stop_at(x, y);
328
329 if (index < 0) {
330 set_focused_stop(-1); // no stop
331 return;
332 }
333
335
336 // check if clicked stop can be moved
337 auto limits = get_stop_limits(index);
338 if (limits.min_offset < limits.max_offset) {
339 // TODO: to facilitate selecting stops without accidentally moving them,
340 // delay dragging mode until mouse cursor moves certain distance...
341 _dragging = true;
342 _pointer_x = x;
343 _stop_offset = _stops.at(index).offset;
344
345 if (_cursor_dragging) {
347 }
348 }
349 } else if (n_press == 2) {
350 // double-click may insert a new stop
351 auto const index = find_stop_at(x, y);
352 if (index >= 0) return;
353
354 auto layout = get_layout();
355 if (layout.width > 0 && x > layout.x && x < layout.x + layout.width) {
356 auto const position = (x - layout.x) / layout.width;
357 // request new stop
358 _signal_add_stop_at.emit(position);
359 }
360 }
361}
362
363void GradientWithStops::on_click_released(int /*n_press*/, double x, double y)
364{
366 _dragging = false;
367}
368
369// move stop by a given amount (delta)
370void GradientWithStops::move_stop(int stop_index, double offset_shift) {
371 auto layout = get_layout();
372 if (layout.width > 0) {
373 auto limits = get_stop_limits(stop_index);
374 if (limits.min_offset < limits.max_offset) {
375 auto new_offset = CLAMP(limits.offset + offset_shift, limits.min_offset, limits.max_offset);
376 if (new_offset != limits.offset) {
377 _signal_stop_offset_changed.emit(stop_index, new_offset);
378 }
379 }
380 }
381}
382
383void GradientWithStops::on_motion(double x, double y)
384{
385 if (!_gradient) return;
386
387 if (_dragging) {
388 // move stop to a new position (adjust offset)
389 auto dx = x - _pointer_x;
390 auto layout = get_layout();
391 if (layout.width > 0) {
392 auto delta = dx / layout.width;
393 auto limits = get_stop_limits(_focused_stop);
394 if (limits.min_offset < limits.max_offset) {
395 auto new_offset = CLAMP(_stop_offset + delta, limits.min_offset, limits.max_offset);
397 }
398 }
399 } else { // !drag but may need to change cursor
401 }
402}
403
404Glib::RefPtr<Gdk::Cursor> const *
405GradientWithStops::get_cursor(double const x, double const y) const
406{
407 if (!_gradient) return nullptr;
408
409 // check if mouse if over stop handle that we can adjust
410 auto index = find_stop_at(x, y);
411 if (index >= 0) {
412 auto limits = get_stop_limits(index);
413 if (limits.min_offset < limits.max_offset && _cursor_mouseover) {
414 return &_cursor_mouseover;
415 }
416 } else if (_cursor_insert) {
417 return &_cursor_insert;
418 }
419
420 return nullptr;
421}
422
423void GradientWithStops::set_stop_cursor(Glib::RefPtr<Gdk::Cursor> const * const cursor)
424{
425 if (_cursor_current == cursor) return;
426
427 if (cursor != nullptr) {
428 set_cursor(*cursor);
429 } else {
430 set_cursor(""); // empty/default
431 }
432
433 _cursor_current = cursor;
434}
435
436void GradientWithStops::draw_func(Cairo::RefPtr<Cairo::Context> const &cr,
437 int /*width*/, int /*height*/)
438{
439 const double scale = get_scale_factor();
440 const auto layout = get_layout();
441
442 if (layout.width <= 0) return;
443
444 // empty gradient checkboard or gradient itself
445 cr->rectangle(layout.x, layout.y, layout.width, GRADIENT_IMAGE_HEIGHT);
446 draw_gradient(cr, _gradient, layout.x, layout.width);
447
448 if (!_gradient) return;
449
450 // draw stop handles
451
452 cr->begin_new_path();
453
454 auto const fg = get_color();
455 auto const &bg = _background_color;
456
457 // stop handle outlines and selection indicator use theme colors:
458 _template.set_style(".outer", "fill", gdk_to_css_color(fg));
459 _template.set_style(".inner", "stroke", gdk_to_css_color(bg));
460 _template.set_style(".hole", "fill", gdk_to_css_color(bg));
461
462 auto tip = _tip_template.render(scale);
463
464 for (size_t i = 0; i < _stops.size(); ++i) {
465 const auto& stop = _stops[i];
466
467 // stop handle shows stop color and opacity:
468 _template.set_style(".color", "fill", stop.color.toString(false));
469 _template.set_style(".opacity", "opacity", Util::format_number(stop.opacity));
470
471 // show/hide selection indicator
472 const auto is_selected = _focused_stop == static_cast<int>(i);
473 _template.set_style(".selected", "opacity", Util::format_number(is_selected ? 1 : 0));
474
475 // render stop handle
476 auto pix = _template.render(scale);
477
478 if (!pix) {
479 g_warning("Rendering gradient stop failed.");
480 break;
481 }
482
483 auto pos = get_stop_position(i, layout);
484
485 // selected handle sports a 'tip' to make it easily noticeable
486 if (is_selected && tip) {
487 cr->save();
488 // scale back to physical pixels
489 cr->scale(1 / scale, 1 / scale);
490 // paint tip bitmap
491 Gdk::Cairo::set_source_pixbuf(cr, tip, round(pos.tip * scale - tip->get_width() / 2),
492 layout.y * scale);
493 cr->paint();
494 cr->restore();
495 }
496
497 // calc space available for stop marker
498 cr->save();
499 cr->rectangle(pos.left, layout.y, pos.right - pos.left, layout.height);
500 cr->clip();
501 // scale back to physical pixels
502 cr->scale(1 / scale, 1 / scale);
503 // paint bitmap
504 Gdk::Cairo::set_source_pixbuf(cr, pix, round(pos.tip * scale - pix->get_width() / 2),
505 pos.top * scale);
506 cr->paint();
507 cr->restore();
508 cr->reset_clip();
509 }
510}
511
512// focused/selected stop indicator
514 if (_focused_stop == index) return;
515
518 update();
519}
520
521} // namespace Inkscape::UI::Widget
522
523/*
524 Local Variables:
525 mode:c++
526 c-file-style:"stroustrup"
527 c-file-offsets:((innamespace . 0)(inline-open . 0))
528 indent-tabs-mode:nil
529 fill-column:99
530 End:
531*/
532// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
double scale
Definition aa.cpp:228
Cairo integration helpers.
sigc::signal< void(size_t)> _signal_delete_stop
Glib::RefPtr< Gdk::Cursor > const * _cursor_current
sigc::signal< void(double)> _signal_add_stop_at
void draw_func(Cairo::RefPtr< Cairo::Context > const &cr, int width, int height)
sigc::signal< void(size_t, double)> _signal_stop_offset_changed
Glib::RefPtr< Gdk::Cursor > const * get_cursor(double x, double y) const
sigc::signal< void(size_t)> _signal_stop_selected
stop_pos_t get_stop_position(size_t index, const layout_t &layout) const
void on_click_pressed(int n_press, double x, double y)
void on_click_released(int n_press, double x, double y)
Glib::RefPtr< Gdk::Cursor > _cursor_dragging
void set_stop_cursor(Glib::RefPtr< Gdk::Cursor > const *cursor)
void move_stop(int stop_index, double offset_shift)
void css_changed(GtkCssStyleChange *change) final
Called after gtk_widget_css_changed(): when a CSS widget node is validated & style changed.
std::optional< bool > focus(Gtk::DirectionType direction) final
Called before gtk_widget_focus(): return true if moving in direction keeps focus w/in self,...
Glib::RefPtr< Gdk::Cursor > _cursor_mouseover
bool on_key_pressed(unsigned keyval, unsigned keycode, Gdk::ModifierType state)
A class you can inherit to access GTK4ʼs Widget.css_changed & .focus vfuncs, missing in gtkmm4.
double get_width_px() const
double get_height_px() const
size_t set_style(const Glib::ustring &selector, const char *name, const Glib::ustring &value)
Glib::RefPtr< Gdk::Pixbuf > render(double scale)
Gradient.
Definition sp-gradient.h:86
SPStop * getFirstStop()
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
sigc::connection connectRelease(sigc::slot< void(SPObject *)> slot)
Connects to the release request signal.
Definition sp-object.h:237
sigc::connection connectModified(sigc::slot< void(SPObject *, unsigned int)> slot)
Connects to the modification notification signal.
Definition sp-object.h:705
Gradient stop.
Definition sp-stop.h:31
float offset
Definition sp-stop.h:38
SPStop * getNextStop()
Virtual write: write object attributes to repr.
Definition sp-stop.cpp:98
Inkscape::Colors::Color getColor() const
Definition sp-stop.cpp:130
Utilities to more easily use Gtk::EventController & subclasses like Gesture.
Utility functions to convert ascii representations to numbers.
static constexpr int GRADIENT_IMAGE_HEIGHT
Gradient image widget with stop handles.
double offset
Definition desktop.h:50
std::string get_filename(Type type, char const *filename, bool localized, bool silent)
Definition resource.cpp:170
Low-level IO code.
bool has_flag(Gdk::ModifierType const state, Gdk::ModifierType const flags)
Helper to query if ModifierType state contains one or more of given flag(s).
Definition controller.h:25
Custom widgets.
Definition desktop.h:126
static constexpr int height
std::string get_stop_template_path(const char *filename)
static Geom::Point direction(Geom::Point const &first, Geom::Point const &second)
Computes an unit vector of the direction from first to second control point.
Definition node.cpp:164
std::string format_number(double val, unsigned int precision=3)
Definition converters.h:110
void draw_gradient(const Cairo::RefPtr< Cairo::Context > &cr, SPGradient *gradient, int x, int width)
Renders a preview of a gradient into the passed context.
static cairo_user_data_key_t key
Inkscape::IO::Resource - simple resource API.
TODO: insert short description here.
int delta
int index
double width
Gdk::RGBA get_color_with_class(Gtk::Widget &widget, Glib::ustring const &css_class)
Definition util.cpp:303
Glib::ustring gdk_to_css_color(const Gdk::RGBA &color)
These GUI related color conversions allow us to convert from SVG xml attributes to Gdk colors,...
Definition util.cpp:340