//
// FreeOS.c
//
//
// Created by Chris Galzerano on 2/7/26.
//
#include "FreeOS.h"
#include "FilesApp.h"
#include "ClockApp.h"
// --- Global Variable Definitions ---
FT_Library ft_library;
FT_Face ft_face;
uint8_t* font_buffer = NULL;
Framebuffer fb;
MyQueueHandle_t g_graphics_queue;
bool setupui = false;
bool touchEnabled = true;
// --- Global Root View ---
CCView* mainWindowView = NULL;
CCScrollView* g_active_scrollview = NULL;
int g_touch_last_y = 0;
int lastOffY = 0;
CCTextView* myDemoTextView = NULL;
// --- Global Scroll State ---
int g_text_scroll_y = 0;
int g_text_total_height = 0;
int g_text_view_h = 300;
const char* g_long_text_ptr = NULL;
bool notFirstTouch = false;
// --- FreeType Cache Globals ---
FTC_Manager g_ftc_manager = NULL;
FTC_ImageCache g_ftc_image_cache = NULL;
FTC_CMapCache g_ftc_cmap_cache = NULL;
// --- View Stack ---
CCView* viewStack[MAX_VIEW_STACK];
int viewStackPointer = 0;
CurrentView currentView = CurrentViewHome;
// --- App/System State ---
bool openedApp = false;
CCArray* files = NULL;
CCArray* settings = NULL;
// --- Keyboard Globals ---
CCView* uiKeyboardView = NULL;
CCLabel* uiTargetLabel = NULL;
CCString* uiInputBuffer = NULL;
// --- Animation & Backup Buffers ---
uint8_t* g_anim_backup_buffer = NULL;
uint8_t* g_ui_backup_buffer_A = NULL;
uint8_t* g_ui_backup_buffer_B = NULL;
CCArray* g_grid_items_registry = NULL;
CCView* g_last_touched_icon = NULL;
// --- Global Cursor State ---
uint8_t* g_cursor_backup_buffer = NULL;
bool addedCursor = false;
int g_cursor_x = 0;
int g_cursor_y = 0;
bool g_cursor_visible = false;
TaskHandle_t g_cursor_blink_handle = NULL;
// --- Scrolling UI State ---
int g_scroll_offset_y = 0;
int g_scroll_total_height = 0;
int g_scroll_viewport_h = 300;
int g_drag_start_y = 0;
float g_drag_velocity = 0.0f;
// --- Keyboard Internal State ---
bool hasDrawnKeyboard = false;
int keyboardCursorPosition = 0;
// --- UI Highlighting ---
CCView* g_pressed_icon_view = NULL;
ColorRGBA g_color_highlight = {.r=0, .g=0, .b=0, .a=25};
ColorRGBA g_color_transparent = {.r=0, .g=0, .b=0, .a=0};
KeyboardMode kbCurrentMode = KB_MODE_ABC_LOWER;
// --- Task Handles ---
TaskHandle_t g_triangle_task_handle = NULL;
TaskHandle_t g_image_task_handle = NULL;
// --- Image Decoders ---
const char *TAG_PNG = "PNG_DECODER";
FILE *png_file_handle = NULL;
uint8_t *png_load_buffer = NULL;
int png_load_width = 0;
int png_load_height = 0;
const char *TAG_JPG = "JPG_DECODER";
// 1. Write custom allocator functions forcing PSRAM
static void* ft_alloc(FT_Memory memory, long size) {
return heap_caps_malloc(size, MALLOC_CAP_SPIRAM); // FORCE PSRAM!
}
static void ft_free(FT_Memory memory, void* block) {
heap_caps_free(block);
}
static void* ft_realloc(FT_Memory memory, long cur_size, long new_size, void* block) {
return heap_caps_realloc(block, new_size, MALLOC_CAP_SPIRAM); // FORCE PSRAM!
}
//
// =================== UPDATED: FreeType Initialization ===================
//
esp_err_t initialize_freetype()
{
FreeOSLogI(TAG, "Initializing FreeType with PSRAM...");
// 1. Allocate the custom memory manager struct in PSRAM
FT_Memory memory = (FT_Memory)heap_caps_malloc(sizeof(*memory), MALLOC_CAP_SPIRAM);
if (!memory) {
FreeOSLogE(TAG, "Failed to allocate FreeType memory struct!");
return ESP_FAIL;
}
memory->alloc = ft_alloc;
memory->free = ft_free;
memory->realloc = ft_realloc;
memory->user = NULL;
// 2. Initialize FreeType using our custom memory manager
// We declare the 'error' variable here to fix the "undeclared" compiler error!
FT_Error error = FT_New_Library(memory, &ft_library);
if (error) {
FreeOSLogE(TAG, "Failed to create FreeType library with custom memory!");
return ESP_FAIL;
}
// 3. Load standard FreeType modules (renderers, format parsers)
FT_Add_Default_Modules(ft_library);
// --- THIS IS THE NEW PART ---
// Read the font file from the mounted /storage partition
const char* font_path = "/spiflash/proximaNovaRegular.ttf";
FreeOSLogI(TAG, "Loading font from %s", font_path);
FILE* file = fopen(font_path, "rb");
if (!file) {
FreeOSLogE(TAG, "Failed to open font file. Make sure it's in your 'storage' folder.");
return ESP_FAIL;
}
// Get file size
fseek(file, 0, SEEK_END);
long font_size = ftell(file);
fseek(file, 0, SEEK_SET);
// Allocate a buffer for the font.
// FreeType needs this buffer to exist for as long as the font is used.
//font_buffer = (uint8_t*)cc_safe_alloc(1, font_size);
font_buffer = (uint8_t*)heap_caps_malloc(font_size, MALLOC_CAP_SPIRAM);
if (!font_buffer) {
FreeOSLogE(TAG, "Failed to allocate memory for font buffer (%ld bytes)", font_size);
fclose(file);
return ESP_FAIL;
}
// Read the file into the buffer
fread(font_buffer, 1, font_size, file);
fclose(file);
// Load the font face from the memory buffer
error = FT_New_Memory_Face(ft_library,
font_buffer, // Font buffer
font_size, // Font size
0, // Face index (0 for first face)
&ft_face); // Output handle
if (error) {
FreeOSLogE(TAG, "Failed to load font face! FreeType error: 0x%X", error);
// Free the buffer if we failed
free(font_buffer);
font_buffer = NULL;
return ESP_FAIL;
}
FreeOSLogI(TAG, "FreeType initialized and font loaded.");
return ESP_OK;
}
// --- Face Requester Callback ---
// The Cache Manager calls this automatically when it needs to load a font file.
// We use the file path string itself as the 'face_id'.
FT_Error face_requester(FTC_FaceID face_id, FT_Library library, FT_Pointer req_data, FT_Face* aface) {
const char* font_path = (const char*)face_id;
// printf("FTC: Loading font from flash: %s\n", font_path);
return FT_New_Face(library, font_path, 0, aface);
}
// --- Initialize and Preload ---
void init_freetype_cache_system(void) {
if (g_ftc_manager != NULL) return; // Prevent double init
printf("Initializing FreeType Cache...\n");
// 1. Create the Cache Manager
// Max Faces: 2 (Keep low for embedded)
// Max Sizes: 4 (e.g., 12, 18, 24, 30)
// Max Bytes: 200KB (Adjust based on your available SPIRAM)
FT_Error error = FTC_Manager_New(
ft_library,
2, // max_faces
4, // max_sizes
200 * 1024, // max_bytes (200KB cache)
face_requester, // Callback to load files
NULL, // req_data
&g_ftc_manager
);
if (error) {
printf("CRITICAL: Failed to create FTC Manager! Error: %d\n", error);
return;
}
// 2. Create the Sub-Caches (Image = Glyphs, CMap = Char-to-Index)
FTC_ImageCache_New(g_ftc_manager, &g_ftc_image_cache);
FTC_CMapCache_New(g_ftc_manager, &g_ftc_cmap_cache);
// --- 3. Preload a Specific Size (Warm Up) ---
// This forces the I/O and parsing to happen NOW.
const char* font_path = "/spiflash/proximaNovaRegular.ttf";
int preload_size = 12;
printf("Preloading Font: %s @ %dpx\n", font_path, preload_size);
FTC_ScalerRec scaler;
scaler.face_id = (FTC_FaceID)font_path; // Cast path to ID
scaler.width = 0;
scaler.height = preload_size;
scaler.pixel = 1; // 1 = Size is pixels
scaler.x_res = 0;
scaler.y_res = 0;
FT_Size size;
// Looking up the size forces the manager to load the file and set the scale
if (FTC_Manager_LookupSize(g_ftc_manager, &scaler, &size) != 0) {
printf("WARNING: Failed to preload font size.\n");
} else {
printf("Font preloaded successfully. Ready for fast rendering.\n");
}
}
void push_view(CCView* currentView) {
if (viewStackPointer < MAX_VIEW_STACK) {
viewStack[viewStackPointer++] = currentView;
printf("View pushed to stack. Stack size: %d\n", viewStackPointer);
} else {
printf("Error: View stack full!\n");
}
}
CCView* pop_view() {
if (viewStackPointer > 0) {
viewStackPointer--;
printf("View popped from stack. Remaining: %d\n", viewStackPointer);
return viewStack[viewStackPointer];
}
return NULL; // Stack is empty
}
void teardown_keyboard_data(void) {
// 1. Free the Input String Buffer (Prevents Memory Leak)
if (uiInputBuffer) {
freeCCString(uiInputBuffer);
uiInputBuffer = NULL;
}
// 2. Clear the View Pointer (Prevents "Dangling Pointer" Crashes)
// Note: We do NOT free(uiKeyboardView) here because freeViewHierarchy
// will handle the actual memory when it frees the parent window.
uiKeyboardView = NULL;
// 3. Clear the Target (Prevents "Ghost Updates")
uiTargetLabel = NULL;
// 4. Reset Touch State (Prevents Stuck Keys)
//reset_keyboard_touch_state();
}
void close_current_app(void) {
// 1. Check if we have anywhere to go back to
CCView* previousView = pop_view();
if (previousView != NULL) {
// 2. Clean up the current app (The Files App)
// We created it with malloc/viewWithFrame, so we should free it
// to prevent memory leaks now that we are done with it.
// Assuming you have a function like freeViewHierarchy(CCView* v)
teardown_keyboard_data();
freeViewHierarchy(mainWindowView);
CurrentView newCurrentView = -999;
if (currentView == CurrentViewFiles) {
freeFilesView();
}
else if (currentView == CurrentViewSettings) {
freeElement(settings);
}
else if (currentView == CurrentViewWifi) {
newCurrentView = CurrentViewSettings;
}
if (newCurrentView != -999) {
currentView = newCurrentView;
}
else {
currentView = CurrentViewHome;
}
openedApp = false;
// 3. Restore the old view
mainWindowView = previousView;
// 4. Refresh
printf("Restored previous view.\n");
update_full_ui();
} else {
printf("Can't go back, stack empty.\n");
}
}
/**
* @brief Returns a CCArray of CCDictionaries containing file metadata.
* Keys: "Name" (String), "Path" (String), "DateModified" (CCDate), "Size" (Number)
*/
CCArray* get_directory_files_as_array(const char *mount_point) {
CCArray* fileList = array();
DIR *dir = NULL;
struct dirent *ent;
char full_path[256];
struct stat st;
dir = opendir(mount_point);
if (!dir) {
FreeOSLogE(TAG, "Failed to open directory: %s", mount_point);
return fileList;
}
while ((ent = readdir(dir)) != NULL) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
if (mount_point[strlen(mount_point) - 1] == '/') {
snprintf(full_path, sizeof(full_path), "%s%s", mount_point, ent->d_name);
} else {
snprintf(full_path, sizeof(full_path), "%s/%s", mount_point, ent->d_name);
}
if (stat(full_path, &st) == 0) {
CCDictionary* fileDict = dictionary();
// 1. Name & Path
// Correct Order: (Dict, Value, Key)
dictionarySetObjectForKey(fileDict, ccs(ent->d_name), ccs("Name"));
dictionarySetObjectForKey(fileDict, ccs(full_path), ccs("Path"));
// 2. Size
// Correct Order: (Dict, Value, Key)
dictionarySetObjectForKey(fileDict, numberWithInt((int)st.st_size), ccs("Size"));
// 3. Date Modified
// Correct Order: (Dict, Value, Key)
CCDate* fileDate = dateWithTimeInterval((double)st.st_mtime);
dictionarySetObjectForKey(fileDict, fileDate, ccs("DateModified"));
// 4. Is Directory
// Correct Order: (Dict, Value, Key)
bool isDir = S_ISDIR(st.st_mode);
dictionarySetObjectForKey(fileDict, numberWithInt(isDir ? 1 : 0), ccs("IsDirectory"));
arrayAddObject(fileList, fileDict);
} else {
ESP_LOGW(TAG, "Failed to stat: %s", full_path);
}
}
closedir(dir);
return fileList;
}
void init_ui_buffers() {
g_ui_backup_buffer_A = (uint8_t*)heap_caps_malloc(BUFFER_SIZE, MALLOC_CAP_SPIRAM);
g_ui_backup_buffer_B = (uint8_t*)heap_caps_malloc(BUFFER_SIZE, MALLOC_CAP_SPIRAM);
if (!g_ui_backup_buffer_A || !g_ui_backup_buffer_B) {
FreeOSLogE(TAG, "Failed to allocate UI backup buffers!");
}
}
CCPoint getAbsoluteOrigin(CCView* view) {
float x = 0;
float y = 0;
CCView* current = view;
while (current != NULL) {
x += current->frame->origin->x;
y += current->frame->origin->y;
// --- FIX: Cast the pointer ---
current = (CCView*)current->superview;
// -----------------------------
}
return (CCPoint){ .x = x, .y = y };
}
// Calculates the actual visible rectangle of a view on screen,
// accounting for all parent offsets AND parent clipping masks.
CCRect getAbsoluteVisibleRect(CCView* view) {
if (!view) return *ccRect(0,0,0,0);
// 1. Start with the view's own bounds relative to itself
float x = 0;
float y = 0;
float w = view->frame->size->width;
float h = view->frame->size->height;
// 2. Walk up the tree to transform coordinates and apply clips
CCView* current = view;
// Accumulate offsets to get to screen space
// We have to do this carefully:
// The clipping rects of parents are in THEIR coordinate space,
// so we need to track where 'current' is relative to screen.
// Actually, it's easier to calculate the Absolute Rect of the view first,
// then walk up and intersect with Absolute Rects of parents.
CCPoint absOrigin = getAbsoluteOrigin(view); // Use your existing helper
float finalX = absOrigin.x;
float finalY = absOrigin.y;
float finalW = w;
float finalH = h;
// Now walk up to check clipping
current = (CCView*)view->superview;
while (current != NULL) {
if (current->layer->masksToBounds) {
CCPoint parentOrigin = getAbsoluteOrigin(current);
// Parent's Visible Absolute Rect
float pX = parentOrigin.x;
float pY = parentOrigin.y;
float pW = current->frame->size->width;
float pH = current->frame->size->height;
// Intersect (Math logic expanded here for clarity)
float interLeft = (finalX > pX) ? finalX : pX;
float interTop = (finalY > pY) ? finalY : pY;
float interRight = (finalX + finalW < pX + pW) ? (finalX + finalW) : (pX + pW);
float interBottom = (finalY + finalH < pY + pH) ? (finalY + finalH) : (pY + pH);
if (interLeft < interRight && interTop < interBottom) {
finalX = interLeft;
finalY = interTop;
finalW = interRight - interLeft;
finalH = interBottom - interTop;
} else {
// Completely hidden
return *ccRect(0,0,0,0);
}
}
current = (CCView*)current->superview;
}
return *ccRect(finalX, finalY, finalW, finalH);
}
// Add this to scan.c
void update_label_safe(CCLabel* label) {
if (!label || !label->view) return;
// 1. Get Coordinates
CCPoint origin = getAbsoluteOrigin(label->view);
int x = (int)origin.x;
int y = (int)origin.y;
int w = (int)label->view->frame->size->width;
int h = (int)label->view->frame->size->height;
printf("\n update_label_safe %d %d \n", w , h);
// 2. Erase Old Text (Draw Background)
// We assume the label is opaque (backgroundColor is set).
GraphicsCommand cmd_bg = {
.cmd = CMD_DRAW_RECT,
.x = x, .y = y, .w = w, .h = h,
.color = convert_cc_color(label->view->backgroundColor),
// No clipping needed for self-update usually, or clip to self
.clipX = 0, .clipY = 0, .clipW = 320, .clipH = 480
};
QueueSend(g_graphics_queue, &cmd_bg, QUEUE_MAX_DELAY);
// 3. Draw New Text
//CCLabel* label = (CCLabel*)object;
ColorRGBA textCol = convert_cc_color(label->textColor);
TextFormat fmt = { 0 };
if (label->textAlignment == CCTextAlignmentCenter) fmt.alignment = TEXT_ALIGN_CENTER;
else if (label->textAlignment == CCTextAlignmentRight) fmt.alignment = TEXT_ALIGN_RIGHT;
else fmt.alignment = TEXT_ALIGN_LEFT;
if (label->textVerticalAlignment == CCTextVerticalAlignmentCenter) fmt.valignment = TEXT_VALIGN_CENTER;
else if (label->textVerticalAlignment == CCTextVerticalAlignmentTop) fmt.valignment = TEXT_VALIGN_TOP;
else fmt.valignment = TEXT_VALIGN_BOTTOM;
//FreeOSLogI(TAG, "drawViewHierarchy5 CMD_DRAW_TEXT_BOX valignment %d", fmt.valignment);
if (label->lineBreakMode == CCLineBreakWordWrapping) fmt.wrapMode = TEXT_WRAP_MODE_WHOLE_WORD;
else fmt.wrapMode = TEXT_WRAP_MODE_TRUNCATE;
fmt.lineSpacing = (int)label->lineSpacing;
fmt.glyphSpacing = (int)label->glyphSpacing;
GraphicsCommand cmd_text = {
.cmd = CMD_DRAW_TEXT_BOX_CACHED,
.x = x, .y = y, .w = w, .h = h,
.color = textCol,
.fontSize = (int)label->fontSize,
.textFormat = fmt,
.clipX = 0,
.clipY = 0,
.clipW = 320,
.clipH = 480
};
if (label->text && label->text->string) {
// strdup allocates memory on heap and copies the string
cmd_text.text = strdup(label->text->string);
} else {
cmd_text.text = NULL;
}
QueueSend(g_graphics_queue, &cmd_text, QUEUE_MAX_DELAY);
// 4. Force Flush of ONLY this area (Optimization)
// If you have a specific flush command, use it.
// Otherwise, the Graphics Task usually flushes after processing.
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0, .w = 320, .h = 480
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
}
void update_view_area_via_parent(CCView* view) {
if (!view || !view->superview) return;
// 1. Calculate Child's Absolute Position (Target for Update)
CCPoint childAbs = getAbsoluteOrigin(view);
int targetX = (int)childAbs.x;
int targetY = (int)childAbs.y;
int targetW = (int)view->frame->size->width;
int targetH = (int)view->frame->size->height;
// --- EXPANSION (Optional, keep 0 for debugging) ---
int expand = 0;
int drawX = targetX - expand;
int drawY = targetY - expand;
int drawW = targetW + (expand * 2);
int drawH = targetH + (expand * 2);
// 2. Create Clip Rect
CCRect* clipRectPtr = ccRect(drawX, drawY, drawW, drawH);
if (!clipRectPtr) return;
// 3. Find the Parent (We draw starting here)
CCView* parent = (CCView*)view->superview;
// 4. Calculate Parent's Absolute Position
// We use the SAME helper function to guarantee coordinates match 'targetX/Y'
CCPoint parentAbs = getAbsoluteOrigin(parent);
// 5. Draw from Parent
// We pass the Parent's absolute screen coordinates as the starting point.
// drawViewHierarchy will take this (X,Y) and add the Child's relative (x,y)
// resulting in exactly (targetX, targetY).
drawViewHierarchy(parent, (int)parentAbs.x, (int)parentAbs.y, *clipRectPtr, true);
freeCCRect(clipRectPtr);
// 6. Safety Clamp for LCD
if (drawX < 0) drawX = 0;
if (drawY < 0) drawY = 0;
if (drawX + drawW > 320) drawW = 320 - drawX;
if (drawY + drawH > 480) drawH = 480 - drawY;
// 7. Push Update
/*GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = drawX, .y = drawY, .w = drawW, .h = drawH
};*/
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0, .w = 320, .h = 480
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
}
void update_view_only(CCView* view) {
if (!view) return;
// --- 1. Use the FULL Frame, not the "Visible/Clipped" Rect ---
// The drawing logic likely generates the full view regardless of clipping.
// We must ensure the hardware window is large enough to accept ALL that data.
CCPoint absOrigin = getAbsoluteOrigin(view);
int rawX = (int)absOrigin.x;
int rawY = (int)absOrigin.y;
int rawW = (int)view->frame->size->width;
int rawH = (int)view->frame->size->height;
// --- 5. Define the Clip Rect for the Renderer ---
// We give the renderer the full padded area so it draws the borders.
CCRect* clipRectPtr = ccRect(rawX, rawY, rawW, rawH);
if (!clipRectPtr) return;
// --- 6. Draw ---
// Calculate parent-relative coordinates for the draw function
int parentAbsX = rawX - (int)view->frame->origin->x;
int parentAbsY = rawY - (int)view->frame->origin->y;
// Use the PADDED size for the clip
drawViewHierarchy(view, parentAbsX, parentAbsY, *clipRectPtr, true);
freeCCRect(clipRectPtr);
// --- 7. Flush the PADDED, ALIGNED Area ---
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0,
.y = 0,
.w = 320,
.h = 480
};
/*GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = rawX,
.y = rawY,
.w = rawW,
.h = rawH
};*/
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
}
void update_full_ui(void) {
if (!mainWindowView) return;
touchEnabled = false;
FreeOSLogI(TAG, "Starting UI Update...");
// 1. Clear the screen first (Optional, but prevents ghosting)
// We use the main window's background color for this
ColorRGBA bgCol = convert_cc_color(mainWindowView->backgroundColor);
GraphicsCommand cmd_clear = {
.cmd = CMD_DRAW_RECT,
.x = 0, .y = 0,
.w = 320, .h = 480,
.color = bgCol,
.fill = true
};
// Use QUEUE_MAX_DELAY to ensure we don't drop the clear command
QueueSend(g_graphics_queue, &cmd_clear, QUEUE_MAX_DELAY);
CCRect* screenRect = ccRect(0, 0, 320, 480);
// 2. Walk the tree and generate all draw commands (Shadows, Borders, Views)
// This calls the recursive function we wrote earlier
drawViewHierarchy(mainWindowView, 0, 0, *screenRect, false);
freeCCRect(screenRect);
// 3. Push the pixels to the LCD (The Chunked Update)
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0,
.w = 320, .h = 480
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
FreeOSLogI(TAG, "UI Update Commands Sent.");
}
void update_full_ui1(void) {
if (!mainWindowView) return;
touchEnabled = false;
FreeOSLogI(TAG, "Starting UI Update...");
// 1. Clear the screen first (Optional, but prevents ghosting)
// We use the main window's background color for this
ColorRGBA bgCol = convert_cc_color(mainWindowView->backgroundColor);
GraphicsCommand cmd_clear = {
.cmd = CMD_DRAW_RECT,
.x = 0, .y = 0,
.w = 320, .h = 480,
.color = bgCol,
.fill = true
};
// Use QUEUE_MAX_DELAY to ensure we don't drop the clear command
QueueSend(g_graphics_queue, &cmd_clear, QUEUE_MAX_DELAY);
CCRect* screenRect = ccRect(0, 0, 320, 480);
// 2. Walk the tree and generate all draw commands (Shadows, Borders, Views)
// This calls the recursive function we wrote earlier
drawViewHierarchy(mainWindowView, 0, 0, *screenRect, true);
freeCCRect(screenRect);
// 3. Push the pixels to the LCD (The Chunked Update)
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0,
.w = 320, .h = 480
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
FreeOSLogI(TAG, "UI Update Commands Sent.");
}
void drawShapeLayer(CCShapeLayer* shapeLayer, int absX, int absY) {
// 1. Basic Null Checks
if (!shapeLayer) return;
if (!shapeLayer->pointPath) return;
if (!shapeLayer->pointPath->points) return;
CCArray* points = shapeLayer->pointPath->points;
int count = points->count;
// 2. Minimum Vertex Check
if (count < 3) return;
// 3. Allocate Vertices
Vector3* rawVertices = (Vector3*)cc_safe_alloc(1, sizeof(Vector3) * count);
if (!rawVertices) {
FreeOSLogE(TAG, "Failed to allocate vertices for shape layer");
return;
}
// 4. Convert Coordinates with SAFETY CHECK
for (int i = 0; i < count; i++) {
CCPoint* pt = (CCPoint*)arrayObjectAtIndex(points, i);
// --- SAFETY CHECK START ---
if (pt == NULL) {
FreeOSLogE(TAG, "Point at index %d is NULL! Aborting shape draw.", i);
free(rawVertices); // Prevent memory leak
return;
}
// --- SAFETY CHECK END ---
rawVertices[i].x = (float)(absX + pt->x);
rawVertices[i].y = (float)(absY + pt->y);
rawVertices[i].z = 0.0f;
}
// 5. Handle Gradient or Solid Fill
Gradient* lowLevelGrad = NULL;
if (shapeLayer->gradient) {
lowLevelGrad = create_low_level_gradient(shapeLayer->gradient);
} else {
// Create "Fake" Gradient for Solid Color
// Ensure shapeLayer->fillColor is valid
ColorRGBA solid;
if (shapeLayer->fillColor) {
solid = convert_cc_color(shapeLayer->fillColor);
} else {
// Default to magenta so you can see the error visibly
solid = (ColorRGBA){255, 0, 255, 255};
}
lowLevelGrad = (Gradient*)cc_safe_alloc(1, sizeof(Gradient));
if (lowLevelGrad) {
lowLevelGrad->type = GRADIENT_TYPE_LINEAR;
lowLevelGrad->angle = 0;
lowLevelGrad->numStops = 2;
lowLevelGrad->stops = (ColorStop*)cc_safe_alloc(1, sizeof(ColorStop) * 2);
if (lowLevelGrad->stops) {
lowLevelGrad->stops[0].color = solid;
lowLevelGrad->stops[0].position = 0.0f;
lowLevelGrad->stops[1].color = solid;
lowLevelGrad->stops[1].position = 1.0f;
}
}
}
// 6. Send Command
if (lowLevelGrad && lowLevelGrad->stops) {
GraphicsCommand cmd_poly = {
.cmd = CMD_DRAW_POLYGON,
.vertices = rawVertices,
.numVertices = count,
.gradientData = lowLevelGrad
};
if (QueueSend(g_graphics_queue, &cmd_poly, QUEUE_MAX_DELAY) != pdTRUE) {
FreeOSLogE(TAG, "Failed to send polygon command");
free(rawVertices);
if(lowLevelGrad->stops) free(lowLevelGrad->stops);
free(lowLevelGrad);
}
} else {
// Cleanup if gradient creation failed
free(rawVertices);
if (lowLevelGrad) free(lowLevelGrad);
}
}
// Helper to find intersection of two rects (for clipping)
CCRect intersectRects(CCRect r1, CCRect r2) {
float r1_min_x = r1.origin->x;
float r1_min_y = r1.origin->y;
float r1_max_x = r1_min_x + r1.size->width;
float r1_max_y = r1_min_y + r1.size->height;
float r2_min_x = r2.origin->x;
float r2_min_y = r2.origin->y;
float r2_max_x = r2_min_x + r2.size->width;
float r2_max_y = r2_min_y + r2.size->height;
float inter_min_x = (r1_min_x > r2_min_x) ? r1_min_x : r2_min_x;
float inter_min_y = (r1_min_y > r2_min_y) ? r1_min_y : r2_min_y;
float inter_max_x = (r1_max_x < r2_max_x) ? r1_max_x : r2_max_x;
float inter_max_y = (r1_max_y < r2_max_y) ? r1_max_y : r2_max_y;
if (inter_min_x < inter_max_x && inter_min_y < inter_max_y) {
return *ccRect(inter_min_x, inter_min_y, inter_max_x - inter_min_x, inter_max_y - inter_min_y);
}
// No intersection (empty rect)
return *ccRect(0, 0, 0, 0);
}
// Safely reads 1 byte from Flash memory (I-ROM/D-ROM), bypassing hardware alignment exceptions
static inline char safe_flash_read_byte(const char *ptr) {
// 1. Calculate the nearest 32-bit aligned memory address
const uint32_t *aligned_ptr = (const uint32_t *)((intptr_t)ptr & ~3);
// 2. Perform a safe 32-bit hardware read
uint32_t word = *aligned_ptr;
// 3. Find out which of the 4 bytes we actually wanted
int byte_offset = ((intptr_t)ptr & 3);
// 4. Shift and mask out the target byte (ESP32 is Little Endian)
return (char)((word >> (byte_offset * 8)) & 0xFF);
}
// Converts a global screen touch (e.g., 480x320) into the local coordinate system of a specific view
CCPoint viewConvertPoint(CCView* targetView, int globalX, int globalY) {
CCPoint absOrigin = getAbsoluteOrigin(targetView);
CCPoint localPoint;
localPoint.x = globalX - absOrigin.x;
localPoint.y = globalY - absOrigin.y;
return localPoint;
}
/**
* @brief Recursively traverses a CCView tree and generates graphics commands.
* * @param view The current view to render.
* @param parentX The accumulated X coordinate of the parent view.
* @param parentY The accumulated Y coordinate of the parent view.
*/
// Function Signature: Accepts void* to handle CCView, CCLabel, and CCImageView
void drawViewHierarchy(void* object, int parentX, int parentY, CCRect currentClip, bool notHierarchy) {
if (!object) return;
//FreeOSLogI(TAG, "CMD_DRAW_GRADIENT_ROUNDED_RECT %d %d %d %d", (int)currentClip.origin->x, (int)currentClip.origin->y, (int)currentClip.size->width, (int)currentClip.size->height);
// 1. Identify Type
// We safely cast to CCType* first because 'type' is the first member of ALL your structs.
CCType type = *((CCType*)object);
FreeOSLogI("drawViewHierarchy", "CCType_FramebufferView %d", (type == CCType_FramebufferView)?1:0);
// 2. Unwrap the "Base View"
// This is the common structure shared by all UI elements
CCView* baseView = NULL;
if (type == CCType_View) {
baseView = (CCView*)object;
}
else if (type == CCType_ImageView) {
baseView = ((CCImageView*)object)->view;
}
else if (type == CCType_Label) {
baseView = ((CCLabel*)object)->view;
}
else if (type == CCType_FramebufferView) {
baseView = ((CCFramebufferView*)object)->view;
}
else {
return; // Unknown type
}
// Safety check
if (!baseView) return;
//FreeOSLogI(TAG, "drawViewHierarchy");
// 3. Calculate Absolute Position
int absX = parentX + (int)baseView->frame->origin->x;
int absY = parentY + (int)baseView->frame->origin->y;
int w = (int)baseView->frame->size->width;
int h = (int)baseView->frame->size->height;
CCRect myEffectiveClip = currentClip;
if (notHierarchy) {
// ============================================================
// USE 'baseView' FOR GEOMETRY, LAYERS, AND BACKGROUNDS
// ============================================================
if (baseView->frame->size->width == 80 && baseView->frame->size->height == 100) {
//printf("DEBUG: drawViewHierarchy | View Abs Pos: X=%d Y=%d | Parent Passed: X=%d Y=%d\n",
// absX, absY, parentX, parentY);
}
// ============================================================
// FIXED VISIBILITY CULLING (Include Shadows!)
// ============================================================
// 1. Calculate the "Visual Padding" needed
int padLeft = 0, padRight = 0, padTop = 0, padBottom = 0;
if (!baseView->layer->masksToBounds) {
if (baseView->layer->shadowOpacity > 0.0f) {
int r = (int)baseView->layer->shadowRadius;
int offX = (int)baseView->layer->shadowOffset->x;
int offY = (int)baseView->layer->shadowOffset->y;
// Expand to cover the shadow direction + radius
padLeft = (offX < 0) ? r + abs(offX) : r;
padRight = (offX > 0) ? r + abs(offX) : r;
padTop = (offY < 0) ? r + abs(offY) : r;
padBottom = (offY > 0) ? r + abs(offY) : r;
}
if (baseView->layer->borderWidth > 0) {
int b = (int)baseView->layer->borderWidth;
padLeft += b; padRight += b; padTop += b; padBottom += b;
}
}
// 2. Define Clipping Bounds
int clipLeft = (int)currentClip.origin->x;
int clipTop = (int)currentClip.origin->y;
int clipRight = clipLeft + (int)currentClip.size->width;
int clipBottom = clipTop + (int)currentClip.size->height;
// 3. Define Visual Bounds
int visualLeft = absX - padLeft;
int visualTop = absY - padTop;
int visualRight = absX + w + padRight;
int visualBottom = absY + h + padBottom;
// 4. Check Intersection
bool intersects = (visualLeft < clipRight && visualRight > clipLeft &&
visualTop < clipBottom && visualBottom > clipTop);
if (!intersects) {
// printf("Skipping view outside dirty rect\n");
return;
}
// 4. Clipping Logic
// A. Calculate Clip for THIS VIEW (Background + Shadows)
// -----------------------------------------------------
// Start with the PARENT'S clip. This allows shadows to spill outside
// the view's frame, provided they are still inside the parent.
CCRect myEffectiveClip = currentClip;
// Only restrict 'myself' to 'my frame' if masksToBounds is strictly TRUE.
if (baseView->layer->masksToBounds) {
CCRect* myFrameAbs = ccRect(absX, absY, w, h);
CCRect intersected = intersectRects(myEffectiveClip, *myFrameAbs);
myEffectiveClip = intersected;
freeCCRect(myFrameAbs);
}
// Optimization: If we can't see the view or its shadow, stop here.
if (myEffectiveClip.size->width <= 0 || myEffectiveClip.size->height <= 0) return;
//FreeOSLogI(TAG, "drawViewHierarchy1");
}
else {
}
// 5. Geometry Properties
int radius = (int)baseView->layer->cornerRadius;
CCLayer* layer = baseView->layer;
// --- STEP A: SHADOWS (Use baseView/layer) ---
if (layer->shadowOpacity > 0.0f) {
int blur = (int)layer->shadowRadius;
int sh_x = absX + (int)layer->shadowOffset->x - blur;
int sh_y = absY + (int)layer->shadowOffset->y - blur;
int sh_w = w + (blur * 2);
int sh_h = h + (blur * 2);
int sh_radius = radius + blur;
ColorRGBA baseColor = convert_cc_color(layer->shadowColor);
ColorRGBA startColor = baseColor;
startColor.a = (uint8_t)(layer->shadowOpacity * 255.0f);
ColorRGBA endColor = baseColor;
endColor.a = 0;
Gradient* shadowGrad = (Gradient*)cc_safe_alloc(1, sizeof(Gradient));
if (shadowGrad) {
shadowGrad->type = GRADIENT_TYPE_BOX;
shadowGrad->angle = 0.0f;
shadowGrad->numStops = 2;
shadowGrad->stops = (ColorStop*)cc_safe_alloc(1, sizeof(ColorStop) * 2);
if (shadowGrad->stops) {
shadowGrad->stops[0].color = startColor;
shadowGrad->stops[0].position = 0.0f;
shadowGrad->stops[1].color = endColor;
shadowGrad->stops[1].position = 1.0f;
if ((int)sh_radius == 0) {
sh_radius = 20.0;
}
GraphicsCommand cmd_shadow = {
.cmd = CMD_DRAW_GRADIENT_ROUNDED_RECT,
.x = sh_x, .y = sh_y, .w = sh_w, .h = sh_h,
.radius = sh_radius,
.gradientData = shadowGrad,
.fill = true,
.clipX = (int)myEffectiveClip.origin->x,
.clipY = (int)myEffectiveClip.origin->y,
.clipW = (int)myEffectiveClip.size->width,
.clipH = (int)myEffectiveClip.size->height
};
//FreeOSLogI(TAG, "CMD_DRAW_GRADIENT_ROUNDED_RECT %d %d %d %d %d %d %d %d ", (int)myEffectiveClip.origin->x, (int)myEffectiveClip.origin->y, (int)myEffectiveClip.size->width, (int)myEffectiveClip.size->height, absX, absY, w, h);
QueueSend(g_graphics_queue, &cmd_shadow, QUEUE_MAX_DELAY);
} else { free(shadowGrad); }
}
}
//FreeOSLogI(TAG, "drawViewHierarchy2");
// --- STEP B: BORDERS (Use baseView/layer) ---
if (layer->borderWidth > 0.0f) {
int b_width = (int)layer->borderWidth;
ColorRGBA borderCol = convert_cc_color(layer->borderColor);
GraphicsCommand cmd_border = {
.cmd = CMD_DRAW_ROUNDED_RECT,
.x = absX - b_width,
.y = absY - b_width,
.w = w + (b_width * 2),
.h = h + (b_width * 2),
.radius = radius + b_width,
.color = borderCol,
.fill = true,
.clipX = (int)myEffectiveClip.origin->x,
.clipY = (int)myEffectiveClip.origin->y,
.clipW = (int)myEffectiveClip.size->width,
.clipH = (int)myEffectiveClip.size->height
};
QueueSend(g_graphics_queue, &cmd_border, QUEUE_MAX_DELAY);
}
//FreeOSLogI(TAG, "drawViewHierarchy3");
// --- STEP C: BACKGROUNDS (Use baseView) ---
// FIX: Changed 'view->backgroundColor' to 'baseView->backgroundColor'
if (layer->gradient != NULL) {
Gradient* lowLevelGrad = create_low_level_gradient(layer->gradient);
// Optimization: Use fast rect if radius is 0 and gradient is linear
if (radius <= 0 && lowLevelGrad->type == GRADIENT_TYPE_LINEAR) {
printf("draw gradient rect command %d %d %d %d", absX, absY, w, h);
GraphicsCommand cmd_grad = {
.cmd = CMD_DRAW_GRADIENT_RECT,
.x = absX, .y = absY, .w = w, .h = h,
.gradientData = lowLevelGrad,
.fill = true,
.clipX = (int)myEffectiveClip.origin->x,
.clipY = (int)myEffectiveClip.origin->y,
.clipW = (int)myEffectiveClip.size->width,
.clipH = (int)myEffectiveClip.size->height
};
QueueSend(g_graphics_queue, &cmd_grad, QUEUE_MAX_DELAY);
} else {
printf("draw gradient rounded rect command %d %d %d %d", absX, absY, w, h);
GraphicsCommand cmd_grad = {
.cmd = CMD_DRAW_GRADIENT_ROUNDED_RECT,
.x = absX, .y = absY, .w = w, .h = h,
.radius = radius,
.gradientData = lowLevelGrad,
.fill = true,
.clipX = (int)myEffectiveClip.origin->x,
.clipY = (int)myEffectiveClip.origin->y,
.clipW = (int)myEffectiveClip.size->width,
.clipH = (int)myEffectiveClip.size->height
};
QueueSend(g_graphics_queue, &cmd_grad, QUEUE_MAX_DELAY);
}
} else {
ColorRGBA bgCol = convert_cc_color(baseView->backgroundColor);
if (bgCol.a > 0) {
GraphicsCommand cmd_bg = {
.cmd = CMD_DRAW_ROUNDED_RECT,
.x = absX, .y = absY, .w = w, .h = h,
.radius = radius,
.color = bgCol,
.fill = true,
.clipX = (int)myEffectiveClip.origin->x,
.clipY = (int)myEffectiveClip.origin->y,
.clipW = (int)myEffectiveClip.size->width,
.clipH = (int)myEffectiveClip.size->height
};
QueueSend(g_graphics_queue, &cmd_bg, QUEUE_MAX_DELAY);
}
}
//FreeOSLogI(TAG, "drawViewHierarchy4");
// ============================================================
// USE 'object' and 'type' FOR CONTENT
// ============================================================
// --- STEP D: HANDLE CONTENT ---
// 1. Handle Shape Layer (Attached to baseView)
// FIX: Changed 'view->shapeLayer' to 'baseView->shapeLayer'
if (baseView->shapeLayer != NULL) {
drawShapeLayer(baseView->shapeLayer, absX, absY);
}
//FreeOSLogI(TAG, "drawViewHierarchy5");
// 2. Handle Label Content
// FIX: Changed 'view->type' to 'type'
if (type == CCType_Label) {
CCLabel* label = (CCLabel*)object;
ColorRGBA textCol = convert_cc_color(label->textColor);
TextFormat fmt = { 0 };
if (label->textAlignment == CCTextAlignmentCenter) fmt.alignment = TEXT_ALIGN_CENTER;
else if (label->textAlignment == CCTextAlignmentRight) fmt.alignment = TEXT_ALIGN_RIGHT;
else fmt.alignment = TEXT_ALIGN_LEFT;
if (label->textVerticalAlignment == CCTextVerticalAlignmentCenter) fmt.valignment = TEXT_VALIGN_CENTER;
else if (label->textVerticalAlignment == CCTextVerticalAlignmentTop) fmt.valignment = TEXT_VALIGN_TOP;
else fmt.valignment = TEXT_VALIGN_BOTTOM;
//FreeOSLogI(TAG, "drawViewHierarchy5 CMD_DRAW_TEXT_BOX valignment %d", fmt.valignment);
if (label->lineBreakMode == CCLineBreakWordWrapping) fmt.wrapMode = TEXT_WRAP_MODE_WHOLE_WORD;
else fmt.wrapMode = TEXT_WRAP_MODE_TRUNCATE;
fmt.lineSpacing = (int)label->lineSpacing;
fmt.glyphSpacing = (int)label->glyphSpacing;
GraphicsCommand cmd_text = {
.cmd = CMD_DRAW_TEXT_BOX_CACHED,
.x = absX, .y = absY, .w = w, .h = h,
.color = textCol,
.fontSize = (int)label->fontSize,
.textFormat = fmt,
.clipX = (int)myEffectiveClip.origin->x,
.clipY = (int)myEffectiveClip.origin->y,
.clipW = (int)myEffectiveClip.size->width,
.clipH = (int)myEffectiveClip.size->height
};
if (label->text && label->text->string) {
size_t len = strlen(label->text->string); // strlen in ROM handles alignment automatically
cmd_text.text = malloc(len + 1);
if (cmd_text.text) {
for (size_t i = 0; i <= len; i++) {
// Extract byte-by-byte using the hardware-safe alignment math
cmd_text.text[i] = safe_flash_read_byte(&label->text->string[i]);
}
}
} else {
cmd_text.text = NULL;
}
QueueSend(g_graphics_queue, &cmd_text, QUEUE_MAX_DELAY);
//FreeOSLogI(TAG, "drawViewHierarchy5 CMD_DRAW_TEXT_BOX %d", myEffectiveClip.size->width);
}
// 3. Handle Image Content
// FIX: Changed 'view->type' to 'type'
// Inside drawViewHierarchy...
// Inside drawViewHierarchy...
// Inside your drawViewHierarchy (or subview iteration loop):
else if (type == CCType_FramebufferView) {
FreeOSLogI("drawViewHierarchy", "CCType_FramebufferView CMD_DRAW_FRAMEBUFFER");
// Cast the generic object to our specific wrapper
CCFramebufferView* fbView = (CCFramebufferView*)object;
// 1. Draw the background view normally (handles background color, corner radius, borders)
//if (fbView->view) {
// drawViewHierarchy(fbView->view);
// Note: If drawViewHierarchy recursively calls this block, ensure you
// have a check for CCType_View so it doesn't infinite loop!
//}
// 2. Queue the Framebuffer content to draw on top of the background
if (fbView->framebuffer) {
GraphicsCommand cmd;
cmd.cmd = CMD_DRAW_FRAMEBUFFER;
cmd.x = absX;
cmd.y = absY;
cmd.pixelBuffer = (void*)fbView->framebuffer;
FreeOSLogI("drawViewHierarchy", "CMD_DRAW_FRAMEBUFFER");
xQueueSend(g_graphics_queue, &cmd, portMAX_DELAY);
}
return; // Done rendering this specific component
}
else if (type == CCType_ImageView) {
CCImageView* imgView = (CCImageView*)object;
// SAFETY CHAIN:
// 1. Check if Image exists
// 2. Check if FilePath object exists (This was the NULL culprit)
// 3. Check if the actual character buffer exists
if (imgView->image != NULL &&
imgView->image->filePath != NULL &&
imgView->image->filePath->string != NULL) {
GraphicsCommand cmd_img = {
.cmd = CMD_DRAW_IMAGE_FILE,
.alpha = imgView->alpha,
.x = absX, .y = absY, .w = w, .h = h,
.clipX = (int)myEffectiveClip.origin->x,
.clipY = (int)myEffectiveClip.origin->y,
.clipW = (int)myEffectiveClip.size->width,
.clipH = (int)myEffectiveClip.size->height
};
// Now it is 100% safe to access the string
strncpy(cmd_img.imagePath, imgView->image->filePath->string, 63);
QueueSend(g_graphics_queue, &cmd_img, QUEUE_MAX_DELAY);
}
if (imgView->image->imageData) {
GraphicsCommand cmd = {
.cmd = CMD_DRAW_PIXEL_BUFFER, // Use the new buffer command
//.alpha = imgView->alpha,
.x = absX,
.y = absY,
.w = imgView->image->size->width, // Use image size
.h = imgView->image->size->height,
.pixelBuffer = imgView->image->imageData // Pass the raw pointer
};
QueueSend(g_graphics_queue, &cmd, 0);
}
}
//FreeOSLogI(TAG, "drawViewHierarchy6");
// ============================================================
// RECURSE SUBVIEWS
// ============================================================
if (notHierarchy) {
CCRect* myExactFrame = ccRect(absX, absY, w, h);
CCRect childClip = intersectRects(currentClip, *myExactFrame);
freeCCRect(myExactFrame);
// Only recurse if the child clip is visible
if (childClip.size->width > 0 && childClip.size->height > 0) {
if (baseView->subviews) {
for (int i = 0; i < baseView->subviews->count; i++) {
drawViewHierarchy(baseView->subviews->array[i], absX, absY, childClip, notHierarchy);
}
}
}
}
else {
if (baseView->subviews) {
for (int i = 0; i < baseView->subviews->count; i++) {
void* subview = arrayObjectAtIndex(baseView->subviews, i);
drawViewHierarchy(subview, absX, absY, myEffectiveClip, notHierarchy);
}
}
}
}
// Factory function to create one grid item
CCIconView* create_icon_view(CCRect* frame, const char* imgPath, const char* title) {
CCIconView* item = (CCIconView*)cc_safe_alloc(1, sizeof(CCIconView));
// 1. Container View (80x100)
item->container = viewWithFrame(frame);
item->container->backgroundColor = color(0, 0, 0, 0.0); // Transparent default
layerSetCornerRadius(item->container->layer, 10.0); // Rounded corners
// Optional: Add a subtle border or just rely on the click effect
// 2. Icon Image (Top, e.g., 60x60)
// Center X = (80 - 60) / 2 = 10. Top Y = 10.
CCRect* imgFrame = ccRect(5, 5, frame->size->width-10, frame->size->width-10);
item->icon = imageViewWithFrame(imgFrame);
item->icon->image = imageWithFile(ccs(imgPath));
viewAddSubview(item->container, item->icon); // Add generic wrapper
// 3. Text Label (Bottom)
// Y = 75 to leave space under image. Height = 20.
CCRect* lblFrame = ccRect(0, imgFrame->origin->y+imgFrame->size->height+3, frame->size->width, frame->size->height - imgFrame->origin->y);
item->label = labelWithFrame(lblFrame);
labelSetText(item->label, ccs(title));
item->label->fontSize = 14;
item->label->textAlignment = CCTextAlignmentCenter;
item->label->textVerticalAlignment = CCTextVerticalAlignmentTop;
item->label->textColor = color(1, 1, 1, 1); // White text
// CRITICAL: Add the label object, not the view wrapper
viewAddSubview(item->container, item->label);
return item;
}
CCArray* create_grid_data_source(void) {
CCArray* gridData = array();
// Define raw data for the 12 items
const char* labels[12] = {
"Files", "Settings", "Text",
"Message", "Paint", "Clock",
"Photos", "Music", "Calculator",
"Search", "Maps", "Net Tools"
};
// We'll use a few generic icon paths for demonstration
const char* icons[12] = {
"/spiflash/files.png", "/spiflash/settings.png", "/spiflash/text.png",
"/spiflash/messages.png", "/spiflash/paint.png", "/spiflash/clock.png",
"/spiflash/photos.png", "/spiflash/music.png", "/spiflash/calculator.png",
"/spiflash/search.png", "/spiflash/maps.png", "/spiflash/net tools.png"
};
for (int i = 0; i < 12; i++) {
CCDictionary* itemDict = dictionary();
dictionarySetObjectForKey(itemDict, ccs(labels[i]), ccs("label"));
dictionarySetObjectForKey(itemDict, ccs(icons[i]), ccs("image"));
arrayAddObject(gridData, itemDict);
}
return gridData;
}
void drawHomeMenu(void) {
int cols = 3;
int rows = 4;
int itemW = 80;
int itemH = 100;
int gapX = 10; // Spacing
int gapY = 10;
int startX = 30;
int startY = 40;
if (g_grid_items_registry == NULL) {
g_grid_items_registry = array();
}
CCArray* dataItems = create_grid_data_source();
for (int i = 0; i < dataItems->count; i++) {
// Calculate Grid Position (Row/Col)
int col = i % cols;
int row = i / cols;
int x = startX + (col * (itemW + gapX));
int y = startY + (row * (itemH + gapY));
// Retrieve Data Model
CCDictionary* itemData = (CCDictionary*)arrayObjectAtIndex(dataItems, i);
CCString* labelStr = (CCString*)dictionaryObjectForKey(itemData, ccs("label"));
CCString* imgStr = (CCString*)dictionaryObjectForKey(itemData, ccs("image"));
// Create View using Data
CCIconView* iconItem = create_icon_view(
ccRect(x, y, itemW, itemH), // Pass by value as expected by your fix
imgStr->string,
labelStr->string
);
// Add to Main Window
viewAddSubview(mainWindowView, iconItem->container);
arrayAddObject(g_grid_items_registry, iconItem->container);
// Cleanup helper struct
free(iconItem);
}
}
void drawSampleViews(void) {
// --- View 1: The "Card" (White, Rounded, Shadow) ---
CCView* cardView = viewWithFrame(ccRect(40, 100, 240, 150));
cardView->backgroundColor = color(1.0, 1.0, 1.0, 1.0); // White
// Configure Layer Properties
layerSetCornerRadius(cardView->layer, 20.0);
// Add Shadow
cardView->layer->shadowColor = color(0.0, 0.0, 0.0, 1.0); // Black
cardView->layer->shadowOpacity = 0.5; // 50% transparent shadow
cardView->layer->shadowOffset = ccPoint(10, 10); // Offset down-right
viewAddSubview(mainWindowView, cardView);
// Create a View to hold the shape
CCView* triangleView = viewWithFrame(ccRect(100, 100, 100, 100));
// Make the view transparent so only the shape shows
triangleView->backgroundColor = color(0,0,0,0);
// Create the Shape Layer
CCShapeLayer* triangle = shapeLayer();
// Add Points (Relative to the view's 0,0)
pointPathAddPoint(triangle->pointPath, ccPoint(50, 0)); // Top Middle
pointPathAddPoint(triangle->pointPath, ccPoint(100, 100)); // Bottom Right
pointPathAddPoint(triangle->pointPath, ccPoint(0, 100)); // Bottom Left
// Add a Gradient
CCArray* colors = array();
arrayAddObject(colors, color(1, 0, 0, 1)); // Red
arrayAddObject(colors, color(0, 0, 1, 1)); // Blue
CCArray* locs = array();
arrayAddObject(locs, numberWithDouble(0.0));
arrayAddObject(locs, numberWithDouble(1.0));
triangle->gradient = gradientWithColors(colors, locs, M_PI_2); // Vertical gradient
triangleView->shapeLayer = triangle;
viewAddSubview(mainWindowView, triangleView);
// Assign to view (You might need to modify CCView struct to hold a ShapeLayer specifically,
// or just assign it to a generic 'void* content' field if you have one.
// For now, let's assume you cast it into the 'layer' field if you modify CCLayer to hold shapes).
// Or, simpler: Just call drawShapeLayer directly inside your render loop for testing.
// Example Image
CCImageView* myIcon = imageViewWithFrame(ccRect(50, 150, 64, 64));
// Note: Ensure you have a valid VFS path string in your CCImage
myIcon->image = imageWithFile(ccs("/spiflash/test.png"));
viewAddSubview(mainWindowView, myIcon);
// --- View 2: The "Button" (Gradient, Border) ---
CCView* buttonView = viewWithFrame(ccRect(60, 300, 200, 60));
// Create Gradient: Chrome/Metal style
CCArray* colors1 = array();
arrayAddObject(colors1, color(0.8, 0.8, 0.9, 1.0)); // Light Blue-ish Gray
arrayAddObject(colors1, color(0.2, 0.2, 0.3, 1.0)); // Dark Blue-ish Gray
CCArray* locs1 = array();
arrayAddObject(locs1, numberWithDouble(0.0));
arrayAddObject(locs1, numberWithDouble(1.0));
// Assign Gradient to Layer
buttonView->layer->gradient = gradientWithColors(colors1, locs1, M_PI_2); // Vertical
// Add Border
buttonView->layer->shadowColor = color(0.0, 0.0, 0.0, 1.0); // Black
buttonView->layer->shadowOpacity = 0.5; // 50% transparent shadow
buttonView->layer->shadowOffset = ccPoint(10, 10); // Offset down-right
//buttonView->layer->borderWidth = 4.0;
//buttonView->layer->borderColor = color(1.0, 1.0, 1.0, 1.0); // White border
layerSetCornerRadius(buttonView->layer, 30.0); // Fully rounded caps
viewAddSubview(mainWindowView, buttonView);
// Example Label
CCLabel* myLabel = labelWithFrame(ccRect(15, 0, buttonView->frame->size->width-30, buttonView->frame->size->height));
myLabel->text = ccs("This is a long string that will now auto-wrap inside the box!");
myLabel->fontSize = 12.0;
myLabel->textAlignment = CCTextAlignmentCenter; // It will center!
myLabel->textVerticalAlignment = CCTextVerticalAlignmentCenter; // It will center!
myLabel->lineBreakMode = CCLineBreakWordWrapping; // It will wrap!
myLabel->textColor = color(1.0, 1.0, 1.0, 1.0);
viewAddSubview(buttonView, myLabel);
// 1. Create Text View
// Position: x=20, y=80, w=280, h=300 (Visible Window)
myDemoTextView = textViewWithFrame(ccRect(20, 80, 280, 300));
/*
FTC_Manager g_ftc_manager = NULL;
FTC_ImageCache g_ftc_image_cache = NULL;
FTC_CMapCache g_ftc_cmap_cache = NULL;
*/
myDemoTextView->ftManager = g_ftc_manager;
myDemoTextView->ftImageCache = g_ftc_image_cache;
myDemoTextView->ftCMapCache = g_ftc_cmap_cache;
// Style the container
myDemoTextView->scrollView->view->backgroundColor = color(0.95, 0.95, 0.95, 1.0);
myDemoTextView->scrollView->view->layer->masksToBounds = true;
layerSetCornerRadius(myDemoTextView->scrollView->view->layer, 10);
// 2. Set Long Text
// We assume ccs() creates a CCString
CCString* longStr = ccs(
"Here is a very long string to demonstrate the new CCTextView.\n\n"
"This view automatically wraps text using the renderTextBox logic we built earlier.\n\n"
"More importantly, it sits inside a CCScrollView. When you drag your finger, "
"the touch loop calculates the delta and updates the contentOffset.\n\n"
"The 'drawViewHierarchy' function then clips this content to the parent frame, "
"creating a seamless scrolling effect.\n\n"
"The 'drawViewHierarchy' function then clips this content to the parent frame, "
"creating a seamless scrolling effect.\n\n"
"Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\n"
"End of text."
);
// This helper calculates height and resizes the content view automatically
myDemoTextView->label->fontName = ccs("/spiflash/proximaNovaRegular.ttf");
textViewSetText(myDemoTextView, longStr);
scrollViewSetContentSize(myDemoTextView->scrollView, ccSize(280, 1000));
// Style the text
myDemoTextView->label->fontSize = 12;
myDemoTextView->label->textColor = color(0.1, 0.1, 0.1, 1.0);
myDemoTextView->label->lineSpacing = 5;
// 3. Add to Main Window
// Important: Add the scrollView->view, NOT the textView itself
viewAddSubview(mainWindowView, myDemoTextView->scrollView->view);
CCImageView* myIcon2 = imageViewWithFrame(ccRect(0, 0, 320, 480));
// Note: Ensure you have a valid VFS path string in your CCImage
myIcon2->image = imageWithFile(ccs("/spiflash/test2.jpg"));
//viewAddSubview(mainWindowView, myIcon2);
}
void setup_ui_demo(void) {
FreeOSLogI(TAG, "setup_ui_demo");
int64_t start_time = esp_timer_get_time();
if (mainWindowView == NULL) {
// Initialize the root view filling the screen
mainWindowView = viewWithFrame(ccRect(0, 0, 320, 480));
mainWindowView->backgroundColor = color(0.2, 0.2, 0.2, 1.0); // Dark gray background
}
// A. Create the Color Array
CCArray* gradColors = array();
// Deep Blue (0, 50, 150) -> Normalized to 0.0 - 1.0
arrayAddObject(gradColors, color(0.0f, 50.0f/255.0f, 150.0f/255.0f, 1.0f));
// Vibrant Aqua (0, 200, 255) -> Normalized
arrayAddObject(gradColors, color(0.0f, 200.0f/255.0f, 1.0f, 1.0f));
// B. Create the Locations Array
CCArray* gradLocs = array();
arrayAddObject(gradLocs, numberWithDouble(0.0));
arrayAddObject(gradLocs, numberWithDouble(1.0));
// C. Create the Gradient Object (Vertical / 90 degrees)
// M_PI_2 is 90 degrees (Top to Bottom)
CCGradient* aquaGradient = gradientWithColors(gradColors, gradLocs, M_PI_2);
// D. Assign it to the layer
mainWindowView->layer->gradient = aquaGradient;
//drawSampleViews();
drawHomeMenu();
checkAvailableMemory();
// 2. Capture End Time
int64_t end_time = esp_timer_get_time();
// 3. Calculate Duration
int64_t time_diff = end_time - start_time;
// 4. Log result (Use %lld for int64_t)
// We log both Microseconds (us) and Milliseconds (ms)
FreeOSLogI("PROFILE", "Code execution took: %lld us (%lld ms)", time_diff, time_diff / 1000);
// --- Trigger Render ---
// Assuming you have a function to start the render pass:
// update_full_ui();
//update_full_ui();
update_full_ui1();
}
esp_err_t setup_cursor_buffers() {
size_t size = CURSOR_W * CURSOR_H * CURSOR_BPP;
// Allocate buffer in internal, DMA-capable memory (very fast)
g_cursor_backup_buffer = (uint8_t*) heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
if (!g_cursor_backup_buffer) {
FreeOSLogE(TAG, "Failed to allocate %d bytes for cursor backup!", size);
return ESP_ERR_NO_MEM;
}
FreeOSLogI(TAG, "setup_cursor_buffers");
return ESP_OK;
}
void save_cursor_background(Framebuffer *fb, int x, int y) {
if (!g_cursor_backup_buffer) {
FreeOSLogI(TAG, "!g_cursor_backup_buffer");
return;
}
// Set the new cursor position
g_cursor_x = x;
g_cursor_y = y;
uint8_t* psram_ptr;
uint8_t* backup_ptr = g_cursor_backup_buffer;
FreeOSLogI(TAG, "save_cursor_background");
// Loop through each row of the cursor rect and copy from the PSRAM framebuffer
for (int i = 0; i < CURSOR_H; i++) {
// Calculate the starting position of the row in the PSRAM framebuffer
psram_ptr = &((uint8_t*)fb->pixelData)[((y + i) * fb->displayWidth + x) * CURSOR_BPP];
// Copy the entire row (CURSOR_W * 3 bytes) to the internal backup buffer
memcpy(backup_ptr, psram_ptr, CURSOR_W * CURSOR_BPP);
backup_ptr += CURSOR_W * CURSOR_BPP;
}
}
void restore_cursor_background(Framebuffer *fb) {
if (!g_cursor_backup_buffer) return;
// 1. Write the background back into the PSRAM framebuffer
uint8_t* psram_ptr;
uint8_t* backup_ptr = g_cursor_backup_buffer;
for (int i = 0; i < CURSOR_H; i++) {
psram_ptr = &((uint8_t*)fb->pixelData)[((g_cursor_y + i) * fb->displayWidth + g_cursor_x) * CURSOR_BPP];
// Copy the row from the internal backup buffer back to the PSRAM framebuffer
memcpy(psram_ptr, backup_ptr, CURSOR_W * CURSOR_BPP);
backup_ptr += CURSOR_W * CURSOR_BPP;
}
FreeOSLogI(TAG, "restore_cursor_background");
// 2. Send the restore command to the graphics queue
GraphicsCommand cmd_update;
cmd_update.cmd = CMD_UPDATE_AREA;
cmd_update.x = g_cursor_x;
cmd_update.y = g_cursor_y;
cmd_update.w = CURSOR_W;
cmd_update.h = CURSOR_H;
// Use the queue send for the update
if (QueueSend(g_graphics_queue, &cmd_update, 0) != pdTRUE) {
FreeOSLogE(TAG, "Cursor restore command failed.");
}
}
void cursor_blink_task(void *pvParameter)
{
ulTaskNotifyTake(pdTRUE, QUEUE_MAX_DELAY);
//FreeOSLogI(TAG, "cursor_blink_task");
while (1) {
GraphicsCommand cmd_blink;
if (g_cursor_visible) {
// If visible, send restore command (blink OFF)
cmd_blink.cmd = CMD_CURSOR_RESTORE;
g_cursor_visible = false;
} else {
// If hidden, send draw command (blink ON)
cmd_blink.cmd = CMD_CURSOR_DRAW;
g_cursor_visible = true;
}
// Send the command with zero wait time
QueueSend(g_graphics_queue, &cmd_blink, 0);
// Wait for the blink interval (500ms)
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/**
* @brief Generates the vertices for a 5-pointed star.
*
* @param centerX Center X coordinate.
* @param centerY Center Y coordinate.
* @param outerRadius Distance to the outer points.
* @param innerRadius Distance to the inner points (the notches).
* @param numVertices Pointer to store the number of vertices generated (will be 10).
* @return A dynamically allocated array of Vector3 vertices. Caller must free.
*/
Vector3* create_star_vertices(float centerX, float centerY, float outerRadius, float innerRadius, int *numVertices) {
const int num_points = 5;
const int total_vertices = num_points * 2;
*numVertices = total_vertices;
// Allocate 10 vertices in memory
Vector3* vertices = (Vector3*)cc_safe_alloc(1, total_vertices * sizeof(Vector3));
if (!vertices) return NULL;
float angle_step = M_PI / num_points; // 36 degrees per point (180/5)
for (int i = 0; i < total_vertices; i++) {
float radius = (i % 2 == 0) ? outerRadius : innerRadius;
float angle = i * angle_step - M_PI_2; // Start star pointed upwards (offset by -90 deg)
vertices[i].x = centerX + radius * cosf(angle);
vertices[i].y = centerY + radius * sinf(angle);
vertices[i].z = 0.0f; // 2D operation, Z is zero
}
return vertices;
}
int randNumberTo(int max) {
return rand() % max;
}
const char* letterForCoordinate(void) {
return "Q";
}
CCScrollView* findParentScrollView(CCView* view) {
CCView* current = view;
while (current != NULL) {
// Check if this view is the 'contentView' of a ScrollView structure
// This is tricky because C structs don't have back-pointers to their wrapper.
// WORKAROUND: We iterate our known scroll views or check a tag.
// BETTER APPROACH:
// In updateTouch, we look at the touchedView's superview chain.
// If current->superview's layer has masksToBounds=true and the logic fits...
// FOR THIS EXAMPLE: We will rely on a global pointer for the active scroll view
// or explicit checking in the touch loop if you track your objects.
current = (CCView*)current->superview;
}
return NULL;
}
CCView* find_grid_item_at(int x, int y) {
// Safety check
FreeOSLogI(TAG, "find_grid_item_at");
if (!g_grid_items_registry) return NULL;
FreeOSLogI(TAG, "find_grid_item_at %d", g_grid_items_registry->count);
// Iterate ONLY the registered grid items
for (int i = 0; i < g_grid_items_registry->count; i++) {
CCView* targetView = (CCView*)arrayObjectAtIndex(g_grid_items_registry, i);
targetView->tag = i;
// Get the frame (relative to parent/screen)
// Since these are direct children of mainWindowView (0,0),
// their frame origin is their absolute screen position.
float vx = targetView->frame->origin->x;
float vy = targetView->frame->origin->y;
float vw = targetView->frame->size->width;
float vh = targetView->frame->size->height;
// Fast Geometry Check
if (x >= vx && x < (vx + vw) &&
y >= vy && y < (vy + vh)) {
return targetView;
}
}
return NULL;
}
// Helper to format bytes into readable KB/MB strings
CCString* formatFileSize(int bytes) {
char buffer[32];
if (bytes < 1024) {
snprintf(buffer, sizeof(buffer), "%d B", bytes);
} else if (bytes < 1048576) {
snprintf(buffer, sizeof(buffer), "%.1f KB", bytes / 1024.0);
} else {
snprintf(buffer, sizeof(buffer), "%.1f MB", bytes / 1048576.0);
}
return stringWithCString(buffer);
}
CCView* create_key_btn(const char* text, int x, int y, int w, int h, int tag) {
// Container
CCView* key = viewWithFrame(ccRect(x, y, w, h));
key->backgroundColor = color(0.3, 0.3, 0.35, 1.0); // Dark Blue-Gray key
layerSetCornerRadius(key->layer, 6.0);
key->tag = tag;
// Shadow for depth
//key->layer->shadowOpacity = 0.3;
//key->layer->shadowRadius = 2;
//key->layer->shadowOffset = ccPoint(0, 2);
// Label
CCLabel* lbl = labelWithFrame(ccRect(0, 0, w, h));
// CRITICAL: Use copyCCString to prevent double-free crashes if layout is rebuilt
CCString* tempStr = ccs(text);
lbl->text = copyCCString(tempStr);
freeCCString(tempStr); // Clean up the temp wrapper
lbl->textColor = color(1, 1, 1, 1);
lbl->fontSize = 18;
lbl->textAlignment = CCTextAlignmentCenter;
lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(key, lbl);
return key;
}
void layout_keyboard_keys(void) {
if (!uiKeyboardView) return;
if (uiKeyboardView && uiKeyboardView->subviews) {
for (int i = uiKeyboardView->subviews->count - 1; i >= 0; i--) {
CCView* keyView = (CCView*)arrayObjectAtIndex(uiKeyboardView->subviews, i);
viewRemoveFromSuperview(keyView);
// Optionally free memory if your system doesn't auto-handle it
freeViewHierarchy(keyView);
}
}
// 1. Clear existing keys (Clean rebuild)
//freeViewHierarchy(uiKeyboardView);
// Re-initialize the container logic (since freeViewHierarchy freed the struct)
// Actually, it's safer to just remove subviews if we want to keep the container.
// But given your framework, let's assume uiKeyboardView is just a pointer we manage.
// Let's assume the container is passed in or managed externally.
// Ideally: remove all subviews.
// For now, let's assume we are rebuilding the subviews of a fresh container.
// Row definitions based on Mode
const char* row1 = "";
const char* row2 = "";
const char* row3 = "";
if (kbCurrentMode == KB_MODE_ABC_LOWER) {
row1 = "qwertyuiop";
row2 = "asdfghjkl";
row3 = "zxcvbnm";
} else if (kbCurrentMode == KB_MODE_ABC_UPPER) {
row1 = "QWERTYUIOP";
row2 = "ASDFGHJKL";
row3 = "ZXCVBNM";
} else if (kbCurrentMode == KB_MODE_NUMBERS) {
row1 = "1234567890";
row2 = "-/:;()$&@";
row3 = ".,?!'";
}
int screenW = 320;
int keyW = (screenW - (KEY_MARGIN * 11)) / 10; // Approx 30px
int startY = 10;
// --- ROW 1 ---
int len1 = strlen(row1);
int x = KEY_MARGIN;
for (int i=0; i<len1; i++) {
char buf[2] = {row1[i], '\0'};
// Tag is the ASCII value + Base
viewAddSubview(uiKeyboardView, create_key_btn(buf, x, startY, keyW, KEY_HEIGHT, TAG_KB_KEY_BASE + row1[i]));
x += keyW + KEY_MARGIN;
}
// --- ROW 2 ---
int len2 = strlen(row2);
x = KEY_MARGIN + (keyW / 2); // Indent row 2
for (int i=0; i<len2; i++) {
char buf[2] = {row2[i], '\0'};
viewAddSubview(uiKeyboardView, create_key_btn(buf, x, startY + KEY_HEIGHT + KEY_MARGIN, keyW, KEY_HEIGHT, TAG_KB_KEY_BASE + row2[i]));
x += keyW + KEY_MARGIN;
}
// --- ROW 3 (Shift, Letters, Back) ---
int y3 = startY + (KEY_HEIGHT + KEY_MARGIN) * 2;
// Shift/Symbol Key (Left)
if (kbCurrentMode == KB_MODE_NUMBERS) {
// Switch back to ABC
viewAddSubview(uiKeyboardView, create_key_btn("ABC", KEY_MARGIN, y3, keyW*1.5, KEY_HEIGHT, TAG_KB_MODE_ABC));
} else {
// Shift Key
const char* shiftLbl = (kbCurrentMode == KB_MODE_ABC_UPPER) ? "S" : "s"; // visual indicator
viewAddSubview(uiKeyboardView, create_key_btn(shiftLbl, KEY_MARGIN, y3, keyW*1.5, KEY_HEIGHT, TAG_KB_SHIFT));
}
// Letters
int len3 = strlen(row3);
x = KEY_MARGIN + (keyW * 1.5) + KEY_MARGIN;
for (int i=0; i<len3; i++) {
char buf[2] = {row3[i], '\0'};
viewAddSubview(uiKeyboardView, create_key_btn(buf, x, y3, keyW, KEY_HEIGHT, TAG_KB_KEY_BASE + row3[i]));
x += keyW + KEY_MARGIN;
}
// Backspace (Right)
int backW = screenW - x - KEY_MARGIN;
viewAddSubview(uiKeyboardView, create_key_btn("<-", x, y3, backW, KEY_HEIGHT, TAG_KB_BACK));
// --- ROW 4 (123, Space, Return) ---
int y4 = startY + (KEY_HEIGHT + KEY_MARGIN) * 3;
// 123 Mode
viewAddSubview(uiKeyboardView, create_key_btn("123", KEY_MARGIN, y4, keyW*2, KEY_HEIGHT, TAG_KB_MODE_123));
// Space
int spaceW = screenW - (keyW*4) - (KEY_MARGIN*4);
viewAddSubview(uiKeyboardView, create_key_btn("Space", KEY_MARGIN + keyW*2 + KEY_MARGIN, y4, spaceW, KEY_HEIGHT, TAG_KB_SPACE));
// Return
viewAddSubview(uiKeyboardView, create_key_btn("Ret", KEY_MARGIN + keyW*2 + KEY_MARGIN + spaceW + KEY_MARGIN, y4, screenW - (KEY_MARGIN + keyW*2 + KEY_MARGIN + spaceW + KEY_MARGIN) - KEY_MARGIN, KEY_HEIGHT, TAG_KB_RETURN));
}
void hide_keyboard_ui(void) {
printf("--- Teardown Keyboard UI Start ---\n");
// 1. Destroy the visual keyboard view
if (uiKeyboardView) {
viewRemoveFromSuperview(uiKeyboardView);
freeViewHierarchy(uiKeyboardView);
uiKeyboardView = NULL;
printf("Keyboard View Freed.\n");
}
// 2. Clear the input string buffer
if (uiInputBuffer) {
freeCCString(uiInputBuffer);
uiInputBuffer = NULL;
}
// 3. Clear the target pointer (we don't free the label, just our link to it!)
uiTargetLabel = NULL;
printf("--- Teardown Keyboard UI Done ---\n");
}
void setup_keyboard_ui(CCLabel* targetLabel) {
printf("--- Setup Keyboard UI Start ---\n");
// 1. Set Globals
uiTargetLabel = targetLabel;
// Setup Input Buffer
if (uiInputBuffer) freeCCString(uiInputBuffer);
if (targetLabel && targetLabel->text) {
uiInputBuffer = copyCCString(targetLabel->text);
} else {
uiInputBuffer = ccs("");
}
// 2. Clear previous keyboard if it exists
if (uiKeyboardView) {
// Since we are about to overwrite the pointer, we should ideally remove the old one
// But for now, just NULLing it to start fresh is safe enough if freeViewHierarchy handles the rest
uiKeyboardView = NULL;
}
// 3. Create Container
// Use CC_CALLOC via viewWithFrame.
// MAKE SURE YOU ARE NOT TYPING "CCView* uiKeyboardView =" here.
uiKeyboardView = viewWithFrame(ccRect(0, 480 - 200, 320, 200));
// --- DEBUG CHECK 1 ---
if (uiKeyboardView == NULL) {
printf("CRITICAL ERROR: uiKeyboardView is NULL. Out of Memory?\n");
return;
} else {
printf("Keyboard View Allocated at %p\n", uiKeyboardView);
}
uiKeyboardView->backgroundColor = color(0.15, 0.15, 0.18, 1.0);
uiKeyboardView->layer->shadowOpacity = 0.5;
uiKeyboardView->layer->shadowRadius = 10;
// Fix crash risk: Explicitly set offset
uiKeyboardView->layer->shadowOffset = ccPoint(0, -5);
// 4. Populate Keys
layout_keyboard_keys();
// --- DEBUG CHECK 2 ---
if (uiKeyboardView->subviews == NULL || uiKeyboardView->subviews->count == 0) {
printf("WARNING: Keyboard Container created, but Keys are missing!\n");
} else {
printf("Keyboard Keys Added: %d keys\n", uiKeyboardView->subviews->count);
}
// 5. Add to Main Window
viewAddSubview(mainWindowView, uiKeyboardView);
// 6. Force Update
update_full_ui();
printf("--- Setup Keyboard UI Done ---\n");
}
// Helper to Traverse Subviews and find touched key
CCView* find_key_at_point(int x, int y) {
if (!uiKeyboardView || !uiKeyboardView->subviews) return NULL;
// Iterate Backwards (Topmost view first)
for (int i = uiKeyboardView->subviews->count - 1; i >= 0; i--) {
CCView* key = (CCView*)arrayObjectAtIndex(uiKeyboardView->subviews, i);
// Convert screen X/Y to Keyboard-Relative X/Y
int localX = x - (int)uiKeyboardView->frame->origin->x;
int localY = y - (int)uiKeyboardView->frame->origin->y;
// Check intersection
if (localX >= key->frame->origin->x &&
localX <= key->frame->origin->x + key->frame->size->width &&
localY >= key->frame->origin->y &&
localY <= key->frame->origin->y + key->frame->size->height) {
return key;
}
}
return NULL;
}
void handle_keyboard_touch(int x, int y) {
// 1. Find the Key
CCView* key = find_key_at_point(x, y);
if (!key) return;
int tag = key->tag;
// 2. Handle Logic
if (tag >= TAG_KB_KEY_BASE && tag < TAG_KB_KEY_BASE + 255) {
// --- Character Key ---
char c = (char)(tag - TAG_KB_KEY_BASE);
// Append to buffer
char* oldStr = cStringOfString(uiInputBuffer);
int len = strlen(oldStr);
char* newBuf = calloc(1, len + 2); // +1 char, +1 null
strcpy(newBuf, oldStr);
newBuf[len] = c;
freeCCString(uiInputBuffer);
uiInputBuffer = ccs(newBuf); // ccs uses strdup/malloc
free(newBuf);
}
else if (tag == TAG_KB_SPACE) {
// Append Space
char* oldStr = cStringOfString(uiInputBuffer);
int len = strlen(oldStr);
char* newBuf = calloc(1, len + 2);
strcpy(newBuf, oldStr);
newBuf[len] = ' ';
freeCCString(uiInputBuffer);
uiInputBuffer = ccs(newBuf);
free(newBuf);
}
else if (tag == TAG_KB_BACK) {
// Backspace
char* oldStr = cStringOfString(uiInputBuffer);
int len = strlen(oldStr);
if (len > 0) {
char* newBuf = strdup(oldStr);
newBuf[len - 1] = '\0'; // Truncate
freeCCString(uiInputBuffer);
uiInputBuffer = ccs(newBuf);
free(newBuf);
}
}
else if (tag == TAG_KB_SHIFT) {
// Toggle Shift
kbCurrentMode = (kbCurrentMode == KB_MODE_ABC_LOWER) ? KB_MODE_ABC_UPPER : KB_MODE_ABC_LOWER;
layout_keyboard_keys(); // Rebuild UI
update_full_ui(); // Redraw
return; // Don't update text label
}
else if (tag == TAG_KB_MODE_123) {
kbCurrentMode = KB_MODE_NUMBERS;
layout_keyboard_keys();
update_full_ui();
return;
}
else if (tag == TAG_KB_MODE_ABC) {
kbCurrentMode = KB_MODE_ABC_LOWER;
layout_keyboard_keys();
update_full_ui();
return;
}
else if (tag == TAG_KB_RETURN) {
// Done editing
// Hide keyboard logic here if you want
printf("Return pressed. Final text: %s\n", cStringOfString(uiInputBuffer));
return;
}
// 3. Update Target Label
if (uiTargetLabel) {
if (uiTargetLabel->text) freeCCString(uiTargetLabel->text);
uiTargetLabel->text = copyCCString(uiInputBuffer);
// Optimized Redraw
//update_view_area_via_parent((CCView*)uiTargetLabel->view);
//update_view_only((CCView*)uiTargetLabel->view);
//update_full_ui();
update_label_safe(uiTargetLabel);
}
}
void debug_print_view_hierarchy(CCView* view, int depth) {
if (!view) {
printf("View is NULL\n");
return;
}
// Indentation
for (int i=0; i<depth; i++) printf(" ");
// Print View Details
printf("- Type: %d | Frame: %d, %d, %d, %d | Subviews: %d | Addr: %p\n",
view->type,
(int)view->frame->origin->x, (int)view->frame->origin->y,
(int)view->frame->size->width, (int)view->frame->size->height,
(view->subviews ? view->subviews->count : 0),
view);
// Recurse
if (view->subviews) {
for (int i=0; i<view->subviews->count; i++) {
void* child = arrayObjectAtIndex(view->subviews, i);
// Unwrap if needed (Label, etc) - simplified for View/Container check
// Assuming child is CCView* or compatible wrapper start
CCView* childView = (CCView*)child;
// Check if it's a wrapper to get the real view for frame printing
// (Logic from viewAddSubview)
CCType type = *((CCType*)child);
if (type == CCType_Label) childView = ((CCLabel*)child)->view;
// ... etc
debug_print_view_hierarchy(childView, depth + 1);
}
}
}
void hideTriangleAnimation(void) {
if (g_triangle_task_handle != NULL) {
FreeOSLogI(TAG, "Stopping Triangle Animation...");
// 1. Delete the task
vTaskDelete(g_triangle_task_handle);
g_triangle_task_handle = NULL;
// 2. Restore the background one last time to clean up the screen
GraphicsCommand cmd_restore = {
.cmd = CMD_ANIM_RESTORE_BG,
.x = ANIM_X, .y = ANIM_Y,
.w = ANIM_W, .h = ANIM_H
};
QueueSend(g_graphics_queue, &cmd_restore, QUEUE_MAX_DELAY);
// 3. Flush the clean screen
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = ANIM_X, .y = ANIM_Y,
.w = ANIM_W, .h = ANIM_H
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
// 4. Free the background buffer
// free_anim_buffer(); // Implement this if your init_anim_buffer uses malloc
}
}
void showRotatingImageAnimation(void) {
// 1. Initialize buffer for background save/restore
init_anim_buffer(); // Ensure this handles the size defined in IMG_ANIM_W/H
// 2. Wait for previous UI to settle
vTaskDelay(pdMS_TO_TICKS(500));
FreeOSLogI(TAG, "CMD_ANIM_SAVE_BG for Image");
// 3. Command: Snapshot the background behind where the image will spin
GraphicsCommand cmd_save = {
.cmd = CMD_ANIM_SAVE_BG,
.x = IMG_ANIM_X-30, .y = IMG_ANIM_Y-30,
.w = IMG_ANIM_W+60, .h = IMG_ANIM_H+60
};
QueueSend(g_graphics_queue, &cmd_save, QUEUE_MAX_DELAY);
// 4. Sync: Wait for snapshot to finish
vTaskDelay(pdMS_TO_TICKS(100));
// 5. Start the Animation Task
xTaskCreatePinnedToCore(
rotating_image_task,
"rot_img_task",
4096,
NULL,
4,
&g_image_task_handle, // Save handle for cancellation
0
);
}
void hideRotatingImageAnimation(void) {
if (g_image_task_handle != NULL) {
FreeOSLogI(TAG, "Stopping Rotating Image Animation...");
// 1. Delete the task
vTaskDelete(g_image_task_handle);
g_image_task_handle = NULL;
// 2. Restore background to wipe the spinner
GraphicsCommand cmd_restore = {
.cmd = CMD_ANIM_RESTORE_BG,
.x = IMG_ANIM_X, .y = IMG_ANIM_Y,
.w = IMG_ANIM_W, .h = IMG_ANIM_H
};
QueueSend(g_graphics_queue, &cmd_restore, QUEUE_MAX_DELAY);
// 3. Flush
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = IMG_ANIM_X, .y = IMG_ANIM_Y,
.w = IMG_ANIM_W, .h = IMG_ANIM_H
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
// 4. Free buffer
// free_anim_buffer();
}
}
void rotating_image_task(void *pvParameter) {
FreeOSLogI(TAG, "rotating_image_task started");
float angle = 0.0f;
const float speed = 0.15f;
// Geometry Constants
float w = (float)IMG_ANIM_W;
float h = (float)IMG_ANIM_H;
float cx = IMG_ANIM_X + (w / 2.0f);
float cy = IMG_ANIM_Y + (h / 2.0f);
// Pre-calculate the "centering" matrix (It never changes)
// This moves the image's top-left from (0,0) to (-w/2, -h/2)
// so that (0,0) becomes the center of the image.
Matrix3x3 mat_center_offset = TranslationMatrix(-w / 2.0f, -h / 2.0f);
Matrix3x3 mat_screen_position = TranslationMatrix(cx, cy);
while (1) {
// --- STEP 1: RESTORE BACKGROUND ---
GraphicsCommand cmd_restore = {
.cmd = CMD_ANIM_RESTORE_BG,
.x = IMG_ANIM_X-30, .y = IMG_ANIM_Y-30,
.w = IMG_ANIM_W+60, .h = IMG_ANIM_H+60
};
QueueSend(g_graphics_queue, &cmd_restore, 0);
// --- STEP 2: CALCULATE TRANSFORM MATRIX ---
// A. Get Rotation Matrix for current angle
Matrix3x3 mat_rot = RotationMatrix(angle);
// B. Combine: Rotate * CenterOffset
// Order matters! This applies the offset first, then the rotation.
Matrix3x3 mat_temp = MultiplyMatrix3x3(&mat_rot, &mat_center_offset);
// C. Combine: ScreenPosition * (Rotate * CenterOffset)
// This moves the rotated shape to the final X,Y on screen.
Matrix3x3 mat_final = MultiplyMatrix3x3(&mat_screen_position, &mat_temp);
// --- STEP 3: SEND DRAW COMMAND ---
GraphicsCommand cmd_draw = {
.cmd = CMD_DRAW_IMAGE_FILE,
.x = IMG_ANIM_X, .y = IMG_ANIM_Y, // Logical bounds (for updates)
.w = IMG_ANIM_W, .h = IMG_ANIM_H,
.hasTransform = true,
.alpha = 1.0,
.transform = mat_final
};
// Set the image path safely
strncpy(cmd_draw.imagePath, "/spiflash/loading.png", 63);
QueueSend(g_graphics_queue, &cmd_draw, 0);
// --- STEP 4: FLUSH ---
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = IMG_ANIM_X-30, .y = IMG_ANIM_Y-30,
.w = IMG_ANIM_W+60, .h = IMG_ANIM_H+60
};
QueueSend(g_graphics_queue, &cmd_flush, 0);
// Update angle
angle += speed;
if (angle > M_PI * 2) angle -= M_PI * 2;
vTaskDelay(pdMS_TO_TICKS(33)); // ~30 FPS
}
}
void showTriangleAnimation(void) {
init_anim_buffer();
//vTaskDelay(pdMS_TO_TICKS(1000));
FreeOSLogI(TAG, "CMD_ANIM_SAVE_BG");
// --- CRITICAL STEP: SYNC ---
// We must wait for the graphics task to finish drawing the gradient
// before we snapshot it. 500ms is plenty.
//vTaskDelay(pdMS_TO_TICKS(1000));
// --- 3. NEW: Start Animation Task (Core 0) ---
xTaskCreatePinnedToCore(
triangle_animation_task,
"anim_task",
4096,
NULL,
4, // Slightly lower priority than graphics/touch
&g_triangle_task_handle,
0 // Run on Core 0 to leave Core 1 free for rendering
);
}
// Helper: Finds which subview is under the user's finger
// Returns: The specific child view (key, row, button), or NULL if nothing found.
/*CCView* find_subview_at_point(CCView* container, int globalX, int globalY) {
if (!container || !container->subviews) return NULL;
// 1. Calculate the Container's Global Position
// We need this because the children's frames are relative to the Container,
// but your touch is relative to the Screen (0,0).
// (Assuming you have getAbsoluteOrigin available from scan.c,
// otherwise we assume container->frame->origin is screen coords if it's a top-level view)
CCPoint containerOrigin = getAbsoluteOrigin(container);
int containerAbsX = (int)containerOrigin.x;
int containerAbsY = (int)containerOrigin.y;
// 2. Convert Touch to "Container-Local" Coordinates
int localTouchX = globalX - containerAbsX;
int localTouchY = globalY - containerAbsY;
// 3. Check Intersections (Iterate Backwards: Top-most view first)
for (int i = container->subviews->count - 1; i >= 0; i--) {
CCView* child = (CCView*)arrayObjectAtIndex(container->subviews, i);
// Simple Rectangle Intersection Check
if (localTouchX >= child->frame->origin->x &&
localTouchX <= child->frame->origin->x + child->frame->size->width &&
localTouchY >= child->frame->origin->y &&
localTouchY <= child->frame->origin->y + child->frame->size->height) {
return child; // Found it!
}
}
return NULL; // Touched empty space/background
}*/
// Helper: Finds which subview is under the user's finger
// Returns: The specific child view (key, row, button), or NULL if nothing found.
CCView* find_subview_at_point(CCView* container, int globalX, int globalY) {
// 1. If the container itself ignores touches, stop immediately
if (!container || !container->subviews || container->ignoreTouch) return NULL;
// 2. Calculate the Container's Global Position
// We need this because the children's frames are relative to the Container,
// but your touch is relative to the Screen (0,0).
CCPoint containerOrigin = getAbsoluteOrigin(container);
int containerAbsX = (int)containerOrigin.x;
int containerAbsY = (int)containerOrigin.y;
// 3. Convert Touch to "Container-Local" Coordinates
int localTouchX = globalX - containerAbsX;
int localTouchY = globalY - containerAbsY;
// 4. Check Intersections (Iterate Backwards: Top-most view first)
for (int i = container->subviews->count - 1; i >= 0; i--) {
CCView* child = (CCView*)arrayObjectAtIndex(container->subviews, i);
// --- THE IGNORE TOUCH FIX ---
// Skip this child entirely if it is set to ignore touches!
if (!child || child->ignoreTouch) continue;
// Simple Rectangle Intersection Check
if (localTouchX >= child->frame->origin->x &&
localTouchX <= child->frame->origin->x + child->frame->size->width &&
localTouchY >= child->frame->origin->y &&
localTouchY <= child->frame->origin->y + child->frame->size->height) {
return child; // Found it!
}
}
return NULL; // Touched empty space/background
}
// Helper: Recursively finds the deepest subview under the user's finger
// Returns: The deepest specific child view, or NULL if nothing found.
CCView* find_subview_at_point_recursive(CCView* container, int globalX, int globalY) {
// 1. Base safety check
if (!container || container->ignoreTouch) return NULL;
// 2. Only attempt to search if this container actually has children
if (container->subviews) {
// Calculate the Container's Global Position once
CCPoint containerOrigin = getAbsoluteOrigin(container);
int containerAbsX = (int)containerOrigin.x;
int containerAbsY = (int)containerOrigin.y;
// Convert Touch to "Container-Local" Coordinates
int localTouchX = globalX - containerAbsX;
int localTouchY = globalY - containerAbsY;
// Check Intersections (Iterate Backwards: Top-most view first)
for (int i = container->subviews->count - 1; i >= 0; i--) {
CCView* child = (CCView*)arrayObjectAtIndex(container->subviews, i);
// Skip this child entirely if it ignores touches
if (!child || child->ignoreTouch) continue;
// Simple Rectangle Intersection Check using pointers
if (localTouchX >= child->frame->origin->x &&
localTouchX <= child->frame->origin->x + child->frame->size->width &&
localTouchY >= child->frame->origin->y &&
localTouchY <= child->frame->origin->y + child->frame->size->height) {
// --- THE RECURSIVE MAGIC ---
// We know the touch landed inside 'child'.
// Let's ask 'child' if the touch landed on anything inside of IT!
CCView* deepHit = find_subview_at_point_recursive(child, globalX, globalY);
// If it hit a nested button, return that deepest button
if (deepHit) {
return deepHit;
}
// Otherwise, it just hit the child wrapper itself (like your toolbar button!)
return child;
}
}
}
return NULL; // Touched empty space/background
}
#define TARGET_APP_MAIN_ADDRESS 0x4200aec8
// Function pointer to the loaded program entry
void (*loaded_program_entry)(void);
// This is an *oversimplified* attempt and likely only works if the
// binary is specifically compiled as Position-Independent Code (PIC)
// or if you manually calculate the needed offsets.
void print_heap_info() {
size_t largest_free_block = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM|MALLOC_CAP_EXEC);// |MALLOC_CAP_EXEC|MALLOC_CAP_32BIT
printf("Largest contiguous block for EXEC/SPIRAM: %zu bytes\n", largest_free_block);
}
long get_file_size(const char *filename) {
FILE *f = fopen(filename, "rb");
if (f == NULL) return 0;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fclose(f);
return size;
}
void load_and_execute_program(const char* file_path) {
// 1. Determine the size of the file
struct stat st;
if (stat(file_path, &st) != 0) {
printf("Failed to get file size for %s.\n", file_path);
return;
}
size_t file_size = st.st_size;
printf("File size to load: %zu bytes\n", file_size);
// 2. Allocate memory for the program from Executable PSRAM
// MALLOC_CAP_SPIRAM: Use External RAM (PSRAM)
// MALLOC_CAP_EXEC: Required for executing code from this memory
// MALLOC_CAP_32BIT: Required for aligned 32-bit instructions
print_heap_info();
void* program_memory = heap_caps_malloc(file_size,
MALLOC_CAP_SPIRAM |
MALLOC_CAP_EXEC |
MALLOC_CAP_32BIT);
if (!program_memory) {
printf("Memory allocation failed for program (needed %zu bytes).\n", file_size);
return;
}
printf("Successfully allocated memory at address: %p\n", program_memory);
// 3. Open and Read the binary into allocated memory
FILE* program_file = fopen(file_path, "rb");
if (!program_file) {
printf("Failed to open program file.\n");
heap_caps_free(program_memory);
return;
}
size_t bytes_read = fread(program_memory, 1, file_size, program_file);
fclose(program_file);
if (bytes_read != file_size) {
printf("Error reading file! Read %zu of %zu bytes.\n", bytes_read, file_size);
heap_caps_free(program_memory);
return;
}
// 4. Point and Jump to the loaded program (app_main is at the start)
// NOTE: This only works if 'app_main' is the *very first instruction*
// in your raw .bin file, which is highly unlikely for a full ESP-IDF bin.
// The REAL RELOCATION PROBLEM still exists after this step.
loaded_program_entry = (void (*)(void))program_memory;
printf("Jumping to loaded program...\n");
loaded_program_entry();
// 5. Clean up
heap_caps_free(program_memory);
printf("Returned to main program after running loaded program.\n");
}
/**
* @brief Lists all files and directories in the given mount point.
* * @param mount_point The base path where the filesystem is mounted (e.g., "/fat").
*/
void list_directory_contents(const char *mount_point) {
DIR *dir = NULL;
struct dirent *ent;
char full_path[128];
struct stat st;
FreeOSLogI(TAG, "--- Files in %s partition: ---", mount_point);
// 1. Open the directory stream
dir = opendir(mount_point);
if (!dir) {
FreeOSLogE(TAG, "Failed to open directory: %s", mount_point);
return;
}
// 2. Read entries one by one
while ((ent = readdir(dir)) != NULL) {
// Skip '.' (current dir) and '..' (parent dir) entries which are common in POSIX
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
// Construct the full path to get file size
snprintf(full_path, sizeof(full_path), "%s/%s", mount_point, ent->d_name);
// 3. Get file status (size and type)
if (stat(full_path, &st) == 0) {
if (ent->d_type == DT_DIR) {
// Directories
printf(" [DIR] %s\n", ent->d_name);
} else {
// Regular Files
printf(" [FILE] %s (Size: %ld bytes)\n", ent->d_name, st.st_size);
}
} else {
ESP_LOGW(TAG, "Failed to stat file/dir: %s", full_path);
}
}
// 4. Close the directory stream
closedir(dir);
FreeOSLogI(TAG, "--- Listing Complete ---");
}
/*void testCPUGraphicsBenchmark(void) {
// 1. Allocate Framebuffer in PSRAM (Slow access) or SRAM (Fast access)
// For benchmarking math, SRAM is better, but for full frame, you likely need PSRAM.
int width = 320;
int height = 240;
size_t fbSize = width * height * 3;
uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(fbSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!display_buffer) {
FreeOSLogE(TAG, "Failed to allocate memory");
return;
}
Framebuffer fb = {
.displayWidth = width,
.displayHeight = height,
.pixelData = display_buffer,
.colorMode = COLOR_MODE_BGR888
};
Gradient grad;
grad.angle = 45.0f * (M_PI / 180.0f);
grad.type = GRADIENT_TYPE_LINEAR; // Initialize the new enum field
grad.numStops = 2;
// 1. Create the storage for the stops (on the stack)
ColorStop stops_storage[2];
// 2. Point the struct pointer to this storage
grad.stops = stops_storage;
// 3. Now it is safe to assign values
grad.stops[0].color = (ColorRGBA){255, 0, 0, 255};
grad.stops[0].position = 0.0f;
grad.stops[1].color = (ColorRGBA){0, 0, 255, 255};
grad.stops[1].position = 1.0f;
// --- BENCHMARK 1: ORIGINAL (Float) ---
// Warm up cache
fillRectangleWithGradientExtended(&fb, 0, 0, width, height, 0, 0, width, height, &grad, NULL);
int64_t t1 = esp_timer_get_time();
fillRectangleWithGradientExtended(&fb, 0, 0, width, height, 0, 0, width, height, &grad, NULL);
int64_t t2 = esp_timer_get_time();
FreeOSLogI(TAG, "Original (Float) Time: %lld us", (t2 - t1));
// --- BENCHMARK 2: SIMD (Fixed Point) ---
// Warm up cache
fillRectangleWithGradientSIMD(&fb, 0, 0, width, height, &grad);
int64_t t3 = esp_timer_get_time();
fillRectangleWithGradientSIMD(&fb, 0, 0, width, height, &grad);
int64_t t4 = esp_timer_get_time();
FreeOSLogI(TAG, "SIMD (Fixed Pt) Time: %lld us", (t4 - t3));
FreeOSLogI(TAG, "Speedup Factor: %.2fx", (float)(t2 - t1) / (float)(t4 - t3));
free(display_buffer);
}*/
void testCPUGraphics(void) {
// 2. Allocate the single framebuffer in PSRAM
int width = DISPLAY_HORIZONTAL_PIXELS;
int height = DISPLAY_VERTICAL_PIXELS;
int num_pixels = width * height;
// Allocate Display buffer (BGR888, 3 bytes/pixel)
// Size = 320 * 480 * 3 bytes = 460,800 bytes
uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(num_pixels * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!display_buffer) {
FreeOSLogE(TAG, "Failed to allocate display buffer in PSRAM");
return;
}
// 3. Initialize the CPUGraphics Framebuffer struct
Framebuffer fb = {
.displayWidth = width,
.displayHeight = height,
.pixelData = display_buffer, // Point to our BGR buffer
.colorMode = COLOR_MODE_BGR888, // Set the direct-write mode
//.colors = {0}
};
FreeOSLogI(TAG, "Framebuffer allocated. Starting draw...");
// 4. Perform drawing operations
// "draw a blank framebuffer" (clear to dark blue)
ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
clearFramebuffer(&fb, blue);
// "draw a rectangle" (solid red)
ColorRGBA red = {.r = 255, .g = 0, .b = 0, .a = 255};
drawRectangleCFramebuffer(&fb, 50, 50, 100, 100, red, true);
// Draw a semi-transparent green rectangle to test blending
ColorRGBA green_alpha = {.r = 0, .g = 255, .b = 0, .a = 128}; // 50% alpha
drawRectangleCFramebuffer(&fb, 100, 100, 150, 150, green_alpha, true);
FreeOSLogI(TAG, "Drawing complete. Sending to LCD...");
// 5. Send the completed buffer to the LCD
// No conversion step is needed!
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer));
FreeOSLogI(TAG, "Buffer sent. Halting in a loop.");
// 6. Clean up (or loop)
//while (1) {
// vTaskDelay(pdMS_TO_TICKS(1000));
//}
}
/**
* @brief This task handles all drawing and screen updates.
*
* It runs in an infinite loop, drawing to the framebuffer and
* sending it to the display, then delaying to feed the WDT.
*/
/*void graphics_task(void *arg)
{
FreeOSLogI(TAG, "Graphics task started.");
// --- NEW: INITIALIZE FREETYPE *HERE* ---
esp_err_t ft_ok = initialize_freetype();
if (ft_ok != ESP_OK) {
FreeOSLogE(TAG, "Failed to initialize FreeType, deleting task.");
vTaskDelete(NULL); // Abort this task
return;
}
// --- END NEW PART ---
FreeOSLogI(TAG, "Graphics task started.");
// 2. Allocate the single framebuffer in PSRAM
int width = DISPLAY_HORIZONTAL_PIXELS;
int height = DISPLAY_VERTICAL_PIXELS;
int num_pixels = width * height;
uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(num_pixels * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!display_buffer) {
FreeOSLogE(TAG, "Failed to allocate display buffer in PSRAM. Deleting task.");
vTaskDelete(NULL); // Delete this task
return;
}
FreeOSLogI(TAG, "Framebuffer allocated.");
// 3. Initialize the CPUGraphics Framebuffer struct
Framebuffer fb = {
.displayWidth = width,
.displayHeight = height,
.pixelData = display_buffer, // Point to our BGR buffer
.colorMode = COLOR_MODE_BGR888 // Set the direct-write mode
};
GraphicsCommand cmd;
// This is now our main application loop
while (1) {
FreeOSLogI(TAG, "Starting draw...");
// 4. Perform drawing operations
// Clear to dark blue
ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
clearFramebuffer(&fb, blue);
// Draw solid red
ColorRGBA red = {.r = 255, .g = 0, .b = 0, .a = 255};
drawRectangleCFramebuffer(&fb, 50, 50, 100, 100, red, true);
// Draw blended green
ColorRGBA green_alpha = {.r = 0, .g = 255, .b = 0, .a = 128}; // 50% alpha
drawRectangleCFramebuffer(&fb, 100, 100, 150, 150, green_alpha, true);
// 4. --- DRAW TEXT (This code is the same) ---
ColorRGBA white = {.r = 255, .g = 255, .b = 255, .a = 255};
renderText(&fb, // Your framebuffer
ft_face, // The font face we loaded
"Hello, World!", // The text to draw
50, // X position
300, // Y position
white, // Text color
24, // Font size in pixels
NULL); // No gradient
FreeOSLogI(TAG, "Drawing complete. Sending to LCD...");
esp_err_t err = esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer);
if (err != ESP_OK) {
FreeOSLogE(TAG, "Failed to draw bitmap! Error: %s", esp_err_to_name(err));
}
// 5. Send the completed buffer to the LCD
// This is the long-running function
//ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer));
FreeOSLogI(TAG, "Buffer sent.");
// Wait forever until a command arrives
if (xQueueReceive(g_graphics_queue, &cmd, QUEUE_MAX_DELAY) == pdTRUE) {
// --- ADD THIS "RECEIVED" LOG ---
FreeOSLogI(TAG, "Graphics task received command: %d", cmd.cmd);
switch (cmd.cmd) {
// --- Case 1: Draw a Rectangle ---
case CMD_DRAW_RECT:
FreeOSLogI(TAG, "Drawing rect to PSRAM");
drawRectangleCFramebuffer(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.color, true);
break;
// --- Case 2: Draw Text ---
case CMD_DRAW_TEXT:
FreeOSLogI(TAG, "Drawing text to PSRAM");
renderText(&fb, ft_face, cmd.text, cmd.x, cmd.y, white, 24, NULL);
break;
// --- Case 3: Update the LCD ---
case CMD_UPDATE_AREA:
{
FreeOSLogI(TAG, "Pushing update to LCD");
// 1. Create a small, *contiguous* buffer for the update
uint8_t* temp_buffer = (uint8_t*) heap_caps_malloc(
cmd.w * cmd.h * 3,
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
if (!temp_buffer) {
FreeOSLogE(TAG, "Failed to alloc temp_buffer!");
continue; // Skip this command
}
// 2. Copy the updated rectangle from PSRAM to the temp_buffer
uint8_t* psram_ptr;
uint8_t* temp_ptr = temp_buffer;
for (int i = 0; i < cmd.h; i++) {
psram_ptr = &((uint8_t*)fb.pixelData)[((cmd.y + i) * fb.displayWidth + cmd.x) * 3];
memcpy(temp_ptr, psram_ptr, cmd.w * 3);
temp_ptr += cmd.w * 3;
}
// 3. Send *only the small temp_buffer* to the LCD
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle,
cmd.x, cmd.y, // x_start, y_start
cmd.x + cmd.w, cmd.y + cmd.h, // x_end, y_end
temp_buffer));
// 4. Free the temp buffer
heap_caps_free(temp_buffer);
break;
}
}
}
// 6. *** THE FIX ***
// Delay for 10 milliseconds.
// This feeds the Task Watchdog and allows other tasks to run.
vTaskDelay(pdMS_TO_TICKS(100));
}
}*/
// =================================================================
// PNG DECODING CALLBACKS
// =================================================================
/**
* @brief PNG file read callback function for libpng.
* @param png_ptr The internal PNG structure pointer.
* @param out_data Pointer to the destination buffer (libpng's internal buffer).
* @param length The number of bytes to read.
*/
static void png_read_data(png_structp png_ptr, png_bytep out_data, png_size_t length) {
if (png_file_handle) {
if (fread(out_data, 1, length, png_file_handle) != length) {
png_error(png_ptr, "Error reading file data");
}
} else {
png_error(png_ptr, "File handle is NULL");
}
}
/**
* @brief Custom warning handler for libpng.
*/
static void png_warning_handler(png_structp png_ptr, png_const_charp warning_message) {
ESP_LOGW(TAG_PNG, "PNG Warning: %s", warning_message);
}
/**
* @brief Custom error handler for libpng.
*/
static void png_error_handler(png_structp png_ptr, png_const_charp error_message) {
FreeOSLogE(TAG_PNG, "PNG Error: %s", error_message);
// Setting longjmp is dangerous in ESP-IDF (may bypass stack unwinding)
// We rely on the caller checking the texture data for NULL/size.
}
ImageFileType get_image_type_from_path(const char* path) {
if (!path) return IMAGE_TYPE_UNKNOWN;
// Find the last dot '.' in the string
const char *dot = strrchr(path, '.');
// If no dot, or dot is the first character, it's not a valid extension
if (!dot || dot == path) return IMAGE_TYPE_UNKNOWN;
// Compare case-insensitive (handles .PNG, .png, .Png)
if (strcasecmp(dot, ".png") == 0) {
return IMAGE_TYPE_PNG;
}
if (strcasecmp(dot, ".jpg") == 0 || strcasecmp(dot, ".jpeg") == 0) {
return IMAGE_TYPE_JPEG;
}
return IMAGE_TYPE_UNKNOWN;
}
// =================================================================
// MAIN LOADING FUNCTION
// =================================================================
ImageTexture* load_image_from_file(const char* imgPath) {
// Reset global state for this decode operation
png_file_handle = NULL;
png_load_buffer = NULL;
// 1. Open the file via VFS
png_file_handle = fopen(imgPath, "rb");
if (!png_file_handle) {
FreeOSLogE(TAG_PNG, "Failed to open image file: %s", imgPath);
return NULL;
}
// --- Check PNG Signature ---
uint8_t header[8];
if (fread(header, 1, 8, png_file_handle) != 8 || png_sig_cmp(header, 0, 8)) {
FreeOSLogE(TAG_PNG, "File is not a valid PNG signature.");
fclose(png_file_handle);
return NULL;
}
// 2. Setup libpng structures
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, png_error_handler, png_warning_handler);
if (!png_ptr) {
FreeOSLogE(TAG_PNG, "Failed to create PNG read structure.");
fclose(png_file_handle);
return NULL;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
FreeOSLogE(TAG_PNG, "Failed to create PNG info structure.");
png_destroy_read_struct(&png_ptr, NULL, NULL);
fclose(png_file_handle);
return NULL;
}
// 3. Set custom read function and read info
png_set_read_fn(png_ptr, NULL, png_read_data);
png_set_sig_bytes(png_ptr, 8); // Already read 8 bytes of header
png_read_info(png_ptr, info_ptr);
// 4. Get image properties
png_uint_32 width = png_get_image_width(png_ptr, info_ptr);
png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
int bit_depth = png_get_bit_depth(png_ptr, info_ptr);
int color_type = png_get_color_type(png_ptr, info_ptr);
//FreeOSLogI(TAG_PNG, "Image found: %lu x %lu, Depth: %d, Type: %d", width, height, bit_depth, color_type);
// 5. Transform image data for consistent output (32-bit RGBA)
// Convert 16-bit to 8-bit (if needed)
if (bit_depth == 16) {
png_set_strip_16(png_ptr);
}
// Convert palette to RGB
if (color_type == PNG_COLOR_TYPE_PALETTE) {
png_set_palette_to_rgb(png_ptr);
}
// Convert transparency (tRGB, tALPHA) to full alpha channel
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
png_set_tRNS_to_alpha(png_ptr);
}
// Convert grayscale to RGB (for consistency)
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
png_set_gray_to_rgb(png_ptr);
}
// Ensure we have an alpha channel (for transparency and blending)
if (color_type != PNG_COLOR_TYPE_RGBA && color_type != PNG_COLOR_TYPE_RGB_ALPHA) {
png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
}
png_set_bgr(png_ptr);
// Update the image info after all transformations
png_read_update_info(png_ptr, info_ptr);
// Final check for 4 bytes per pixel (RGBA)
size_t row_bytes = png_get_rowbytes(png_ptr, info_ptr);
if (row_bytes != (width * 4)) {
FreeOSLogE(TAG_PNG, "Final row size mismatch. Expected %lu, got %lu.", width * 4, row_bytes);
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file_handle);
return NULL;
}
// 6. Allocate buffer in PSRAM (Critical step!)
png_load_width = width;
png_load_height = height;
size_t data_size = width * height * 4; // 4 bytes/pixel (RGBA)
png_load_buffer = (uint8_t*)heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM);
if (!png_load_buffer) {
FreeOSLogE(TAG_PNG, "Failed to allocate %u bytes in PSRAM for texture.", data_size);
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file_handle);
return NULL;
}
// 7. Read image data row by row
png_bytep* row_pointers = (png_bytep*)cc_safe_alloc(1, height * sizeof(png_bytep));
if (!row_pointers) {
FreeOSLogE(TAG_PNG, "Failed to allocate row pointers.");
heap_caps_free(png_load_buffer);
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file_handle);
return NULL;
}
// Set pointers to rows within the single PSRAM buffer
for (int y_pos = 0; y_pos < height; y_pos++) {
row_pointers[y_pos] = png_load_buffer + y_pos * row_bytes;
}
png_read_image(png_ptr, row_pointers);
// 8. Cleanup and return ImageTexture
ImageTexture* new_texture = (ImageTexture*)cc_safe_alloc(1, sizeof(ImageTexture));
if (new_texture) {
new_texture->width = width;
new_texture->height = height;
// Assign the PSRAM buffer to the ImageTexture struct
new_texture->data = (ColorRGBA*)png_load_buffer;
} else {
heap_caps_free(png_load_buffer);
}
// Final libpng cleanup
free(row_pointers);
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file_handle);
//FreeOSLogI(TAG_PNG, "PNG file loaded and decoded to PSRAM successfully.");
return new_texture;
}
ImageTexture* load_jpeg_from_file(const char* imgPath) {
FreeOSLogI(TAG_JPG, "Loading JPEG: %s", imgPath);
// 1. Open File
FILE *f = fopen(imgPath, "rb");
if (!f) {
FreeOSLogE(TAG_JPG, "Failed to open file: %s", imgPath);
return NULL;
}
fseek(f, 0, SEEK_END);
size_t file_size = ftell(f);
fseek(f, 0, SEEK_SET);
FreeOSLogI(TAG_JPG, "File Size: %d bytes", file_size); // <--- DEBUG 1
if (file_size == 0) {
FreeOSLogE(TAG_JPG, "File size is 0");
fclose(f);
return NULL;
}
// Allocate Input Buffer in PSRAM
uint8_t *jpeg_input_buf = (uint8_t *)heap_caps_malloc(file_size, MALLOC_CAP_SPIRAM);
if (!jpeg_input_buf) {
FreeOSLogE(TAG_JPG, "Failed to alloc input buffer");
fclose(f);
return NULL;
}
size_t bytes_read = fread(jpeg_input_buf, 1, file_size, f);
fclose(f);
FreeOSLogI(TAG_JPG, "Bytes Read: %d", bytes_read); // <--- DEBUG 2
// --- DEBUG 3: PRINT HEADER BYTES ---
if (bytes_read > 4) {
FreeOSLogI(TAG_JPG, "Header Bytes: %02X %02X %02X %02X",
jpeg_input_buf[0], jpeg_input_buf[1], jpeg_input_buf[2], jpeg_input_buf[3]);
}
// Valid JPEG should start with: FF D8 FF ...
// 2. Configure Decoder
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = jpeg_input_buf,
.indata_size = bytes_read, // Use actual bytes read
.out_format = JPEG_IMAGE_FORMAT_RGB888,
.out_scale = JPEG_IMAGE_SCALE_0,
.flags = { .swap_color_bytes = 0 }
};
esp_jpeg_image_output_t info;
// Get width/height without decoding the whole thing yet
esp_err_t ret = esp_jpeg_get_image_info(&jpeg_cfg, &info);
if (ret != ESP_OK) {
FreeOSLogE(TAG_JPG, "Failed to parse JPEG header");
heap_caps_free(jpeg_input_buf);
return NULL;
}
int w = info.width;
int h = info.height;
FreeOSLogI(TAG_JPG, "JPEG Info: %d x %d", w, h);
// --- 3. Allocate Temporary RGB Output Buffer ---
// The decoder needs a place to dump the 3-byte RGB data
size_t rgb_buf_size = w * h * 3;
uint8_t *temp_rgb_buf = (uint8_t *)heap_caps_malloc(rgb_buf_size, MALLOC_CAP_SPIRAM);
if (!temp_rgb_buf) {
FreeOSLogE(TAG_JPG, "Failed to alloc temp RGB buffer");
heap_caps_free(jpeg_input_buf);
return NULL;
}
// Setup config for full decode
jpeg_cfg.outbuf = temp_rgb_buf;
jpeg_cfg.outbuf_size = rgb_buf_size;
// --- 4. Decode ---
ret = esp_jpeg_decode(&jpeg_cfg, &info);
// We are done with the compressed input file now
heap_caps_free(jpeg_input_buf);
if (ret != ESP_OK) {
FreeOSLogE(TAG_JPG, "Failed to decode JPEG");
heap_caps_free(temp_rgb_buf);
return NULL;
}
// --- 5. Convert to ImageTexture (RGBA) ---
// We need to expand 3-byte RGB to 4-byte RGBA and handle the R/B swap
// for your display driver logic.
ImageTexture* new_texture = (ImageTexture*)cc_safe_alloc(1, sizeof(ImageTexture));
if (!new_texture) {
heap_caps_free(temp_rgb_buf);
return NULL;
}
new_texture->width = w;
new_texture->height = h;
size_t rgba_size = w * h * sizeof(ColorRGBA);
new_texture->data = (ColorRGBA*)heap_caps_malloc(rgba_size, MALLOC_CAP_SPIRAM);
if (!new_texture->data) {
free(new_texture);
heap_caps_free(temp_rgb_buf);
return NULL;
}
uint8_t *src = temp_rgb_buf;
ColorRGBA *dst = new_texture->data;
int num_pixels = w * h;
for (int i = 0; i < num_pixels; i++) {
// Read RGB from decoder output
uint8_t r = *src++;
uint8_t g = *src++;
uint8_t b = *src++;
// Write RGBA to texture (Perform R/B swap for your display)
// Your display expects BGR logic, so we store B in Red, R in Blue
dst->r = b;
dst->g = g;
dst->b = r;
dst->a = 255; // JPEGs are opaque
dst++;
}
// Free the temporary 3-byte buffer
heap_caps_free(temp_rgb_buf);
FreeOSLogI(TAG_JPG, "JPEG Loaded and Converted.");
return new_texture;
}
/*void updateArea(Framebuffer fb, GraphicsCommand cmd) {
FreeOSLogI(TAG, "Pushing update to LCD (chunked)...");
// Define a chunk size (e.g., 10 rows at a time)
int rows_per_chunk = 10;
// Calculate the size of our small, safe DMA buffer
size_t chunk_size_bytes = cmd.w * rows_per_chunk * 3;
// 1. Create the small, reusable DMA buffer
uint8_t* temp_buffer = (uint8_t*) heap_caps_malloc(
chunk_size_bytes,
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
if (!temp_buffer) {
FreeOSLogE(TAG, "Failed to alloc temp_buffer for chunking!");
return; // Skip this command
}
// 2. Loop over the update area in chunks
for (int y_chunk = 0; y_chunk < cmd.h; y_chunk += rows_per_chunk) {
// Calculate how many rows to send in this specific chunk
int rows_to_send = cmd.h - y_chunk;
if (rows_to_send > rows_per_chunk) {
rows_to_send = rows_per_chunk;
}
// Calculate the start/end coordinates for this chunk
int chunk_y_start = cmd.y + y_chunk;
int chunk_y_end = chunk_y_start + rows_to_send;
// 3. Copy this chunk from PSRAM to our internal temp_buffer
uint8_t* psram_ptr;
uint8_t* temp_ptr = temp_buffer;
for (int i = 0; i < rows_to_send; i++) {
psram_ptr = &((uint8_t*)fb.pixelData)[((chunk_y_start + i) * fb.displayWidth + cmd.x) * 3];
memcpy(temp_ptr, psram_ptr, cmd.w * 3);
temp_ptr += cmd.w * 3;
}
// 4. Send *only this small chunk* to the LCD
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle,
cmd.x, chunk_y_start, // x_start, y_start
cmd.x + cmd.w, chunk_y_end, // x_end, y_end
temp_buffer));
}
// 5. Free the temp buffer
heap_caps_free(temp_buffer);
touchEnabled = true;
}*/
void updateArea(Framebuffer fb, GraphicsCommand cmd) {
FreeOSLogI(TAG, "Pushing update to LCD (chunked)...");
int rows_per_chunk = 10;
size_t chunk_size_bytes = cmd.w * rows_per_chunk * 3;
// 1. Create the small, reusable DMA buffer
uint8_t* temp_buffer = (uint8_t*) heap_caps_malloc(
chunk_size_bytes,
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
if (!temp_buffer) {
FreeOSLogE(TAG, "Failed to alloc temp_buffer for chunking!");
return;
}
// 2. Loop over the update area in chunks
for (int y_chunk = 0; y_chunk < cmd.h; y_chunk += rows_per_chunk) {
int rows_to_send = cmd.h - y_chunk;
if (rows_to_send > rows_per_chunk) {
rows_to_send = rows_per_chunk;
}
int chunk_y_start = cmd.y + y_chunk;
int chunk_y_end = chunk_y_start + rows_to_send;
// 3. Copy this chunk from PSRAM to our internal DMA buffer
uint8_t* psram_ptr;
uint8_t* temp_ptr = temp_buffer;
for (int i = 0; i < rows_to_send; i++) {
psram_ptr = &((uint8_t*)fb.pixelData)[((chunk_y_start + i) * fb.displayWidth + cmd.x) * 3];
memcpy(temp_ptr, psram_ptr, cmd.w * 3);
temp_ptr += cmd.w * 3;
}
// 4. Send *only this small chunk* to the LCD
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle,
cmd.x, chunk_y_start,
cmd.x + cmd.w, chunk_y_end,
temp_buffer));
// 5. THE DMA FIX: Wait for the SPI hardware to finish sending THIS chunk!
// This stops the loop from overwriting temp_buffer while DMA is actively reading it.
if (lcd_flush_ready_sem) {
xSemaphoreTake(lcd_flush_ready_sem, pdMS_TO_TICKS(100)); // 100ms safety timeout
}
}
// 6. Loop is completely finished, 100% safe to free the buffer now
heap_caps_free(temp_buffer);
touchEnabled = true;
}
//#include "freertos/FreeRTOS.h"
//#include "freertos/task.h"
//#include "freertos/queue.h"
// The mailbox that passes coordinates between the cores
QueueHandle_t display_queue = NULL;
// The background task running on Core 1
void display_task(void *pvParameter) {
GraphicsCommand cmd;
while(1) {
// 1. Wait here peacefully until the UI thread sends a dirty rectangle
if (xQueueReceive(display_queue, &cmd, portMAX_DELAY)) {
// 2. We received a command! Do the heavy lifting.
size_t rect_bytes = cmd.w * cmd.h * 3;
uint8_t* safe_buffer = (uint8_t*) heap_caps_malloc(rect_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
if (safe_buffer) {
uint8_t* dest_ptr = safe_buffer;
for (int y = 0; y < cmd.h; y++) {
uint8_t* src_row_ptr = &((uint8_t*)fb.pixelData)[((cmd.y + y) * fb.displayWidth + cmd.x) * 3];
memcpy(dest_ptr, src_row_ptr, cmd.w * 3);
dest_ptr += cmd.w * 3;
}
// 3. Fire the DMA SPI transfer
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle, cmd.x, cmd.y, cmd.x + cmd.w, cmd.y + cmd.h, safe_buffer));
// 4. Block THIS core until SPI is done.
// Core 0 is completely unaffected and continues reading touch!
if (lcd_flush_ready_sem) {
xSemaphoreTake(lcd_flush_ready_sem, portMAX_DELAY);
}
// 5. Free memory and loop back to wait for the next command
heap_caps_free(safe_buffer);
}
}
}
}
void updateArea1(Framebuffer fb, GraphicsCommand cmd) {
// 1. First time this is called, set up the Queue and Dual-Core Task!
if (display_queue == NULL) {
// Create a queue that can hold 10 pending screen updates
display_queue = xQueueCreate(10, sizeof(GraphicsCommand));
// Boot up the Display Task and strictly pin it to Core 1
xTaskCreatePinnedToCore(
display_task, // Function to run
"DisplayTask", // Task name
16384, // Stack size (4KB is plenty)
NULL, // Parameters
5, // Priority (High)
NULL, // Task handle
1 // PIN TO CORE 1!
);
FreeOSLogI("LCD", "Dual-Core Rendering Pipeline Initialized!");
}
// 2. Send the command to the Queue.
// The '0' means if the queue is full, just drop the frame and return instantly.
// This naturally prevents the OS from lagging if you draw too fast!
xQueueSend(display_queue, &cmd, 0);
}
// Validated for: Framebuffer = BGR888 (3-byte), ILI9488 (3-byte SPI)
/*void updateArea(Framebuffer fb, GraphicsCommand cmd) {
// 1. Configuration
int bpp = 3; // BGR888 = 3 Bytes Per Pixel
int rows_per_chunk = 20; // Safe chunk size (adjust if needed)
// 2. Allocate the DMA Buffer ONCE
// Size = Width * Lines * 3 bytes
size_t chunk_size = cmd.w * rows_per_chunk * bpp;
// Using INTERNAL | DMA memory is strictly required for ESP32 SPI DMA
uint8_t* temp_buffer = (uint8_t*) heap_caps_malloc(chunk_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
if (!temp_buffer) {
FreeOSLogE("LCD", "Failed to allocate DMA buffer for updateArea");
return;
}
// 3. Process the Area in Chunks
for (int y_chunk = 0; y_chunk < cmd.h; y_chunk += rows_per_chunk) {
// Handle the final chunk (might be smaller than rows_per_chunk)
int rows_to_send = (cmd.h - y_chunk > rows_per_chunk) ? rows_per_chunk : (cmd.h - y_chunk);
// --- A. THE STRIDE COPY (Fixes the "Slice" glitch) ---
// We pack the non-contiguous framebuffer rows into our contiguous temp_buffer
for (int i = 0; i < rows_to_send; i++) {
// Calculate Source Address: (Row * Width + Col) * 3
// We use (cmd.y + y_chunk + i) to get the absolute row index
int src_offset = ((cmd.y + y_chunk + i) * fb.displayWidth + cmd.x) * bpp;
// Calculate Destination Address: i * Width * 3
int dst_offset = (i * cmd.w) * bpp;
memcpy(temp_buffer + dst_offset,
(uint8_t*)fb.pixelData + src_offset,
cmd.w * bpp);
}
// --- B. THE DRAW ---
// Send the packed buffer to the display
esp_err_t ret = esp_lcd_panel_draw_bitmap(lcd_handle,
cmd.x,
cmd.y + y_chunk,
cmd.x + cmd.w,
cmd.y + y_chunk + rows_to_send,
temp_buffer);
if (ret != ESP_OK) {
FreeOSLogE("LCD", "Draw bitmap failed");
break;
}
// --- C. THE RACE CONDITION FIX ---
// We MUST wait for the SPI transaction to finish before we loop back
// and overwrite 'temp_buffer' with new data.
// Method 1: If you have a 'flush_ready' callback using a Semaphore, wait on it here.
// Method 2: The "Brute Force" Polling (Simplest for now)
// Note: Check your specific esp_lcd driver initialization.
// If you didn't set a callback, verify if the driver blocks automatically.
// Most ESP-IDF SPI drivers are non-blocking by default.
// A simple delay is crude but proves the fix.
// Ideally, you use xSemaphoreTake(transfer_done_sem, QUEUE_MAX_DELAY);
// For testing, this ensures the bus is clear (assuming 40MHz SPI, 20 lines takes <1ms)
// Better: Application-level wait logic or polling.
// If your driver has a "wait for done" function, use it.
// *CRITICAL*: If you don't have semaphores set up, the buffer will corrupt.
// If you are unsure, UNCOMMENT the line below to allocate a NEW buffer every time (slower, but safer)
// OR add a delay:
vTaskDelay(pdMS_TO_TICKS(1)); // Give DMA time to breathe
}
// 4. Cleanup
heap_caps_free(temp_buffer);
}
*/
// Example Master Drawing Function
void draw_current_view(Framebuffer *fb, FT_Face ft_face) {
// --- 2. Draw the SCROLLABLE Text Box ---
const char *scroll_text = "The quick brown fox jumps over the lazy dog. This is the content that needs to scroll smoothly behind the viewport frame.";
ColorRGBA white = {.r = 255, .g = 255, .b = 255, .a = 255};
TextFormat format = {.alignment = TEXT_ALIGN_LEFT, .wrapMode = TEXT_WRAP_MODE_WHOLE_WORD, .lineSpacing = 5, .glyphSpacing = 0};
// CALL RENDER TEXT BOX
renderTextBoxScroll(
fb,
ft_face,
scroll_text,
50, // x_start
100, // y_start (Top of the viewport)
220, // clipWidth
g_scroll_viewport_h, // clipHeight (e.g., 300)
g_scroll_offset_y, // <--- 🔑 SCROLL OFFSET APPLIED HERE
white,
16,
&format
);
// ... (any other static foreground elements) ...
}
#include <math.h>
#include "esp_heap_caps.h" // Ensure we use capable memory for large vertex arrays
// ==========================================================
// HELPER: Generate Gear Vertices
// ==========================================================
/**
* @brief Generates vertices for a gear-like shape.
* This creates a star shape with many short teeth, resembling a gear.
*/
Vector3* create_gear_vertices(float centerX, float centerY, float outerRadius, float innerRadius, int numTeeth, int *outNumVertices) {
int total_vertices = numTeeth * 2;
*outNumVertices = total_vertices;
// Allocate memory for the vertices.
// Using heap_caps_malloc to ensure enough general RAM is available for larger shapes.
Vector3* vertices = (Vector3*)heap_caps_malloc(total_vertices * sizeof(Vector3), MALLOC_CAP_DEFAULT);
if (!vertices) {
FreeOSLogE(TAG, "Failed to allocate gear vertices.");
return NULL;
}
// The angle step is full circle divided by total points (inner + outer)
float angle_step = (2.0f * M_PI) / total_vertices;
for (int i = 0; i < total_vertices; i++) {
// Alternate between outer and inner radii to create teeth
float r = (i % 2 == 0) ? outerRadius : innerRadius;
float angle = i * angle_step;
// Calculate vertex position
vertices[i].x = centerX + r * cosf(angle);
vertices[i].y = centerY + r * sinf(angle);
// Assumes Vector3 has a z component, set to 0 for 2D
#ifdef VECTOR3_HAS_Z
vertices[i].z = 0.0f;
#endif
}
return vertices;
}
/**
* @brief Creates a gear with a "spiked" or toothed hole in the center.
* * @param holeOuterRadius The "tip" of the inner spikes.
* @param holeInnerRadius The "valley" of the inner spikes.
*/
Vector3* create_spiked_center_gear_vertices(float centerX, float centerY, float outerRadius, float innerRadius, float holeOuterRadius, float holeInnerRadius, int numTeeth, int *outNumVertices) {
int gear_points = numTeeth * 2;
int total_vertices = gear_points * 2 + 2;
*outNumVertices = total_vertices;
Vector3* vertices = (Vector3*)heap_caps_malloc(total_vertices * sizeof(Vector3), MALLOC_CAP_DEFAULT);
if (!vertices) return NULL;
float angle_step = (2.0f * M_PI) / gear_points;
// --- 1. Trace Outer Gear (Clockwise) ---
int v_index = 0;
for (int i = 0; i < gear_points; i++) {
float r = (i % 2 == 0) ? outerRadius : innerRadius;
float angle = i * angle_step;
vertices[v_index].x = centerX + r * cosf(angle);
vertices[v_index].y = centerY + r * sinf(angle);
vertices[v_index].z = 0.0f;
v_index++;
}
// --- 2. Cut to Inner Spikes ---
vertices[v_index] = vertices[v_index - 1];
v_index++;
// --- 3. Trace Inner Spikes (Counter-Clockwise) ---
for (int i = gear_points; i >= 0; i--) {
// Logic for inner teeth:
// We want the inner spike to align or offset with the outer spike.
// Using the same modulo logic keeps them aligned.
float r = (i % 2 == 0) ? holeOuterRadius : holeInnerRadius;
float angle = i * angle_step;
vertices[v_index].x = centerX + r * cosf(angle);
vertices[v_index].y = centerY + r * sinf(angle);
vertices[v_index].z = 0.0f;
v_index++;
}
return vertices;
}
// ==========================================================
// MAIN EXAMPLE FUNCTION TO CALL IN GRAPHICS TASK
// ==========================================================
void draw_complex_gear_gradient_example(Framebuffer *fb) {
FreeOSLogI(TAG, "Starting complex polygon draw...");
// --- 1. Define the Complex Geometry (48-vertex Gear) ---
int num_vertices = 0;
// Center the gear at (160, 240)
// Outer radius 130px, Inner radius 110px (short, stubby teeth)
// 24 teeth = 48 total vertices
Vector3* gear_vertices = create_gear_vertices(160.0f, 240.0f, 130.0f, 110.0f, 24, &num_vertices);
if (!gear_vertices) {
return; // Exit if allocation failed
}
FreeOSLogI(TAG, "Generated %d vertices for gear.", num_vertices);
// --- 2. Define the 10-Stop "Thermal" Gradient ---
// We define 10 colors creating a spectrum from cold (blue) to hot (white).
ColorRGBA col0 = {.r = 0, .g = 0, .b = 128, .a = 255}; // Deep Blue (0.0)
ColorRGBA col1 = {.r = 0, .g = 0, .b = 255, .a = 255}; // Blue (0.1)
ColorRGBA col2 = {.r = 0, .g = 128, .b = 255, .a = 255}; // Light Blue (0.2)
ColorRGBA col3 = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan (0.3)
ColorRGBA col4 = {.r = 0, .g = 255, .b = 0, .a = 255}; // Green (0.4)
ColorRGBA col5 = {.r = 255, .g = 255, .b = 0, .a = 255}; // Yellow (0.5)
ColorRGBA col6 = {.r = 255, .g = 128, .b = 0, .a = 255}; // Orange (0.6)
ColorRGBA col7 = {.r = 255, .g = 0, .b = 0, .a = 255}; // Red (0.7)
ColorRGBA col8 = {.r = 128, .g = 0, .b = 128, .a = 255}; // Purple (0.8)
ColorRGBA col9 = {.r = 255, .g = 255, .b = 255, .a = 255}; // White Hot (1.0)
ColorStop stops[10] = {
{.color = col0, .position = 0.0f},
{.color = col1, .position = 0.1f},
{.color = col2, .position = 0.2f},
{.color = col3, .position = 0.3f},
{.color = col4, .position = 0.4f},
{.color = col5, .position = 0.5f},
{.color = col6, .position = 0.6f},
{.color = col7, .position = 0.7f},
{.color = col8, .position = 0.8f},
{.color = col9, .position = 1.0f}
};
Gradient complex_gradient = {
.stops = stops,
.numStops = 10,
// Set a 30-degree angle for the thermal sweep across the gear
.angle = M_PI / 6.0f
};
// --- 3. Draw the Polygon ---
uint64_t start_time = esp_timer_get_time();
fillPolygonWithGradient(
fb,
gear_vertices,
num_vertices,
&complex_gradient,
NULL, // No Transform matrix needed for this test
false // Anti-aliasing off (scanline fill is usually binary)
);
uint64_t end_time = esp_timer_get_time();
FreeOSLogI(TAG, "Complex polygon drawn in %llu microseconds.", (end_time - start_time));
// --- 4. Clean up memory ---
// IMPORTANT: Free the vertex array allocated by the helper function
free(gear_vertices);
}
void draw_star(Framebuffer *fb) {
int num_vertices = 0;
Vector3* star_vertices = create_star_vertices(
160.0f, // Center X (middle of 320 screen)
240.0f, // Center Y (middle of 480 screen)
100.0f, // Outer Radius
40.0f, // Inner Radius
&num_vertices
);
if (!star_vertices) {
FreeOSLogE(TAG, "Failed to allocate star vertices.");
return;
}
// --- 2. Define the Gradient ---
// Gradient Stop 1: Bright Gold
ColorRGBA gold = {.r = 255, .g = 215, .b = 0, .a = 255};
// Gradient Stop 2: Deep Red-Orange
ColorRGBA fire_red = {.r = 178, .g = 34, .b = 34, .a = 255};
ColorStop stops[2] = {
{.color = gold, .position = 0.0f},
{.color = fire_red, .position = 1.0f}
};
Gradient star_gradient = {
.stops = stops,
.numStops = 2,
.angle = M_PI_4 // 45 degrees diagonal gradient
};
// --- 3. Draw the Polygon ---
// No transform or anti-aliasing for this example
fillPolygonWithGradient(
fb,
star_vertices,
num_vertices,
&star_gradient,
NULL, // No Transform
false // No AntiAlias (since the scanline code is complex)
);
// 4. Clean up memory
free(star_vertices);
}
void setup_scroll_text_demo() {
g_long_text_ptr = "This is the beginning of a very long string.\n"
"It will demonstrate scrolling text rendering.\n\n"
"We need to measure the total height of this text "
"so we know how far we are allowed to scroll.\n"
"The renderTextBoxScroll function handles the "
"vertical offset calculation.\n\n"
"Line 6\nLine 7\nLine 8\nLine 9\nLine 10\n"
"End of text.";
// Define formatting for measurement
TextFormat fmt = { .alignment = TEXT_ALIGN_LEFT, .wrapMode = TEXT_WRAP_MODE_WHOLE_WORD, .lineSpacing = 5, .glyphSpacing = 0 };
// Measure total height (using the helper function we wrote earlier)
// Assuming viewport width is 200
g_text_total_height = measureTextHeight(ft_face, g_long_text_ptr, 200, 16, &fmt);
FreeOSLogI(TAG, "Total Text Height: %d px", g_text_total_height);
}
// Helper to allocate the buffer (Call this in app_main or graphics_task setup)
void init_anim_buffer() {
g_anim_backup_buffer = (uint8_t*)heap_caps_malloc(ANIM_W * ANIM_H * 3, MALLOC_CAP_SPIRAM);
if (!g_anim_backup_buffer) {
FreeOSLogE(TAG, "Failed to allocate animation backup buffer!");
} else {
FreeOSLogI(TAG, "Animation backup buffer allocated (120KB).");
}
}
void triangle_animation_task(void *pvParameter) {
FreeOSLogI(TAG, "triangle_animation_task");
// 1. Wait a moment for the main UI (Gradient/Rects) to finish drawing
//vTaskDelay(pdMS_TO_TICKS(1000));
// 2. Send command to CAPTURE the background (Snapshot the gradient behind us)
GraphicsCommand cmd_save = { .cmd = CMD_ANIM_SAVE_BG, .x = ANIM_X, .y = ANIM_Y, .w = ANIM_W, .h = ANIM_H};
QueueSend(g_graphics_queue, &cmd_save, QUEUE_MAX_DELAY);
float phase = 0.0f;
const float speed = 0.1f;
int center_x = 160;
int center_y = 240;
// Background is now handled by Restore, so we don't need a clear color
while (1) {
// --- STEP 1: RESTORE BACKGROUND (Erase previous triangle) ---
// This copies the saved pixels back to the framebuffer
GraphicsCommand cmd_restore = { .cmd = CMD_ANIM_RESTORE_BG, .x = ANIM_X, .y = ANIM_Y, .w = ANIM_W, .h = ANIM_H };
QueueSend(g_graphics_queue, &cmd_restore, 0);
// --- STEP 2: CALCULATE AND DRAW NEW TRIANGLE ---
float radius = 50.0f + 30.0f * sinf(phase);
phase += speed;
Vector3* vertices = (Vector3*)cc_safe_alloc(1, 3 * sizeof(Vector3));
if (vertices) {
vertices[0].x = center_x;
vertices[0].y = center_y - radius;
vertices[0].z = 0;
vertices[1].x = center_x + (radius * 0.866f);
vertices[1].y = center_y + (radius * 0.5f);
vertices[1].z = 0;
vertices[2].x = center_x - (radius * 0.866f);
vertices[2].y = center_y + (radius * 0.5f);
vertices[2].z = 0;
Gradient* triGrad = (Gradient*)cc_safe_alloc(1, sizeof(Gradient));
triGrad->type = GRADIENT_TYPE_LINEAR;
triGrad->angle = M_PI_2;
triGrad->numStops = 2;
triGrad->stops = (ColorStop*)cc_safe_alloc(1, sizeof(ColorStop) * 2);
triGrad->stops[0].color = (ColorRGBA){255, 165, 0, 255};
triGrad->stops[0].position = 0.0f;
triGrad->stops[1].color = (ColorRGBA){255, 0, 0, 255};
triGrad->stops[1].position = 1.0f;
GraphicsCommand cmd_poly = {
.cmd = CMD_DRAW_POLYGON,
.vertices = vertices,
.numVertices = 3,
.gradientData = triGrad
};
QueueSend(g_graphics_queue, &cmd_poly, 0);
}
// --- STEP 3: UPDATE SCREEN ---
// Push the dirty area (Background + Triangle) to the LCD
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = ANIM_X, .y = ANIM_Y,
.w = ANIM_W, .h = ANIM_H
};
QueueSend(g_graphics_queue, &cmd_flush, 0);
vTaskDelay(pdMS_TO_TICKS(66));
}
}
/**
* @brief This task handles all drawing and screen updates.
* It draws the initial UI *once*, then enters an event loop
* to process partial updates from a queue.
*/
void graphics_task(void *arg)
{
// ==========================================================
// PART 1: INITIALIZATION (Runs ONCE)
// ==========================================================
FreeOSLogI(TAG, "Graphics task started.");
// --- INITIALIZE FREETYPE ---
esp_err_t ft_ok = initialize_freetype();
if (ft_ok != ESP_OK) {
FreeOSLogE(TAG, "Failed to initialize FreeType, deleting task.");
vTaskDelete(NULL); // Abort this task
return;
}
init_freetype_cache_system();
// --- Allocate the single framebuffer in PSRAM ---
int width = DISPLAY_HORIZONTAL_PIXELS;
int height = DISPLAY_VERTICAL_PIXELS;
int num_pixels = width * height;
uint8_t *display_buffer = (uint8_t *)heap_caps_malloc(num_pixels * 3, MALLOC_CAP_SPIRAM);
if (!display_buffer) {
FreeOSLogE(TAG, "Failed to allocate display buffer in PSRAM. Deleting task.");
vTaskDelete(NULL); // Delete this task
return;
}
FreeOSLogI(TAG, "Framebuffer allocated.");
// --- Initialize the CPUGraphics Framebuffer struct ---
fb = (Framebuffer){
.displayWidth = width,
.displayHeight = height,
.pixelData = display_buffer,
.colorMode = COLOR_MODE_BGR888
};
// ==========================================================
// PART 2: DRAW INITIAL SCREEN (Runs ONCE)
// ==========================================================
FreeOSLogI(TAG, "Drawing initial UI...");
// --- Define colors ---
ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
ColorRGBA red = {.r = 255, .g = 0, .b = 0, .a = 255};
ColorRGBA green_alpha = {.r = 0, .g = 255, .b = 0, .a = 128};
ColorRGBA white = {.r = 255, .g = 255, .b = 255, .a = 255};
// --- Draw initial UI to PSRAM buffer ---
clearFramebuffer(&fb, blue);
drawRectangleCFramebuffer(&fb, 50, 50, 100, 100, red, true);
drawRectangleCFramebuffer(&fb, 100, 100, 150, 150, green_alpha, true);
//renderText(&fb, ft_face, "Hello, World!", 50, 300, white, 24, NULL);
// Start Color: A deep, rich blue
ColorRGBA deep_blue = {.r = 0, .g = 50, .b = 150, .a = 255};
// End Color: A bright, vibrant cyan/aqua
ColorRGBA vibrant_aqua = {.r = 0, .g = 200, .b = 255, .a = 255};
// Define the positions (0.0 to 1.0)
ColorStop stops1[2] = {
{.color = deep_blue, .position = 0.0f},
{.color = vibrant_aqua, .position = 1.0f}
};
// --- 2. Define the Gradient Structure ---
Gradient aqua_gradient = {
.stops = stops1,
.numStops = 2,
.angle = M_PI_2 // 90 degrees (vertical gradient, from top to bottom)
// You could also try 0.0 for horizontal, or M_PI_4 for diagonal
};
// --- 3. Define the Draw Parameters (Full Screen) ---
int rect_x1 = 0;
int rect_y1 = 0;
int rect_w1 = 320;
int rect_h1 = 480;
// No transformation needed for a full-screen background
Matrix3x3 identity_mat1 = IdentityMatrix();
// --- 4. Call the Gradient Drawing Function ---
fillRectangleWithGradient(
&fb,
rect_x1,
rect_y1,
rect_w1,
rect_h1,
&aqua_gradient,
&identity_mat1 // No transformation
);
/*const float rotation_angle_deg = 45.0f;
const float rotation_angle_rad = rotation_angle_deg * (M_PI / 180.0f);
const int startX = 20;
const int startY = 20;
const int fontSize = 36;
ColorRGBA yellow = {.r = 255, .g = 255, .b = 0, .a = 255};
// --- 2. Build the Rotation Matrix (R) ---
Matrix3x3 R = RotationMatrix(rotation_angle_rad);
// --- 3. Build the Full Transform Matrix (T * R * -T) ---
// The matrix order for pivot rotation is: T(x, y) * R(angle) * T(-x, -y)
// First, translate the origin to the pivot point (startX, startY)
Matrix3x3 T_pivot = TranslationMatrix((float)startX, (float)startY);
// Second, apply the rotation (R)
Matrix3x3 T_R = MultiplyMatrix3x3(&T_pivot, &R);
// Third, translate the pivot point back to the origin
Matrix3x3 T_neg = TranslationMatrix((float)-startX, (float)-startY);
// Final Transform: (T_pivot * R) * T_neg
Matrix3x3 final_transform = MultiplyMatrix3x3(&T_R, &T_neg);
// --- 4. Render the Text ---
// Draw a small square at the pivot point for reference
drawRectangleCFramebuffer(&fb, startX - 3, startY - 3, 6, 6, yellow, true);
renderTextWithTransform(
&fb,
ft_face,
"Rotated by 45°",
startX,
startY,
yellow,
fontSize,
&final_transform, // Pass the composite rotation matrix
NULL
);*/
// 1. Load the PNG (and decode it into PSRAM)
const char* imgPath = "/spiflash/test.png";
ImageTexture* png_texture = load_image_from_file(imgPath);
if (!png_texture) {
return;
}
// 2. Define the target area for drawing
int draw_x = 20;
int draw_y = 80;
int draw_w = png_texture->width * 2; // Example: draw it scaled up by 2x
int draw_h = png_texture->height * 2;
// 3. Draw the scaled texture onto the framebuffer
// This uses your existing drawImageTexture function
//drawImageTexture(&fb, png_texture, draw_x, draw_y, png_texture->width, png_texture->height);
// 4. Free the temporary PSRAM texture buffer
heap_caps_free(png_texture->data);
free(png_texture);
ColorRGBA orange = {.r = 255, .g = 165, .b = 0, .a = 255}; // Opaque Orange
ColorRGBA aqua_alpha = {.r = 0, .g = 0, .b = 255, .a = 180}; // Semi-transparent Aqua
ColorRGBA dark_blue = {.r = 0, .g = 0, .b = 50, .a = 255}; // Background blue
/*drawRoundedRectangle_AntiAliasing(
&fb,
40, // X Start
100, // Y Start
200, // Width (200px)
150, // Height (150px)
orange, // Color: Opaque Orange
30, // Radius (30px corner radius)
true // Fill: True (solid rectangle)
);
// 3. Draw the second Anti-aliased Rounded Rectangle (Semi-transparent Aqua)
// This will overlap the first, and the alpha blending will occur on the edges
// and the overlapping area, showing a smooth transition.
drawRoundedRectangle_AntiAliasing(
&fb,
160, // X Start (overlaps the first rect)
180, // Y Start (overlaps the first rect)
100, // Width (200px)
150, // Height (150px)
aqua_alpha, // Color: Semi-transparent Aqua (A=180)
50, // Radius (50px corner radius)
true // Fill: True (solid rectangle)
);*/
ColorRGBA border_alpha = {.r = 0, .g = 0, .b = 0, .a = 200};
drawRoundedRectangle_AntiAliasing(
&fb,
17, // X Start
17, // Y Start
286, // Width
56, // Height
border_alpha, // Color: White
28, // Radius
false // Fill: False (Draws only the border)
);
drawRoundedRectangle_AntiAliasing(
&fb,
20, // X Start
20, // Y Start
280, // Width
50, // Height
white, // Color: White
25, // Radius
false // Fill: False (Draws only the border)
);
// --- Define Colors ---
ColorRGBA white_alpha = {.r = 255, .g = 255, .b = 255, .a = 150}; // Semi-transparent White
ColorRGBA yellow_opaque = {.r = 255, .g = 255, .b = 0, .a = 255}; // Opaque Yellow
ColorRGBA magenta_opaque = {.r = 255, .g = 0, .b = 255, .a = 255}; // Opaque Magenta
ColorRGBA cyan_alpha = {.r = 0, .g = 255, .b = 255, .a = 100}; // Highly transparent Cyan
// Get screen dimensions for cleaner coordinates
int w = 320;
int h = 480;
// --- 1. Draw a thin, opaque diagonal line (for precision test) ---
/*drawLineWithThickness(&fb,
10, 10, // Start near top-left
w - 10, h - 10, // End near bottom-right
magenta_opaque,
1); // Thickness: 1 pixel
// --- 2. Draw a thick, opaque horizontal line ---
drawLineWithThickness(&fb,
0, 400, // Start at left edge
w, 400, // End at right edge
yellow_opaque,
10); // Thickness: 10 pixels
// --- 3. Draw a semi-transparent line over the yellow one (blending test) ---
// This line should appear orange where it crosses the yellow line.
drawLineWithThickness(&fb,
w / 2, 350, // Start near center
w / 2, 450, // End in the yellow line area
white_alpha,
15); // Thickness: 15 pixels
// --- 4. Draw a highly transparent overlay (Alpha Blending Test) ---
// This line will barely change the color of the background/lines it crosses.
drawLineWithThickness(&fb,
50, h / 2,
w - 50, h / 2,
cyan_alpha,
5); // Thickness: 5 pixels
*/
// Gradient Stop 1: Starts as opaque dark pink/magenta
ColorRGBA stop1_color = {.r = 200, .g = 0, .b = 150, .a = 255};
// Gradient Stop 2: Transitions to opaque yellow/green
ColorRGBA stop2_color = {.r = 100, .g = 255, .b = 0, .a = 255};
// Define the positions (0.0 to 1.0)
ColorStop stops[2] = {
{.color = stop1_color, .position = 0.0f},
{.color = stop2_color, .position = 1.0f}
};
// --- 2. Define the Gradient Structure ---
Gradient my_gradient = {
.stops = stops,
.numStops = 2,
.angle = M_PI_4 // 45 degrees (M_PI_4 is a common constant for PI/4)
};
// --- 3. Define the Draw Parameters ---
int rect_x = 50;
int rect_y = 350;
int rect_w = 250;
int rect_h = 100;
int corner_radius = 20;
// Optional: Define a 2D rotation matrix (45 degrees clockwise rotation)
Matrix3x3 rotation_mat = RotationMatrix(-M_PI_4);
// Note: If you don't want the rectangle rotated, use an Identity Matrix:
Matrix3x3 identity_mat = IdentityMatrix();
// --- 4. Call the Gradient Drawing Function ---
// This draws a large, rotated, anti-aliased, rounded rectangle
/*fillRoundedRectangleWithGradient(
&fb,
rect_x,
rect_y,
rect_w,
rect_h,
&my_gradient,
corner_radius,
&identity_mat, // Passing the rotation matrix
true // AntiAlias: True
);*/
// --- Send the *entire* buffer to the LCD *once* ---
/*esp_err_t err = esp_lcd_panel_draw_bitmap(lcd_handle, 0, 0, width, height, display_buffer);
if (err != ESP_OK) {
FreeOSLogE(TAG, "Failed to draw initial bitmap! Error: %s", esp_err_to_name(err));
}
FreeOSLogI(TAG, "Initial UI drawn. GRAPHICS TASK READY FOR COMMANDS.");*/
GraphicsCommand cmd_update;
cmd_update.cmd = CMD_UPDATE_AREA;
cmd_update.x = 0;
cmd_update.y = 0; // A rough bounding box
cmd_update.w = 320;
cmd_update.h = 480; // A bit larger to be safe
updateArea(fb, cmd_update);
// ==========================================================
// PART 3: EVENT LOOP (Runs FOREVER)
// ==========================================================
GraphicsCommand cmd;
while (1) {
// Wait forever until a command arrives
if (xQueueReceive(g_graphics_queue, &cmd, QUEUE_MAX_DELAY) == pdTRUE) {
//FreeOSLogI(TAG, "Graphics task received command: %d", cmd.cmd);
switch (cmd.cmd) {
// --- Case 1: Draw a Rectangle ---
case CMD_DRAW_RECT:
FreeOSLogI(TAG, "Drawing rect to PSRAM");
drawRectangleCFramebuffer(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.color, true);
break;
case CMD_DRAW_FRAMEBUFFER: {
// Unpack the struct and call the reusable blitter!
// 'framebuffer' is your main OS screen buffer
drawFramebuffer(&fb, cmd.pixelBuffer, cmd.x, cmd.y);
break;
}
// --- Case 2: Draw Text ---
case CMD_DRAW_TEXT:
FreeOSLogI(TAG, "Drawing text to PSRAM");
renderText(&fb, ft_face, cmd.text, cmd.x, cmd.y, cmd.color, 12, NULL);
break;
// --- Case 3: Update the LCD (Chunked) ---
case CMD_DRAW_TEXT_BOX:
// --- Define a large block of text ---
/*const char *long_text = "The quick brown fox jumps over the lazy dog. "
"This text needs to demonstrate word-wrapping and line-breaking "
"within a confined area for our new scroll view. \n\n" // Explicit newline
"This final sentence should appear on a completely new line.";
ColorRGBA text_color = {.r = 255, .g = 255, .b = 255, .a = 255}; // White
// Define the Clipping Area (The Scroll View Window)
int clip_x = cmd.x;
int clip_y = cmd.y;
int clip_w = cmd.w; // Constrain the width to 200 pixels
int clip_h = cmd.h; // Constrain the height to 300 pixels
// --- 3. Define and Initialize the TextFormat Struct ---
TextFormat text_format = {
// Property 1: Center the text horizontally within the clip_w area
.alignment = TEXT_ALIGN_CENTER,
// Property 2: Never split words mid-line (will move "boundaries" to next line)
.wrapMode = TEXT_WRAP_MODE_WHOLE_WORD,
// Property 3: Add 10 extra pixels of space between the calculated line height
.lineSpacing = -2,
// Property 4: Use FreeType's kerning metrics for a polished look
.glyphSpacing = -1
};
// --- Render the Multi-Line Text ---
renderTextBox(
&fb,
ft_face,
long_text,
clip_x,
clip_y,
clip_w,
clip_h,
cmd.color,
16, // Font Size
&text_format
);*/
FreeOSLogI(TAG, "Drawing Text Box: %s", cmd.text);
/*renderTextBoxExtended(
&fb, ft_face, cmd.text,
cmd.x, cmd.y, cmd.w, cmd.h, // Logical Layout Box
// Pass the Clip Rects from the command (which come from View Hierarchy)
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
0, // ScrollY (Use this param if you have a specific scroll command)
cmd.color, cmd.fontSize, &cmd.textFormat
);*/
renderTextBox(
&fb,
ft_face,
cmd.text,
cmd.x, cmd.y, // Start X, Y
cmd.w, cmd.h, // Clip Width, Clip Height
cmd.color,
cmd.fontSize,
&cmd.textFormat // Pass the format struct
);
break;
case CMD_DRAW_TEXT_BOX_CACHED: {
const char* myFont = "/spiflash/proximaNovaRegular.ttf";
renderTextBoxExtendedCached(
&fb,
g_ftc_manager, // The Global Manager
g_ftc_image_cache, // The Global Image Cache
g_ftc_cmap_cache, // The Global CMap Cache
(FTC_FaceID)myFont, // Pass path as ID
cmd.text,
cmd.x, cmd.y, // Start X, Y
cmd.w, cmd.h, // Clip Width, Clip Height
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
0,
cmd.color,
cmd.fontSize,
&cmd.textFormat // Pass the format struct
);
break;
}
case CMD_UPDATE_AREA:
{
updateArea(fb, cmd);
break;
}
case CMD_CURSOR_SETUP:
{ // 1. Save the background under the new cursor location
// The x/y are passed in the generic cmd struct
FreeOSLogI(TAG, "calling function save_cursor_background");
save_cursor_background(&fb, cmd.x, cmd.y);
// 2. Draw the cursor immediately (first blink ON)
drawRectangleCFramebuffer(&fb, cmd.x, cmd.y, CURSOR_W, CURSOR_H, white, true);
// 3. Send the small update area to show the cursor
GraphicsCommand cmd_update = {.cmd = CMD_UPDATE_AREA, .x = cmd.x, .y = cmd.y, .w = CURSOR_W, .h = CURSOR_H};
//QueueSend(g_graphics_queue, &cmd_update, 0);
updateArea(fb, cmd_update);
xTaskCreatePinnedToCore(
cursor_blink_task, // New task function
"cursor_blink",
2048,
NULL,
4,
&g_cursor_blink_handle, // Capture the handle here
0
);
// 4. --- NEW: SEND THE NOTIFICATION TO START THE BLINKER ---
if (g_cursor_blink_handle != NULL) {
xTaskNotifyGive(g_cursor_blink_handle);
}
break;
}
case CMD_CURSOR_DRAW:
{ // Draw the cursor (uses its current global x/y location)
drawRectangleCFramebuffer(&fb, g_cursor_x, g_cursor_y, CURSOR_W, CURSOR_H, white, true);
// Send the update area
GraphicsCommand cmd_draw = {.cmd = CMD_UPDATE_AREA, .x = g_cursor_x, .y = g_cursor_y, .w = CURSOR_W, .h = CURSOR_H};
//QueueSend(g_graphics_queue, &cmd_draw, 0);
updateArea(fb, cmd_draw);
break;
}
case CMD_CURSOR_RESTORE:
{ // Restore the background (which also sends the update to the LCD)
restore_cursor_background(&fb);
break;
}
// Inside graphics_task's while(1) loop, switch (cmd.cmd):
case CMD_SCROLL_CONTENT:
{
int delta_y = cmd.y; // The scroll delta sent from updateTouch
int max_scroll = g_scroll_total_height - g_scroll_viewport_h;
// Ensure max_scroll is not negative (i.e., content fits entirely in viewport)
if (max_scroll < 0) max_scroll = 0;
// 1. Update the offset
g_scroll_offset_y += delta_y;
// 2. Clamp the offset (ensure it stays within boundaries)
if (g_scroll_offset_y < 0) {
g_scroll_offset_y = 0; // Cannot scroll past the top edge
} else if (g_scroll_offset_y > max_scroll) {
g_scroll_offset_y = max_scroll; // Cannot scroll past the bottom edge
}
// 3. Trigger a full redraw of the viewport
// This uses the existing CMD_LOAD_PAGE_1 logic to redraw the whole scene *once*.
GraphicsCommand cmd_redraw_viewport = {.cmd = CMD_LOAD_PAGE_1};
QueueSend(g_graphics_queue, &cmd_redraw_viewport, 0);
FreeOSLogI(TAG, "Scroll updated to: %d/%d", g_scroll_offset_y, max_scroll);
break;
}
// Inside graphics_task switch (cmd.cmd):
case CMD_LOAD_PAGE_1:{ // Triggered by CMD_SCROLL_CONTENT
FreeOSLogI(TAG, "Redrawing entire view due to scroll event.");
// 1. Redraw the entire scene, applying the new g_scroll_offset_y
draw_current_view(&fb, ft_face);
// 2. Send the full buffer update to the LCD
// (This is triggered by the scroll event, so we push the full screen.)
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(lcd_handle,
0, 0,
fb.displayWidth, fb.displayHeight,
fb.pixelData));
break;}
case CMD_DRAW_STAR: {
draw_star(&fb);
draw_complex_gear_gradient_example(&fb);
}
// Inside graphics_task switch ...
// Inside graphics_task while loop... switch(cmd.cmd) ...
case CMD_DRAW_ROUNDED_RECT:
{
// This function handles the AA and the color mode internally
/*drawRoundedRectangle_AntiAliasingExtended(
&fb,
cmd.x, cmd.y, cmd.w, cmd.h,
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
cmd.color,
cmd.radius,
cmd.fill
);
int64_t t1 = esp_timer_get_time();
drawRoundedRectangle_AntiAliasingExtended(
&fb,
cmd.x, cmd.y, cmd.w, cmd.h,
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
cmd.color,
cmd.radius,
cmd.fill
);
int64_t t2 = esp_timer_get_time();
drawRoundedRectangle_AntiAliasingOptimized(
&fb,
cmd.x, cmd.y, cmd.w, cmd.h,
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
cmd.color,
cmd.radius,
cmd.fill
);
int64_t t3 = esp_timer_get_time();*/
drawRoundedRectangle_AntiAliasingOptimized(
&fb,
cmd.x, cmd.y, cmd.w, cmd.h,
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
cmd.color,
cmd.radius,
cmd.fill
);
//int64_t t4 = esp_timer_get_time();
//FreeOSLogI(TAG, "SIMD (Fixed Pt) Time: %lld us", (t4 - t3));
//FreeOSLogI(TAG, "Speedup Factor: %.2fx", (float)(t2 - t1) / (float)(t4 - t3));
break;
}
case CMD_DRAW_GRADIENT_ROUNDED_RECT:
{
if (cmd.gradientData) {
fillRoundedRectangleWithGradientExtended(
&fb,
cmd.x, cmd.y, cmd.w, cmd.h,
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
cmd.gradientData,
cmd.radius,
NULL, // No transform for standard views
true // Anti-alias
);
printf("shadowRadius: %d", (int)cmd.radius);
// CRITICAL: Clean up the memory allocated by the main task
if (cmd.gradientData->stops) free(cmd.gradientData->stops);
free(cmd.gradientData);
}
break;
}
case CMD_DRAW_GRADIENT_RECT:
{
if (cmd.gradientData) {
// Use the optimized function for non-rounded rectangles
printf("CMD_DRAW_GRADIENT_RECT: %d %d %d %d %d %d %d %d", cmd.x, cmd.y, cmd.w, cmd.h, cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH);
Matrix3x3 identity_mat1 = IdentityMatrix();
/*fillRectangleWithGradientExtended(
&fb,
cmd.x, cmd.y, cmd.w, cmd.h,
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
cmd.gradientData,
&identity_mat1 // No transform for standard views
);*/
//fillRectangleWithGradientSIMD(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.gradientData);
fillRectangleWithGradientOptimized(
&fb,
cmd.x, cmd.y, cmd.w, cmd.h,
cmd.clipX, cmd.clipY, cmd.clipW, cmd.clipH,
cmd.gradientData
);
// Clean up memory
if (cmd.gradientData->stops) free(cmd.gradientData->stops);
free(cmd.gradientData);
}
break;
}
// --- NEW: DRAW IMAGE FROM FILE ---
case CMD_DRAW_IMAGE_FILE:
{
ESP_LOGD(TAG, "Drawing Image: %s", cmd.imagePath);
// Inside your Load function
ImageTexture* tex = NULL;
ImageFileType type = get_image_type_from_path(cmd.imagePath);
if (type == IMAGE_TYPE_PNG) {
tex = load_image_from_file(cmd.imagePath); // LibPNG
} else if (type == IMAGE_TYPE_JPEG) {
tex = load_jpeg_from_file(cmd.imagePath); // ESP-JPEG
}
if (tex) {
// 2. Draw (Scales to the view's width/height)
// Note: Your CCView frame is the destination size.
if (cmd.hasTransform) {
// A. Draw with Affine Transform (Rotation, Scale, Skew)
// The x, y, w, h here usually define the base rect before transformation
drawImageTextureWithTransform(
&fb,
tex,
cmd.x, cmd.y,
cmd.w, cmd.h,
&cmd.transform
);
} else {
// B. Standard Draw (Fast Axis-Aligned Blit)
drawImageTextureWithAlpha(
&fb,
tex,
cmd.x, cmd.y,
cmd.w, cmd.h, cmd.alpha
);
}
if (tex->data) heap_caps_free(tex->data);
free(tex);
}
break;
}
case CMD_DRAW_POLYGON: {
if (cmd.vertices && cmd.gradientData) {
// Use your existing scanline filler
fillPolygonWithGradient(
&fb,
cmd.vertices,
cmd.numVertices,
cmd.gradientData,
NULL, // No extra transform needed (handled in Bridge)
false // Anti-aliasing (set to true if you implement AA in polygon fill)
);
// --- CRITICAL CLEANUP ---
// 1. Free the vertices array allocated in the Bridge
free(cmd.vertices);
// 2. Free the gradient structure allocated in the Bridge
if (cmd.gradientData->stops) free(cmd.gradientData->stops);
free(cmd.gradientData);
}
break;
}
/*case CMD_DRAW_PIXEL_BUFFER: {
// User's custom struct handling
if (cmd.pixelBuffer) {
FreeOSLogI("GFX", "Received Pixel Buffer Command");
// cmd.pixelBuffer is (ColorRGBA*), fb->pixelData is (ColorRGBA*)
// Simple Memcpy because formats match
size_t bufferSize = cmd.w * cmd.h * sizeof(ColorRGBA);
memcpy(fb.pixelData, cmd.pixelBuffer, bufferSize);
}
break;
}*/
case CMD_DRAW_PIXEL_BUFFER: {
if (cmd.pixelBuffer) {
ColorRGBA *srcPixels = (ColorRGBA *)cmd.pixelBuffer;
if (fb.colorMode == COLOR_MODE_BGR888) {
uint8_t *dst = (uint8_t *)fb.pixelData;
for (int y = 0; y < cmd.h; y++) {
if ((cmd.y + y) >= fb.displayHeight) break;
for (int x = 0; x < cmd.w; x++) {
if ((cmd.x + x) >= fb.displayWidth) break;
ColorRGBA p = srcPixels[y * cmd.w + x];
int dstIdx = ((cmd.y + y) * fb.displayWidth + (cmd.x + x)) * 3;
// --- VIVID FILTER (Contrast Stretch) ---
// We assume video is 16-235 range. We stretch it to 0-255.
// Formula: (Color - 16) * 1.16
// Using Fast Integer Math: (Color - 16) * 296 / 256
int r = ((int)p.r - 16) * 296 >> 8;
int g = ((int)p.g - 16) * 296 >> 8;
int b = ((int)p.b - 16) * 296 >> 8;
// Clamp values to 0-255 (prevent overflow glitches)
if (r < 0) r = 0; else if (r > 255) r = 255;
if (g < 0) g = 0; else if (g > 255) g = 255;
if (b < 0) b = 0; else if (b > 255) b = 255;
// Write BGR (The order that made the cat Orange)
dst[dstIdx + 0] = (uint8_t)b; // Blue
dst[dstIdx + 1] = (uint8_t)g; // Green
dst[dstIdx + 2] = (uint8_t)r; // Red
}
}
}
}
break;
}
case CMD_ANIM_SAVE_BG:
{
// Capture the current screen state (clean background)
if (g_anim_backup_buffer) {
anim_save_background(&fb, g_anim_backup_buffer, cmd.x, cmd.y, cmd.w, cmd.h);
FreeOSLogI(TAG, "Animation background saved.");
}
break;
}
case CMD_ANIM_RESTORE_BG:
{
// Erase whatever was drawn by putting the clean background back
if (g_anim_backup_buffer) {
anim_restore_background(&fb, g_anim_backup_buffer, cmd.x, cmd.y, cmd.w, cmd.h);
}
break;
}
case CMD_UI_SAVE_TO_A: {
if (g_ui_backup_buffer_A) {
anim_save_background(&fb, g_ui_backup_buffer_A, cmd.x, cmd.y, cmd.w, cmd.h);
}
break;
}
case CMD_UI_RESTORE_FROM_A: {
if (g_ui_backup_buffer_A) {
anim_restore_background(&fb, g_ui_backup_buffer_A, cmd.x, cmd.y, cmd.w, cmd.h);
}
break;
}
case CMD_UI_SAVE_TO_B: {
if (g_ui_backup_buffer_B) {
anim_save_background(&fb, g_ui_backup_buffer_B, cmd.x, cmd.y, cmd.w, cmd.h);
}
break;
}
case CMD_UI_COPY_B_TO_A: {
if (g_ui_backup_buffer_A && g_ui_backup_buffer_B) {
// We just promoted the new icon to be the "Active" one.
// Copy the data we just saved in B over to A so we can restore it later.
memcpy(g_ui_backup_buffer_A, g_ui_backup_buffer_B, BUFFER_SIZE);
}
break;
}
case CMD_DRAW_ROUNDED_HAND: {
// x,y = Start Point
// w,h = End Point (We reused these fields)
// radius = Thickness
drawRoundedHand(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.radius, cmd.color);
break;
}
case CMD_DRAW_DAY_NIGHT_OVERLAY: {
drawDayNightOverlay(&fb, cmd.x, cmd.y, cmd.w, cmd.h, cmd.radius, cmd.fontSize);
break;
}
}
}
// There is NO vTaskDelay here.
// xQueueReceive handles all the waiting.
}
}