1// Aseprite
2// Copyright (C) 2020-2022 Igara Studio S.A.
3//
4// This program is distributed under the terms of
5// the End-User License Agreement for Aseprite.
6
7#ifdef HAVE_CONFIG_H
8#include "config.h"
9#endif
10
11#include "app/ui/dynamics_popup.h"
12
13#include "app/ui/dithering_selector.h"
14#include "app/ui/skin/skin_theme.h"
15#include "os/font.h"
16#include "os/surface.h"
17#include "ui/message.h"
18#include "ui/paint_event.h"
19#include "ui/scale.h"
20#include "ui/size_hint_event.h"
21#include "ui/widget.h"
22
23#include "dynamics.xml.h"
24
25#include <algorithm>
26
27namespace app {
28
29using namespace ui;
30using namespace skin;
31
32namespace {
33
34enum {
35 NONE,
36 PRESSURE_HEADER,
37 VELOCITY_HEADER,
38 SIZE_HEADER,
39 SIZE_WITH_PRESSURE,
40 SIZE_WITH_VELOCITY,
41 ANGLE_HEADER,
42 ANGLE_WITH_PRESSURE,
43 ANGLE_WITH_VELOCITY,
44 GRADIENT_HEADER,
45 GRADIENT_WITH_PRESSURE,
46 GRADIENT_WITH_VELOCITY,
47};
48
49} // anonymous namespace
50
51// Special slider to set the min/max threshold values of a sensor
52class DynamicsPopup::ThresholdSlider : public Widget {
53public:
54 ThresholdSlider() {
55 setExpansive(true);
56 initTheme();
57 }
58
59 float minThreshold() const { return m_minThreshold; }
60 float maxThreshold() const { return m_maxThreshold; }
61 void setSensorValue(float v) {
62 m_sensorValue = v;
63 invalidate();
64 }
65
66private:
67 void onInitTheme(InitThemeEvent& ev) override {
68 Widget::onInitTheme(ev);
69 auto theme = SkinTheme::get(this);
70 setBorder(
71 gfx::Border(
72 theme->parts.miniSliderEmpty()->bitmapW()->width(),
73 theme->parts.miniSliderEmpty()->bitmapN()->height(),
74 theme->parts.miniSliderEmpty()->bitmapE()->width(),
75 theme->parts.miniSliderEmpty()->bitmapS()->height()));
76 }
77
78 void onSizeHint(SizeHintEvent& ev) override {
79 ev.setSizeHint(
80 border().width(),
81 textHeight()+2*guiscale() + border().height());
82 }
83
84 void onPaint(PaintEvent& ev) override {
85 Graphics* g = ev.graphics();
86 auto theme = SkinTheme::get(this);
87 gfx::Rect rc = clientBounds();
88 gfx::Color bgcolor = bgColor();
89 g->fillRect(bgcolor, rc);
90
91 rc.shrink(border());
92 rc = clientBounds();
93
94 // Draw customized background
95 const skin::SkinPartPtr& nw = theme->parts.miniSliderEmpty();
96 os::Surface* thumb =
97 (hasFocus() ? theme->parts.miniSliderThumbFocused()->bitmap(0):
98 theme->parts.miniSliderThumb()->bitmap(0));
99
100 // Draw background
101 g->fillRect(bgcolor, rc);
102
103 // Draw thumb
104 int thumb_y = rc.y;
105 rc.shrink(gfx::Border(0, thumb->height(), 0, 0));
106
107 // Draw borders
108 if (rc.h > 4*guiscale()) {
109 rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale());
110 theme->drawRect(g, rc, nw.get());
111 }
112
113 const int minX = this->minX();
114 const int maxX = this->maxX();
115 const int sensorW = float(rc.w)*m_sensorValue;
116
117 // Draw background
118 if (m_minThreshold > 0.0f) {
119 theme->drawRect(
120 g, gfx::Rect(rc.x, rc.y, minX-rc.x, rc.h),
121 theme->parts.miniSliderFull().get());
122 }
123 if (m_maxThreshold < 1.0f) {
124 theme->drawRect(
125 g, gfx::Rect(maxX, rc.y, rc.x2()-maxX, rc.h),
126 theme->parts.miniSliderFull().get());
127 }
128
129 g->fillRect(theme->colors.sliderEmptyText(),
130 gfx::Rect(rc.x, rc.y+rc.h/2-rc.h/8, sensorW, rc.h/4));
131
132 g->drawRgbaSurface(thumb, minX-thumb->width()/2, thumb_y);
133 g->drawRgbaSurface(thumb, maxX-thumb->width()/2, thumb_y);
134 }
135
136 bool onProcessMessage(Message* msg) override {
137 switch (msg->type()) {
138
139 case kMouseDownMessage: {
140 auto mouseMsg = static_cast<MouseMessage*>(msg);
141 const int u = mouseMsg->position().x - (origin().x+border().left()+3*guiscale());
142 const int minX = this->minX();
143 const int maxX = this->maxX();
144 if (ABS(u-minX) <
145 ABS(u-maxX))
146 capture = Capture::Min;
147 else
148 capture = Capture::Max;
149 captureMouse();
150 break;
151 }
152
153 case kMouseUpMessage:
154 if (hasCapture())
155 releaseMouse();
156 break;
157
158 case kMouseMoveMessage: {
159 if (!hasCapture())
160 break;
161
162 auto mouseMsg = static_cast<MouseMessage*>(msg);
163 const gfx::Rect rc = bounds();
164 float u = (mouseMsg->position().x - rc.x) / float(rc.w);
165 u = std::clamp(u, 0.0f, 1.0f);
166 switch (capture) {
167 case Capture::Min:
168 m_minThreshold = u;
169 if (m_maxThreshold < u)
170 m_maxThreshold = u;
171 invalidate();
172 break;
173 case Capture::Max:
174 m_maxThreshold = u;
175 if (m_minThreshold > u)
176 m_minThreshold = u;
177 invalidate();
178 break;
179 }
180 break;
181 }
182 }
183 return Widget::onProcessMessage(msg);
184 }
185
186 int minX() const {
187 gfx::Rect rc = clientBounds();
188 rc.shrink(border());
189 rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale());
190 return rc.x + float(rc.w)*m_minThreshold;
191 }
192
193 int maxX() const {
194 gfx::Rect rc = clientBounds();
195 rc.shrink(border());
196 rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale());
197 return rc.x + float(rc.w)*m_maxThreshold;
198 }
199
200 enum Capture { Min, Max };
201
202 float m_minThreshold = 0.1f;
203 float m_sensorValue = 0.0f;
204 float m_maxThreshold = 0.9f;
205 Capture capture;
206};
207
208DynamicsPopup::DynamicsPopup(Delegate* delegate)
209 : PopupWindow("",
210 PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion,
211 PopupWindow::EnterBehavior::DoNothingOnEnter)
212 , m_delegate(delegate)
213 , m_dynamics(new gen::Dynamics)
214 , m_ditheringSel(new DitheringSelector(DitheringSelector::SelectMatrix))
215 , m_fromTo(tools::ColorFromTo::BgToFg)
216{
217 m_dynamics->stabilizer()->Click.connect(
218 [this](){
219 if (m_dynamics->stabilizer()->isSelected() &&
220 m_dynamics->stabilizerFactor()->getValue() == 0) {
221 // TODO default value when we enable stabilizer when it's zero
222 m_dynamics->stabilizerFactor()->setValue(16);
223 }
224 });
225 m_dynamics->stabilizerFactor()->Change.connect(
226 [this](){
227 m_dynamics->stabilizer()->setSelected(m_dynamics->stabilizerFactor()->getValue() > 0);
228 });
229
230 m_dynamics->values()->ItemChange.connect(
231 [this](ButtonSet::Item* item){
232 onValuesChange(item);
233 });
234 m_dynamics->maxSize()->Change.connect(
235 [this]{
236 m_delegate->setMaxSize(m_dynamics->maxSize()->getValue());
237 });
238 m_dynamics->maxAngle()->Change.connect(
239 [this]{
240 m_delegate->setMaxAngle(m_dynamics->maxAngle()->getValue());
241 });
242 m_dynamics->gradientFromTo()->Click.connect(
243 [this]{
244 if (m_fromTo == tools::ColorFromTo::BgToFg)
245 m_fromTo = tools::ColorFromTo::FgToBg;
246 else
247 m_fromTo = tools::ColorFromTo::BgToFg;
248 updateFromToText();
249 });
250 m_ditheringSel->OpenListBox.connect(
251 [this]{
252 if (auto comboboxWindow = m_ditheringSel->getWindowWidget()) {
253 m_hotRegion |= gfx::Region(comboboxWindow->boundsOnScreen());
254 setHotRegion(m_hotRegion);
255 }
256 });
257
258 m_dynamics->gradientPlaceholder()->addChild(m_ditheringSel);
259 m_dynamics->pressurePlaceholder()->addChild(m_pressureThreshold = new ThresholdSlider);
260 m_dynamics->velocityPlaceholder()->addChild(m_velocityThreshold = new ThresholdSlider);
261 addChild(m_dynamics);
262
263 onValuesChange(nullptr);
264}
265
266void DynamicsPopup::setOptionsGridVisibility(bool state)
267{
268 m_dynamics->grid()->setVisible(state);
269 if (isVisible())
270 expandWindow(sizeHint());
271}
272
273tools::DynamicsOptions DynamicsPopup::getDynamics() const
274{
275 tools::DynamicsOptions opts;
276
277 if (m_dynamics->stabilizer()->isSelected()) {
278 opts.stabilizerFactor = m_dynamics->stabilizerFactor()->getValue();
279 }
280 else {
281 opts.stabilizerFactor = 0;
282 }
283
284 opts.size =
285 (isCheck(SIZE_WITH_PRESSURE) ? tools::DynamicSensor::Pressure:
286 isCheck(SIZE_WITH_VELOCITY) ? tools::DynamicSensor::Velocity:
287 tools::DynamicSensor::Static);
288 opts.angle =
289 (isCheck(ANGLE_WITH_PRESSURE) ? tools::DynamicSensor::Pressure:
290 isCheck(ANGLE_WITH_VELOCITY) ? tools::DynamicSensor::Velocity:
291 tools::DynamicSensor::Static);
292 opts.gradient =
293 (isCheck(GRADIENT_WITH_PRESSURE) ? tools::DynamicSensor::Pressure:
294 isCheck(GRADIENT_WITH_VELOCITY) ? tools::DynamicSensor::Velocity:
295 tools::DynamicSensor::Static);
296 opts.minSize = m_dynamics->minSize()->getValue();
297 opts.minAngle = m_dynamics->minAngle()->getValue();
298 opts.ditheringMatrix = m_ditheringSel->ditheringMatrix();
299 opts.colorFromTo = m_fromTo;
300
301 opts.minPressureThreshold = m_pressureThreshold->minThreshold();
302 opts.maxPressureThreshold = m_pressureThreshold->maxThreshold();
303 opts.minVelocityThreshold = m_velocityThreshold->minThreshold();
304 opts.maxVelocityThreshold = m_velocityThreshold->maxThreshold();
305
306 return opts;
307}
308
309void DynamicsPopup::setCheck(int i, bool state)
310{
311 auto theme = SkinTheme::get(this);
312 m_dynamics->values()
313 ->getItem(i)
314 ->setIcon(state ? theme->parts.dropPixelsOk(): nullptr);
315}
316
317bool DynamicsPopup::isCheck(int i) const
318{
319 auto theme = SkinTheme::get(this);
320 return (m_dynamics->values()
321 ->getItem(i)
322 ->icon() == theme->parts.dropPixelsOk());
323}
324
325void DynamicsPopup::onValuesChange(ButtonSet::Item* item)
326{
327 auto theme = SkinTheme::get(this);
328 const skin::SkinPartPtr& ok = theme->parts.dropPixelsOk();
329 const int i = (item ? m_dynamics->values()->getItemIndex(item): -1);
330
331 // Switch item off
332 if (item && item->icon().get() == ok.get()) {
333 item->setIcon(nullptr);
334 }
335 else {
336 switch (i) {
337 case SIZE_WITH_PRESSURE:
338 case SIZE_WITH_VELOCITY:
339 setCheck(SIZE_WITH_PRESSURE, i == SIZE_WITH_PRESSURE);
340 setCheck(SIZE_WITH_VELOCITY, i == SIZE_WITH_VELOCITY);
341 break;
342 case ANGLE_WITH_PRESSURE:
343 case ANGLE_WITH_VELOCITY:
344 setCheck(ANGLE_WITH_PRESSURE, i == ANGLE_WITH_PRESSURE);
345 setCheck(ANGLE_WITH_VELOCITY, i == ANGLE_WITH_VELOCITY);
346 break;
347 case GRADIENT_WITH_PRESSURE:
348 case GRADIENT_WITH_VELOCITY:
349 setCheck(GRADIENT_WITH_PRESSURE, i == GRADIENT_WITH_PRESSURE);
350 setCheck(GRADIENT_WITH_VELOCITY, i == GRADIENT_WITH_VELOCITY);
351 break;
352 }
353 }
354
355 const bool hasPressure = (isCheck(SIZE_WITH_PRESSURE) ||
356 isCheck(ANGLE_WITH_PRESSURE) ||
357 isCheck(GRADIENT_WITH_PRESSURE));
358 const bool hasVelocity = (isCheck(SIZE_WITH_VELOCITY) ||
359 isCheck(ANGLE_WITH_VELOCITY) ||
360 isCheck(GRADIENT_WITH_VELOCITY));
361 const bool needsSize = (isCheck(SIZE_WITH_PRESSURE) ||
362 isCheck(SIZE_WITH_VELOCITY));
363 const bool needsAngle = (isCheck(ANGLE_WITH_PRESSURE) ||
364 isCheck(ANGLE_WITH_VELOCITY));
365 const bool needsGradient = (isCheck(GRADIENT_WITH_PRESSURE) ||
366 isCheck(GRADIENT_WITH_VELOCITY));
367 const bool any = (needsSize || needsAngle || needsGradient);
368 doc::BrushRef brush = m_delegate->getActiveBrush();
369
370 if (needsSize && !m_dynamics->minSize()->isVisible()) {
371 m_dynamics->minSize()->setValue(1);
372
373 int maxSize = brush->size();
374 if (maxSize == 1) {
375 // If brush size == 1, we put it to 4 so the user has some size
376 // change by default.
377 maxSize = 4;
378 m_delegate->setMaxSize(maxSize);
379 }
380 m_dynamics->maxSize()->setValue(maxSize);
381 }
382 m_dynamics->sizeLabel()->setVisible(needsSize);
383 m_dynamics->minSize()->setVisible(needsSize);
384 m_dynamics->maxSize()->setVisible(needsSize);
385
386 if (needsAngle && !m_dynamics->minAngle()->isVisible()) {
387 m_dynamics->minAngle()->setValue(brush->angle());
388 m_dynamics->maxAngle()->setValue(brush->angle());
389 }
390 m_dynamics->angleLabel()->setVisible(needsAngle);
391 m_dynamics->minAngle()->setVisible(needsAngle);
392 m_dynamics->maxAngle()->setVisible(needsAngle);
393
394 m_dynamics->gradientLabel()->setVisible(needsGradient);
395 m_dynamics->gradientPlaceholder()->setVisible(needsGradient);
396 m_dynamics->gradientFromTo()->setVisible(needsGradient);
397 updateFromToText();
398
399 m_dynamics->separator()->setVisible(any);
400 m_dynamics->options()->setVisible(any);
401 m_dynamics->separator2()->setVisible(any);
402 m_dynamics->pressureLabel()->setVisible(hasPressure);
403 m_dynamics->pressurePlaceholder()->setVisible(hasPressure);
404 m_dynamics->velocityLabel()->setVisible(hasVelocity);
405 m_dynamics->velocityPlaceholder()->setVisible(hasVelocity);
406
407 expandWindow(sizeHint());
408
409 m_hotRegion |= gfx::Region(boundsOnScreen());
410 setHotRegion(m_hotRegion);
411}
412
413void DynamicsPopup::updateFromToText()
414{
415 m_dynamics->gradientFromTo()->setText(
416 m_fromTo == tools::ColorFromTo::BgToFg ? "BG > FG":
417 m_fromTo == tools::ColorFromTo::FgToBg ? "FG > BG": "-");
418}
419
420void DynamicsPopup::updateWidgetsWithBrush()
421{
422 doc::BrushRef brush = m_delegate->getActiveBrush();
423 m_dynamics->maxSize()->setValue(brush->size());
424 m_dynamics->maxAngle()->setValue(brush->angle());
425}
426
427bool DynamicsPopup::onProcessMessage(Message* msg)
428{
429 switch (msg->type()) {
430
431 case kOpenMessage:
432 m_hotRegion = gfx::Region(boundsOnScreen());
433 setHotRegion(m_hotRegion);
434 manager()->addMessageFilter(kMouseMoveMessage, this);
435 manager()->addMessageFilter(kMouseDownMessage, this);
436 disableFlags(IGNORE_MOUSE);
437
438 updateWidgetsWithBrush();
439 break;
440
441 case kCloseMessage:
442 m_hotRegion.clear();
443 manager()->removeMessageFilter(kMouseMoveMessage, this);
444 manager()->removeMessageFilter(kMouseDownMessage, this);
445 break;
446
447 case kMouseEnterMessage:
448 m_velocity.reset();
449 break;
450
451 case kMouseMoveMessage: {
452 auto mouseMsg = static_cast<MouseMessage*>(msg);
453
454 if (mouseMsg->pointerType() == PointerType::Pen ||
455 mouseMsg->pointerType() == PointerType::Eraser) {
456 if (m_dynamics->pressurePlaceholder()->isVisible()) {
457 m_pressureThreshold->setSensorValue(mouseMsg->pressure());
458 }
459 }
460
461 if (m_dynamics->velocityPlaceholder()->isVisible()) {
462 m_velocity.updateWithDisplayPoint(mouseMsg->position());
463
464 float v = m_velocity.velocity().magnitude()
465 / tools::VelocitySensor::kScreenPixelsForFullVelocity;
466 v = std::clamp(v, 0.0f, 1.0f);
467
468 m_velocityThreshold->setSensorValue(v);
469 }
470 break;
471 }
472
473 case kMouseDownMessage: {
474 if (!msg->display())
475 break;
476
477 auto mouseMsg = static_cast<const MouseMessage*>(msg);
478 auto screenPos = mouseMsg->screenPosition();
479 auto picked = manager()->pickFromScreenPos(screenPos);
480 if ((picked == nullptr) ||
481 (picked->window() != this &&
482 picked->window() != m_ditheringSel->getWindowWidget())) {
483 closeWindow(nullptr);
484 }
485 break;
486 }
487
488 }
489 return PopupWindow::onProcessMessage(msg);
490}
491
492} // namespace app
493