mirror of https://gitlab.com/nakst/essence
				
				
				
			
		
			
				
	
	
		
			932 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			932 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			C++
		
	
	
	
| // This file is part of the Essence operating system.
 | |
| // It is released under the terms of the MIT license -- see LICENSE.md.
 | |
| // Written by: nakst.
 | |
| 
 | |
| // TODO Saving.
 | |
| // TODO Don't use an EsPaintTarget for the bitmap?
 | |
| // TODO Show brush preview.
 | |
| // TODO Other tools: text, selection.
 | |
| // TODO Resize and crop image.
 | |
| // TODO Clipboard.
 | |
| // TODO Clearing textbox undo from EsTextboxInsert?
 | |
| // TODO Handling out of memory.
 | |
| // TODO Color palette.
 | |
| // TODO More brushes?
 | |
| // TODO Grid.
 | |
| // TODO Zoom and pan with EsCanvasPane.
 | |
| // TODO Status bar.
 | |
| // TODO Clearing undo if too much memory is used.
 | |
| 
 | |
| #define ES_INSTANCE_TYPE Instance
 | |
| #include <essence.h>
 | |
| #include <shared/array.cpp>
 | |
| #include <shared/strings.cpp>
 | |
| 
 | |
| #define STB_IMAGE_WRITE_IMPLEMENTATION
 | |
| #define STBI_WRITE_NO_STDIO
 | |
| #define STBIW_MEMMOVE EsCRTmemmove
 | |
| #define STBIW_MALLOC(sz) EsCRTmalloc(sz)
 | |
| #define STBIW_REALLOC(p,newsz) EsCRTrealloc(p,newsz)
 | |
| #define STBIW_FREE(p) EsCRTfree(p)
 | |
| #define STBIW_ASSERT EsAssert
 | |
| #include <shared/stb_image_write.h>
 | |
| 
 | |
| #define TILE_SIZE (128)
 | |
| 
 | |
| struct Tile {
 | |
| 	size_t referenceCount;
 | |
| 	uint64_t ownerID;
 | |
| 	uint32_t bits[TILE_SIZE * TILE_SIZE];
 | |
| };
 | |
| 
 | |
| struct Image {
 | |
| 	Tile **tiles;
 | |
| 	size_t tileCountX, tileCountY;
 | |
| 	uint64_t id;
 | |
| 	uint32_t width, height;
 | |
| };
 | |
| 
 | |
| struct Instance : EsInstance {
 | |
| 	EsElement *canvas;
 | |
| 	EsColorWell *colorWell;
 | |
| 	EsTextbox *brushSize;
 | |
| 
 | |
| 	EsPanel *toolPanel;
 | |
| 	EsButton *toolDropdown;
 | |
| 
 | |
| 	EsPaintTarget *bitmap;
 | |
| 	uint32_t bitmapWidth, bitmapHeight;
 | |
| 
 | |
| 	Image image;
 | |
| 	uint64_t nextImageID;
 | |
| 
 | |
| 	EsCommand commandBrush;
 | |
| 	EsCommand commandFill;
 | |
| 	EsCommand commandRectangle;
 | |
| 	EsCommand commandSelect;
 | |
| 	EsCommand commandText;
 | |
| 
 | |
| 	// Data while drawing:
 | |
| 	EsRectangle modifiedBounds;
 | |
| 	float previousPointX, previousPointY;
 | |
| 	bool dragged;
 | |
| };
 | |
| 
 | |
| const EsInstanceClassEditorSettings editorSettings = {
 | |
| 	INTERFACE_STRING(ImageEditorNewFileName),
 | |
| 	INTERFACE_STRING(ImageEditorNewDocument),
 | |
| 	ES_ICON_IMAGE_X_GENERIC,
 | |
| };
 | |
| 
 | |
| const EsStyle styleImageMenuTable = {
 | |
| 	.inherit = ES_STYLE_PANEL_FORM_TABLE,
 | |
| 
 | |
| 	.metrics = {
 | |
| 		.mask = ES_THEME_METRICS_INSETS,
 | |
| 		.insets = ES_RECT_4(20, 20, 5, 8),
 | |
| 	},
 | |
| };
 | |
| 
 | |
