//
// ClockApp.c
//
//
// Created by Chris Galzerano on 2/19/26.
//
#include "ClockApp.h"
// --- CLOCK APP GLOBALS ---
ClockTab currentClockTab = TAB_CLOCK_FACE;
bool clock_app_running = false;
TaskHandle_t clock_anim_task_handle = NULL;
CCView* clockMainContainer = NULL;
CCView* tabContentView = NULL;
CCView* tabBar = NULL;
// --- WORLD CLOCK DATA ---
TimeZone zones[] = {
{"New York", -5}, {"London", 0}, {"Paris", 1},
{"Tokyo", 9}, {"Sydney", 11}, {"Los Angeles", -8},
{"Dubai", 4}, {"Hong Kong", 8}
};
int zonesCount = 8;
// --- WORLD CLOCK PAGINATION ---
int worldClockPage = 0;
const int CLOCK_ITEMS_PER_PAGE = 3;
// --- WORLD CLOCK GLOBALS ---
CCView* mapContainerView = NULL;
CCImageView* worldMapImageView = NULL;
CCView* dayNightOverlayView = NULL;
// --- ALARM DATA ---
int alarmHour = 7;
int alarmMinute = 30;
bool alarmEnabled = false;
// --- TIMER DATA ---
int timerSecondsTotal = 0;
int timerSecondsLeft = 0;
bool timerRunning = false;
uint64_t lastTimerTick = 0;
// --- Clock Touch State Globals ---
static uint64_t clockTouchStartTime = 0;
static int clockTouchStartX = 0;
static int clockTouchStartY = 0;
static bool clock_is_pressing = false;
static bool clock_long_press_fired = false;
CCPoint* getHandTip(int cx, int cy, float radius, float angleRad) {
// -PI/2 rotates the "0" angle from 3 o'clock to 12 o'clock
float adj = angleRad - M_PI_2;
return ccPoint(cx + cosf(adj)*radius, cy + sinf(adj)*radius);
}
void switch_clock_tab(ClockTab tab);
void clock_animation_task(void *pvParam) {
// 1. Wait for UI to settle
vTaskDelay(pdMS_TO_TICKS(500));
// 2. Define Dirty Area (The square containing the clock)
// Add 10px padding for safety
int dirty_x = CLOCK_CENTER_X - CLOCK_RADIUS - 10;
int dirty_y = CLOCK_CENTER_Y - CLOCK_RADIUS - 10;
int dirty_w = (CLOCK_RADIUS * 2) + 20;
int dirty_h = (CLOCK_RADIUS * 2) + 20;
// 3. Save the Static Background (Numbers, Black BG)
GraphicsCommand cmd_save = {
.cmd = CMD_ANIM_SAVE_BG,
.x = dirty_x, .y = dirty_y,
.w = dirty_w, .h = dirty_h
};
QueueSend(g_graphics_queue, &cmd_save, QUEUE_MAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(100)); // Allow save to complete
while (clock_app_running) {
// --- 1. HANDLE TIMER LOGIC (Always runs) ---
if (timerRunning && timerSecondsLeft > 0) {
uint64_t now = esp_timer_get_time() / 1000; // ms
if (now - lastTimerTick >= 1000) {
timerSecondsLeft--;
lastTimerTick = now;
// If we are looking at the timer tab, refresh UI
if (currentClockTab == TAB_TIMER) {
// Quick hack: just redraw the whole tab to update the label
// A better way is to update just the label text
// But since we are in a task, we need to trigger the main UI thread
// For now, assume we can trigger an update:
// QueueSend(ui_event_queue, &UPDATE_MSG, 0);
}
if (timerSecondsLeft == 0) {
timerRunning = false;
// TODO: Play Sound!
FreeOSLogI(TAG, "TIMER FINISHED!");
}
}
}
if (currentClockTab == TAB_CLOCK_FACE) {
// A. Get Time
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
float sec = timeinfo.tm_sec;
float min = timeinfo.tm_min;
float hour = timeinfo.tm_hour % 12;
// B. Restore Background (Erase old hands)
GraphicsCommand cmd_restore = {
.cmd = CMD_ANIM_RESTORE_BG,
.x = dirty_x, .y = dirty_y,
.w = dirty_w, .h = dirty_h
};
QueueSend(g_graphics_queue, &cmd_restore, 0);
// C. Calculate Angles
float secAng = (sec / 60.0f) * M_PI * 2;
float minAng = ((min + sec/60.0f) / 60.0f) * M_PI * 2;
float hourAng = ((hour + min/60.0f) / 12.0f) * M_PI * 2;
// D. Draw Hands (Order: Hour -> Minute -> Second)
// Hour Hand (Short, Thick)
CCPoint* hourTip = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, 50, hourAng);
GraphicsCommand cmd_h = {
.cmd = CMD_DRAW_ROUNDED_HAND,
.x = CLOCK_CENTER_X, .y = CLOCK_CENTER_Y,
.w = (int)hourTip->x, .h = (int)hourTip->y,
.radius = 6, // 6px Thick
.color = {200, 200, 200, 255} // Light Gray
};
QueueSend(g_graphics_queue, &cmd_h, 0);
// Minute Hand (Long, Medium)
CCPoint* minTip = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, 80, minAng);
GraphicsCommand cmd_m = {
.cmd = CMD_DRAW_ROUNDED_HAND,
.x = CLOCK_CENTER_X, .y = CLOCK_CENTER_Y,
.w = (int)minTip->x, .h = (int)minTip->y,
.radius = 4, // 4px Thick
.color = {255, 255, 255, 255} // White
};
QueueSend(g_graphics_queue, &cmd_m, 0);
// Second Hand (Long, Thin, Red)
CCPoint* secTip = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, 90, secAng);
GraphicsCommand cmd_s = {
.cmd = CMD_DRAW_ROUNDED_HAND,
.x = CLOCK_CENTER_X, .y = CLOCK_CENTER_Y,
.w = (int)secTip->x, .h = (int)secTip->y,
.radius = 2, // 2px Thick
.color = {0, 0, 255, 255} // Red (BGR format: Blue is 0, Red is 255)
};
QueueSend(g_graphics_queue, &cmd_s, 0);
// Center Dot (Cover the joints)
// (Optional: send a CMD_DRAW_CIRCLE_FILLED here if you have one exposed)
// E. Flush Screen
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = dirty_x, .y = dirty_y,
.w = dirty_w, .h = dirty_h
};
QueueSend(g_graphics_queue, &cmd_flush, 0);
}
if (currentClockTab == TAB_TIMER && timerRunning) {
// We can use a GraphicsCommand to overwrite the text area
// Or simpler: switch_clock_tab(TAB_TIMER) call from main loop?
// Since we are in a task, we shouldn't touch UI objects directly.
// Ideally, send a message to main loop.
}
// 30 FPS update is plenty for a smooth second hand
vTaskDelay(pdMS_TO_TICKS(33));
}
vTaskDelete(NULL);
}
// Helper to format time strings (HH:MM)
void formatTimeStr(char* buf, int h, int m, bool seconds) {
if (seconds) sprintf(buf, "%02d:%02d:%02d", h, m, timerSecondsLeft % 60);
else sprintf(buf, "%02d:%02d", h, m);
}
void setup_clock_app(void) {
FreeOSLogI(TAG, "Starting Clock App");
clock_app_running = true;
currentView = CurrentViewClock;
// 1. Create Main Container (Black Background)
clockMainContainer = viewWithFrame(ccRect(0, 0, 320, 480));
clockMainContainer->backgroundColor = color(0, 0, 0, 1);
mainWindowView = clockMainContainer;
// 2. Create Content Area (Top 420px)
tabContentView = viewWithFrame(ccRect(0, 0, 320, 420));
viewAddSubview(clockMainContainer, tabContentView);
// 3. Create Tab Bar (Bottom 60px)
tabBar = viewWithFrame(ccRect(0, 420, 320, 60));
tabBar->backgroundColor = color(0.15, 0.15, 0.15, 1);
viewAddSubview(clockMainContainer, tabBar);
// Add Tab Buttons
const char* titles[] = {"World", "Clock", "Alarm", "Timer"};
int tabW = 320 / 4;
for (int i=0; i<4; i++) {
CCView* tab = viewWithFrame(ccRect(i*tabW, 0, tabW, 60));
// Tab Label
CCLabel* lbl = labelWithFrame(ccRect(0, 35, tabW, 20));
lbl->text = ccs(titles[i]);
lbl->fontSize = 12;
lbl->textColor = (i == TAB_CLOCK_FACE) ? color(1, 0.6, 0, 1) : color(0.6, 0.6, 0.6, 1);
lbl->textAlignment = CCTextAlignmentCenter;
viewAddSubview(tab, lbl);
// Simple Circle Icon (Placeholder for real icons)
CCView* icon = viewWithFrame(ccRect((tabW-24)/2, 8, 24, 24));
icon->backgroundColor = (i == TAB_CLOCK_FACE) ? color(1, 0.6, 0, 1) : color(0.4, 0.4, 0.4, 1);
icon->layer->cornerRadius = 12;
viewAddSubview(tab, icon);
viewAddSubview(tabBar, tab);
}
// 4. Start the Animation Task
//xTaskCreatePinnedToCore(clock_animation_task, "clock_anim", 4096, NULL, 5, &clock_anim_task_handle, 1);
// 5. Load Initial Tab
switch_clock_tab(TAB_CLOCK_FACE);
}
void draw_analog_face_ui(void); // Forward Declaration
void draw_analog_face_ui(void) {
// Draw the static parts of the clock (Numbers, Ticks)
// We do this via Views so they persist
// Draw Dial Ticks
for (int i=0; i<12; i++) {
float angle = (i / 12.0f) * M_PI * 2;
CCPoint* start = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_RADIUS - 10, angle);
CCPoint* end = getHandTip(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_RADIUS, angle);
// Since we don't have a "Line View", we use small thin Views
// Or simpler: Just rely on the Animation Task to draw the static face too?
// BETTER: Let's put 4 main labels for 12, 3, 6, 9
}
CCLabel* l12 = labelWithFrame(ccRect(CLOCK_CENTER_X-20, CLOCK_CENTER_Y-CLOCK_RADIUS+10, 40, 30));
l12->text = ccs("12"); l12->fontSize=24; l12->textAlignment=CCTextAlignmentCenter; l12->textColor=color(1,1,1,1);
viewAddSubview(tabContentView, l12);
CCLabel* l6 = labelWithFrame(ccRect(CLOCK_CENTER_X-20, CLOCK_CENTER_Y+CLOCK_RADIUS-40, 40, 30));
l6->text = ccs("6"); l6->fontSize=24; l6->textAlignment=CCTextAlignmentCenter; l6->textColor=color(1,1,1,1);
viewAddSubview(tabContentView, l6);
CCLabel* l3 = labelWithFrame(ccRect(CLOCK_CENTER_X+CLOCK_RADIUS-40, CLOCK_CENTER_Y-15, 40, 30));
l3->text = ccs("3"); l3->fontSize=24; l3->textAlignment=CCTextAlignmentCenter; l3->textColor=color(1,1,1,1);
viewAddSubview(tabContentView, l3);
CCLabel* l9 = labelWithFrame(ccRect(CLOCK_CENTER_X-CLOCK_RADIUS, CLOCK_CENTER_Y-15, 40, 30));
l9->text = ccs("9"); l9->fontSize=24; l9->textAlignment=CCTextAlignmentCenter; l9->textColor=color(1,1,1,1);
viewAddSubview(tabContentView, l9);
}
/*void handle_clock_touch(int x, int y, int type) {
// Only handle "Touch Up" (Release)
if (type != 1) return;
// Check Tab Bar Area (Bottom 60px)
if (y > 420) {
int tabWidth = 320 / 4;
int index = x / tabWidth;
if (index >= 0 && index <= 3 && index != currentClockTab) {
switch_clock_tab((ClockTab)index);
// Re-trigger the BG Save if we went back to Clock Face
// (Because switching tabs likely destroyed the screen content)
if (index == TAB_CLOCK_FACE) {
// The animation task needs a signal to re-save the background
// A simple way is to delete and recreate the task,
// or use a flag/semaphore.
// For simplicity, recreation is safest:
if (clock_anim_task_handle) vTaskDelete(clock_anim_task_handle);
xTaskCreatePinnedToCore(clock_animation_task, "clock_anim", 4096, NULL, 5, &clock_anim_task_handle, 1);
}
}
}
CCView* row = find_subview_at_point(mainWindowView, x, y);
// Inside handle_clock_touch...
if (currentClockTab == TAB_WORLD_CLOCK) {
if (row->tag == TAG_CLOCK_PREV) {
if (worldClockPage > 0) {
worldClockPage--;
switch_clock_tab(TAB_WORLD_CLOCK); // Refresh
}
}
else if (row->tag == TAG_CLOCK_NEXT) {
// Safety check to ensure we don't go past end
if ((worldClockPage + 1) * CLOCK_ITEMS_PER_PAGE < zonesCount) {
worldClockPage++;
switch_clock_tab(TAB_WORLD_CLOCK); // Refresh
}
}
}
}*/
void draw_world_clock_ui(void) {
// 1. Container & Base Image
// We keep these so the UI system knows the map exists
mapContainerView = viewWithFrame(ccRect(MAP_X, MAP_Y, MAP_W, MAP_H));
viewAddSubview(tabContentView, mapContainerView);
worldMapImageView = imageViewWithFrame(ccRect(0, 0, MAP_W, MAP_H));
worldMapImageView->image = imageWithFile(ccs("/spiflash/clockmap.png"));
viewAddSubview(mapContainerView, worldMapImageView);
// 2. Calculate Day/Night Curve (Run Math ONCE)
time_t now;
struct tm timeinfo;
time(&now);
gmtime_r(&now, &timeinfo); // UTC Time
// A. Time Parameter (0.0 - 1.0)
float currentMinuteUTC = (timeinfo.tm_hour * 60.0f) + timeinfo.tm_min;
// Offset by 0.5 to center Noon on the map (assuming Greenwich centered map)
float time01 = fmodf((currentMinuteUTC / 1440.0f) + 0.5f, 1.0f);
// B. Season Parameter (-1.0 - 1.0)
float dayOfYear = timeinfo.tm_yday;
float seasonStrength = -cosf((dayOfYear / 365.0f) * M_PI * 2.0f);
// 3. Send Draw Command
// We queue this to happen *after* the current UI update cycle.
// This ensures the base map image is drawn first, and we draw the darkness on top.
GraphicsCommand cmd_overlay = {
.cmd = CMD_DRAW_DAY_NIGHT_OVERLAY,
.x = MAP_X, .y = MAP_Y, .w = MAP_W, .h = MAP_H,
.radius = time01, // Time
.fontSize = seasonStrength // Season
};
// Send to queue.
// If your UI draws very fast, you might need a small vTaskDelay before sending this,
// or ensure your graphics_task processes this command AFTER the standard view rendering.
QueueSend(g_graphics_queue, &cmd_overlay, 0);
// Force a specific area update for the map just in case
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = MAP_X, .y = MAP_Y, .w = MAP_W, .h = MAP_H
};
QueueSend(g_graphics_queue, &cmd_flush, 0);
int startY = MAP_Y+MAP_H;
int rowH = 60;
int screenW = 320;
// 2. Calculate Range
int totalItems = zonesCount;
int startIndex = worldClockPage * CLOCK_ITEMS_PER_PAGE;
int endIndex = startIndex + CLOCK_ITEMS_PER_PAGE;
if (endIndex > totalItems) endIndex = totalItems;
// 3. Draw Rows
int currentY = startY;
for (int i = startIndex; i < endIndex; i++) {
// Calculate Local Time
int h = (timeinfo.tm_hour + zones[i].offset);
if (h < 0) h += 24;
if (h >= 24) h -= 24;
// Row Container
CCView* row = viewWithFrame(ccRect(0, currentY, screenW, rowH));
// Zebra striping
row->backgroundColor = (i % 2 == 0) ? color(0.12, 0.12, 0.12, 1) : color(0.08, 0.08, 0.08, 1);
// City Name
CCLabel* lblCity = labelWithFrame(ccRect(20, 0, 200, rowH));
lblCity->text = ccs(zones[i].city);
lblCity->fontSize = 20;
lblCity->textColor = color(1, 1, 1, 1);
lblCity->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(row, lblCity);
// Time String
char timeBuf[16];
sprintf(timeBuf, "%02d:%02d", h, timeinfo.tm_min);
CCLabel* lblTime = labelWithFrame(ccRect(220, 0, 80, rowH));
lblTime->text = ccs(timeBuf);
lblTime->fontSize = 24;
lblTime->textColor = color(0.8, 0.8, 0.8, 1);
lblTime->textAlignment = CCTextAlignmentRight;
lblTime->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(row, lblTime);
viewAddSubview(tabContentView, row);
currentY += rowH;
}
// 4. Draw Pagination Buttons (Bottom of list)
int btnY = currentY + 10;
int btnW = 100;
int btnH = 40;
// PREV BUTTON
if (worldClockPage > 0) {
CCView* btnPrev = viewWithFrame(ccRect(20, btnY, btnW, btnH));
btnPrev->backgroundColor = color(0.3, 0.3, 0.4, 1.0);
btnPrev->layer->cornerRadius = 5;
btnPrev->tag = TAG_CLOCK_PREV;
CCLabel* lbl = labelWithFrame(ccRect(0,0,btnW,btnH));
lbl->text = ccs("<< Back");
lbl->textColor = color(1,1,1,1);
lbl->textAlignment = CCTextAlignmentCenter;
lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(btnPrev, lbl);
viewAddSubview(tabContentView, btnPrev);
}
// NEXT BUTTON
if (endIndex < totalItems) {
CCView* btnNext = viewWithFrame(ccRect(screenW - 20 - btnW, btnY, btnW, btnH));
btnNext->backgroundColor = color(0.2, 0.5, 0.2, 1.0);
btnNext->layer->cornerRadius = 5;
btnNext->tag = TAG_CLOCK_NEXT;
CCLabel* lbl = labelWithFrame(ccRect(0,0,btnW,btnH));
lbl->text = ccs("Next >>");
lbl->textColor = color(1,1,1,1);
lbl->textAlignment = CCTextAlignmentCenter;
lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(btnNext, lbl);
viewAddSubview(tabContentView, btnNext);
}
}
void draw_alarm_ui(void) {
CCLabel* lblTime = labelWithFrame(ccRect(0, 80, 320, 80));
lblTime->text = ccs("test");
lblTime->fontSize = 60;
lblTime->textAlignment = CCTextAlignmentCenter;
lblTime->textColor = alarmEnabled ? color(1,1,1,1) : color(0.5,0.5,0.5,1);
viewAddSubview(tabContentView, lblTime);
// 1. Alarm Time Display (Big Center Text)
char buf[16];
formatTimeStr(buf, alarmHour, alarmMinute, false);
// 2. Control Buttons (Row of 4 buttons)
int btnY = 200;
int btnW = 60;
int gap = 15;
int startX = (320 - (4*btnW + 3*gap)) / 2;
// Helper to make button
void makeBtn(int idx, char* txt, int tag) {
CCView* btn = viewWithFrame(ccRect(startX + idx*(btnW+gap), btnY, btnW, 50));
btn->backgroundColor = color(0.2, 0.2, 0.2, 1);
btn->layer->cornerRadius = 10;
btn->tag = tag;
CCLabel* l = labelWithFrame(ccRect(0,0,btnW,50));
l->text = ccs(txt); l->textAlignment = CCTextAlignmentCenter; l->textVerticalAlignment = CCTextVerticalAlignmentCenter;
l->textColor = color(1,1,1,1);
viewAddSubview(btn, l);
viewAddSubview(tabContentView, btn);
}
makeBtn(0, "H-", TAG_ALARM_H_DOWN);
makeBtn(1, "H+", TAG_ALARM_H_UP);
makeBtn(2, "M-", TAG_ALARM_M_DOWN);
makeBtn(3, "M+", TAG_ALARM_M_UP);
// 3. Enable Toggle
CCView* btnToggle = viewWithFrame(ccRect(100, 300, 120, 50));
btnToggle->backgroundColor = alarmEnabled ? color(0, 0.6, 0, 1) : color(0.6, 0.2, 0.2, 1);
btnToggle->layer->cornerRadius = 25;
btnToggle->tag = TAG_ALARM_TOGGLE;
CCLabel* lTog = labelWithFrame(ccRect(0,0,120,50));
lTog->text = ccs(alarmEnabled ? "ON" : "OFF");
lTog->textAlignment = CCTextAlignmentCenter; lTog->textVerticalAlignment = CCTextVerticalAlignmentCenter;
lTog->textColor = color(1,1,1,1);
viewAddSubview(btnToggle, lTog);
viewAddSubview(tabContentView, btnToggle);
}
void draw_timer_ui(void) {
FreeOSLogI("draw_timer_ui", "Starting draw_timer_ui App");
// 1. Countdown Display
int m = timerSecondsLeft / 60;
int s = timerSecondsLeft % 60;
char buf[16];
sprintf(buf, "%02d:%02d", m, s);
FreeOSLogI("draw_timer_ui", "1Starting draw_timer_ui App");
CCLabel* lblTimer = labelWithFrame(ccRect(0, 80, 320, 80));
lblTimer->text = ccs(buf);
lblTimer->fontSize = 24;
lblTimer->textAlignment = CCTextAlignmentCenter;
lblTimer->textColor = timerRunning ? color(1, 0.8, 0, 1) : color(1,1,1,1);
viewAddSubview(tabContentView, lblTimer);
FreeOSLogI("draw_timer_ui", "2Starting draw_timer_ui App");
// 2. Add Time Button (+1 Min)
CCView* btnAdd = viewWithFrame(ccRect(110, 180, 100, 40));
btnAdd->backgroundColor = color(0.2, 0.2, 0.2, 1);
btnAdd->tag = TAG_TIMER_ADD_1MIN;
CCLabel* lAdd = labelWithFrame(ccRect(0,0,100,40));
lAdd->text = ccs("+1 Min"); lAdd->textAlignment=CCTextAlignmentCenter; lAdd->textVerticalAlignment=CCTextVerticalAlignmentCenter;
lAdd->textColor = color(1,1,1,1);
viewAddSubview(btnAdd, lAdd);
viewAddSubview(tabContentView, btnAdd);
FreeOSLogI("draw_timer_ui", "3Starting draw_timer_ui App");
// 3. Start/Stop Button
CCView* btnStart = viewWithFrame(ccRect(40, 260, 100, 60));
btnStart->backgroundColor = timerRunning ? color(0.6, 0.2, 0, 1) : color(0, 0.6, 0, 1);
btnStart->tag = TAG_TIMER_START;
CCLabel* lStart = labelWithFrame(ccRect(0,0,100,60));
lStart->text = ccs(timerRunning ? "Stop" : "Start"); lStart->textAlignment=CCTextAlignmentCenter; lStart->textVerticalAlignment=CCTextVerticalAlignmentCenter;
lStart->textColor = color(1,1,1,1);
viewAddSubview(btnStart, lStart);
viewAddSubview(tabContentView, btnStart);
FreeOSLogI("draw_timer_ui", "4Starting draw_timer_ui App");
// 4. Reset Button
CCView* btnReset = viewWithFrame(ccRect(180, 260, 100, 60));
btnReset->backgroundColor = color(0.3, 0.3, 0.3, 1);
btnReset->tag = TAG_TIMER_RESET;
CCLabel* lReset = labelWithFrame(ccRect(0,0,100,60));
lReset->text = ccs("Reset"); lReset->textAlignment=CCTextAlignmentCenter; lReset->textVerticalAlignment=CCTextVerticalAlignmentCenter;
lReset->textColor = color(1,1,1,1);
viewAddSubview(btnReset, lReset);
viewAddSubview(tabContentView, btnReset);
}
void handle_clock_touch(int x, int y, int type) {
// ==========================================
// FINGER IS TOUCHING THE SCREEN (DOWN / HELD)
// ==========================================
if (type == 1) {
if (!clock_is_pressing) {
// First frame of touch down
clock_is_pressing = true;
clock_long_press_fired = false;
clockTouchStartTime = esp_timer_get_time() / 1000;
clockTouchStartX = x;
clockTouchStartY = y;
} else {
// Finger is held down or dragging
if (abs(x - clockTouchStartX) > 15 || abs(y - clockTouchStartY) > 15) {
clockTouchStartTime = 0; // Dragged too far, cancel the tap
}
// --- REAL-TIME LONG PRESS CHECK ---
if (clockTouchStartTime != 0 && !clock_long_press_fired) {
uint64_t now = esp_timer_get_time() / 1000;
if ((now - clockTouchStartTime) >= 1000) { // 1 SECOND LONG PRESS
clock_long_press_fired = true;
// FUTURE HOOK: Add Long Press actions here!
// e.g., Deleting an alarm, resetting a specific world clock, etc.
FreeOSLogI("ClockApp", "Long press detected at %d, %d", clockTouchStartX, clockTouchStartY);
}
}
}
return;
}
// ==========================================
// FINGER LIFTED (RELEASED) - SHORT TAP
// ==========================================
if (type == 0) {
if (!clock_is_pressing) return; // Ignore if we are already released
clock_is_pressing = false; // Reset state for next tap
// If they scrolled or the long press already fired, DO NOTHING on release!
if (clockTouchStartTime == 0 || clock_long_press_fired) {
clock_long_press_fired = false;
return;
}
// --- If we reach here, it was a valid SHORT TAP! ---
// CRITICAL: Use the saved X and Y from when the finger was actually down!
int tapX = clockTouchStartX;
int tapY = clockTouchStartY;
// 1. Tab Bar Logic
if (tapY > 420) {
int tabWidth = 320 / 4;
int index = tapX / tabWidth;
if (index >= 0 && index <= 3 && index != currentClockTab) {
printf("switch_clock_tab %d\n", index);
switch_clock_tab((ClockTab)index);
update_full_ui(); // Push changes to screen
}
return;
}
// 2. Tab Specific Logic
// Helper to find clicked view by tag
CCView* clicked = find_subview_at_point(tabContentView, tapX, tapY);
if (!clicked) return;
// --- THE FIX: Prevent Labels from Stealing Touches ---
// If we tapped the text inside the button, 'clicked' is the Label (tag 0).
// Grab the tag from the Label's parent (the Button) instead!
// --- THE FIX: Prevent Labels from Stealing Touches ---
int tag = clicked->tag;
if (tag == 0 && clicked->superview) {
// Cast the raw superview pointer to your known CCView typedef
// to bypass the compiler's "undefined struct" quirk!
CCView* parentView = (CCView*)clicked->superview;
tag = parentView->tag;
}
if (currentClockTab == TAB_ALARM) {
bool ui_changed = false; // Track if we actually hit a valid button
if (tag == TAG_ALARM_H_UP) { alarmHour = (alarmHour + 1) % 24; ui_changed = true; }
else if (tag == TAG_ALARM_H_DOWN) { alarmHour = (alarmHour - 1 + 24) % 24; ui_changed = true; }
else if (tag == TAG_ALARM_M_UP) { alarmMinute = (alarmMinute + 5) % 60; ui_changed = true; }
else if (tag == TAG_ALARM_M_DOWN) { alarmMinute = (alarmMinute - 5 + 60) % 60; ui_changed = true; }
else if (tag == TAG_ALARM_TOGGLE) { alarmEnabled = !alarmEnabled; ui_changed = true; }
// --- THE FIX: Only rebuild if a valid button was pressed ---
if (ui_changed) {
switch_clock_tab(TAB_ALARM);
update_full_ui(); // Push to screen!
}
}
else if (currentClockTab == TAB_TIMER) {
if (tag == TAG_TIMER_ADD_1MIN) {
timerSecondsLeft += 60;
switch_clock_tab(TAB_TIMER);
update_full_ui();
}
else if (tag == TAG_TIMER_RESET) {
timerRunning = false;
timerSecondsLeft = 0;
switch_clock_tab(TAB_TIMER);
update_full_ui();
}
else if (tag == TAG_TIMER_START) {
timerRunning = !timerRunning;
if (timerRunning) lastTimerTick = esp_timer_get_time() / 1000;
switch_clock_tab(TAB_TIMER);
update_full_ui();
}
}
else if (currentClockTab == TAB_WORLD_CLOCK) {
if (tag == TAG_CLOCK_PREV) {
if (worldClockPage > 0) {
worldClockPage--;
switch_clock_tab(TAB_WORLD_CLOCK);
update_full_ui();
}
}
else if (tag == TAG_CLOCK_NEXT) {
if ((worldClockPage + 1) * CLOCK_ITEMS_PER_PAGE < zonesCount) {
worldClockPage++;
switch_clock_tab(TAB_WORLD_CLOCK);
update_full_ui();
}
}
}
}
}
void switch_clock_tab(ClockTab tab) {
currentClockTab = tab;
// Clear previous content
viewRemoveFromSuperview(tabContentView);
freeViewHierarchy(tabContentView);
tabContentView = viewWithFrame(ccRect(0, 0, 320, 420));
viewAddSubview(clockMainContainer, tabContentView);
// Draw new content
if (tab == TAB_WORLD_CLOCK) draw_world_clock_ui();
else if (tab == TAB_CLOCK_FACE) {
draw_analog_face_ui();
if (clock_anim_task_handle) vTaskDelete(clock_anim_task_handle);
xTaskCreatePinnedToCore(clock_animation_task, "clock_anim", 4096, NULL, 5, &clock_anim_task_handle, 1);
}
else if (tab == TAB_ALARM) draw_alarm_ui();
else if (tab == TAB_TIMER) draw_timer_ui();
// Trigger update
update_full_ui();
}