1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/color_shades.h"
13
14#include "app/app.h"
15#include "app/modules/gfx.h"
16#include "app/modules/palettes.h"
17#include "app/shade.h"
18#include "app/ui/color_bar.h"
19#include "app/ui/skin/skin_theme.h"
20#include "doc/color_mode.h"
21#include "doc/palette.h"
22#include "doc/palette_picks.h"
23#include "doc/remap.h"
24#include "ui/graphics.h"
25#include "ui/message.h"
26#include "ui/paint_event.h"
27#include "ui/size_hint_event.h"
28#include "ui/system.h"
29
30#include <algorithm>
31
32namespace app {
33
34ColorShades::ColorShades(const Shade& colors, ClickType click)
35 : Widget(ui::kGenericWidget)
36 , m_click(click)
37 , m_shade(colors)
38 , m_minColors(1)
39 , m_hotIndex(-1)
40 , m_dragIndex(-1)
41 , m_boxSize(12)
42{
43 setText("No colors");
44 initTheme();
45}
46
47void ColorShades::setMinColors(int minColors)
48{
49 m_minColors = minColors;
50 invalidate();
51}
52
53void ColorShades::reverseShadeColors()
54{
55 std::reverse(m_shade.begin(), m_shade.end());
56 invalidate();
57}
58
59doc::Remap* ColorShades::createShadeRemap(bool left)
60{
61 // We need two or more colors to create a shading remap. In
62 // other case, the ShadingInkProcessing will use the full
63 // color palette.
64 Shade colors = getShade();
65 if (colors.size() <= 1)
66 return nullptr;
67
68 std::unique_ptr<doc::Remap> remap(
69 new doc::Remap(get_current_palette()->size()));
70
71 for (int i=0; i<remap->size(); ++i)
72 remap->map(i, i);
73
74 if (left) {
75 for (int i=1; i<int(colors.size()); ++i) {
76 int j = colors[i].getIndex();
77 if (j >= 0 && j < remap->size())
78 remap->map(j, colors[i-1].getIndex());
79 }
80 }
81 else {
82 for (int i=0; i<int(colors.size())-1; ++i) {
83 int j = colors[i].getIndex();
84 if (j >= 0 && j < remap->size())
85 remap->map(j, colors[i+1].getIndex());
86 }
87 }
88 return remap.release();
89}
90
91void ColorShades::setShade(const Shade& shade)
92{
93 m_shade = shade;
94 invalidate();
95 parent()->parent()->layout();
96}
97
98void ColorShades::onInitTheme(ui::InitThemeEvent& ev)
99{
100 Widget::onInitTheme(ev);
101
102 auto theme = skin::SkinTheme::get(this);
103
104 switch (m_click) {
105 case ClickEntries:
106 case DragAndDropEntries:
107 setStyle(theme->styles.normalShadeView());
108 break;
109 case ClickWholeShade:
110 setStyle(theme->styles.menuShadeView());
111 break;
112 }
113}
114
115bool ColorShades::onProcessMessage(ui::Message* msg)
116{
117 switch (msg->type()) {
118
119 case ui::kSetCursorMessage:
120 if (hasCapture()) {
121 ui::set_mouse_cursor(ui::kMoveCursor);
122 return true;
123 }
124 else if (m_click == ClickEntries &&
125 m_hotIndex >= 0 &&
126 m_hotIndex < int(m_shade.size())) {
127 ui::set_mouse_cursor(ui::kHandCursor);
128 return true;
129 }
130 break;
131
132 case ui::kMouseEnterMessage:
133 case ui::kMouseLeaveMessage:
134 if (!hasCapture())
135 m_hotIndex = -1;
136
137 invalidate();
138 break;
139
140 case ui::kMouseDownMessage:
141 if (m_hotIndex >= 0 &&
142 m_hotIndex < int(m_shade.size())) {
143 switch (m_click) {
144 case ClickEntries: {
145 ClickEvent ev(static_cast<ui::MouseMessage*>(msg)->button());
146 Click(ev);
147
148 m_hotIndex = -1;
149 invalidate();
150 break;
151 }
152 case DragAndDropEntries:
153 m_dragIndex = m_hotIndex;
154 m_dropBefore = false;
155 captureMouse();
156 break;
157 }
158 }
159 break;
160
161 case ui::kMouseUpMessage: {
162 auto button = static_cast<ui::MouseMessage*>(msg)->button();
163
164 if (m_click == ClickWholeShade) {
165 setSelected(true);
166
167 ClickEvent ev(button);
168 Click(ev);
169
170 closeWindow();
171 }
172
173 if (m_dragIndex >= 0) {
174 ASSERT(m_dragIndex < int(m_shade.size()));
175
176 auto color = m_shade[m_dragIndex];
177 m_shade.erase(m_shade.begin()+m_dragIndex);
178 if (m_hotIndex >= 0)
179 m_shade.insert(m_shade.begin()+m_hotIndex, color);
180
181 m_dragIndex = -1;
182 invalidate();
183
184 ClickEvent ev(button);
185 Click(ev);
186
187 // Relayout the context bar if we have removed an entry.
188 //
189 // TODO it looks like this should be handled in some kind of
190 // Change() event in the ColorBar
191 if (m_hotIndex < 0 &&
192 parent() &&
193 parent()->parent()) {
194 parent()->parent()->layout();
195 }
196 }
197
198 if (hasCapture())
199 releaseMouse();
200 break;
201 }
202
203 case ui::kMouseMoveMessage: {
204 ui::MouseMessage* mouseMsg = static_cast<ui::MouseMessage*>(msg);
205 gfx::Point mousePos = mouseMsg->position() - bounds().origin();
206 gfx::Rect bounds = clientBounds();
207 int hot = -1;
208
209 bounds.shrink(3*ui::guiscale());
210
211 if (bounds.contains(mousePos)) {
212 int count = std::max(1, size());
213 int boxWidth = std::max(1, bounds.w / count);
214 hot = (mousePos.x - bounds.x) / boxWidth;
215 hot = std::clamp(hot, 0, count-1);
216 }
217
218 if (m_hotIndex != hot) {
219 m_hotIndex = hot;
220 invalidate();
221 }
222
223 bool dropBefore =
224 (hot >= 0 && mousePos.x < (bounds.x+m_boxSize*ui::guiscale()*hot)+m_boxSize*ui::guiscale()/2);
225 if (m_dropBefore != dropBefore) {
226 m_dropBefore = dropBefore;
227 invalidate();
228 }
229 break;
230 }
231 }
232 return Widget::onProcessMessage(msg);
233}
234
235void ColorShades::onSizeHint(ui::SizeHintEvent& ev)
236{
237 int size = this->size();
238 if (size < 2)
239 ev.setSizeHint(gfx::Size((16+m_boxSize)*ui::guiscale()+textWidth(), 18*ui::guiscale()));
240 else {
241 if (m_click == ClickWholeShade && size > 16)
242 size = 16;
243 ev.setSizeHint(gfx::Size(6+m_boxSize*size, 18)*ui::guiscale());
244 }
245}
246
247void ColorShades::onPaint(ui::PaintEvent& ev)
248{
249 auto theme = skin::SkinTheme::get(this);
250 ui::Graphics* g = ev.graphics();
251 gfx::Rect bounds = clientBounds();
252
253 theme->paintWidget(g, this, style(), bounds);
254
255 bounds.shrink(3*ui::guiscale());
256
257 Shade colors = getShade();
258 if (colors.size() >= m_minColors) {
259 gfx::Rect box(bounds.x, bounds.y,
260 bounds.w / std::max(1, int(colors.size())),
261 bounds.h);
262 gfx::Rect hotBounds;
263
264 int j = 0;
265 for (int i=0; box.x<bounds.x2(); ++i, box.x += box.w) {
266 // Make the last box a little bigger to just use all
267 // available size
268 if (i == int(colors.size())-1)
269 box.w = bounds.x2()-box.x;
270
271 app::Color color;
272
273 if (m_dragIndex >= 0 &&
274 m_hotIndex == i) {
275 color = colors[m_dragIndex];
276 }
277 else {
278 if (j == m_dragIndex) {
279 ++j;
280 }
281 if (j < int(colors.size()))
282 color = colors[j++];
283 else
284 color = app::Color::fromMask();
285 }
286
287 draw_color(g, box, color,
288 (doc::ColorMode)app_get_current_pixel_format());
289
290 if (m_hotIndex == i)
291 hotBounds = box;
292 }
293
294 if (!hotBounds.isEmpty() &&
295 isHotEntryVisible()) {
296 hotBounds.enlarge(3*ui::guiscale());
297
298 ui::PaintWidgetPartInfo info;
299 theme->paintWidgetPart(
300 g, theme->styles.shadeSelection(), hotBounds, info);
301 }
302 }
303 else {
304 g->fillRect(theme->colors.editorFace(), bounds);
305 g->drawAlignedUIText(text(), theme->colors.face(), gfx::ColorNone, bounds,
306 ui::CENTER | ui::MIDDLE);
307 }
308}
309
310} // namespace app
311