| Image ImageFork(Instance *instance, Image image, uint32_t newWidth = 0, uint32_t newHeight = 0) {
 | |
| 	Image copy = image;
 | |
| 
 | |
| 	copy.width = newWidth ?: image.width;
 | |
| 	copy.height = newHeight ?: image.height;
 | |
| 	copy.tileCountX = newWidth ? (newWidth + TILE_SIZE - 1) / TILE_SIZE : image.tileCountX;
 | |
| 	copy.tileCountY = newHeight ? (newHeight + TILE_SIZE - 1) / TILE_SIZE : image.tileCountY;
 | |
| 	copy.id = instance->nextImageID++;
 | |
| 	copy.tiles = (Tile **) EsHeapAllocate(copy.tileCountX * copy.tileCountY * sizeof(Tile *), true);
 | |
| 
 | |
| 	for (uintptr_t y = 0; y < copy.tileCountY; y++) {
 | |
| 		for (uintptr_t x = 0; x < copy.tileCountX; x++) {
 | |
| 			uintptr_t source = y * image.tileCountX + x;
 | |
| 			uintptr_t destination = y * copy.tileCountX + x;
 | |
| 
 | |
| 			if (y < image.tileCountY && x < image.tileCountX && image.tiles[source]) {
 | |
| 				image.tiles[source]->referenceCount++;
 | |
| 				copy.tiles[destination] = image.tiles[source];
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return copy;
 | |
| }
 | |
| 
 | |
| void ImageDelete(Image image) {
 | |
| 	for (uintptr_t i = 0; i < image.tileCountX * image.tileCountY; i++) {
 | |
| 		image.tiles[i]->referenceCount--;
 | |
| 
 | |
| 		if (!image.tiles[i]->referenceCount) {
 | |
| 			// EsPrint("free tile %d, %d from image %d\n", i % image.tileCountX, i / image.tileCountX, image.tiles[i]->ownerID);
 | |
| 			EsHeapFree(image.tiles[i]);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	EsHeapFree(image.tiles);
 | |
| }
 | |
| 
 | |
| void ImageCopyToPaintTarget(Instance *instance, const Image *image) {
 | |
| 	uint32_t *bits;
 | |
| 	size_t width, height, stride;
 | |
| 	EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
 | |
| 
 | |
| 	for (int32_t i = 0; i < (int32_t) image->tileCountY; i++) {
 | |
| 		for (int32_t j = 0; j < (int32_t) image->tileCountX; j++) {
 | |
| 			Tile *tile = image->tiles[i * image->tileCountX + j];
 | |
| 			int32_t copyWidth = TILE_SIZE, copyHeight = TILE_SIZE;
 | |
| 
 | |
| 			if (j * TILE_SIZE + copyWidth > (int32_t) width) {
 | |
| 				copyWidth = width - j * TILE_SIZE;
 | |
| 			}
 | |
| 
 | |
| 			if (i * TILE_SIZE + copyHeight > (int32_t) height) {
 | |
| 				copyHeight = height - i * TILE_SIZE;
 | |
| 			}
 | |
| 
 | |
| 			if (tile) {
 | |
| 				for (int32_t y = 0; y < copyHeight; y++) {
 | |
| 					for (int32_t x = 0; x < copyWidth; x++) {
 | |
| 						bits[stride / 4 * (y + i * TILE_SIZE) + (x + j * TILE_SIZE)] = tile->bits[y * TILE_SIZE + x];
 | |
| 					}
 | |
| 				}
 | |
| 			} else {
 | |
| 				for (int32_t y = 0; y < copyHeight; y++) {
 | |
| 					for (int32_t x = 0; x < copyWidth; x++) {
 | |
| 						bits[stride / 4 * (y + i * TILE_SIZE) + (x + j * TILE_SIZE)] = 0;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	EsPaintTargetEndDirectAccess(instance->bitmap);
 | |
| }
 | |
| 
 | |
| Tile *ImageUpdateTile(Image *image, uint32_t x, uint32_t y, bool copyOldBits) {
 | |
| 	EsAssert(x < image->tileCountX && y < image->tileCountY);
 | |
| 	Tile **tileReference = image->tiles + y * image->tileCountX + x;
 | |
| 	Tile *tile = *tileReference;
 | |
| 
 | |
| 	if (!tile || tile->ownerID != image->id) {
 | |
| 		if (tile && tile->referenceCount == 1) {
 | |
| 			tile->ownerID = image->id;
 | |
| 
 | |
| 			// EsPrint("reuse tile %d, %d for image %d\n", x, y, image->id);
 | |
| 		} else {
 | |
| 			Tile *old = tile;
 | |
| 			if (old) old->referenceCount--;
 | |
| 
 | |
| 			*tileReference = tile = (Tile *) EsHeapAllocate(sizeof(Tile), false);
 | |
| 			tile->referenceCount = 1;
 | |
| 			tile->ownerID = image->id;
 | |
| 
 | |
| 			if (copyOldBits && old) {
 | |
| 				EsMemoryCopy(tile->bits, old->bits, sizeof(old->bits));
 | |
| 			}
 | |
| 
 | |
| 			// EsPrint("allocate new tile %d, %d for image %d\n", x, y, image->id);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return tile;
 | |
| }
 | |
| 
 | |
| void ImageCopyFromPaintTarget(Instance *instance, Image *image, EsRectangle modifiedBounds) {
 | |
| 	uint32_t *bits;
 | |
| 	size_t width, height, stride;
 | |
| 	EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
 | |
| 
 | |
| 	modifiedBounds = EsRectangleIntersection(modifiedBounds, ES_RECT_4(0, width, 0, height));
 | |
| 
 | |
| 	for (int32_t i = modifiedBounds.t / TILE_SIZE; i <= modifiedBounds.b / TILE_SIZE; i++) {
 | |
| 		for (int32_t j = modifiedBounds.l / TILE_SIZE; j <= modifiedBounds.r / TILE_SIZE; j++) {
 | |
| 			if ((uint32_t) j >= image->tileCountX || (uint32_t) i >= image->tileCountY) {
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			Tile *tile = ImageUpdateTile(image, j, i, false);
 | |
| 
 | |
| 			int32_t copyWidth = TILE_SIZE, copyHeight = TILE_SIZE;
 | |
| 
 | |
| 			if (j * TILE_SIZE + copyWidth > (int32_t) width) {
 | |
| 				copyWidth = width - j * TILE_SIZE;
 | |
| 			}
 | |
| 
 | |
| 			if (i * TILE_SIZE + copyHeight > (int32_t) height) {
 | |
| 				copyHeight = height - i * TILE_SIZE;
 | |
| 			}
 | |
| 
 | |
| 			for (int32_t y = 0; y < copyHeight; y++) {
 | |
| 				for (int32_t x = 0; x < copyWidth; x++) {
 | |
| 					tile->bits[y * TILE_SIZE + x] = bits[stride / 4 * (y + i * TILE_SIZE) + (x + j * TILE_SIZE)];
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	EsPaintTargetEndDirectAccess(instance->bitmap);
 | |
| }
 | |
| 
 | |
| void ImageUndoMessage(const void *item, EsUndoManager *manager, EsMessage *message) {
 | |
| 	const Image *image = (const Image *) item;
 | |
| 	Instance *instance = EsUndoGetInstance(manager);
 | |
| 
 | |
| 	if (message->type == ES_MSG_UNDO_INVOKE) {
 | |
| 		EsUndoPush(manager, ImageUndoMessage, &instance->image, sizeof(Image));
 | |
| 		instance->image = *image;
 | |
| 
 | |
| 		if (instance->bitmapWidth != image->width || instance->bitmapHeight != image->height) {
 | |
| 			instance->bitmapWidth = image->width;
 | |
| 			instance->bitmapHeight = image->height;
 | |
| 			EsPaintTargetDestroy(instance->bitmap);
 | |
| 			instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
 | |
| 			EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
 | |
| 		}
 | |
| 
 | |
| 		ImageCopyToPaintTarget(instance, image);
 | |
| 		EsElementRepaint(instance->canvas);
 | |
| 	} else if (message->type == ES_MSG_UNDO_CANCEL) {
 | |
| 		ImageDelete(*image);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int BrushSizeMessage(EsElement *element, EsMessage *message) {
 | |
| 	EsTextbox *textbox = (EsTextbox *) element;
 | |
| 
 | |
| 	if (message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA) {
 | |
| 		double oldValue = EsTextboxGetContentsAsDouble(textbox); 
 | |
| 		double newValue = oldValue + message->numberDragDelta.delta * (message->numberDragDelta.fast ? 1 : 0.1);
 | |
| 
 | |
| 		if (newValue < 1) {
 | |
| 			newValue = 1;
 | |
| 		} else if (newValue > 200) {
 | |
| 			newValue = 200;
 | |
| 		}
 | |
| 
 | |
| 		char result[64];
 | |
| 		size_t resultBytes = EsStringFormat(result, sizeof(result), "%d.%d", (int) newValue, (int) (newValue * 10) % 10);
 | |
| 		EsTextboxSelectAll(textbox);
 | |
| 		EsTextboxInsert(textbox, result, resultBytes);
 | |
| 	}
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| EsRectangle DrawFill(Instance *instance, EsPoint point) {
 | |
| 	uint32_t color = 0xFF000000 | EsColorWellGetRGB(instance->colorWell);
 | |
| 	EsRectangle modifiedBounds = ES_RECT_4(point.x, point.x, point.y, point.y);
 | |
| 
 | |
| 	if ((uint32_t) point.x >= instance->bitmapWidth || (uint32_t) point.y >= instance->bitmapHeight) {
 | |
| 		return {};
 | |
| 	}
 | |
| 
 | |
| 	uint32_t *bits;
 | |
| 	size_t width, height, stride;
 | |
| 	EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
 | |
| 	stride /= 4;
 | |
| 
 | |
| 	Array<EsPoint> pointsToVisit = {};
 | |
| 
 | |
| 	uint32_t replaceColor = bits[point.y * stride + point.x];
 | |
| 	if (replaceColor != color) pointsToVisit.Add(point);
 | |
| 
 | |
| 	while (pointsToVisit.Length()) {
 | |
| 		EsPoint startPoint = pointsToVisit.Pop();
 | |
| 
 | |
| 		if (startPoint.y < modifiedBounds.t) {
 | |
| 			modifiedBounds.t = startPoint.y;
 | |
| 		}
 | |
| 
 | |
| 		if (startPoint.y > modifiedBounds.b) {
 | |
| 			modifiedBounds.b = startPoint.y;
 | |
| 		}
 | |
| 
 | |
| 		for (ptrdiff_t delta = -1; delta <= 1; delta += 2) {
 | |
| 			EsPoint point = startPoint;
 | |
| 			uint32_t *pointer = bits + point.y * stride + point.x;
 | |
| 
 | |
| 			bool spaceAbove = false;
 | |
| 			bool spaceBelow = false;
 | |
| 
 | |
| 			if (delta == 1) {
 | |
| 				point.x += delta;
 | |
| 				pointer += delta;
 | |
| 
 | |
| 				if (point.x == (int32_t) width) {
 | |
| 					break;
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			while (true) {
 | |
| 				if (*pointer != replaceColor) {
 | |
| 					break;
 | |
| 				}
 | |
| 
 | |
| 				*pointer = color;
 | |
| 
 | |
| 				if (point.x < modifiedBounds.l) {
 | |
| 					modifiedBounds.l = point.x;
 | |
| 				}
 | |
| 
 | |
| 				if (point.x > modifiedBounds.r) {
 | |
| 					modifiedBounds.r = point.x;
 | |
| 				}
 | |
| 
 | |
| 				if (point.y) {
 | |
| 					if (!spaceAbove && pointer[-stride] == replaceColor) {
 | |
| 						spaceAbove = true;
 | |
| 						pointsToVisit.Add({ point.x, point.y - 1 });
 | |
| 					} else if (spaceAbove && pointer[-stride] != replaceColor) {
 | |
| 						spaceAbove = false;
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				if (point.y != (int32_t) height - 1) {
 | |
| 					if (!spaceBelow && pointer[stride] == replaceColor) {
 | |
| 						spaceBelow = true;
 | |
| 						pointsToVisit.Add({ point.x, point.y + 1 });
 | |
| 					} else if (spaceBelow && pointer[stride] != replaceColor) {
 | |
| 						spaceBelow = false;
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				point.x += delta;
 | |
| 				pointer += delta;
 | |
| 
 | |
| 				if (point.x == (int32_t) width || point.x < 0) {
 | |
| 					break;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	modifiedBounds.r++, modifiedBounds.b += 2;
 | |
| 	pointsToVisit.Free();
 | |
| 	EsPaintTargetEndDirectAccess(instance->bitmap);
 | |
| 	EsElementRepaint(instance->canvas, &modifiedBounds); 
 | |
| 	return modifiedBounds;
 | |
| }
 | |
| 
 | |
| EsRectangle DrawLine(Instance *instance, bool force = false) {
 | |
| 	EsPoint point = EsMouseGetPosition(instance->canvas); 
 | |
| 	float brushSize = EsTextboxGetContentsAsDouble(instance->brushSize);
 | |
| 	float spacing = brushSize * 0.1f;
 | |
| 	uint32_t color = 0xFF000000 | EsColorWellGetRGB(instance->colorWell);
 | |
| 	EsRectangle modifiedBounds = ES_RECT_4(instance->bitmapWidth, 0, instance->bitmapHeight, 0);
 | |
| 
 | |
| 	uint32_t *bits;
 | |
| 	size_t width, height, stride;
 | |
| 	EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
 | |
| 	stride /= 4;
 | |
| 
 | |
| 	// Draw the line.
 | |
| 
 | |
| 	while (true) {
 | |
| 		float dx = point.x - instance->previousPointX;
 | |
| 		float dy = point.y - instance->previousPointY;
 | |
| 		float distance = EsCRTsqrtf(dx * dx + dy * dy);
 | |
| 
 | |
| 		if (distance < spacing && !force) {
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		int32_t x0 = instance->previousPointX;
 | |
| 		int32_t y0 = instance->previousPointY;
 | |
| 
 | |
| 		EsRectangle bounds = ES_RECT_4(x0 - brushSize / 2 - 1, x0 + brushSize / 2 + 1, 
 | |
| 				y0 - brushSize / 2 - 1, y0 + brushSize / 2 + 1);
 | |
| 		bounds = EsRectangleIntersection(bounds, ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight));
 | |
| 		modifiedBounds = EsRectangleBounding(modifiedBounds, bounds);
 | |
| 
 | |
| 		for (int32_t y = bounds.t; y < bounds.b; y++) {
 | |
| 			for (int32_t x = bounds.l; x < bounds.r; x++) {
 | |
| 				float distance = (x - x0) * (x - x0) + (y - y0) * (y - y0);
 | |
| 
 | |
| 				if (distance < brushSize * brushSize * 0.25f) {
 | |
| 					bits[y * stride + x] = color;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if (force) {
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		instance->previousPointX += dx / distance * spacing;
 | |
| 		instance->previousPointY += dy / distance * spacing;
 | |
| 	}
 | |
| 
 | |
| 	// Repaint the canvas.
 | |
| 
 | |
| 	modifiedBounds.r++, modifiedBounds.b++; 
 | |
| 	EsPaintTargetEndDirectAccess(instance->bitmap);
 | |
| 	EsElementRepaint(instance->canvas, &modifiedBounds); 
 | |
| 	return modifiedBounds;
 | |
| }
 | |
| 
 | |
| int CanvasMessage(EsElement *element, EsMessage *message) {
 | |
| 	Instance *instance = element->instance;
 | |
| 
 | |
| 	if (message->type == ES_MSG_PAINT) {
 | |
| 		EsPainter *painter = message->painter;
 | |
| 		EsRectangle bounds = EsPainterBoundsInset(painter); 
 | |
| 		EsRectangle area = ES_RECT_4(bounds.l, bounds.l + instance->bitmapWidth, bounds.t, bounds.t + instance->bitmapHeight);
 | |
| 		EsDrawPaintTarget(painter, instance->bitmap, area, ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight), 0xFF); 
 | |
| 
 | |
| 		if (instance->commandRectangle.check == ES_CHECK_CHECKED && instance->dragged) {
 | |
| 			EsRectangle rectangle = instance->modifiedBounds;
 | |
| 			rectangle.l += painter->offsetX, rectangle.r += painter->offsetX;
 | |
| 			rectangle.t += painter->offsetY, rectangle.b += painter->offsetY;
 | |
| 			EsDrawBlock(painter, EsRectangleIntersection(rectangle, area), 0xFF000000 | EsColorWellGetRGB(instance->colorWell));
 | |
| 		}
 | |
| 	} else if ((message->type == ES_MSG_MOUSE_LEFT_DRAG || message->type == ES_MSG_MOUSE_MOVED) 
 | |
| 			&& EsMouseIsLeftHeld() && instance->commandBrush.check == ES_CHECK_CHECKED) {
 | |
| 		instance->modifiedBounds = EsRectangleBounding(DrawLine(instance), instance->modifiedBounds);
 | |
| 	} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG && instance->commandRectangle.check == ES_CHECK_CHECKED) {
 | |
| 		EsRectangle previous = instance->modifiedBounds;
 | |
| 		EsPoint point = EsMouseGetPosition(element);
 | |
| 		EsRectangle rectangle;
 | |
| 		if (point.x < instance->previousPointX) rectangle.l = point.x, rectangle.r = instance->previousPointX + 1;
 | |
| 		else rectangle.l = instance->previousPointX, rectangle.r = point.x + 1;
 | |
| 		if (point.y < instance->previousPointY) rectangle.t = point.y, rectangle.b = instance->previousPointY + 1;
 | |
| 		else rectangle.t = instance->previousPointY, rectangle.b = point.y + 1;
 | |
| 		instance->modifiedBounds = rectangle;
 | |
| 		EsRectangle bounding = EsRectangleBounding(rectangle, previous);
 | |
| 		EsElementRepaint(element, &bounding);
 | |
| 		instance->dragged = true;
 | |
| 	} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN && instance->commandBrush.check == ES_CHECK_CHECKED) {
 | |
| 		EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
 | |
| 		instance->image = ImageFork(instance, instance->image);
 | |
| 		EsPoint point = EsMouseGetPosition(element); 
 | |
| 		instance->previousPointX = point.x, instance->previousPointY = point.y;
 | |
| 		instance->modifiedBounds = ES_RECT_4(instance->bitmapWidth, 0, instance->bitmapHeight, 0);
 | |
| 		DrawLine(instance, true);
 | |
| 	} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN && instance->commandRectangle.check == ES_CHECK_CHECKED) {
 | |
| 		EsPoint point = EsMouseGetPosition(element); 
 | |
| 		instance->previousPointX = point.x, instance->previousPointY = point.y;
 | |
| 		instance->dragged = false;
 | |
| 	} else if (message->type == ES_MSG_MOUSE_LEFT_UP && instance->commandBrush.check == ES_CHECK_CHECKED) {
 | |
| 		ImageCopyFromPaintTarget(instance, &instance->image, instance->modifiedBounds);
 | |
| 	} else if (message->type == ES_MSG_MOUSE_LEFT_UP && instance->commandRectangle.check == ES_CHECK_CHECKED && instance->dragged) {
 | |
| 		instance->dragged = false;
 | |
| 		EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
 | |
| 		instance->image = ImageFork(instance, instance->image);
 | |
| 		EsPainter painter = {};
 | |
| 		painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
 | |
| 		painter.target = instance->bitmap;
 | |
| 		EsDrawBlock(&painter, instance->modifiedBounds, 0xFF000000 | EsColorWellGetRGB(instance->colorWell));
 | |
| 		ImageCopyFromPaintTarget(instance, &instance->image, instance->modifiedBounds);
 | |
| 	} else if (message->type == ES_MSG_MOUSE_LEFT_UP && instance->commandFill.check == ES_CHECK_CHECKED) {
 | |
| 		EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
 | |
| 		instance->image = ImageFork(instance, instance->image);
 | |
| 
 | |
| 		EsRectangle modifiedBounds = DrawFill(instance, EsMouseGetPosition(element));
 | |
| 		ImageCopyFromPaintTarget(instance, &instance->image, modifiedBounds);
 | |
| 	} else if (message->type == ES_MSG_GET_CURSOR) {
 | |
| 		message->cursorStyle = ES_CURSOR_CROSS_HAIR_PICK;
 | |
| 	} else if (message->type == ES_MSG_GET_WIDTH) {
 | |
| 		message->measure.width = instance->bitmapWidth;
 | |
| 	} else if (message->type == ES_MSG_GET_HEIGHT) {
 | |
| 		message->measure.height = instance->bitmapHeight;
 | |
| 	} else {
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	return ES_HANDLED;
 | |
| }
 | |
| 
 | |
| void CommandSelectTool(Instance *instance, EsElement *, EsCommand *command) {
 | |
| 	if (command->check == ES_CHECK_CHECKED) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	EsCommandSetCheck(&instance->commandBrush, ES_CHECK_UNCHECKED, false);
 | |
| 	EsCommandSetCheck(&instance->commandFill, ES_CHECK_UNCHECKED, false);
 | |
| 	EsCommandSetCheck(&instance->commandRectangle, ES_CHECK_UNCHECKED, false);
 | |
| 	EsCommandSetCheck(&instance->commandSelect, ES_CHECK_UNCHECKED, false);
 | |
| 	EsCommandSetCheck(&instance->commandText, ES_CHECK_UNCHECKED, false);
 | |
| 	EsCommandSetCheck(command, ES_CHECK_CHECKED, false);
 | |
| 
 | |
| 	if (command == &instance->commandBrush) EsButtonSetIcon(instance->toolDropdown, ES_ICON_DRAW_FREEHAND);
 | |
| 	if (command == &instance->commandFill) EsButtonSetIcon(instance->toolDropdown, ES_ICON_COLOR_FILL);
 | |
| 	if (command == &instance->commandRectangle) EsButtonSetIcon(instance->toolDropdown, ES_ICON_DRAW_RECTANGLE);
 | |
| 	if (command == &instance->commandSelect) EsButtonSetIcon(instance->toolDropdown, ES_ICON_OBJECT_GROUP);
 | |
| 	if (command == &instance->commandText) EsButtonSetIcon(instance->toolDropdown, ES_ICON_DRAW_TEXT);
 | |
| 
 | |
| 	instance->dragged = false;
 | |
| }
 | |
| 
 | |
| int BitmapSizeTextboxMessage(EsElement *element, EsMessage *message) {
 | |
| 	EsTextbox *textbox = (EsTextbox *) element;
 | |
| 	Instance *instance = textbox->instance;
 | |
| 
 | |
| 	if (message->type == ES_MSG_TEXTBOX_EDIT_END || message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_END) {
 | |
| 		char *expression = EsTextboxGetContents(textbox);
 | |
| 		EsCalculationValue value = EsCalculateFromUserExpression(expression); 
 | |
| 		EsHeapFree(expression);
 | |
| 
 | |
| 		if (value.error) {
 | |
| 			return ES_REJECTED;
 | |
| 		}
 | |
| 
 | |
| 		if (value.number < 1) value.number = 1;
 | |
| 		else if (value.number > 20000) value.number = 20000;
 | |
| 		int newSize = (int) (value.number + 0.5);
 | |
| 		char result[64];
 | |
| 		size_t resultBytes = EsStringFormat(result, sizeof(result), "%d", newSize);
 | |
| 		EsTextboxSelectAll(textbox);
 | |
| 		EsTextboxInsert(textbox, result, resultBytes);
 | |
| 
 | |
| 		int oldSize = textbox->userData.i ? instance->bitmapHeight : instance->bitmapWidth;
 | |
| 
 | |
| 		if (oldSize == newSize) {
 | |
| 			return ES_HANDLED;
 | |
| 		}
 | |
| 
 | |
| 		EsRectangle clearRegion;
 | |
| 
 | |
| 		if (textbox->userData.i) {
 | |
| 			instance->bitmapHeight = newSize;
 | |
| 			clearRegion = ES_RECT_4(0, instance->bitmapWidth, oldSize, newSize);
 | |
| 		} else {
 | |
| 			instance->bitmapWidth = newSize;
 | |
| 			clearRegion = ES_RECT_4(oldSize, newSize, 0, instance->bitmapHeight);
 | |
| 		}
 | |
| 
 | |
| 		EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
 | |
| 		instance->image = ImageFork(instance, instance->image, instance->bitmapWidth, instance->bitmapHeight);
 | |
| 		EsPaintTargetDestroy(instance->bitmap);
 | |
| 		instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
 | |
| 		ImageCopyToPaintTarget(instance, &instance->image);
 | |
| 
 | |
| 		EsPainter painter = {};
 | |
| 		painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
 | |
| 		painter.target = instance->bitmap;
 | |
| 		EsDrawBlock(&painter, clearRegion, 0xFFFFFFFF);
 | |
| 		ImageCopyFromPaintTarget(instance, &instance->image, clearRegion);
 | |
| 
 | |
| 		EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
 | |
| 
 | |
| 		return ES_HANDLED;
 | |
| 	} else if (message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA) {
 | |
| 		int oldValue = EsTextboxGetContentsAsDouble(textbox); 
 | |
| 		int newValue = oldValue + message->numberDragDelta.delta * (message->numberDragDelta.fast ? 10 : 1);
 | |
| 		if (newValue < 1) newValue = 1;
 | |
| 		else if (newValue > 20000) newValue = 20000;
 | |
| 		char result[64];
 | |
| 		size_t resultBytes = EsStringFormat(result, sizeof(result), "%d", newValue);
 | |
| 		EsTextboxSelectAll(textbox);
 | |
| 		EsTextboxInsert(textbox, result, resultBytes);
 | |
| 		return ES_HANDLED;
 | |
| 	}
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| void ImageTransform(EsMenu *menu, EsGeneric context) {
 | |
| 	Instance *instance = menu->instance;
 | |
| 	EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
 | |
| 
 | |
| 	uint32_t *bits;
 | |
| 	size_t width, height, stride;
 | |
| 	EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
 | |
| 
 | |
| 	EsPaintTarget *newTarget = nullptr;
 | |
| 	uint32_t *newBits = nullptr;
 | |
| 	size_t newStride = 0;
 | |
| 	
 | |
| 	if (context.i == 1 || context.i == 2) {
 | |
| 		instance->image = ImageFork(instance, instance->image, height, width);
 | |
| 		newTarget = EsPaintTargetCreate(height, width, false);
 | |
| 		EsPaintTargetStartDirectAccess(newTarget, &newBits, &height, &width, &newStride);
 | |
| 	} else {
 | |
| 		instance->image = ImageFork(instance, instance->image);
 | |
| 	}
 | |
| 
 | |
| 	if (context.i == 1 /* rotate left */) {
 | |
| 		for (uintptr_t i = 0; i < height; i++) {
 | |
| 			for (uintptr_t j = 0; j < width; j++) {
 | |
| 				newBits[(width - j - 1) * newStride / 4 + i] = bits[i * stride / 4 + j];
 | |
| 			}
 | |
| 		}
 | |
| 	} else if (context.i == 2 /* rotate right */) {
 | |
| 		for (uintptr_t i = 0; i < height; i++) {
 | |
| 			for (uintptr_t j = 0; j < width; j++) {
 | |
| 				newBits[j * newStride / 4 + (height - i - 1)] = bits[i * stride / 4 + j];
 | |
| 			}
 | |
| 		}
 | |
| 	} else if (context.i == 3 /* flip horizontally */) {
 | |
| 		for (uintptr_t i = 0; i < height; i++) {
 | |
| 			for (uintptr_t j = 0; j < width / 2; j++) {
 | |
| 				uint32_t temporary = bits[i * stride / 4 + j];
 | |
| 				bits[i * stride / 4 + j] = bits[i * stride / 4 + (width - j - 1)];
 | |
| 				bits[i * stride / 4 + (width - j - 1)] = temporary;
 | |
| 			}
 | |
| 		}
 | |
| 	} else if (context.i == 4 /* flip vertically */) {
 | |
| 		for (uintptr_t i = 0; i < height / 2; i++) {
 | |
| 			for (uintptr_t j = 0; j < width; j++) {
 | |
| 				uint32_t temporary = bits[i * stride / 4 + j];
 | |
| 				bits[i * stride / 4 + j] = bits[(height - i - 1) * stride / 4 + j];
 | |
| 				bits[(height - i - 1) * stride / 4 + j] = temporary;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	EsPaintTargetEndDirectAccess(instance->bitmap);
 | |
| 
 | |
| 	if (newTarget) {
 | |
| 		EsPaintTargetDestroy(instance->bitmap);
 | |
| 		instance->bitmap = newTarget;
 | |
| 		size_t width, height;
 | |
| 		EsPaintTargetGetSize(instance->bitmap, &width, &height);
 | |
| 		instance->bitmapWidth = width;
 | |
| 		instance->bitmapHeight = height;
 | |
| 		EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
 | |
| 	}
 | |
| 
 | |
| 	ImageCopyFromPaintTarget(instance, &instance->image, ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight));
 | |
| 	EsElementRepaint(instance->canvas);
 | |
| }
 | |
| 
 | |
| void MenuTools(Instance *instance, EsElement *element, EsCommand *) {
 | |
| 	EsMenu *menu = EsMenuCreate(element);
 | |
| 	EsMenuAddCommandsFromToolbar(menu, instance->toolPanel);
 | |
| 	EsMenuShow(menu);
 | |
| }
 | |
| 
 | |
| void MenuImage(Instance *instance, EsElement *element, EsCommand *) {
 | |
| 	EsMenu *menu = EsMenuCreate(element);
 | |
| 
 | |
| 	EsMenuAddItem(menu, ES_MENU_ITEM_HEADER, INTERFACE_STRING(ImageEditorCanvasSize));
 | |
| 	EsPanel *table = EsPanelCreate(menu, ES_PANEL_HORIZONTAL | ES_PANEL_TABLE, EsStyleIntern(&styleImageMenuTable));
 | |
| 	EsPanelSetBands(table, 2, 2);
 | |
| 
 | |
| 	char buffer[64];
 | |
| 	size_t bytes;
 | |
| 	EsTextbox *textbox;
 | |
| 
 | |
| 	bytes = EsStringFormat(buffer, sizeof(buffer), "%d", instance->bitmapWidth);
 | |
| 	EsTextDisplayCreate(table, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(ImageEditorPropertyWidth));
 | |
| 	textbox = EsTextboxCreate(table, ES_TEXTBOX_EDIT_BASED, ES_STYLE_TEXTBOX_BORDERED_SINGLE_MEDIUM);
 | |
| 	EsTextboxInsert(textbox, buffer, bytes, false);
 | |
| 	textbox->userData.i = 0;
 | |
| 	textbox->messageUser = BitmapSizeTextboxMessage;
 | |
| 	EsTextboxUseNumberOverlay(textbox, true);
 | |
| 
 | |
| 	bytes = EsStringFormat(buffer, sizeof(buffer), "%d", instance->bitmapHeight);
 | |
| 	EsTextDisplayCreate(table, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(ImageEditorPropertyHeight));
 | |
| 	textbox = EsTextboxCreate(table, ES_TEXTBOX_EDIT_BASED, ES_STYLE_TEXTBOX_BORDERED_SINGLE_MEDIUM);
 | |
| 	EsTextboxInsert(textbox, buffer, bytes, false);
 | |
| 	textbox->userData.i = 1;
 | |
| 	textbox->messageUser = BitmapSizeTextboxMessage;
 | |
| 	EsTextboxUseNumberOverlay(textbox, true);
 | |
| 
 | |
| 	EsMenuAddSeparator(menu);
 | |
| 	EsMenuAddItem(menu, ES_MENU_ITEM_HEADER, INTERFACE_STRING(ImageEditorImageTransformations));
 | |
| 	EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorRotateLeft), ImageTransform, 1);
 | |
| 	EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorRotateRight), ImageTransform, 2);
 | |
| 	EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorFlipHorizontally), ImageTransform, 3);
 | |
| 	EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorFlipVertically), ImageTransform, 4);
 | |
| 
 | |
| 	EsMenuShow(menu);
 | |
| }
 | |
| 
 | |
| void WriteCallback(void *context, void *data, int size) {
 | |
| 	EsBufferWrite((EsBuffer *) context, data, size);
 | |
| }
 | |
| 
 | |
| void SwapRedAndBlueChannels(uint32_t *bits, size_t width, size_t height, size_t stride) {
 | |
| 	for (uintptr_t i = 0; i < height; i++) {
 | |
| 		for (uintptr_t j = 0; j < width; j++) {
 | |
| 			uint32_t *pixel = &bits[i * stride / 4 + j];
 | |
| 			*pixel = (*pixel & 0xFF00FF00) | (((*pixel >> 16) | (*pixel << 16)) & 0x00FF00FF);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int InstanceCallback(Instance *instance, EsMessage *message) {
 | |
| 	if (message->type == ES_MSG_INSTANCE_DESTROY) {
 | |
| 		EsPaintTargetDestroy(instance->bitmap);
 | |
| 		ImageDelete(instance->image);
 | |
| 	} else if (message->type == ES_MSG_INSTANCE_SAVE) {
 | |
| 		// TODO Error handling.
 | |
| 
 | |
| 		uintptr_t extensionOffset = message->instanceSave.nameOrPathBytes;
 | |
| 
 | |
| 		while (extensionOffset) {
 | |
| 			if (message->instanceSave.nameOrPath[extensionOffset - 1] == '.') {
 | |
| 				break;
 | |
| 			} else {
 | |
| 				extensionOffset--;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		const char *extension = extensionOffset ? message->instanceSave.nameOrPath + extensionOffset : "png";
 | |
| 		size_t extensionBytes = extensionOffset ? message->instanceSave.nameOrPathBytes - extensionOffset : 3;
 | |
| 
 | |
| 		uint32_t *bits;
 | |
| 		size_t width, height, stride;
 | |
| 		EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
 | |
| 		EsAssert(stride == width * 4); // TODO Other strides.
 | |
| 		SwapRedAndBlueChannels(bits, width, height, stride); // stbi_write uses the other order. We swap back below.
 | |
| 
 | |
| 		size_t _bufferBytes = 262144;
 | |
| 		uint8_t *_buffer = (uint8_t *) EsHeapAllocate(_bufferBytes, false);
 | |
| 		EsBuffer buffer = { .out = _buffer, .bytes = _bufferBytes };
 | |
| 		buffer.fileStore = message->instanceSave.file;
 | |
| 
 | |
| 		EsUniqueIdentifier typeJPG = (EsUniqueIdentifier) 
 | |
| 			{{ 0xD8, 0xC2, 0x13, 0xB0, 0x53, 0x64, 0x82, 0x11, 0x48, 0x7B, 0x5B, 0x64, 0x0F, 0x92, 0xB9, 0x38 }};
 | |
| 		EsUniqueIdentifier typeBMP = (EsUniqueIdentifier) 
 | |
| 			{{ 0x40, 0x15, 0xB7, 0x82, 0x99, 0x6D, 0xD5, 0x41, 0x96, 0xD5, 0x3B, 0x6D, 0xA6, 0x5F, 0x07, 0x34 }};
 | |
| 		EsUniqueIdentifier typeTGA = (EsUniqueIdentifier) 
 | |
| 			{{ 0x69, 0x62, 0x4E, 0x28, 0xA1, 0xE1, 0x7B, 0x35, 0x64, 0x2E, 0x36, 0x01, 0x65, 0x91, 0xBE, 0xA1 }};
 | |
| 		EsUniqueIdentifier typePNG = (EsUniqueIdentifier) 
 | |
| 			{{ 0x59, 0x21, 0x05, 0x4D, 0x34, 0x40, 0xAB, 0x61, 0xEC, 0xF9, 0x7D, 0x5C, 0x6E, 0x04, 0x96, 0xAE }};
 | |
| 
 | |
| 		if (0 == EsStringCompare(extension, extensionBytes, EsLiteral("jpg"))
 | |
| 				|| 0 == EsStringCompare(extension, extensionBytes, EsLiteral("jpeg"))) {
 | |
| 			EsFileStoreSetContentType(message->instanceSave.file, typeJPG);
 | |
| 			stbi_write_jpg_to_func(WriteCallback, &buffer, width, height, 4, bits, 90);
 | |
| 		} else if (0 == EsStringCompare(extension, extensionBytes, EsLiteral("bmp"))) {
 | |
| 			EsFileStoreSetContentType(message->instanceSave.file, typeBMP);
 | |
| 			stbi_write_bmp_to_func(WriteCallback, &buffer, width, height, 4, bits);
 | |
| 		} else if (0 == EsStringCompare(extension, extensionBytes, EsLiteral("tga"))) {
 | |
| 			EsFileStoreSetContentType(message->instanceSave.file, typeTGA);
 | |
| 			stbi_write_tga_to_func(WriteCallback, &buffer, width, height, 4, bits);
 | |
| 		} else {
 | |
| 			EsFileStoreSetContentType(message->instanceSave.file, typePNG);
 | |
| 			stbi_write_png_to_func(WriteCallback, &buffer, width, height, 4, bits, stride);
 | |
| 		}
 | |
| 
 | |
| 		SwapRedAndBlueChannels(bits, width, height, stride); // Swap back.
 | |
| 		EsBufferFlushToFileStore(&buffer);
 | |
| 		EsHeapFree(_buffer);
 | |
| 		EsPaintTargetEndDirectAccess(instance->bitmap);
 | |
| 		EsInstanceSaveComplete(instance, message->instanceSave.file, true);
 | |
| 	} else if (message->type == ES_MSG_INSTANCE_OPEN) {
 | |
| 		size_t fileSize;
 | |
| 		uint8_t *file = (uint8_t *) EsFileStoreReadAll(message->instanceOpen.file, &fileSize);
 | |
| 
 | |
| 		if (!file) {
 | |
| 			EsInstanceOpenComplete(instance, message->instanceOpen.file, false);
 | |
| 			return ES_HANDLED;
 | |
| 		}
 | |
| 
 | |
| 		uint32_t width, height;
 | |
| 		uint8_t *bits = EsImageLoad(file, fileSize, &width, &height, 4);
 | |
| 		EsHeapFree(file);
 | |
| 
 | |
| 		if (!bits) {
 | |
| 			EsInstanceOpenComplete(instance, message->instanceOpen.file, false, INTERFACE_STRING(ImageEditorUnsupportedFormat));
 | |
| 			return ES_HANDLED;
 | |
| 		}
 | |
| 
 | |
| 		EsPaintTargetDestroy(instance->bitmap);
 | |
| 		ImageDelete(instance->image);
 | |
| 
 | |
| 		instance->bitmapWidth = width;
 | |
| 		instance->bitmapHeight = height;
 | |
| 		instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
 | |
| 		EsPainter painter = {};
 | |
| 		painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
 | |
| 		painter.target = instance->bitmap;
 | |
| 		EsDrawBitmap(&painter, painter.clip, (uint32_t *) bits, width * 4, 0xFF);
 | |
| 		instance->image = ImageFork(instance, {}, instance->bitmapWidth, instance->bitmapHeight);
 | |
| 		ImageCopyFromPaintTarget(instance, &instance->image, painter.clip);
 | |
| 		EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
 | |
| 
 | |
| 		EsHeapFree(bits);
 | |
| 		EsInstanceOpenComplete(instance, message->instanceOpen.file, true);
 | |
| 	} else {
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	return ES_HANDLED;
 | |
| }
 | |
| 
 | |
| void InstanceCreate(EsMessage *message) {
 | |
| 	Instance *instance = EsInstanceCreate(message, INTERFACE_STRING(ImageEditorTitle));
 | |
| 	instance->callback = InstanceCallback;
 | |
| 
 | |
| 	EsElement *toolbar = EsWindowGetToolbar(instance->window);
 | |
| 	EsInstanceSetClassEditor(instance, &editorSettings);
 | |
| 
 | |
| 	// Register commands.
 | |
| 
 | |
| 	EsCommandRegister(&instance->commandBrush, instance, INTERFACE_STRING(ImageEditorToolBrush), CommandSelectTool, 1, "N", true);
 | |
| 	EsCommandRegister(&instance->commandFill, instance, INTERFACE_STRING(ImageEditorToolFill), CommandSelectTool, 2, "Shift+B", true);
 | |
| 	EsCommandRegister(&instance->commandRectangle, instance, INTERFACE_STRING(ImageEditorToolRectangle), CommandSelectTool, 3, "Shift+R", true);
 | |
| 	EsCommandRegister(&instance->commandSelect, instance, INTERFACE_STRING(ImageEditorToolSelect), CommandSelectTool, 4, "R", false);
 | |
| 	EsCommandRegister(&instance->commandText, instance, INTERFACE_STRING(ImageEditorToolText), CommandSelectTool, 5, "T", false);
 | |
| 
 | |
| 	EsCommandSetCheck(&instance->commandBrush, ES_CHECK_CHECKED, false);
 | |
| 
 | |
| 	// Create the toolbar.
 | |
| 
 | |
| 	EsButton *button;
 | |
| 
 | |
| 	EsFileMenuAddToToolbar(toolbar);
 | |
| 	button = EsButtonCreate(toolbar, ES_BUTTON_DROPDOWN, ES_STYLE_PUSH_BUTTON_TOOLBAR, INTERFACE_STRING(ImageEditorImage));
 | |
| 	EsButtonSetIcon(button, ES_ICON_IMAGE_X_GENERIC);
 | |
| 	button->accessKey = 'I';
 | |
| 	EsButtonOnCommand(button, MenuImage);
 | |
| 	EsPanel *buttonGroup = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL | ES_ELEMENT_AUTO_GROUP);
 | |
| 	button = EsButtonCreate(buttonGroup);
 | |
| 	EsCommandAddButton(EsCommandByID(instance, ES_COMMAND_UNDO), button);
 | |
| 	EsButtonSetIcon(button, ES_ICON_EDIT_UNDO_SYMBOLIC);
 | |
| 	button->accessKey = 'U';
 | |
| 	EsSpacerCreate(buttonGroup, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
 | |
| 	button = EsButtonCreate(buttonGroup);
 | |
| 	EsCommandAddButton(EsCommandByID(instance, ES_COMMAND_REDO), button);
 | |
| 	EsButtonSetIcon(button, ES_ICON_EDIT_REDO_SYMBOLIC);
 | |
| 	button->accessKey = 'R';
 | |
| 
 | |
| 	EsSpacerCreate(toolbar, ES_CELL_FILL);
 | |
| 
 | |
| 	button = instance->toolDropdown = EsButtonCreate(toolbar, ES_BUTTON_DROPDOWN,
 | |
| 			ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorPickTool));
 | |
| 	EsButtonSetIcon(button, ES_ICON_DRAW_FREEHAND);
 | |
| 	EsButtonOnCommand(button, MenuTools);
 | |
| 	button->accessKey = 'T';
 | |
| 
 | |
| 	instance->toolPanel = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL | ES_ELEMENT_AUTO_GROUP);
 | |
| 	button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolBrush));
 | |
| 	EsCommandAddButton(&instance->commandBrush, button);
 | |
| 	EsButtonSetIcon(button, ES_ICON_DRAW_FREEHAND);
 | |
| 	button->accessKey = 'B';
 | |
| 	EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
 | |
| 	button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolFill));
 | |
| 	EsCommandAddButton(&instance->commandFill, button);
 | |
| 	EsButtonSetIcon(button, ES_ICON_COLOR_FILL);
 | |
| 	button->accessKey = 'F';
 | |
| 	EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
 | |
| 	button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolRectangle));
 | |
| 	EsCommandAddButton(&instance->commandRectangle, button);
 | |
| 	EsButtonSetIcon(button, ES_ICON_DRAW_RECTANGLE);
 | |
| 	button->accessKey = 'E';
 | |
| 	EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
 | |
| 	button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolSelect));
 | |
| 	EsCommandAddButton(&instance->commandSelect, button);
 | |
| 	EsButtonSetIcon(button, ES_ICON_OBJECT_GROUP);
 | |
| 	button->accessKey = 'S';
 | |
| 	EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
 | |
| 	button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolText));
 | |
| 	EsCommandAddButton(&instance->commandText, button);
 | |
| 	EsButtonSetIcon(button, ES_ICON_DRAW_TEXT);
 | |
| 	button->accessKey = 'T';
 | |
| 
 | |
| 	EsWindowAddSizeAlternative(instance->window, instance->toolDropdown, instance->toolPanel, 1100, 0);
 | |
| 
 | |
| 	EsSpacerCreate(toolbar, ES_CELL_FILL);
 | |
| 
 | |
| 	EsPanel *section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
 | |
| 	EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, INTERFACE_STRING(ImageEditorPropertyColor));
 | |
| 	instance->colorWell = EsColorWellCreate(section, ES_FLAGS_DEFAULT, 0xFFFF0000);
 | |
| 	instance->colorWell->accessKey = 'C';
 | |
| 	EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 5, 0);
 | |
| 
 | |
| 	section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
 | |
| 	EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, INTERFACE_STRING(ImageEditorPropertyBrushSize));
 | |
| 	instance->brushSize = EsTextboxCreate(section, ES_TEXTBOX_EDIT_BASED, ES_STYLE_TEXTBOX_BORDERED_SINGLE_COMPACT);
 | |
| 	instance->brushSize->messageUser = BrushSizeMessage;
 | |
| 	EsTextboxUseNumberOverlay(instance->brushSize, false);
 | |
| 	EsTextboxInsert(instance->brushSize, EsLiteral("5.0"));
 | |
| 	instance->brushSize->accessKey = 'Z';
 | |
| 	EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 1, 0);
 | |
| 
 | |
| 	// Create the user interface.
 | |
| 
 | |
| 	EsWindowSetIcon(instance->window, ES_ICON_MULTIMEDIA_PHOTO_MANAGER);
 | |
| 
 | |
| 	EsCanvasPane *canvasPane = EsCanvasPaneCreate(instance->window, ES_CELL_FILL | ES_CANVAS_PANE_SHOW_SHADOW, ES_STYLE_PANEL_WINDOW_BACKGROUND);
 | |
| 	instance->canvas = EsCustomElementCreate(canvasPane, ES_CELL_CENTER | ES_ELEMENT_FOCUSABLE);
 | |
| 	instance->canvas->messageUser = CanvasMessage;
 | |
| 	EsElementFocus(instance->canvas, false);
 | |
| 
 | |
| 	// Setup the paint target and the image.
 | |
| 
 | |
| 	instance->bitmapWidth = 500;
 | |
| 	instance->bitmapHeight = 400;
 | |
| 	instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
 | |
| 	EsPainter painter = {};
 | |
| 	painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
 | |
| 	painter.target = instance->bitmap;
 | |
| 	EsDrawBlock(&painter, painter.clip, 0xFFFFFFFF);
 | |
| 	instance->image = ImageFork(instance, {}, instance->bitmapWidth, instance->bitmapHeight);
 | |
| 	ImageCopyFromPaintTarget(instance, &instance->image, painter.clip);
 | |
| }
 | |
| 
 | |
| void _start() {
 | |
| 	_init();
 | |
| 
 | |
| 	while (true) {
 | |
| 		EsMessage *message = EsMessageReceive();
 | |
| 
 | |
| 		if (message->type == ES_MSG_INSTANCE_CREATE) {
 | |
| 			InstanceCreate(message);
 | |
| 		}
 | |
| 	}
 | |
| }
 |