Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
color-plate.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2
3#include "color-plate.h"
4#include <cassert>
5#include <cairo.h>
6#include <cairomm/context.h>
7#include <2geom/rect.h>
8#include <cairomm/refptr.h>
9#include <cairomm/surface.h>
10#include <gtkmm/eventcontrollermotion.h>
11#include <gtkmm/gestureclick.h>
12
13#include "colors/color.h"
14#include "colors/utils.h"
15#include "ui/controller.h"
16#include "util/drawing-utils.h"
17#include "util/theme-utils.h"
18
19namespace Inkscape::UI::Widget {
20
21using namespace Colors;
22
23void circle(const Cairo::RefPtr<Cairo::Context>& ctx, const Geom::Point& center, double radius) {
24 ctx->arc(center.x(), center.y(), radius, 0, 2 * M_PI);
25}
26
27// draw a circle around given point to show currently selected color
28static void draw_point_indicator(const Cairo::RefPtr<Cairo::Context>& ctx, const Geom::Point& point, double size) {
29 ctx->save();
30
31 auto pt = point.round();
32 ctx->set_line_width(1.0);
33 circle(ctx, pt, (size - 2) / 2);
34 ctx->set_source_rgb(1, 1, 1);
35 ctx->stroke();
36 circle(ctx, pt, size / 2);
37 ctx->set_source_rgb(0, 0, 0);
38 ctx->stroke();
39
40 ctx->restore();
41}
42
43static void draw_color_plate(const Cairo::RefPtr<Cairo::Context>& ctx, const Geom::Rect& area, double radius, const Cairo::RefPtr<Cairo::ImageSurface>& preview, bool circular) {
44 if (area.width() <= 0 || area.height() <= 0) return;
45
46 ctx->save();
47 if (circular) {
48 //TODO: on low-res display - align midpoint to whole pixels
49 circle(ctx, area.midpoint(), area.minExtent() / 2);
50 }
51 else {
52 Util::rounded_rectangle(ctx, area, radius);
53 }
54 ctx->clip();
55
57 auto offset = area.min();
58 if (circular) {
59 // Note: circular color preview needs to be larger than requested area to make sure
60 // that there are no miscolored pixels visible after clip path is applied.
61 // Note 2: comment out clip() path above to verify that circle is centered with respect to border
62
63 auto size = std::min(area.width(), area.height());
64 // uniform scaling of the circular preview; it is square, so just use width;
65 // subtract pixels - it's a border for clipping
66 auto s = size / (preview->get_width() - 2);
67 scale = Geom::Point(s, s);
68 offset += {-1.0 * s, -1.0 * s}; // move outline to hide extra border pixels
69 // center the preview
70 auto d = area.width() - area.height();
71 if (d > 0) {
72 // center horizontally
73 offset += {d / 2, 0};
74 }
75 else if (d < 0) {
76 // in the middle
77 offset += {0, -d / 2};
78 }
79 }
80 else {
81 // rectangular color preview; stretch it to cover "area"
82 // subtract pixels - it's a border for clipping
83 auto scale_x = area.width() / (preview->get_width() - 2);
84 auto scale_y = area.height() / (preview->get_height() - 2);
85 scale = Geom::Point(scale_x, scale_y);
86 offset -= scale;
87 }
88
89 ctx->scale(scale.x(), scale.y());
90 ctx->set_source(preview, offset.x() / scale.x(), offset.y() / scale.y());
91 ctx->paint();
92
93 ctx->restore();
94}
95
96static Geom::Point get_color_coordinates(double val1, double val2, bool circular) {
97 if (circular) {
98 // point in a circle
99 // val1 is an angle (0..1 -> -2pi..2pi), while val2 is a distance
100 auto angle = (val1 * 2 * M_PI) - M_PI;
101 auto x = sin(angle) * val2;
102 auto y = cos(angle) * val2;
103 return {x, y};
104 }
105 else {
106 // point in a rectangle
107 return {val1, 1 - val2};
108 }
109}
110
111static void set_color_helper(Color& color, int channel1, int channel2, double x, double y, bool disc) {
112 if (disc) {
113 auto dist = std::hypot(x, y);
114 auto angle = (atan2(x, y) + M_PI) / (2 * M_PI); // angle in 0..1 range
115 color.set(channel1, angle);
116 color.set(channel2, dist);
117 }
118 else {
119 // rectangle
120 color.set(channel1, x);
121 color.set(channel2, 1 - y);
122 }
123}
124
125static Cairo::RefPtr<Cairo::ImageSurface> create_color_preview(int size, const std::function<void (std::vector<std::uint32_t>&, int width)>& draw) {
126 auto fmt = Cairo::ImageSurface::Format::ARGB32;
127 auto stride = Cairo::ImageSurface::format_stride_for_width(fmt, size);
128 auto width = stride / sizeof(std::uint32_t);
129 std::vector<std::uint32_t> data(size * width, 0x00000000);
130
131 draw(data, width);
132
133 void* buffer = data.data();
134 auto src = Cairo::ImageSurface::create(static_cast<unsigned char*>(buffer), fmt, size, size, stride);
135 auto dest = Cairo::ImageSurface::create(fmt, size, size);
136 auto ctx = Cairo::Context::create(dest);
137 ctx->set_source(src, 0, 0);
138 ctx->paint();
139 return dest;
140}
141
142// rectangular color picker
143static Cairo::RefPtr<Cairo::ImageSurface> create_color_plate(unsigned int resolution, const Color& base, int channel1, int channel2) {
144 const double limit = resolution;
145 const int size = resolution + 1;
146
147 return create_color_preview(size, [=](auto& data, auto width) {
148 auto color = base;
149 color.addOpacity();
150 int row = 0;
151 //TODO: add duplicated border pixels
152 for (int iy = 0; iy <= limit; ++iy, ++row) {
153 auto y = iy / limit;
154 color.set(channel2, 1 - y);
155 auto index = static_cast<size_t>(row * width);
156 for (int ix = 0; ix <= limit; ++ix) {
157 auto x = ix / limit;
158 color.set(channel1, x);
159 data[index++] = color.toARGB();
160 }
161 }
162 //todo: compilation error on linux:
163 // assert(index <= data.size());
164 });
165}
166
167static Cairo::RefPtr<Cairo::ImageSurface> create_color_wheel(unsigned int resolution, const Color& base, int channel1, int channel2) {
168 const int radius = resolution / 2;
169 const double limit = radius;
170 const int size = radius * 2 + 1;
171 Color color = base;
172
173 return create_color_preview(size, [&](auto& data, auto width) {
174 // extra pixels at the borderline (that's the +1/radius), so clipping doesn't expose anything "unpainted"
175 double rsqr = std::pow(1.0 + 1.0/radius, 2);
176 int row = 0;
177 for (int iy = -radius; iy <= radius; ++iy, ++row) {
178 int index = row * width;
179 auto y = iy / limit;
180 auto sy = y * y;
181 for (int ix = -radius; ix <= radius; ++ix, ++index) {
182 auto x = ix / limit;
183 auto sx = x * x;
184 // transparent pixels outside the circle
185 if (sx + sy > rsqr) continue;
186
187 set_color_helper(color, channel1, channel2, x, y, true);
188 data[index] = color.toARGB();
189 }
190 }
191 });
192}
193
194static Geom::Point screen_to_local(const Geom::Rect& active, Geom::Point point, bool circular, bool* inside = nullptr) {
195 if (inside) {
196 *inside = active.contains(point);
197 }
198 // normalize point
199 point = active.clamp(point);
200 point = (point - active.min()) / active.dimensions();
201
202 if (circular) {
203 // restrict point to a circle
204 auto min = active.minExtent();
205 auto scale = Geom::Point(min, min) / active.dimensions();
206 // coords in -1..1 range:
207 auto c = (point * 2 - Geom::Point(1, 1)) / scale;
208 auto dist = L2(c);
209 if (dist > 1) {
210 c /= dist;
211 if (inside) {
212 *inside = false;
213 }
214 }
215 point = c;
216 }
217
218 return point;
219}
220
221static Geom::Point local_to_screen(const Geom::Rect& active, Geom::Point point, bool circular) {
222 if (circular) {
223 auto min = active.minExtent();
224 auto scale = Geom::Point(min, min) / active.dimensions();
225 point = (point * scale + Geom::Point(1, 1)) / 2;
226 }
227
228 return active.min() + point * active.dimensions();
229}
230
232 set_name("ColorPlate");
233 set_disc(_disc); // add right CSS class
234
235 set_draw_func([this](const Cairo::RefPtr<Cairo::Context>& ctx, int /*width*/, int /*height*/){
236 draw_plate(ctx);
237 });
238
239 auto const motion = Gtk::EventControllerMotion::create();
240 motion->set_propagation_phase(Gtk::PropagationPhase::TARGET);
241 motion->signal_motion().connect([this, &motion = *motion](auto &&...args) { on_motion(motion, args...); });
242 add_controller(motion);
243
244 auto const click = Gtk::GestureClick::create();
245 click->set_button(1); // left
246 click->signal_pressed().connect(Controller::use_state([this](auto &, int, double x, double y) {
247 // verify click location
248 if (auto area = get_active_area()) {
249 bool inside = false;
250 auto down = screen_to_local(*area, Geom::Point(x, y), _disc, &inside);
251 if (inside) {
252 _down = down;
253 _drag = true;
254 queue_draw();
256 return Gtk::EventSequenceState::CLAIMED;
257 }
258 }
259 _down = {};
260 _drag = false;
261 return Gtk::EventSequenceState::NONE;
262 }, *click));
263 add_controller(click);
264}
265
266void ColorPlate::draw_plate(const Cairo::RefPtr<Cairo::Context>& ctx) {
267 auto maybe_area = get_area();
268 if (!maybe_area) return;
269
270 auto area = *maybe_area;
271
272 if (!_plate) {
273 // color preview resolution in discrete color steps;
274 // in-betweens will be interpolated in sRGB as the preview image gets stretched;
275 // this number should be kept small for faster interactive color plate refresh
276 constexpr int resolution = 64; // this number impacts performance big time!
277 _plate = _disc ?
280 }
281 draw_color_plate(ctx, area, _radius, _plate, _disc);
282 bool dark = Util::is_current_theme_dark(*this);
283 Util::draw_standard_border(ctx, area, dark, _radius, get_scale_factor(), _disc);
284
285 if (auto maybe = get_active_area(); _down && maybe) {
286 auto pt = local_to_screen(*maybe, *_down, _disc);
287 double size = 8;
288 draw_point_indicator(ctx, pt, size);
289 }
290}
291
292void ColorPlate::set_base_color(Color color, int fixed_channel, int var_channel1, int var_channel2) {
293 color.setOpacity(1);
294 if (_base_color != color) {
295 // optimization: rebuild the plate only if "fixed" channel value has changed, necessitating new rendering
296 if (_base_color.getSpace() != color.getSpace() || fabs(_fixed_channel_val - color[fixed_channel]) > 0.005 ||
297 _channel1 != var_channel1 || _channel2 != var_channel2) {
298 _plate.reset();
299 _fixed_channel_val = color[fixed_channel];
300 _channel1 = var_channel1;
301 _channel2 = var_channel2;
302 queue_draw();
303 }
304 _base_color = std::move(color);
305 }
306}
307
309 auto alloc = get_allocation();
310 auto min = 2 * _padding;
311 if (alloc.get_width() <= min || alloc.get_height() <= min) return {};
312
313 return Geom::Rect(0, 0, alloc.get_width(), alloc.get_height()).shrunkBy(_padding);
314}
315
317 auto area = get_area();
318 if (!area || area->minExtent() < 1) return {};
319
320 return area->shrunkBy(1, 1);
321}
322
324 auto color = _base_color;
325 set_color_helper(color, _channel1, _channel2, point.x(), point.y(), _disc);
326 return color;
327}
328
330 if (_down.has_value()) {
331 auto color = get_color_at(*_down);
332 _signal_color_changed.emit(color);
333 }
334}
335
336void ColorPlate::on_motion(Gtk::EventControllerMotion const &motion, double x, double y) {
337 if (!_drag) return;
338
339 auto state = motion.get_current_event_state();
340 auto drag = Controller::has_flag(state, Gdk::ModifierType::BUTTON1_MASK);
341 if (!drag) return;
342
343 // drag move
344 if (auto area = get_active_area()) {
345 _down = screen_to_local(*area, Geom::Point(x, y), _disc);
346 queue_draw();
348 }
349}
350
351void ColorPlate::set_disc(bool disc) {
352 _disc = disc;
353 if (disc) {
354 remove_css_class("rectangular");
355 add_css_class("circular");
356 }
357 else {
358 remove_css_class("circular");
359 add_css_class("rectangular");
360 }
361 queue_draw();
362}
363
365 return _disc;
366}
367
369 if (pad >= 0 && _padding != pad) {
370 _padding = pad;
371 queue_draw();
372 }
373}
374
376 // find 'color' on the plate and move indicator to it
377 auto point = get_color_coordinates(color[_channel1], color[_channel2], _disc);
378 if (_down && _down == point) return;
379
380 _down = point;
381 queue_draw();
382}
383
384sigc::signal<void(const Color&)>& ColorPlate::signal_color_changed() {
386}
387
388}
double scale
Definition aa.cpp:228
CPoint clamp(CPoint const &p) const
Clamp point to the rectangle.
bool contains(GenericRect< C > const &r) const
Check whether the rectangle includes all points in the given rectangle.
CPoint midpoint() const
Get the point in the geometric center of the rectangle.
C height() const
Get the vertical extent of the rectangle.
C minExtent() const
Get the smaller extent (width or height) of the rectangle.
C width() const
Get the horizontal extent of the rectangle.
CPoint min() const
Get the corner of the rectangle with smallest coordinate values.
CPoint dimensions() const
Get rectangle's width and height as a point.
Axis-aligned rectangle that can be empty.
Definition rect.h:203
Two-dimensional point that doubles as a vector.
Definition point.h:66
constexpr Coord y() const noexcept
Definition point.h:106
constexpr Coord x() const noexcept
Definition point.h:104
IntPoint round() const
Round to nearest integer coordinates.
Definition point.h:202
Axis aligned, non-empty rectangle.
Definition rect.h:92
Rect shrunkBy(Coord amount) const
Return a new rectangle which results from shrinking this one by the same amount along both axes.
Definition rect.cpp:144
uint32_t toARGB(double opacity=1.0) const
Return the RGBA int32 as an ARGB format number.
Definition color.cpp:125
bool set(unsigned int index, double value)
Set a specific channel in the color.
Definition color.cpp:350
bool addOpacity(double opacity=1.0)
Definition color.h:56
bool setOpacity(double opacity)
Set the opacity of this color object.
Definition color.cpp:444
std::shared_ptr< Space::AnySpace > const & getSpace() const
Definition color.h:39
void draw_plate(const Cairo::RefPtr< Cairo::Context > &ctx)
Cairo::RefPtr< Cairo::ImageSurface > _plate
Definition color-plate.h:50
void move_indicator_to(const Colors::Color &color)
Geom::OptRect get_active_area() const
Colors::Color get_color_at(const Geom::Point &point) const
sigc::signal< void(const Colors::Color &)> & signal_color_changed()
Geom::OptRect get_area() const
void set_base_color(Colors::Color color, int fixed_channel, int var_channel1, int var_channel2)
void on_motion(Gtk::EventControllerMotion const &motion, double x, double y)
sigc::signal< void(const Colors::Color &)> _signal_color_changed
Definition color-plate.h:57
std::optional< Geom::Point > _down
Definition color-plate.h:51
void draw(cairo_t *cr, xAx C, Rect bnd)
Definition conic-5.cpp:63
Utilities to more easily use Gtk::EventController & subclasses like Gesture.
double c[8][4]
double offset
auto use_state(Slot &&slot)
Definition controller.h:43
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 Geom::Point local_to_screen(const Geom::Rect &active, Geom::Point point, bool circular)
static Geom::Point screen_to_local(const Geom::Rect &active, Geom::Point point, bool circular, bool *inside=nullptr)
static void draw_point_indicator(const Cairo::RefPtr< Cairo::Context > &ctx, const Geom::Point &point, double size)
static void draw_color_plate(const Cairo::RefPtr< Cairo::Context > &ctx, const Geom::Rect &area, double radius, const Cairo::RefPtr< Cairo::ImageSurface > &preview, bool circular)
static Geom::Point get_color_coordinates(double val1, double val2, bool circular)
static Cairo::RefPtr< Cairo::ImageSurface > create_color_wheel(unsigned int resolution, const Color &base, int channel1, int channel2)
static void set_color_helper(Color &color, int channel1, int channel2, double x, double y, bool disc)
static Cairo::RefPtr< Cairo::ImageSurface > create_color_preview(int size, const std::function< void(std::vector< std::uint32_t > &, int width)> &draw)
static Cairo::RefPtr< Cairo::ImageSurface > create_color_plate(unsigned int resolution, const Color &base, int channel1, int channel2)
void circle(const Cairo::RefPtr< Cairo::Context > &ctx, const Geom::Point &center, double radius)
Geom::Rect rounded_rectangle(const Cairo::RefPtr< Cairo::Context > &ctx, const Geom::Rect &rect, double radius)
bool is_current_theme_dark(Gtk::Widget &widget)
void draw_standard_border(const Cairo::RefPtr< Cairo::Context > &ctx, Geom::Rect rect, bool dark_theme, double radius, int device_scale, bool circular)
int stride
Axis-aligned rectangle.
int const char * fmt
Definition safe-printf.h:18
static const Point data[]
int index
Gtk::Picture & preview
double width