//
// PaintApp.c
//
//
// Created by Chris Galzerano on 2/22/26.
//
#include "main.h"
#include "PaintApp.h"
#include "ColorWheel.h"
// --- APP STATE & MEMORY ---
CCView* colorPickerContainer = NULL;
Framebuffer* canvasFramebuffer = NULL;
CCFramebufferView* canvasView = NULL;
bool isColorPickerVisible = true; // We start with it open for testing
CCView* shapesMenu = NULL;
CCView* fileMenu = NULL;
static bool isShapesMenuOpen = false;
static bool isFileMenuOpen = false;
static bool isColorPickerOpen = false;
// --- TOUCH STATE TRACKING ---
static bool is_pressing = false;
static bool long_press_fired = false;
static uint64_t touchStartTime = 0;
static int touchStartX = 0;
static int touchStartY = 0;
static int activeTool = TAG_PAINT_PEN;
static ColorRGBA activeColor = {0.0, 0.0, 0.0, 1.0}; // Default to black brush
static int lastDragX = -1;
static int lastDragY = -1;
static int dirtyMinX = 9999, dirtyMinY = 9999, dirtyMaxX = -1, dirtyMaxY = -1;
static uint64_t lastCanvasUpdateTime = 0;
static uint64_t lastPointProcessTime = 0;
static int currentBrushThickness = 5;
// Helper: Safely draw a single pixel to the PSRAM buffer
static void draw_pixel_to_canvas(int x, int y, ColorRGBA color) {
if (!canvasFramebuffer || !canvasFramebuffer->pixelData) return;
// Safety clip so we don't crash if dragging outside the canvas
if (x < 0 || x >= canvasFramebuffer->displayWidth || y < 0 || y >= canvasFramebuffer->displayHeight) return;
int idx = (y * canvasFramebuffer->displayWidth + x) * 3;
uint8_t* pixels = (uint8_t*)canvasFramebuffer->pixelData;
pixels[idx] = (uint8_t)(color.r * 255.0f); // BGR/RGB depending on your engine
pixels[idx+1] = (uint8_t)(color.g * 255.0f);
pixels[idx+2] = (uint8_t)(color.b * 255.0f);
}
// Helper: Bresenham's Line Algorithm to connect fast touch drags
static void draw_line_to_canvas(int x0, int y0, int x1, int y1, ColorRGBA color) {
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy, e2;
while (1) {
draw_pixel_to_canvas(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
// Helper: Stamps a solid circle of pixels to create a thick, rounded brush tip
static void draw_brush_stamp(int cx, int cy, int radius, ColorRGBA color) {
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
// Check if the pixel is inside the circle's radius
if (x * x + y * y <= radius * radius) {
draw_pixel_to_canvas(cx + x, cy + y, color);
}
}
}
}
// Helper: Bresenham's line updated to stamp the thick brush!
static void draw_thick_line_to_canvas(int x0, int y0, int x1, int y1, int thickness, ColorRGBA color) {
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy, e2;
int radius = thickness / 2;
while (1) {
draw_brush_stamp(x0, y0, radius, color); // Stamp the thick circle!
if (x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
static void push_partial_canvas_update(int localX, int localY, int w, int h) {
if (!canvasFramebuffer || !fb.pixelData) return;
// 1. Get the global screen coordinates of the canvas
CCPoint absOrigin = getAbsoluteOrigin(canvasView->view);
int globalX = absOrigin.x + localX;
int globalY = absOrigin.y + localY;
// 2. Blit ONLY the dirty rectangle from the Canvas to the Main OS Framebuffer
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int srcIdx = ((localY + y) * canvasFramebuffer->displayWidth + (localX + x)) * 3;
int destIdx = ((globalY + y) * fb.displayWidth + (globalX + x)) * 3;
uint8_t* srcPixels = (uint8_t*)canvasFramebuffer->pixelData;
uint8_t* destPixels = (uint8_t*)fb.pixelData;
destPixels[destIdx] = srcPixels[srcIdx];
destPixels[destIdx+1] = srcPixels[srcIdx+1];
destPixels[destIdx+2] = srcPixels[srcIdx+2];
}
}
// 3. Package the command
GraphicsCommand cmd;
cmd.x = globalX;
cmd.y = globalY;
cmd.w = w;
cmd.h = h;
// --- START PROFILING TIMER ---
uint64_t start_time = esp_timer_get_time();
// Fire the SPI update!
updateArea1(fb, cmd);
// --- END PROFILING TIMER ---
uint64_t end_time = esp_timer_get_time();
// Calculate the duration
uint64_t duration_us = end_time - start_time;
uint64_t duration_ns = duration_us * 1000; // Convert microseconds to nanoseconds
// Log the exact time and the size of the box we just sent
FreeOSLogI("PaintApp", "updateArea pushed [%dx%d] box in %llu us (%llu ns)", w, h, duration_us, duration_ns);
}
// --- REUSABLE ICON BUTTON BUILDER ---
static CCView* make_icon_button(int x, int y, int tag, CCString* img) {
// 1. Use CCRect* pointer
CCRect* btnRect = ccRect(x, y, 40, 40);
CCView* btn = viewWithFrame(btnRect);
btn->backgroundColor = color(0.25, 0.25, 0.25, 1.0);
btn->tag = tag;
// 2. Use CCRect* pointer for the image view frame
CCRect* imgRect = ccRect(5, 5, 30, 30);
CCImageView* icon = imageViewWithFrame(imgRect);
icon->ignoreTouch = true;
imageViewSetImage(icon, imageWithFile(img));
// 3. Add the wrapper directly!
viewAddSubview(btn, icon);
return btn;
}
// --- FILE MENU BUILDER (160px Wide with Text) ---
static CCView* create_file_menu(int x, int y) {
CCRect* menuRect = ccRect(x, y, 160, 250);
CCView* menu = viewWithFrame(menuRect);
menu->backgroundColor = color(0.2, 0.2, 0.2, 1.0);
const char* labels[] = {"New", "Open", "Save", "Save As", "Copy", "Paste"};
int tags[] = {TAG_FILE_NEW, TAG_FILE_OPEN, TAG_FILE_SAVE, TAG_FILE_SAVEAS, TAG_FILE_COPY, TAG_FILE_PASTE};
for (int i = 0; i < 6; i++) {
CCRect* itemRect = ccRect(5, 5 + (i * 40), 150, 35);
CCView* itemBtn = viewWithFrame(itemRect);
itemBtn->backgroundColor = color(0.3, 0.3, 0.3, 1.0);
itemBtn->tag = tags[i];
CCRect* lblRect = ccRect(10, 8, 130, 20);
CCLabel* lbl = labelWithFrame(lblRect);
lbl->fontSize = 18.0;
lbl->textColor = color(1,1,1,1);
lbl->ignoreTouch = true;
labelSetText(lbl, ccs(labels[i]));
// Add the wrapper directly!
viewAddSubview(itemBtn, lbl);
viewAddSubview(menu, itemBtn);
}
return menu;
}
// --- SHAPES MENU BUILDER (Vertical Icons) ---
static CCView* create_shapes_menu(int x, int y) {
CCRect* menuRect = ccRect(x, y, 50, 186);
CCView* menu = viewWithFrame(menuRect);
menu->backgroundColor = color(0.2, 0.2, 0.2, 1.0);
// Using leftArrow20 for now until you add the specific shape glyphs
viewAddSubview(menu, make_icon_button(5, 5 + (44 * 0), TAG_SHAPE_LINE, ccs("/spiflash/leftArrow20.png")));
viewAddSubview(menu, make_icon_button(5, 5 + (44 * 1), TAG_SHAPE_RECT, ccs("/spiflash/leftArrow20.png")));
viewAddSubview(menu, make_icon_button(5, 5 + (44 * 2), TAG_SHAPE_CIRCLE, ccs("/spiflash/leftArrow20.png")));
viewAddSubview(menu, make_icon_button(5, 5 + (44 * 3), TAG_SHAPE_POLYGON, ccs("/spiflash/leftArrow20.png")));
return menu;
}
void setup_paint_ui(void) {
FreeOSLogI("PaintApp", "Starting Paint App Initialization");
// 1. Container & Canvas Setup
CCRect* screenRect = ccRect(0, 0, 320, 480);
mainWindowView = viewWithFrame(screenRect);
mainWindowView->backgroundColor = color(0.1, 0.1, 0.1, 1.0);
if (!canvasFramebuffer) {
canvasFramebuffer = (Framebuffer*)cc_safe_alloc(1, sizeof(Framebuffer));
canvasFramebuffer->displayWidth = 320;
canvasFramebuffer->displayHeight = 430;
canvasFramebuffer->colorMode = COLOR_MODE_BGR888;
canvasFramebuffer->pixelData = heap_caps_malloc(320 * 430 * 3, MALLOC_CAP_SPIRAM);
memset(canvasFramebuffer->pixelData, 255, 320 * 430 * 3); // Fill White
}
CCRect* canvasRect = ccRect(0, 50, 320, 430);
canvasView = framebufferViewWithFrame(canvasRect);
canvasView->tag = TAG_CANVAS;
framebufferViewSetFramebuffer(canvasView, canvasFramebuffer);
// Add the Framebuffer wrapper directly!
viewAddSubview(mainWindowView, canvasView);
// 2. Toolbar Setup
CCRect* toolbarRect = ccRect(0, 23, 320, 50);
CCView* toolbar = viewWithFrame(toolbarRect);
toolbar->backgroundColor = color(0.15, 0.15, 0.15, 1.0);
viewAddSubview(mainWindowView, toolbar);
// 3. Add the 7 Main Toolbar Buttons
int startX = 5;
int spacing = 44;
// Note: Y is passed as 5 to keep them centered vertically in the 50px toolbar
viewAddSubview(toolbar, make_icon_button(startX + (spacing * 0), 5, TAG_PAINT_PEN, ccs("/spiflash/brush.png")));
viewAddSubview(toolbar, make_icon_button(startX + (spacing * 1), 5, TAG_PAINT_SHAPES, ccs("/spiflash/leftArrow20.png"))); // Shape Menu Trigger
viewAddSubview(toolbar, make_icon_button(startX + (spacing * 2), 5, TAG_PAINT_BUCKET, ccs("/spiflash/leftArrow20.png")));
viewAddSubview(toolbar, make_icon_button(startX + (spacing * 3), 5, TAG_PAINT_TEXT, ccs("/spiflash/leftArrow20.png")));
viewAddSubview(toolbar, make_icon_button(startX + (spacing * 4), 5, TAG_PAINT_COLOR, ccs("/spiflash/colorIcon.png")));
viewAddSubview(toolbar, make_icon_button(startX + (spacing * 5), 5, TAG_PAINT_PROPERTIES, ccs("/spiflash/leftArrow20.png")));
viewAddSubview(toolbar, make_icon_button(startX + (spacing * 6), 5, TAG_PAINT_FILEMENU, ccs("/spiflash/leftArrow20.png"))); // File Menu Trigger
// 4. Pre-build the Dropdown Menus
// Positioned directly under their respective toolbar buttons!
shapesMenu = create_shapes_menu(startX + (spacing * 1), afb(toolbar));
fileMenu = create_file_menu(320 - 165, afb(toolbar));
// 6. Add the Color Picker View!
// (We put it at X:40, Y:100 so it floats nicely over the canvas)
//colorPickerContainer = colorPickerView(40, 100);
//viewAddSubview(mainWindowView, colorPickerContainer);
}
void handle_paint_touch(int x, int y, int touchState) {
// ==========================================
// 1. FINGER DOWN OR DRAGGING
// ==========================================
if (touchState == 1) {
if (!is_pressing) {
// --- FIRST FRAME OF TOUCH ---
is_pressing = true;
long_press_fired = false;
touchStartTime = esp_timer_get_time() / 1000;
touchStartX = x;
touchStartY = y;
// Set up our initial drag coordinates by converting the global touch
// into the Canvas's local coordinate system!
CCPoint localPt = viewConvertPoint(canvasView->view, x, y);
lastDragX = localPt.x;
lastDragY = localPt.y;
// Dismiss menus if clicking outside...
CCView* downHit = find_subview_at_point_recursive(mainWindowView, x, y);
if (isShapesMenuOpen && (!downHit || (downHit->tag < TAG_SHAPE_LINE || downHit->tag > TAG_SHAPE_POLYGON))) {
viewRemoveFromSuperview(shapesMenu);
isShapesMenuOpen = false;
update_full_ui();
}
if (isFileMenuOpen && (!downHit || (downHit->tag < TAG_FILE_NEW || downHit->tag > TAG_FILE_PASTE))) {
viewRemoveFromSuperview(fileMenu);
isFileMenuOpen = false;
update_full_ui();
}
} else {
// --- FINGER IS HELD DOWN OR DRAGGING ---
if (abs(x - touchStartX) > 15 || abs(y - touchStartY) > 15) {
touchStartTime = 0;
}
CCPoint localPt = viewConvertPoint(canvasView->view, x, y);
int canvasX = localPt.x;
int canvasY = localPt.y;
if (activeTool == TAG_PAINT_PEN && canvasY >= 0 && canvasY < canvasFramebuffer->displayHeight) {
if (lastDragX != -1 && lastDragY != -1) {
// 1. INSTANT PSRAM UPDATE
// This takes ~0.001ms. The CPU catches every tiny curve of your signature!
draw_thick_line_to_canvas(lastDragX, lastDragY, canvasX, canvasY, currentBrushThickness, activeColor);
// 2. EXPAND THE DIRTY ACCUMULATOR
// Keep stretching the box to fit the new lines
int currentMinX = (lastDragX < canvasX) ? lastDragX : canvasX;
int currentMaxX = (lastDragX > canvasX) ? lastDragX : canvasX;
int currentMinY = (lastDragY < canvasY) ? lastDragY : canvasY;
int currentMaxY = (lastDragY > canvasY) ? lastDragY : canvasY;
if (currentMinX < dirtyMinX) dirtyMinX = currentMinX;
if (currentMaxX > dirtyMaxX) dirtyMaxX = currentMaxX;
if (currentMinY < dirtyMinY) dirtyMinY = currentMinY;
if (currentMaxY > dirtyMaxY) dirtyMaxY = currentMaxY;
// 3. THROTTLE THE SPI UPDATE (The 6.7ms Bottleneck)
uint64_t now = esp_timer_get_time() / 1000; // Get current milliseconds
// Only push to the screen every 30 milliseconds (~33 FPS)
if (now - lastCanvasUpdateTime > 30) {
// Add brush padding to the accumulated box
int pad = (currentBrushThickness / 2) + 2;
dirtyMinX = (dirtyMinX > pad) ? dirtyMinX - pad : 0;
dirtyMinY = (dirtyMinY > pad) ? dirtyMinY - pad : 0;
dirtyMaxX = (dirtyMaxX < canvasFramebuffer->displayWidth - pad - 1) ? dirtyMaxX + pad : canvasFramebuffer->displayWidth - 1;
dirtyMaxY = (dirtyMaxY < canvasFramebuffer->displayHeight - pad - 1) ? dirtyMaxY + pad : canvasFramebuffer->displayHeight - 1;
int rectW = dirtyMaxX - dirtyMinX + 1;
int rectH = dirtyMaxY - dirtyMinY + 1;
// Push the combined movements over SPI!
push_partial_canvas_update(dirtyMinX, dirtyMinY, rectW, rectH);
// Reset the accumulator box and timer
dirtyMinX = 9999; dirtyMinY = 9999; dirtyMaxX = -1; dirtyMaxY = -1;
lastCanvasUpdateTime = now;
}
}
}
// Save the coordinate for the next frame
lastDragX = canvasX;
lastDragY = canvasY;
}
return;
}
// ==========================================
// 2. FINGER LIFTED (RELEASED)
// ==========================================
if (touchState == 0) {
if (!is_pressing) return;
is_pressing = false;
// FORCE FLUSH ANY REMAINING DRAWING!
/*if (dirtyMaxX != -1) {
int rectW = dirtyMaxX - dirtyMinX + 1;
int rectH = dirtyMaxY - dirtyMinY + 1;
push_partial_canvas_update(dirtyMinX, dirtyMinY, rectW, rectH);
dirtyMinX = 9999; dirtyMinY = 9999; dirtyMaxX = -1; dirtyMaxY = -1;
}*/
// Flush any remaining accumulated drawing to the screen!
if (dirtyMaxX != -1) {
int pad = (currentBrushThickness / 2) + 2;
dirtyMinX = (dirtyMinX > pad) ? dirtyMinX - pad : 0;
dirtyMinY = (dirtyMinY > pad) ? dirtyMinY - pad : 0;
dirtyMaxX = (dirtyMaxX < canvasFramebuffer->displayWidth - pad - 1) ? dirtyMaxX + pad : canvasFramebuffer->displayWidth - 1;
dirtyMaxY = (dirtyMaxY < canvasFramebuffer->displayHeight - pad - 1) ? dirtyMaxY + pad : canvasFramebuffer->displayHeight - 1;
int rectW = dirtyMaxX - dirtyMinX + 1;
int rectH = dirtyMaxY - dirtyMinY + 1;
push_partial_canvas_update(dirtyMinX, dirtyMinY, rectW, rectH);
dirtyMinX = 9999; dirtyMinY = 9999; dirtyMaxX = -1; dirtyMaxY = -1;
}
lastDragX = -1;
lastDragY = -1;
if (touchStartTime == 0 || long_press_fired) {
long_press_fired = false;
return;
}
// --- DYNAMIC HIT TESTING! ---
CCView* tappedView = find_subview_at_point_recursive(mainWindowView, touchStartX, touchStartY);
if (!tappedView) {
FreeOSLogI("PaintApp", "Tapped the blank background.");
return;
}
FreeOSLogI("PaintApp", "Tapped the blank background %d", tappedView->tag);
// Just route the action based on the View's Tag!
switch (tappedView->tag) {
case TAG_PAINT_PEN:
case TAG_PAINT_BUCKET:
case TAG_PAINT_TEXT:
activeTool = tappedView->tag; // Switch current tool!
FreeOSLogI("PaintApp", "Active tool changed to: %d", activeTool);
break;
// --- TOOLBAR TOGGLES ---
case TAG_PAINT_SHAPES:
isShapesMenuOpen = !isShapesMenuOpen;
if (isShapesMenuOpen) viewAddSubview(mainWindowView, shapesMenu);
else viewRemoveFromSuperview(shapesMenu);
update_full_ui();
break;
case TAG_PAINT_FILEMENU:
isFileMenuOpen = !isFileMenuOpen;
if (isFileMenuOpen) viewAddSubview(mainWindowView, fileMenu);
else viewRemoveFromSuperview(fileMenu);
update_full_ui();
break;
case TAG_PAINT_COLOR:
isColorPickerOpen = !isColorPickerOpen;
if (isColorPickerOpen) {
colorPickerContainer = colorPickerView(40, 100);
viewAddSubview(mainWindowView, colorPickerContainer);
}
else viewRemoveFromSuperview(colorPickerContainer);
update_full_ui();
break;
case TAG_FILE_NEW:
FreeOSLogI("PaintApp", "New File Selected!");
viewRemoveFromSuperview(fileMenu);
isFileMenuOpen = false;
update_full_ui();
break;
case TAG_FILE_OPEN:
case TAG_FILE_SAVE:
FreeOSLogI("PaintApp", "File Menu Action %d triggered", tappedView->tag);
viewRemoveFromSuperview(fileMenu);
isFileMenuOpen = false;
update_full_ui();
break;
case TAG_SHAPE_LINE:
case TAG_SHAPE_RECT:
case TAG_SHAPE_CIRCLE:
case TAG_SHAPE_POLYGON:
activeTool = tappedView->tag;
FreeOSLogI("PaintApp", "Active tool changed to Shape: %d", activeTool);
viewRemoveFromSuperview(shapesMenu);
isShapesMenuOpen = false;
update_full_ui();
break;
// --- COLOR PICKER INTERACTION ---
case TAG_CANVAS:
// Using your new viewConvertPoint for the color wheel math!
if (isColorPickerOpen) {
CCPoint localTap = viewConvertPoint(tappedView, touchStartX, touchStartY);
CCColor* picked = getColorFromWheelTouch(localTap.x, localTap.y, tappedView->frame->size->width, tappedView->frame->size->height);
FreeOSLogI("PaintApp", "Picked Color: R:%f G:%f B:%f", picked->r, picked->g, picked->b);
}
break;
}
}
}