Inkscape
Vector Graphics Editor
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 <sigc++/functors/mem_fun.h>
22
23#include "display/cairo-utils.h"
24#include "io/resource.h"
25#include "object/sp-gradient.h"
26#include "object/sp-stop.h"
27#include "ui/controller.h"
28#include "ui/cursor-utils.h"
29#include "ui/util.h"
31
32// c.f. share/ui/style.css
33// gradient's image height (multiple of checkerboard tiles, they are 6x6)
34constexpr static int GRADIENT_IMAGE_HEIGHT = 3 * 6;
35
36namespace Inkscape::UI::Widget {
37
38using std::round;
39using namespace Inkscape::IO;
40
41std::string get_stop_template_path(const char* filename) {
42 // "stop handle" template files path
43 return Resource::get_filename(Resource::UIS, filename);
44}
45
47 Glib::ObjectBase{"GradientWithStops"},
49 Gtk::DrawingArea{},
50 _template(get_stop_template_path("gradient-stop.svg").c_str()),
51 _tip_template(get_stop_template_path("gradient-tip.svg").c_str())
52{
53 // default color, it will be updated
54 _background_color.set_grey(0.5);
55
56 // for theming
57 set_name("GradientEdit");
58
59 set_draw_func(sigc::mem_fun(*this, &GradientWithStops::draw_func));
60
62 sigc::mem_fun(*this, &GradientWithStops::on_click_released),
64 Controller::add_motion<nullptr, &GradientWithStops::on_motion, nullptr>(*this, *this);
65 Controller::add_key<&GradientWithStops::on_key_pressed>(*this, *this);
66 set_focusable(true);
67}
68
70
72 _gradient = gradient;
73
74 // listen to release & changes
75 _release = gradient ? gradient->connectRelease([=](SPObject*){ set_gradient(nullptr); }) : sigc::connection();
76 _modified = gradient ? gradient->connectModified([=](SPObject*, guint){ modified(); }) : sigc::connection();
77
78 // TODO: check selected/focused stop index
79
80 modified();
81
82 set_sensitive(gradient != nullptr);
83}
84
86 // gradient has been modified
87
88 // read all stops
89 _stops.clear();
90
91 if (_gradient) {
92 SPStop* stop = _gradient->getFirstStop();
93 while (stop) {
94 _stops.push_back(stop_t {
95 .offset = stop->offset, .color = stop->getColor(), .opacity = stop->getOpacity()
96 });
97 stop = stop->getNextStop();
98 }
99 }
100
101 update();
102}
103
105 queue_draw();
106}
107
108// capture background color when styles change
109void GradientWithStops::css_changed(GtkCssStyleChange * /*change*/)
110{
111 if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_root())) {
112 _background_color = get_color_with_class(*wnd, "theme_bg_color");
113 }
114
115 // load and cache cursors
116 if (!_cursor_mouseover) {
119 _cursor_insert = Gdk::Cursor::create("crosshair");
120 set_stop_cursor(nullptr);
121 }
122}
123
124// return on-screen position of the UI stop corresponding to the gradient's color stop at 'index'
126 if (!_gradient || index >= _stops.size()) {
127 return stop_pos_t {};
128 }
129
130 // half of the stop template width; round it to avoid half-pixel coordinates
131 const auto dx = round((_template.get_width_px() + 1) / 2);
132
133 auto pos = [&](double offset) { return round(layout.x + layout.width * CLAMP(offset, 0, 1)); };
134 const auto& v = _stops;
135
136 auto offset = pos(v[index].offset);
137 auto left = offset - dx;
138 if (index > 0) {
139 // check previous stop; it may overlap
140 auto prev = pos(v[index - 1].offset) + dx;
141 if (prev > left) {
142 // overlap
143 left = round((left + prev) / 2);
144 }
145 }
146
147 auto right = offset + dx;
148 if (index + 1 < v.size()) {
149 // check next stop for overlap
150 auto next = pos(v[index + 1].offset) - dx;
151 if (right > next) {
152 // overlap
153 right = round((right + next) / 2);
154 }
155 }
156
157 return stop_pos_t {
158 .left = left,
159 .tip = offset,
160 .right = right,
161 .top = layout.height - _template.get_height_px(),
162 .bottom = layout.height
163 };
164}
165
166// widget's layout; mainly location of the gradient's image and stop handles
168 const auto stop_width = _template.get_width_px();
169 const auto half_stop = round((stop_width + 1) / 2);
170 const auto x = half_stop;
171 const double width = get_width() - stop_width;
172 const double height = get_height();
173
174 return layout_t {
175 .x = x,
176 .y = 0,
177 .width = width,
178 .height = height
179 };
180}
181
182// check if stop handle is under (x, y) location, return its index or -1 if not hit
183int GradientWithStops::find_stop_at(double x, double y) const {
184 if (!_gradient) return -1;
185
186 const auto& v = _stops;
187 const auto& layout = get_layout();
188
189 // find stop handle at (x, y) position; note: stops may not be ordered by offsets
190 for (size_t i = 0; i < v.size(); ++i) {
191 auto pos = get_stop_position(i, layout);
192 if (x >= pos.left && x <= pos.right && y >= pos.top && y <= pos.bottom) {
193 return static_cast<int>(i);
194 }
195 }
196
197 return -1;
198}
199
200// this is range of offset adjustment for a given stop
202 if (!_gradient) return limits_t {};
203
204 // let negative index turn into a large out-of-range number
205 auto index = static_cast<size_t>(maybe_index);
206
207 const auto& v = _stops;
208
209 if (index < v.size()) {
210 double min = 0;
211 double max = 1;
212
213 if (v.size() > 1) {
214 std::vector<double> offsets;
215 offsets.reserve(v.size());
216 for (auto& s : _stops) {
217 offsets.push_back(s.offset);
218 }
219 std::sort(offsets.begin(), offsets.end());
220
221 // special cases:
222 if (index == 0) { // first stop
223 max = offsets[index + 1];
224 }
225 else if (index + 1 == v.size()) { // last stop
226 min = offsets[index - 1];
227 }
228 else {
229 // stops "inside" gradient
230 min = offsets[index - 1];
231 max = offsets[index + 1];
232 }
233 }
234 return limits_t { .min_offset = min, .max_offset = max, .offset = v[index].offset };
235 }
236 else {
237 return limits_t {};
238 }
239}
240
241std::optional<bool> GradientWithStops::focus(Gtk::DirectionType const direction)
242{
243 // On arrow key, let ::key-pressed move focused stop (horz) / nothing (vert)
244 if (!(direction == Gtk::DirectionType::TAB_FORWARD || direction == Gtk::DirectionType::TAB_BACKWARD)) {
245 return true;
246 }
247
248 auto const backward = direction == Gtk::DirectionType::TAB_BACKWARD;
249 auto const n_stops = _stops.size();
250
251 if (has_focus()) {
252 auto const new_stop = _focused_stop + (backward ? -1 : +1);
253 // out of range: keep _focused_stop, but give up focus on widget overall
254 if (!(new_stop >= 0 && new_stop < n_stops)) {
255 return false; // let focus go
256 }
257 // in range: next/prev stop
258 set_focused_stop(new_stop);
259 } else {
260 // didnʼt have focus: grab on 1st or last stop, relevant to direction
261 grab_focus();
262 if (n_stops > 0) { // …unless we have no stop, then just focus widget
263 set_focused_stop(backward ? n_stops - 1 : 0);
264 }
265 }
266
267 return true;
268}
269
270bool GradientWithStops::on_key_pressed(GtkEventControllerKey const * /*controller*/,
271 unsigned const keyval, unsigned /*keycode*/,
272 GdkModifierType const state)
273{
274 // currently all keyboard activity involves acting on focused stop handle; bail if nothing's selected
275 if (_focused_stop < 0) return false;
276
277 auto delta = _stop_move_increment;
278 if (Controller::has_flag(state, GDK_SHIFT_MASK)) {
279 delta *= 10;
280 }
281
282 switch (keyval) {
283 case GDK_KEY_Left:
284 case GDK_KEY_KP_Left:
285 move_stop(_focused_stop, -delta);
286 return true;
287
288 case GDK_KEY_Right:
289 case GDK_KEY_KP_Right:
290 move_stop(_focused_stop, delta);
291 return true;
292
293 case GDK_KEY_BackSpace:
294 case GDK_KEY_Delete:
296 return true;
297 }
298
299 return false;
300}
301
302Gtk::EventSequenceState GradientWithStops::on_click_pressed(Gtk::GestureClick const & /*click*/,
303 int const n_press,
304 double const x, double const y)
305{
307
308 if (n_press == 1) {
309 // single button press selects stop and can start dragging it
310
311 if (!has_focus()) {
312 // grab focus, so we can show selection indicator and move selected stop with left/right keys
313 grab_focus();
314 }
315
316 // find stop handle
317 auto const index = find_stop_at(x, y);
318
319 if (index < 0) {
320 set_focused_stop(-1); // no stop
322 }
323
324 set_focused_stop(index);
325
326 // check if clicked stop can be moved
327 auto limits = get_stop_limits(index);
328 if (limits.min_offset < limits.max_offset) {
329 // TODO: to facilitate selecting stops without accidentally moving them,
330 // delay dragging mode until mouse cursor moves certain distance...
331 _dragging = true;
332 _pointer_x = x;
333 _stop_offset = _stops.at(index).offset;
334
335 if (_cursor_dragging) {
337 }
338 }
339 } else if (n_press == 2) {
340 // double-click may insert a new stop
341 auto const index = find_stop_at(x, y);
342 if (index >= 0) return Gtk::EventSequenceState::NONE;
343
344 auto layout = get_layout();
345 if (layout.width > 0 && x > layout.x && x < layout.x + layout.width) {
346 auto const position = (x - layout.x) / layout.width;
347 // request new stop
348 _signal_add_stop_at.emit(position);
349 }
350 }
351
353}
354
355Gtk::EventSequenceState GradientWithStops::on_click_released(Gtk::GestureClick const & /*click*/,
356 int /*n_press*/,
357 double const x, double const y)
358{
360 _dragging = false;
362}
363
364// move stop by a given amount (delta)
365void GradientWithStops::move_stop(int stop_index, double offset_shift) {
366 auto layout = get_layout();
367 if (layout.width > 0) {
368 auto limits = get_stop_limits(stop_index);
369 if (limits.min_offset < limits.max_offset) {
370 auto new_offset = CLAMP(limits.offset + offset_shift, limits.min_offset, limits.max_offset);
371 if (new_offset != limits.offset) {
372 _signal_stop_offset_changed.emit(stop_index, new_offset);
373 }
374 }
375 }
376}
377
378void GradientWithStops::on_motion(GtkEventControllerMotion const * /*motion*/,
379 double const x, double const y)
380{
381 if (!_gradient) return;
382
383 if (_dragging) {
384 // move stop to a new position (adjust offset)
385 auto dx = x - _pointer_x;
386 auto layout = get_layout();
387 if (layout.width > 0) {
388 auto delta = dx / layout.width;
389 auto limits = get_stop_limits(_focused_stop);
390 if (limits.min_offset < limits.max_offset) {
391 auto new_offset = CLAMP(_stop_offset + delta, limits.min_offset, limits.max_offset);
393 }
394 }
395 } else { // !drag but may need to change cursor
397 }
398}
399
400Glib::RefPtr<Gdk::Cursor> const *
401GradientWithStops::get_cursor(double const x, double const y) const
402{
403 if (!_gradient) return nullptr;
404
405 // check if mouse if over stop handle that we can adjust
406 auto index = find_stop_at(x, y);
407 if (index >= 0) {
408 auto limits = get_stop_limits(index);
409 if (limits.min_offset < limits.max_offset && _cursor_mouseover) {
410 return &_cursor_mouseover;
411 }
412 } else if (_cursor_insert) {
413 return &_cursor_insert;
414 }
415
416 return nullptr;
417}
418
419void GradientWithStops::set_stop_cursor(Glib::RefPtr<Gdk::Cursor> const * const cursor)
420{
421 if (_cursor_current == cursor) return;
422
423 if (cursor != nullptr) {
424 set_cursor(*cursor);
425 } else {
426 set_cursor(""); // empty/default
427 }
428
429 _cursor_current = cursor;
430}
431
432void GradientWithStops::draw_func(Cairo::RefPtr<Cairo::Context> const &cr,
433 int /*width*/, int /*height*/)
434{
435 const double scale = get_scale_factor();
436 const auto layout = get_layout();
437
438 if (layout.width <= 0) return;
439
440 // empty gradient checkboard or gradient itself
441 cr->rectangle(layout.x, layout.y, layout.width, GRADIENT_IMAGE_HEIGHT);
442 draw_gradient(cr, _gradient, layout.x, layout.width);
443
444 if (!_gradient) return;
445
446 // draw stop handles
447
448 cr->begin_new_path();
449
450 auto const fg = get_color();
451 auto const &bg = _background_color;
452
453 // stop handle outlines and selection indicator use theme colors:
454 _template.set_style(".outer", "fill", rgba_to_css_color(fg));
455 _template.set_style(".inner", "stroke", rgba_to_css_color(bg));
456 _template.set_style(".hole", "fill", rgba_to_css_color(bg));
457
458 auto tip = _tip_template.render(scale);
459
460 for (size_t i = 0; i < _stops.size(); ++i) {
461 const auto& stop = _stops[i];
462
463 // stop handle shows stop color and opacity:
464 _template.set_style(".color", "fill", rgba_to_css_color(stop.color));
465 _template.set_style(".opacity", "opacity", double_to_css_value(stop.opacity));
466
467 // show/hide selection indicator
468 const auto is_selected = _focused_stop == static_cast<int>(i);
469 _template.set_style(".selected", "opacity", double_to_css_value(is_selected ? 1 : 0));
470
471 // render stop handle
472 auto pix = _template.render(scale);
473
474 if (!pix) {
475 g_warning("Rendering gradient stop failed.");
476 break;
477 }
478
479 auto pos = get_stop_position(i, layout);
480
481 // selected handle sports a 'tip' to make it easily noticeable
482 if (is_selected && tip) {
483 cr->save();
484 // scale back to physical pixels
485 cr->scale(1 / scale, 1 / scale);
486 // paint tip bitmap
487 Gdk::Cairo::set_source_pixbuf(cr, tip, round(pos.tip * scale - tip->get_width() / 2),
488 layout.y * scale);
489 cr->paint();
490 cr->restore();
491 }
492
493 // calc space available for stop marker
494 cr->save();
495 cr->rectangle(pos.left, layout.y, pos.right - pos.left, layout.height);
496 cr->clip();
497 // scale back to physical pixels
498 cr->scale(1 / scale, 1 / scale);
499 // paint bitmap
500 Gdk::Cairo::set_source_pixbuf(cr, pix, round(pos.tip * scale - pix->get_width() / 2),
501 pos.top * scale);
502 cr->paint();
503 cr->restore();
504 cr->reset_clip();
505 }
506}
507
508// focused/selected stop indicator
510 if (_focused_stop == index) return;
511
512 _focused_stop = index;
513 _signal_stop_selected.emit(index);
514 update();
515}
516
517} // namespace Inkscape::UI::Widget
518
519/*
520 Local Variables:
521 mode:c++
522 c-file-style:"stroustrup"
523 c-file-offsets:((innamespace . 0)(inline-open . 0))
524 indent-tabs-mode:nil
525 fill-column:99
526 End:
527*/
528// 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
Gtk::EventSequenceState on_click_released(Gtk::GestureClick const &click, 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.
void on_motion(GtkEventControllerMotion const *motion, double x, double y)
std::optional< bool > focus(Gtk::DirectionType direction) final
Called before gtk_widget_focus(): return true if moving in direction keeps focus w/in self,...
bool on_key_pressed(GtkEventControllerKey const *controller, unsigned keyval, unsigned keycode, GdkModifierType state)
Glib::RefPtr< Gdk::Cursor > _cursor_insert
Gtk::EventSequenceState on_click_pressed(Gtk::GestureClick const &click, int n_press, double x, double y)
Glib::RefPtr< Gdk::Cursor > _cursor_mouseover
int find_stop_at(double x, double y) const
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:146
sigc::connection connectRelease(sigc::slot< void(SPObject *)> slot)
Connects to the release request signal.
Definition: sp-object.h:223
sigc::connection connectModified(sigc::slot< void(SPObject *, unsigned int)> slot)
Connects to the modification notification signal.
Definition: sp-object.h:691
Gradient stop.
Definition: sp-stop.h:28
gfloat getOpacity() const
Definition: sp-stop.cpp:139
float offset
Definition: sp-stop.h:35
SPStop * getNextStop()
Virtual write: write object attributes to repr.
Definition: sp-stop.cpp:99
SPColor getColor() const
Definition: sp-stop.cpp:131
Utilities to more easily use Gtk::EventController & subclasses like Gesture.
static constexpr int GRADIENT_IMAGE_HEIGHT
Gradient image widget with stop handles.
double offset
size_t v
Definition: multi-index.h:105
MultiDegree< n > max(MultiDegree< n > const &p, MultiDegree< n > const &q)
Returns the maximal degree appearing in the two arguments for each variables.
Definition: sbasisN.h:158
Piecewise< SBasis > min(SBasis const &f, SBasis const &g)
Return the more negative of the two functions pointwise.
Definition: desktop.h:51
std::string get_filename(Type type, char const *filename, bool localized, bool silent)
Definition: resource.cpp:167
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:47
Gtk::GestureClick & add_click(Gtk::Widget &widget, ClickSlot on_pressed, ClickSlot on_released, Button const button, Gtk::PropagationPhase const phase, When const when)
Create a click gesture for the given widget; by default claim sequence.
Definition: controller.cpp:105
static void set_sensitive(Gtk::SearchEntry2 &entry, bool const sensitive)
static double get_width(SprayTool *tc)
Definition: spray-tool.cpp:307
Custom widgets.
Definition: desktop.h:127
static constexpr int height
std::string get_stop_template_path(const char *filename)
static constexpr int dx
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:152
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.
Glib::ustring rgba_to_css_color(double r, double g, double b)
Glib::ustring double_to_css_value(double value)
@ NONE
Definition: axis-manip.h:35
Inkscape::IO::Resource - simple resource API.
TODO: insert short description here.
double width
std::unique_ptr< Toolbar >(* create)(SPDesktop *desktop)
Definition: toolbars.cpp:63
Gdk::RGBA get_color_with_class(Gtk::Widget &widget, Glib::ustring const &css_class)
Definition: util.cpp:272