1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2016-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#define COLSEL_TRACE(...)
9
10#ifdef HAVE_CONFIG_H
11#include "config.h"
12#endif
13
14#include "app/ui/color_selector.h"
15
16#include "app/app.h"
17#include "app/color_spaces.h"
18#include "app/color_utils.h"
19#include "app/modules/gfx.h"
20#include "app/pref/preferences.h"
21#include "app/ui/skin/skin_theme.h"
22#include "app/ui/status_bar.h"
23#include "app/util/shader_helpers.h"
24#include "base/concurrent_queue.h"
25#include "base/scoped_value.h"
26#include "base/thread.h"
27#include "os/surface.h"
28#include "os/system.h"
29#include "ui/manager.h"
30#include "ui/message.h"
31#include "ui/paint_event.h"
32#include "ui/register_message.h"
33#include "ui/scale.h"
34#include "ui/size_hint_event.h"
35#include "ui/system.h"
36
37#include <algorithm>
38#include <cmath>
39#include <condition_variable>
40#include <cstdio>
41#include <thread>
42
43#if SK_ENABLE_SKSL
44 #include "os/skia/skia_surface.h"
45
46 #include "include/core/SkCanvas.h"
47 #include "include/effects/SkRuntimeEffect.h"
48#endif
49
50namespace app {
51
52using namespace app::skin;
53using namespace ui;
54
55// TODO This logic could be used to redraw any widget:
56// 1. We send a onPaintSurfaceInBgThread() to paint the widget on a
57// offscreen buffer
58// 2. When the painting is done, we flip the buffer onto the screen
59// 3. If we receive another onPaint() we can cancel the background
60// painting and start another onPaintSurfaceInBgThread()
61//
62// An alternative (better) way:
63// 1. Create an alternative ui::Graphics implementation that generates
64// a list commands for the render thread
65// 2. Widgets can still use the same onPaint()
66// 3. The background threads consume the list of commands and render
67// the screen.
68//
69// The bad side is that is harder to invalidate the commands that will
70// render an old state of the widget. So the render thread should
71// start caring about invalidating old commands (outdated regions) or
72// cleaning the queue if it gets too big.
73//
74class ColorSelector::Painter {
75public:
76 Painter() : m_canvas(nullptr) {
77 }
78
79 ~Painter() {
80 ASSERT(!m_canvas);
81 }
82
83 void addRef() {
84 assert_ui_thread();
85
86 if (m_ref == 0)
87 m_paintingThread = std::thread([this]{ paintingProc(); });
88
89 ++m_ref;
90 }
91
92 void releaseRef() {
93 assert_ui_thread();
94
95 --m_ref;
96 if (m_ref == 0) {
97 {
98 std::unique_lock<std::mutex> lock(m_mutex);
99 stopCurrentPainting(lock);
100
101 m_killing = true;
102 m_paintingCV.notify_one();
103 }
104
105 m_paintingThread.join();
106 if (m_canvas)
107 m_canvas.reset();
108 }
109 }
110
111 os::Surface* getCanvas(int w, int h, gfx::Color bgColor) {
112 assert_ui_thread();
113
114 auto activeCS = get_current_color_space();
115
116 if (!m_canvas ||
117 m_canvas->width() != w ||
118 m_canvas->height() != h ||
119 m_canvas->colorSpace() != activeCS) {
120 std::unique_lock<std::mutex> lock(m_mutex);
121 stopCurrentPainting(lock);
122
123 os::SurfaceRef oldCanvas = m_canvas;
124 m_canvas = os::instance()->makeSurface(w, h, activeCS);
125 os::Paint paint;
126 paint.color(bgColor);
127 paint.style(os::Paint::Fill);
128 m_canvas->drawRect(gfx::Rect(0, 0, w, h), paint);
129 if (oldCanvas) {
130 m_canvas->drawSurface(
131 oldCanvas.get(),
132 gfx::Rect(0, 0, oldCanvas->width(), oldCanvas->height()),
133 gfx::Rect(0, 0, w, h),
134 os::Sampling(),
135 nullptr);
136 }
137 }
138 return m_canvas.get();
139 }
140
141 void startBgPainting(ColorSelector* colorSelector,
142 const gfx::Rect& mainBounds,
143 const gfx::Rect& bottomBarBounds,
144 const gfx::Rect& alphaBarBounds) {
145 assert_ui_thread();
146 COLSEL_TRACE("COLSEL: startBgPainting for %p\n", colorSelector);
147
148 std::unique_lock<std::mutex> lock(m_mutex);
149 stopCurrentPainting(lock);
150
151 m_colorSelector = colorSelector;
152 m_manager = colorSelector->manager();
153 m_mainBounds = mainBounds;
154 m_bottomBarBounds = bottomBarBounds;
155 m_alphaBarBounds = alphaBarBounds;
156
157 m_stopPainting = false;
158 m_paintingCV.notify_one();
159 }
160
161private:
162
163 void stopCurrentPainting(std::unique_lock<std::mutex>& lock) {
164 // Stop current
165 if (m_colorSelector) {
166 COLSEL_TRACE("COLSEL: stoppping painting of %p\n", m_colorSelector);
167
168 // TODO use another condition variable here
169 m_stopPainting = true;
170 m_waitStopCV.wait(lock);
171 }
172
173 ASSERT(m_colorSelector == nullptr);
174 }
175
176 void paintingProc() {
177 COLSEL_TRACE("COLSEL: paintingProc starts\n");
178
179 std::unique_lock<std::mutex> lock(m_mutex);
180 while (true) {
181 m_paintingCV.wait(lock);
182
183 if (m_killing)
184 break;
185
186 auto colorSel = m_colorSelector;
187 if (!colorSel)
188 break;
189
190 COLSEL_TRACE("COLSEL: starting painting in bg for %p\n", colorSel);
191
192 // Do the intesive painting in the background thread
193 {
194 lock.unlock();
195 colorSel->onPaintSurfaceInBgThread(
196 m_canvas.get(),
197 m_mainBounds,
198 m_bottomBarBounds,
199 m_alphaBarBounds,
200 m_stopPainting);
201 lock.lock();
202 }
203
204 m_colorSelector = nullptr;
205
206 if (m_stopPainting) {
207 COLSEL_TRACE("COLSEL: painting for %p stopped\n");
208 m_waitStopCV.notify_one();
209 }
210 else {
211 COLSEL_TRACE("COLSEL: painting for %p done and sending message\n");
212 colorSel->m_paintFlags |= DoneFlag;
213 }
214 }
215
216 COLSEL_TRACE("COLSEL: paintingProc ends\n");
217 }
218
219 int m_ref = 0;
220 bool m_killing = false;
221 bool m_stopPainting = false;
222 std::mutex m_mutex;
223 std::condition_variable m_paintingCV;
224 std::condition_variable m_waitStopCV;
225 os::SurfaceRef m_canvas;
226 ColorSelector* m_colorSelector;
227 ui::Manager* m_manager;
228 gfx::Rect m_mainBounds;
229 gfx::Rect m_bottomBarBounds;
230 gfx::Rect m_alphaBarBounds;
231 std::thread m_paintingThread;
232};
233
234static ColorSelector::Painter painter;
235
236#if SK_ENABLE_SKSL
237// static
238sk_sp<SkRuntimeEffect> ColorSelector::m_alphaEffect;
239#endif
240
241ColorSelector::ColorSelector()
242 : Widget(kGenericWidget)
243 , m_paintFlags(AllAreasFlag)
244 , m_lockColor(false)
245 , m_timer(100, this)
246{
247 initTheme();
248 painter.addRef();
249
250 m_appConn = App::instance()
251 ->ColorSpaceChange.connect(
252 &ColorSelector::updateColorSpace, this);
253}
254
255ColorSelector::~ColorSelector()
256{
257 painter.releaseRef();
258}
259
260void ColorSelector::selectColor(const app::Color& color)
261{
262 if (m_lockColor)
263 return;
264
265 if (m_color != color)
266 m_paintFlags |= onNeedsSurfaceRepaint(color);
267
268 m_color = color;
269 invalidate();
270}
271
272app::Color ColorSelector::getColorByPosition(const gfx::Point& pos)
273{
274 gfx::Rect rc = childrenBounds();
275 if (rc.isEmpty())
276 return app::Color::fromMask();
277
278 const int u = pos.x - rc.x;
279 const int umax = std::max(1, rc.w-1);
280
281 const gfx::Rect bottomBarBounds = this->bottomBarBounds();
282 if (( hasCapture() && m_capturedInBottom) ||
283 (!hasCapture() && bottomBarBounds.contains(pos)))
284 return getBottomBarColor(u, umax);
285
286 const gfx::Rect alphaBarBounds = this->alphaBarBounds();
287 if (( hasCapture() && m_capturedInAlpha) ||
288 (!hasCapture() && alphaBarBounds.contains(pos)))
289 return getAlphaBarColor(u, umax);
290
291 const int v = pos.y - rc.y;
292 const int vmax = std::max(1, rc.h-bottomBarBounds.h-alphaBarBounds.h-1);
293 return getMainAreaColor(u, umax,
294 v, vmax);
295}
296
297app::Color ColorSelector::getAlphaBarColor(const int u, const int umax)
298{
299 int alpha = (255 * u / umax);
300 app::Color color = m_color;
301 color.setAlpha(std::clamp(alpha, 0, 255));
302 return color;
303}
304
305void ColorSelector::onSizeHint(SizeHintEvent& ev)
306{
307 ev.setSizeHint(gfx::Size(32*ui::guiscale(), 32*ui::guiscale()));
308}
309
310bool ColorSelector::onProcessMessage(ui::Message* msg)
311{
312 switch (msg->type()) {
313
314 case kMouseDownMessage:
315 if (manager()->getCapture())
316 break;
317
318 captureMouse();
319 [[fallthrough]];
320
321 case kMouseMoveMessage: {
322 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
323 const gfx::Point pos = mouseMsg->position();
324
325 if (msg->type() == kMouseDownMessage) {
326 m_capturedInBottom = bottomBarBounds().contains(pos);
327 m_capturedInAlpha = alphaBarBounds().contains(pos);
328 m_capturedInMain = (hasCapture() &&
329 !m_capturedInMain &&
330 !m_capturedInBottom);
331 }
332
333 app::Color color = getColorByPosition(pos);
334 if (color != app::Color::fromMask()) {
335 base::ScopedValue<bool> switcher(m_lockColor, subColorPicked(), false);
336
337 StatusBar::instance()->showColor(0, color);
338 if (hasCapture())
339 ColorChange(color, mouseMsg->button());
340 }
341 break;
342 }
343
344 case kMouseUpMessage:
345 if (hasCapture()) {
346 m_capturedInBottom = false;
347 m_capturedInAlpha = false;
348 m_capturedInMain = false;
349 releaseMouse();
350 }
351 return true;
352
353 case kSetCursorMessage: {
354 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
355 app::Color color = getColorByPosition(mouseMsg->position());
356 if (color.getType() != app::Color::MaskType) {
357 auto theme = skin::SkinTheme::get(this);
358 ui::set_mouse_cursor(kCustomCursor, theme->cursors.eyedropper());
359 return true;
360 }
361 break;
362 }
363
364 case kMouseWheelMessage:
365 if (!hasCapture()) {
366 double scale = 1.0;
367 if (msg->shiftPressed() ||
368 msg->ctrlPressed() ||
369 msg->altPressed()) {
370 scale = 15.0;
371 }
372
373 double newHue = m_color.getHsvHue()
374 + scale*(+ static_cast<MouseMessage*>(msg)->wheelDelta().x
375 - static_cast<MouseMessage*>(msg)->wheelDelta().y);
376
377 while (newHue < 0.0)
378 newHue += 360.0;
379 newHue = std::fmod(newHue, 360.0);
380
381 if (newHue != m_color.getHsvHue()) {
382 app::Color newColor =
383 app::Color::fromHsv(
384 newHue,
385 m_color.getHsvSaturation(),
386 m_color.getHsvValue(),
387 getCurrentAlphaForNewColor());
388
389 ColorChange(newColor, kButtonNone);
390 }
391 }
392 break;
393
394 case kTimerMessage:
395 if (m_paintFlags & DoneFlag) {
396 m_timer.stop();
397 invalidate();
398 return true;
399 }
400 break;
401
402 }
403
404 return Widget::onProcessMessage(msg);
405}
406
407void ColorSelector::onInitTheme(ui::InitThemeEvent& ev)
408{
409 auto theme = SkinTheme::get(this);
410
411 Widget::onInitTheme(ev);
412 setBorder(theme->calcBorder(this, theme->styles.editorView()));
413}
414
415void ColorSelector::onResize(ui::ResizeEvent& ev)
416{
417 Widget::onResize(ev);
418
419 // We'll need to redraw the whole surface again with the new widget
420 // size.
421 m_paintFlags = AllAreasFlag;
422}
423
424void ColorSelector::onPaint(ui::PaintEvent& ev)
425{
426 ui::Graphics* g = ev.graphics();
427 auto theme = SkinTheme::get(this);
428
429 theme->drawRect(g, clientBounds(),
430 theme->parts.editorNormal().get(),
431 false); // Do not fill the center
432
433 gfx::Rect rc = clientChildrenBounds();
434 if (rc.isEmpty())
435 return;
436
437 gfx::Rect bottomBarBounds = this->bottomBarBounds();
438 gfx::Rect alphaBarBounds = this->alphaBarBounds();
439
440 os::Surface* painterSurface = nullptr;
441
442#if SK_ENABLE_SKSL // Paint with shaders
443 if (buildEffects()) {
444 SkCanvas* canvas;
445 bool isSRGB;
446 // TODO compare both color spaces
447 if ((!get_current_color_space() ||
448 get_current_color_space()->isSRGB()) &&
449 (!g->getInternalSurface()->colorSpace() ||
450 g->getInternalSurface()->colorSpace()->isSRGB())) {
451 // We can render directly in the ui::Graphics surface
452 canvas = &static_cast<os::SkiaSurface*>(g->getInternalSurface())->canvas();
453 isSRGB = true;
454 }
455 else {
456 // We'll paint in the ColorSelector::Painter canvas, and so we
457 // can convert color spaces.
458 painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
459 canvas = &static_cast<os::SkiaSurface*>(painterSurface)->canvas();
460 isSRGB = false;
461 }
462
463 canvas->save();
464 {
465 SkPaint p;
466 p.setStyle(SkPaint::kFill_Style);
467
468 // Main area
469 gfx::Rect rc2(0, 0, rc.w, std::max(1, rc.h-bottomBarBounds.h-alphaBarBounds.h));
470
471 SkRuntimeShaderBuilder builder1(m_mainEffect);
472 builder1.uniform("iRes") = SkV3{float(rc2.w), float(rc2.h), 0.0f};
473 setShaderParams(builder1, true);
474 p.setShader(builder1.makeShader());
475
476 if (isSRGB)
477 canvas->translate(rc.x+g->getInternalDeltaX(),
478 rc.y+g->getInternalDeltaY());
479
480 canvas->drawRect(SkRect::MakeXYWH(0, 0, rc2.w, rc2.h), p);
481
482 // Bottom bar
483 canvas->translate(0.0, rc2.h);
484 rc2.h = bottomBarBounds.h;
485
486 SkRuntimeShaderBuilder builder2(m_bottomEffect);
487 builder2.uniform("iRes") = SkV3{float(rc2.w), float(rc2.h), 0.0f};
488 setShaderParams(builder2, false);
489 p.setShader(builder2.makeShader());
490
491 canvas->drawRect(SkRect::MakeXYWH(0, 0, rc2.w, rc2.h), p);
492
493 // Alpha bar
494 canvas->translate(0.0, rc2.h);
495 rc2.h = alphaBarBounds.h;
496
497 SkRuntimeShaderBuilder builder3(m_alphaEffect);
498 builder3.uniform("iRes") = SkV3{float(rc2.w), float(rc2.h), 0.0f};
499 builder3.uniform("iColor") = appColor_to_SkV4(m_color);
500 builder3.uniform("iBg1") = gfxColor_to_SkV4(grid_color1());
501 builder3.uniform("iBg2") = gfxColor_to_SkV4(grid_color2());
502 p.setShader(builder3.makeShader());
503
504 canvas->drawRect(SkRect::MakeXYWH(0, 0, rc2.w, rc2.h), p);
505 }
506 canvas->restore();
507
508 // We already painted all areas
509 m_paintFlags = 0;
510 }
511 else
512#endif // SK_ENABLE_SKSL
513 {
514 painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
515 }
516
517 if (painterSurface)
518 g->drawSurface(painterSurface, rc.x, rc.y);
519
520 rc.h -= bottomBarBounds.h + alphaBarBounds.h;
521 onPaintMainArea(g, rc);
522
523 if (!bottomBarBounds.isEmpty()) {
524 bottomBarBounds.offset(-bounds().origin());
525 onPaintBottomBar(g, bottomBarBounds);
526 }
527
528 if (!alphaBarBounds.isEmpty()) {
529 alphaBarBounds.offset(-bounds().origin());
530 onPaintAlphaBar(g, alphaBarBounds);
531 }
532
533 if ((m_paintFlags & AllAreasFlag) != 0) {
534 m_paintFlags &= ~DoneFlag;
535 m_timer.start();
536
537 gfx::Point d = -rc.origin();
538 rc.offset(d);
539 if (!bottomBarBounds.isEmpty()) bottomBarBounds.offset(d);
540 if (!alphaBarBounds.isEmpty()) alphaBarBounds.offset(d);
541 painter.startBgPainting(this, rc, bottomBarBounds, alphaBarBounds);
542 }
543}
544
545void ColorSelector::onPaintAlphaBar(ui::Graphics* g, const gfx::Rect& rc)
546{
547 const double lit = m_color.getHslLightness();
548 const int alpha = m_color.getAlpha();
549 const gfx::Point pos(rc.x + int(rc.w * alpha / 255),
550 rc.y + rc.h/2);
551 paintColorIndicator(g, pos, lit < 0.5);
552}
553
554void ColorSelector::onPaintSurfaceInBgThread(
555 os::Surface* s,
556 const gfx::Rect& main,
557 const gfx::Rect& bottom,
558 const gfx::Rect& alpha,
559 bool& stop)
560{
561 if ((m_paintFlags & AlphaBarFlag) && !alpha.isEmpty()) {
562 draw_alpha_slider(s, alpha, m_color);
563 if (stop)
564 return;
565 m_paintFlags ^= AlphaBarFlag;
566 }
567}
568
569int ColorSelector::onNeedsSurfaceRepaint(const app::Color& newColor)
570{
571 return (m_color.getRed() != newColor.getRed() ||
572 m_color.getGreen() != newColor.getGreen() ||
573 m_color.getBlue() != newColor.getBlue() ? AlphaBarFlag: 0);
574}
575
576void ColorSelector::paintColorIndicator(ui::Graphics* g,
577 const gfx::Point& pos,
578 const bool white)
579{
580 auto theme = SkinTheme::get(this);
581 os::Surface* icon = theme->parts.colorWheelIndicator()->bitmap(0);
582
583 g->drawColoredRgbaSurface(
584 icon,
585 white ? gfx::rgba(255, 255, 255): gfx::rgba(0, 0, 0),
586 pos.x-icon->width()/2,
587 pos.y-icon->height()/2);
588}
589
590int ColorSelector::getCurrentAlphaForNewColor() const
591{
592 if (m_color.getType() != Color::MaskType)
593 return m_color.getAlpha();
594 else
595 return 255;
596}
597
598gfx::Rect ColorSelector::bottomBarBounds() const
599{
600 auto theme = SkinTheme::get(this);
601 const gfx::Rect rc = childrenBounds();
602 const int size = theme->dimensions.colorSelectorBarSize();
603 if (rc.h > 2*size) {
604 if (rc.h > 3*size) // Alpha bar is visible too
605 return gfx::Rect(rc.x, rc.y2()-size*2, rc.w, size);
606 else
607 return gfx::Rect(rc.x, rc.y2()-size, rc.w, size);
608 }
609 else
610 return gfx::Rect();
611}
612
613gfx::Rect ColorSelector::alphaBarBounds() const
614{
615 auto theme = SkinTheme::get(this);
616 const gfx::Rect rc = childrenBounds();
617 const int size = theme->dimensions.colorSelectorBarSize();
618 if (rc.h > 3*size)
619 return gfx::Rect(rc.x, rc.y2()-size, rc.w, size);
620 else
621 return gfx::Rect();
622}
623
624void ColorSelector::updateColorSpace()
625{
626 m_paintFlags |= AllAreasFlag;
627 invalidate();
628}
629
630#if SK_ENABLE_SKSL
631
632// static
633const char* ColorSelector::getAlphaBarShader()
634{
635 return R"(
636uniform half3 iRes;
637uniform half4 iColor, iBg1, iBg2;
638
639half4 main(vec2 fragcoord) {
640 vec2 d = (fragcoord.xy / iRes.xy);
641 half4 p = (mod((fragcoord.x / iRes.y) + floor(d.y+0.5), 2.0) > 1.0) ? iBg2: iBg1;
642 half4 q = iColor.rgb1;
643 float a = d.x;
644 return (1.0-a)*p + a*q;
645}
646)";
647}
648
649bool ColorSelector::buildEffects()
650{
651 if (!Preferences::instance().experimental.useShadersForColorSelectors())
652 return false;
653
654 if (!m_mainEffect) {
655 if (const char* code = getMainAreaShader())
656 m_mainEffect = buildEffect(code);
657 }
658
659 if (!m_bottomEffect) {
660 if (const char* code = getBottomBarShader())
661 m_bottomEffect = buildEffect(code);
662 }
663
664 if (!m_alphaEffect) {
665 if (const char* code = getAlphaBarShader())
666 m_alphaEffect = buildEffect(code);
667 }
668
669 return (m_mainEffect && m_bottomEffect && m_alphaEffect);
670}
671
672sk_sp<SkRuntimeEffect> ColorSelector::buildEffect(const char* code)
673{
674 auto result = SkRuntimeEffect::MakeForShader(SkString(code));
675 if (!result.errorText.isEmpty()) {
676 LOG(ERROR, "Shader error: %s\n", result.errorText.c_str());
677 std::printf("Shader error: %s\n", result.errorText.c_str());
678 return nullptr;
679 }
680 else {
681 return result.effect;
682 }
683}
684
685void ColorSelector::resetBottomEffect()
686{
687 m_bottomEffect.reset();
688}
689
690#endif // SK_ENABLE_SKSL
691
692} // namespace app
693