1// Aseprite
2// Copyright (C) 2019-2021 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#define BP_TRACE(...) // TRACEARGS(__VA_ARGS__)
9
10#ifdef HAVE_CONFIG_H
11#include "config.h"
12#endif
13
14#include "app/ui/editor/brush_preview.h"
15
16#include "app/app.h"
17#include "app/color.h"
18#include "app/color_utils.h"
19#include "app/doc.h"
20#include "app/site.h"
21#include "app/tools/controller.h"
22#include "app/tools/ink.h"
23#include "app/tools/intertwine.h"
24#include "app/tools/point_shape.h"
25#include "app/tools/tool.h"
26#include "app/tools/tool_loop.h"
27#include "app/ui/color_bar.h"
28#include "app/ui/context_bar.h"
29#include "app/ui/editor/editor.h"
30#include "app/ui/editor/tool_loop_impl.h"
31#include "app/ui_context.h"
32#include "app/util/wrap_value.h"
33#include "base/debug.h"
34#include "base/scoped_value.h"
35#include "doc/algo.h"
36#include "doc/blend_internals.h"
37#include "doc/brush.h"
38#include "doc/cel.h"
39#include "doc/image_impl.h"
40#include "doc/layer.h"
41#include "doc/primitives.h"
42#include "os/surface.h"
43#include "os/system.h"
44#include "os/window.h"
45#include "render/render.h"
46#include "ui/manager.h"
47#include "ui/system.h"
48
49#include <array>
50
51namespace app {
52
53using namespace doc;
54
55static int g_crosshair_pattern[7*7] = {
56 0, 0, 0, 1, 0, 0, 0,
57 0, 0, 0, 1, 0, 0, 0,
58 0, 0, 0, 0, 0, 0, 0,
59 1, 1, 0, 0, 0, 1, 1,
60 0, 0, 0, 0, 0, 0, 0,
61 0, 0, 0, 1, 0, 0, 0,
62 0, 0, 0, 1, 0, 0, 0,
63};
64
65// We're going to keep a cache of native mouse cursor for each
66// possibility of the following crosshair:
67//
68// 2
69// 2
70// 22 3 22
71// 2
72// 2
73//
74// Number of crosshair cursors 2^8 * 3 + 2 = 770
75// Here the center can be black, white or hidden. When the center is
76// black or white, the crosshair can be empty.
77//
78// The index/key of this array is calculated in
79// BrushPreview::createCrosshairCursor().
80//
81// Win32: This is needed to avoid converting from a os::Surface ->
82// HCURSOR calling CreateIconIndirect() as many times as possible for
83// each mouse movement (because it's a slow function).
84//
85static std::array<os::CursorRef, 770> g_bwCursors;
86static int g_cacheCursorScale = 0;
87
88// 3 cached cursors when we use a solid cursor (1 dot, crosshair
89// without dot at the center, crosshair with dot in the center)
90static std::array<os::CursorRef, 3> g_solidCursors;
91static gfx::Color g_solidCursorColor = gfx::ColorNone;
92
93// static
94void BrushPreview::destroyInternals()
95{
96 g_bwCursors.fill(nullptr);
97 g_solidCursors.fill(nullptr);
98}
99
100BrushPreview::BrushPreview(Editor* editor)
101 : m_editor(editor)
102{
103}
104
105BrushPreview::~BrushPreview()
106{
107}
108
109BrushRef BrushPreview::getCurrentBrush()
110{
111 return App::instance()
112 ->contextBar()
113 ->activeBrush(m_editor->getCurrentEditorTool());
114}
115
116// static
117color_t BrushPreview::getBrushColor(Sprite* sprite, Layer* layer)
118{
119 app::Color c = Preferences::instance().colorBar.fgColor();
120 ASSERT(sprite != NULL);
121
122 // Avoid using invalid colors
123 if (!c.isValid())
124 return 0;
125
126 if (layer != NULL)
127 return color_utils::color_for_layer(c, layer);
128 else
129 return color_utils::color_for_image(c, sprite->pixelFormat());
130}
131
132// Draws the brush cursor in the specified absolute mouse position
133// given in 'pos' param. Warning: You should clean the cursor before
134// to use this routine with other editor.
135//
136// TODO the logic in this function is really complex, we should think
137// a way to simplify all possibilities
138void BrushPreview::show(const gfx::Point& screenPos)
139{
140 if (m_onScreen)
141 hide();
142
143 Doc* document = m_editor->document();
144 Sprite* sprite = m_editor->sprite();
145 Layer* layer = (m_editor->layer() &&
146 m_editor->layer()->isImage() ? m_editor->layer():
147 nullptr);
148 ASSERT(sprite);
149
150 // Get drawable region
151 m_editor->getDrawableRegion(m_clippingRegion, ui::Widget::kCutTopWindows);
152
153 // Remove the invalidated region in the editor.
154 m_clippingRegion.createSubtraction(m_clippingRegion,
155 m_editor->getUpdateRegion());
156
157 // Get cursor color
158 const auto& pref = Preferences::instance();
159 app::Color appCursorColor = pref.cursor.cursorColor();
160 m_blackAndWhiteNegative = (appCursorColor.getType() == app::Color::MaskType);
161
162 // Cursor in the screen (view)
163 m_screenPosition = screenPos;
164
165 // Get cursor position in the editor
166 gfx::Point spritePos = m_editor->screenToEditor(screenPos);
167
168 // Get the current tool
169 tools::Ink* ink = m_editor->getCurrentEditorInk();
170
171 // Get current tilemap mode
172 TilemapMode tilemapMode = ColorBar::instance()->tilemapMode();
173
174 const bool isFloodfill = m_editor->getCurrentEditorTool()->getPointShape(0)->isFloodFill();
175 const auto& dynamics = App::instance()->contextBar()->getDynamics();
176
177 // Setup the cursor type depending on several factors (current tool,
178 // foreground color, layer transparency, brush size, etc.).
179 BrushRef brush = getCurrentBrush();
180 color_t brush_color = getBrushColor(sprite, layer);
181 color_t mask_index = sprite->transparentColor();
182
183 if (brush->type() != doc::kImageBrushType &&
184 (dynamics.size != tools::DynamicSensor::Static ||
185 dynamics.angle != tools::DynamicSensor::Static)) {
186 brush.reset(
187 new Brush(
188 brush->type(),
189 (dynamics.size != tools::DynamicSensor::Static ? dynamics.minSize: brush->size()),
190 (dynamics.angle != tools::DynamicSensor::Static ? dynamics.minAngle: brush->angle())));
191 }
192
193 if (ink->isSelection() || ink->isSlice()) {
194 m_type = SELECTION_CROSSHAIR;
195 }
196 else if (
197 (tilemapMode == TilemapMode::Pixels) &&
198 (brush->type() == kImageBrushType ||
199 ((isFloodfill ? 1: brush->size()) > (1.0 / m_editor->zoom().scale()))) &&
200 (// Use cursor bounds for inks that are effects (eraser, blur, etc.)
201 (ink->isEffect()) ||
202 // or when the brush color is transparent and we are not in the background layer
203 (!ink->isShading() &&
204 (layer && layer->isTransparent()) &&
205 ((sprite->pixelFormat() == IMAGE_INDEXED && brush_color == mask_index) ||
206 (sprite->pixelFormat() == IMAGE_RGB && rgba_geta(brush_color) == 0) ||
207 (sprite->pixelFormat() == IMAGE_GRAYSCALE && graya_geta(brush_color) == 0))))) {
208 m_type = BRUSH_BOUNDARIES;
209 }
210 else {
211 m_type = CROSSHAIR;
212 }
213
214 bool showPreview = false;
215 bool showPreviewWithEdges = false;
216 bool cancelEdges = false;
217 auto brushPreview = pref.cursor.brushPreview();
218 if (!m_editor->docPref().show.brushPreview())
219 brushPreview = app::gen::BrushPreview::NONE;
220
221 switch (brushPreview) {
222 case app::gen::BrushPreview::NONE:
223 m_type = CROSSHAIR;
224 break;
225 case app::gen::BrushPreview::EDGES:
226 m_type = BRUSH_BOUNDARIES;
227 break;
228 case app::gen::BrushPreview::FULL:
229 case app::gen::BrushPreview::FULLALL:
230 case app::gen::BrushPreview::FULLNEDGES:
231 showPreview = m_editor->getState()->requireBrushPreview();
232 switch (brushPreview) {
233 case app::gen::BrushPreview::FULLALL:
234 if (showPreview)
235 m_type = CROSSHAIR;
236 cancelEdges = true;
237 break;
238 case app::gen::BrushPreview::FULLNEDGES:
239 if (showPreview)
240 showPreviewWithEdges = true;
241 break;
242 }
243 break;
244 }
245
246 if (m_type & SELECTION_CROSSHAIR)
247 showPreview = false;
248
249 // When the extra cel is locked (e.g. we are flashing the active
250 // layer) we don't show the brush preview temporally.
251 if (showPreview && m_editor->isExtraCelLocked()) {
252 showPreview = false;
253 showPreviewWithEdges = false;
254 cancelEdges = false;
255 m_type |= BRUSH_BOUNDARIES;
256 }
257
258 // Use a simple cross
259 if (pref.cursor.paintingCursorType() == gen::PaintingCursorType::SIMPLE_CROSSHAIR) {
260 m_type &= ~(CROSSHAIR | SELECTION_CROSSHAIR);
261 m_type |= NATIVE_CROSSHAIR;
262 }
263
264 // For cursor type 'bounds' we have to generate cursor boundaries
265 if (m_type & BRUSH_BOUNDARIES) {
266 if (brush->type() != kImageBrushType)
267 showPreview = showPreviewWithEdges;
268 if (cancelEdges)
269 m_type &= ~BRUSH_BOUNDARIES;
270 }
271 if (m_type & BRUSH_BOUNDARIES)
272 generateBoundaries();
273
274 // Draw pixel/brush preview
275 if (showPreview) {
276 Site site = m_editor->getSite();
277
278 // TODO add support for "tile-brushes"
279 gfx::Rect origBrushBounds =
280 (isFloodfill || site.tilemapMode() == TilemapMode::Tiles ? gfx::Rect(0, 0, 1, 1):
281 brush->bounds());
282 gfx::Rect brushBounds = origBrushBounds;
283 brushBounds.offset(spritePos);
284 gfx::Rect extraCelBoundsInCanvas = brushBounds;
285
286 // Tiled mode might require a bigger extra cel (to show the tiled)
287 if (int(m_editor->docPref().tiled.mode()) & int(filters::TiledMode::X_AXIS)) {
288 brushBounds.x = wrap_value(brushBounds.x, sprite->width());
289 extraCelBoundsInCanvas.x = brushBounds.x;
290 if ((extraCelBoundsInCanvas.x < 0 && extraCelBoundsInCanvas.x2() > 0) ||
291 (extraCelBoundsInCanvas.x < sprite->width() && extraCelBoundsInCanvas.x2() > sprite->width())) {
292 extraCelBoundsInCanvas.x = 0;
293 extraCelBoundsInCanvas.w = sprite->width();
294 }
295 }
296 if (int(m_editor->docPref().tiled.mode()) & int(filters::TiledMode::Y_AXIS)) {
297 brushBounds.y = wrap_value(brushBounds.y, sprite->height());
298 extraCelBoundsInCanvas.y = brushBounds.y;
299 if ((extraCelBoundsInCanvas.y < 0 && extraCelBoundsInCanvas.y2() > 0) ||
300 (extraCelBoundsInCanvas.y < sprite->height() && extraCelBoundsInCanvas.y2() > sprite->height())) {
301 extraCelBoundsInCanvas.y = 0;
302 extraCelBoundsInCanvas.h = sprite->height();
303 }
304 }
305
306 gfx::Rect extraCelBounds;
307 if (site.tilemapMode() == TilemapMode::Tiles) {
308 ASSERT(layer->isTilemap());
309 doc::Grid grid = site.grid();
310 extraCelBounds = grid.canvasToTile(extraCelBoundsInCanvas);
311 extraCelBoundsInCanvas = grid.tileToCanvas(extraCelBounds);
312 }
313 else {
314 extraCelBounds = extraCelBoundsInCanvas;
315 }
316
317 BP_TRACE("BrushPreview:",
318 "brushBounds", brushBounds,
319 "extraCelBounds", extraCelBounds,
320 "extraCelBoundsInCanvas", extraCelBoundsInCanvas);
321
322 // Create the extra cel to show the brush preview
323 Cel* cel = site.cel();
324
325 int t, opacity = 255;
326 if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t);
327 if (layer) opacity = MUL_UN8(opacity, static_cast<LayerImage*>(layer)->opacity(), t);
328
329 if (!m_extraCel)
330 m_extraCel.reset(new ExtraCel);
331
332 m_extraCel->create(
333 site.tilemapMode(),
334 document->sprite(),
335 extraCelBoundsInCanvas,
336 extraCelBounds.size(),
337 site.frame(),
338 opacity);
339 m_extraCel->setType(render::ExtraType::NONE);
340 m_extraCel->setBlendMode(
341 (layer ? static_cast<LayerImage*>(layer)->blendMode():
342 BlendMode::NORMAL));
343
344 document->setExtraCel(m_extraCel);
345
346 Image* extraImage = m_extraCel->image();
347 if (extraImage->pixelFormat() == IMAGE_TILEMAP) {
348 extraImage->setMaskColor(notile);
349 clear_image(extraImage, notile);
350 }
351 else {
352 extraImage->setMaskColor(mask_index);
353 clear_image(extraImage,
354 (extraImage->pixelFormat() == IMAGE_INDEXED ? mask_index: 0));
355 }
356
357 if (layer) {
358 render::Render().renderLayer(
359 extraImage, layer, site.frame(),
360 gfx::Clip(0, 0, extraCelBoundsInCanvas),
361 BlendMode::SRC);
362
363 // This extra cel is a patch for the current layer/frame
364 m_extraCel->setType(render::ExtraType::PATCH);
365 }
366
367 {
368 std::unique_ptr<tools::ToolLoop> loop(
369 create_tool_loop_preview(
370 m_editor, brush, extraImage,
371 extraCelBounds.origin()));
372 if (loop) {
373 loop->getInk()->prepareInk(loop.get());
374 loop->getController()->prepareController(loop.get());
375 loop->getIntertwine()->prepareIntertwine(loop.get());
376 loop->getPointShape()->preparePointShape(loop.get());
377
378 tools::Stroke::Pt pt(brushBounds.x-origBrushBounds.x,
379 brushBounds.y-origBrushBounds.y);
380 pt.size = brush->size();
381 pt.angle = brush->angle();
382 loop->getPointShape()->transformPoint(loop.get(), pt);
383 }
384 }
385
386 document->notifySpritePixelsModified(
387 sprite, gfx::Region(m_lastBounds = extraCelBoundsInCanvas),
388 m_lastFrame = site.frame());
389
390 m_withRealPreview = true;
391 }
392
393 // Save area and draw the cursor
394 if (!(m_type & NATIVE_CROSSHAIR) ||
395 (m_type & BRUSH_BOUNDARIES)) {
396 ui::ScreenGraphics g(m_editor->display());
397 ui::SetClip clip(&g);
398 gfx::Color uiCursorColor = color_utils::color_for_ui(appCursorColor);
399
400 if (!(m_type & NATIVE_CROSSHAIR)) {
401 createCrosshairCursor(&g, uiCursorColor);
402 }
403
404 forEachBrushPixel(&g, spritePos, uiCursorColor, &BrushPreview::savePixelDelegate);
405 forEachBrushPixel(&g, spritePos, uiCursorColor, &BrushPreview::drawPixelDelegate);
406 m_withModifiedPixels = true;
407 }
408
409 // Cursor in the editor (model)
410 m_onScreen = true;
411 m_editorPosition = spritePos;
412
413 // Save the clipping-region to know where to clean the pixels
414 m_oldClippingRegion = m_clippingRegion;
415
416 if (m_type & NATIVE_CROSSHAIR)
417 ui::set_mouse_cursor(ui::kCrosshairCursor);
418}
419
420// Cleans the brush cursor from the specified editor.
421//
422// The mouse position is got from the last call to showBrushPreview()
423// (m_cursorEditor). So you must to use this routine only if you
424// called showBrushPreview() before.
425void BrushPreview::hide()
426{
427 if (!m_onScreen)
428 return;
429
430 // Don't hide the cursor to avoid flickering, the native mouse
431 // cursor will be changed anyway after the hide() by the caller.
432 //
433 //if (m_cursor)
434 // m_editor->display()->nativeWindow()->setCursor(os::NativeCursor::Hidden);
435
436 // Get drawable region
437 m_editor->getDrawableRegion(m_clippingRegion, ui::Widget::kCutTopWindows);
438
439 // Remove the invalidated region in the editor.
440 m_clippingRegion.createSubtraction(m_clippingRegion,
441 m_editor->getUpdateRegion());
442
443 if (m_withModifiedPixels) {
444 // Restore pixels
445 ui::ScreenGraphics g(m_editor->display());
446 ui::SetClip clip(&g);
447 forEachBrushPixel(&g, m_editorPosition, gfx::ColorNone,
448 &BrushPreview::clearPixelDelegate);
449 }
450
451 // Clean pixel/brush preview
452 if (m_withRealPreview) {
453 Doc* document = m_editor->document();
454 doc::Sprite* sprite = m_editor->sprite();
455
456 ASSERT(document);
457 ASSERT(sprite);
458
459 if (document && sprite) {
460 document->setExtraCel(ExtraCelRef(nullptr));
461 document->notifySpritePixelsModified(
462 sprite, gfx::Region(m_lastBounds), m_lastFrame);
463 }
464
465 m_withRealPreview = false;
466 }
467
468 m_onScreen = false;
469
470 m_clippingRegion.clear();
471 m_oldClippingRegion.clear();
472}
473
474void BrushPreview::discardBrushPreview()
475{
476 Doc* document = m_editor->document();
477 ASSERT(document);
478
479 if (document && m_onScreen && m_withRealPreview) {
480 document->setExtraCel(ExtraCelRef(nullptr));
481 }
482}
483
484void BrushPreview::redraw()
485{
486 if (m_onScreen) {
487 gfx::Point screenPos = m_screenPosition;
488 hide();
489 show(screenPos);
490 }
491}
492
493void BrushPreview::invalidateRegion(const gfx::Region& region)
494{
495 m_clippingRegion.createSubtraction(m_clippingRegion, region);
496}
497
498void BrushPreview::generateBoundaries()
499{
500 BrushRef brush = getCurrentBrush();
501
502 if (!m_brushBoundaries.isEmpty() &&
503 m_brushGen == brush->gen())
504 return;
505
506 const bool isOnePixel =
507 (m_editor->getCurrentEditorTool()->getPointShape(0)->isPixel() ||
508 m_editor->getCurrentEditorTool()->getPointShape(0)->isFloodFill());
509 Image* brushImage = brush->image();
510 m_brushGen = brush->gen();
511
512 Image* mask = nullptr;
513 bool deleteMask = true;
514 if (isOnePixel) {
515 mask = Image::create(IMAGE_BITMAP, 1, 1);
516 mask->putPixel(0, 0, (color_t)1);
517 }
518 else if (brushImage->pixelFormat() != IMAGE_BITMAP) {
519 ASSERT(brush->maskBitmap());
520 deleteMask = false;
521 mask = brush->maskBitmap();
522 }
523
524 m_brushBoundaries.regen(mask ? mask: brushImage);
525 if (!isOnePixel)
526 m_brushBoundaries.offset(-brush->center().x,
527 -brush->center().y);
528
529 if (deleteMask)
530 delete mask;
531}
532
533void BrushPreview::createCrosshairCursor(ui::Graphics* g,
534 const gfx::Color cursorColor)
535{
536 ASSERT(!(m_type & NATIVE_CROSSHAIR));
537
538 gfx::Rect cursorBounds;
539 gfx::Point cursorCenter;
540
541 // Depending on the editor zoom, maybe we need subpixel movement (a
542 // little dot inside the active pixel)
543 const bool requireLittleCenterDot = (m_editor->zoom().scale() >= 4.0);
544
545 if (m_type & CROSSHAIR) {
546 // Regular crosshair of 7x7
547 cursorBounds |= gfx::Rect(-3, -3, 7, 7);
548 cursorCenter = -cursorBounds.origin();
549 }
550 else if (requireLittleCenterDot) {
551 // Special case of a cursor for one pixel
552 cursorBounds = gfx::Rect(0, 0, 1, 1);
553 cursorCenter = gfx::Point(0, 0);
554 }
555 else {
556 // TODO should we use ui::set_mouse_cursor()?
557 ui::set_mouse_cursor_reset_info();
558 m_editor->display()->nativeWindow()->setCursor(os::NativeCursor::Hidden);
559 return;
560 }
561
562 os::Window* window = m_editor->display()->nativeWindow();
563 const int scale = window->scale();
564 os::CursorRef cursor = nullptr;
565
566 // Invalidate the entire cache if the scale has changed
567 if (g_cacheCursorScale != scale) {
568 g_cacheCursorScale = scale;
569 g_bwCursors.fill(nullptr);
570 g_solidCursors.fill(nullptr);
571 }
572
573 // Cursor with black/white colors (we create a key/index for
574 // g_cachedCursors depending on the colors on the screen)
575 if (m_blackAndWhiteNegative) {
576 int k = 0;
577 if (m_type & CROSSHAIR) {
578 int bit = 0;
579 for (int v=0; v<7; v++) {
580 for (int u=0; u<7; u++) {
581 if (g_crosshair_pattern[v*7+u]) {
582 color_t c = g->getPixel(m_screenPosition.x-3+u,
583 m_screenPosition.y-3+v);
584 c = color_utils::blackandwhite_neg(c);
585 if (rgba_getr(c) == 255) { // White
586 k |= (1 << bit);
587 }
588 ++bit;
589 }
590 }
591 }
592 }
593 if (requireLittleCenterDot) {
594 color_t c = g->getPixel(m_screenPosition.x,
595 m_screenPosition.y);
596 c = color_utils::blackandwhite_neg(c);
597 if (rgba_getr(c) == 255) { // White
598 k |= (m_type & CROSSHAIR ? 0x200: 0x301);
599 }
600 else { // Black
601 k |= (m_type & CROSSHAIR ? 0x100: 0x300);
602 }
603 }
604
605 ASSERT(k < int(g_bwCursors.size()));
606 if (k >= int(g_bwCursors.size())) // Unexpected key value in release mode
607 return;
608
609 // Use cached cursor
610 if (g_bwCursors[k]) {
611 cursor = g_bwCursors[k];
612 }
613 else {
614 const gfx::Color black = gfx::rgba(0, 0, 0);
615 const gfx::Color white = gfx::rgba(255, 255, 255);
616 os::SurfaceRef cursorSurface =
617 os::instance()->makeRgbaSurface(cursorBounds.w,
618 cursorBounds.h);
619 cursorSurface->clear();
620 int bit = 0;
621 if (m_type & CROSSHAIR) {
622 for (int v=0; v<7; v++) {
623 for (int u=0; u<7; u++) {
624 if (g_crosshair_pattern[v*7+u]) {
625 cursorSurface->putPixel(
626 (k & (1 << bit) ? white: black), u, v);
627 ++bit;
628 }
629 }
630 }
631 }
632 if (requireLittleCenterDot) {
633 if (m_type & CROSSHAIR) {
634 if (k & (0x100 | 0x200)) { // 0x100 or 0x200
635 cursorSurface->putPixel(
636 (k & 0x100 ? black: white),
637 cursorBounds.w/2, cursorBounds.h/2);
638 }
639 }
640 else { // 0x300 or 0x301
641 cursorSurface->putPixel(
642 (k == 0x300 ? black: white),
643 cursorBounds.w/2, cursorBounds.h/2);
644 }
645 }
646
647 cursor = g_bwCursors[k] =
648 os::instance()->makeCursor(
649 cursorSurface.get(),
650 cursorCenter,
651 scale);
652 }
653 }
654 // Cursor with solid color (easiest case, we don't have to check the
655 // colors in the screen to create the crosshair)
656 else {
657 // We have to recreate all cursors if the color has changed.
658 if (g_solidCursorColor != cursorColor) {
659 g_solidCursors.fill(nullptr);
660 g_solidCursorColor = cursorColor;
661 }
662
663 int k = 0;
664 if (m_type & CROSSHAIR) {
665 if (requireLittleCenterDot)
666 k = 2;
667 else
668 k = 1;
669 }
670
671 // Use cached cursor
672 if (g_solidCursors[k]) {
673 cursor = g_solidCursors[k];
674 }
675 else {
676 os::SurfaceRef cursorSurface =
677 os::instance()->makeRgbaSurface(cursorBounds.w,
678 cursorBounds.h);
679 cursorSurface->clear();
680 if (m_type & CROSSHAIR) {
681 for (int v=0; v<7; v++)
682 for (int u=0; u<7; u++)
683 if (g_crosshair_pattern[v*7+u])
684 cursorSurface->putPixel(cursorColor, u, v);
685 }
686 if (requireLittleCenterDot)
687 cursorSurface->putPixel(cursorColor, cursorBounds.w/2, cursorBounds.h/2);
688
689 cursor = g_solidCursors[k] =
690 os::instance()->makeCursor(
691 cursorSurface.get(),
692 cursorCenter,
693 scale);
694 }
695 }
696
697 if (cursor) {
698 // TODO should we use ui::set_mouse_cursor()?
699 ui::set_mouse_cursor_reset_info();
700 window->setCursor(cursor);
701 }
702}
703
704void BrushPreview::forEachBrushPixel(
705 ui::Graphics* g,
706 const gfx::Point& spritePos,
707 gfx::Color color,
708 PixelDelegate pixelDelegate)
709{
710 m_savedPixelsIterator = 0;
711
712 if (m_type & SELECTION_CROSSHAIR)
713 traceSelectionCrossPixels(g, spritePos, color, 1, pixelDelegate);
714
715 if (m_type & BRUSH_BOUNDARIES)
716 traceBrushBoundaries(g, spritePos, color, pixelDelegate);
717
718 m_savedPixelsLimit = m_savedPixelsIterator;
719}
720
721// Old thick cross (used for selection tools)
722void BrushPreview::traceSelectionCrossPixels(
723 ui::Graphics* g,
724 const gfx::Point& pt, gfx::Color color,
725 int thickness, PixelDelegate pixelDelegate)
726{
727 static int cross[6*6] = {
728 0, 0, 1, 1, 0, 0,
729 0, 0, 1, 1, 0, 0,
730 1, 1, 1, 1, 1, 1,
731 1, 1, 1, 1, 1, 1,
732 0, 0, 1, 1, 0, 0,
733 0, 0, 1, 1, 0, 0,
734 };
735 gfx::Point out, outpt = m_editor->editorToScreen(pt);
736 const render::Projection& proj = m_editor->projection();
737 gfx::Size size(proj.applyX(thickness/2),
738 proj.applyY(thickness/2));
739 gfx::Size size2(proj.applyX(thickness),
740 proj.applyY(thickness));
741 if (size2.w == 0) size2.w = 1;
742 if (size2.h == 0) size2.h = 1;
743
744 for (int v=0; v<6; v++) {
745 for (int u=0; u<6; u++) {
746 if (!cross[v*6+u])
747 continue;
748
749 out = outpt;
750 out.x += ((u<3) ? u-size.w-3: u-size.w-3+size2.w);
751 out.y += ((v<3) ? v-size.h-3: v-size.h-3+size2.h);
752
753 (this->*pixelDelegate)(g, out, color);
754 }
755 }
756}
757
758// Current brush edges
759void BrushPreview::traceBrushBoundaries(ui::Graphics* g,
760 gfx::Point pos,
761 gfx::Color color,
762 PixelDelegate pixelDelegate)
763{
764 for (const auto& seg : m_brushBoundaries) {
765 gfx::Rect bounds = seg.bounds();
766 bounds.offset(pos);
767 bounds = m_editor->editorToScreen(bounds);
768
769 if (seg.open()) {
770 if (seg.vertical()) --bounds.x;
771 else --bounds.y;
772 }
773
774 gfx::Point pt(bounds.x, bounds.y);
775 if (seg.vertical()) {
776 for (; pt.y<bounds.y+bounds.h; ++pt.y)
777 (this->*pixelDelegate)(g, pt, color);
778 }
779 else {
780 for (; pt.x<bounds.x+bounds.w; ++pt.x)
781 (this->*pixelDelegate)(g, pt, color);
782 }
783 }
784}
785
786//////////////////////////////////////////////////////////////////////
787// Pixel delegates
788
789void BrushPreview::savePixelDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color)
790{
791 if (m_clippingRegion.contains(pt)) {
792 color_t c = g->getPixel(pt.x, pt.y);
793
794 if (m_savedPixelsIterator < (int)m_savedPixels.size())
795 m_savedPixels[m_savedPixelsIterator] = c;
796 else
797 m_savedPixels.push_back(c);
798
799 ++m_savedPixelsIterator;
800 }
801}
802
803void BrushPreview::drawPixelDelegate(ui::Graphics* gfx, const gfx::Point& pt, gfx::Color color)
804{
805 if (m_savedPixelsIterator < (int)m_savedPixels.size() &&
806 m_clippingRegion.contains(pt)) {
807 if (m_blackAndWhiteNegative) {
808 int c = m_savedPixels[m_savedPixelsIterator];
809 int r = gfx::getr(c);
810 int g = gfx::getg(c);
811 int b = gfx::getb(c);
812
813 gfx->putPixel(color_utils::blackandwhite_neg(gfx::rgba(r, g, b)), pt.x, pt.y);
814 }
815 else {
816 gfx->putPixel(color, pt.x, pt.y);
817 }
818 ++m_savedPixelsIterator;
819 }
820}
821
822void BrushPreview::clearPixelDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color)
823{
824 if (m_savedPixelsIterator < (int)m_savedPixels.size()) {
825 if (m_oldClippingRegion.contains(pt)) {
826 if (m_clippingRegion.contains(pt))
827 g->putPixel(m_savedPixels[m_savedPixelsIterator], pt.x, pt.y);
828 ++m_savedPixelsIterator;
829 }
830 }
831
832#if _DEBUG
833 if (!(m_savedPixelsIterator <= m_savedPixelsLimit)) {
834 TRACE("m_savedPixelsIterator <= m_savedPixelsLimit: %d <= %d failed\n",
835 m_savedPixelsIterator, m_savedPixelsLimit);
836 }
837#endif
838}
839
840} // namespace app
841