diff --git a/apps_source_code/paint/CHANGELOG.md b/apps_source_code/paint/CHANGELOG.md new file mode 100644 index 00000000..a61773a5 --- /dev/null +++ b/apps_source_code/paint/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +## 1.4 + +- Fix the pen getting stuck down after pressing Ok together with a direction key: drawing mode was toggled by a long Ok press with no visual indication, and a short Ok press could not lift it +- Replace the hidden drawing flag with three modes cycled by a long Ok press: Move, Draw and Erase; the cursor shape shows the active mode (small dot, full cell, hollow frame) +- A short Ok press toggles the cell under the cursor in Move mode and lifts the pen or eraser in Draw and Erase modes +- Hold a direction key to keep moving (with the pen down this draws or erases a line); previously held keys did nothing +- Save the canvas on exit and restore it on the next start; a failed save shows a storage error message instead of silently losing the drawing +- Hold Back to clear the canvas and lift the pen +- Start with the cursor in the center of the canvas instead of the top-left corner + +## 1.3 + +- Version bump for catalog compatibility (no functional changes) + +## 1.2 + +- Version bump for catalog compatibility (no functional changes) + +## 1.1 + +- Version bump for catalog compatibility (no functional changes) + +## 1.0 + +- Initial release: draw on a 32x16 board with a dot cursor, hold Ok to toggle continuous drawing, hold Back to clear the screen diff --git a/apps_source_code/paint/application.fam b/apps_source_code/paint/application.fam index 2dc1bd3c..9f2aacf3 100644 --- a/apps_source_code/paint/application.fam +++ b/apps_source_code/paint/application.fam @@ -11,6 +11,6 @@ App( fap_category="Games", fap_author="@n-o-T-I-n-s-a-n-e", fap_weburl="https://github.com/n-o-T-I-n-s-a-n-e", - fap_version="1.3", - fap_description="A basic Paint app, Click Ok to draw dot, hold Ok to enable drawing continuously, hold Back to clear the screen", + fap_version="1.4", + fap_description="A basic Paint app. Ok: toggle dot or lift the pen, hold Ok: cycle Move/Draw/Erase mode, hold Back: clear, Back: save and exit. The canvas is restored on the next start", ) diff --git a/apps_source_code/paint/paint.c b/apps_source_code/paint/paint.c index 0d4124b0..74732316 100644 --- a/apps_source_code/paint/paint.c +++ b/apps_source_code/paint/paint.c @@ -1,10 +1,29 @@ #include -#include +#include #include #include -#include -#include -#include // Header-file for boolean data-type. +#include +#include +#include + +#define BOARD_WIDTH 32 +#define BOARD_HEIGHT 16 +#define CELL_SIZE 4 + +#define PAINT_SAVE_PATH APP_DATA_PATH("canvas.bin") +#define PAINT_SAVE_TMP_PATH APP_DATA_PATH("canvas.bin.tmp") + +//saved_struct tags the file with this magic + version and a checksum, so a +//foreign, truncated or corrupt save is rejected on load +#define PAINT_SAVE_MAGIC 0x50 +#define PAINT_SAVE_VERSION 1 + +typedef enum { + PaintModeMove, //cursor moves without touching the board + PaintModeDraw, //cursor paints every cell it visits + PaintModeErase, //cursor clears every cell it visits + PaintModeCount, +} PaintMode; typedef struct selected_position { int x; @@ -14,8 +33,8 @@ typedef struct selected_position { typedef struct { FuriMutex* mutex; selected_position selected; - bool board[32][16]; - bool isDrawing; + bool board[BOARD_WIDTH][BOARD_HEIGHT]; + PaintMode mode; } PaintData; void paint_draw_callback(Canvas* canvas, void* ctx) { @@ -25,23 +44,34 @@ void paint_draw_callback(Canvas* canvas, void* ctx) { canvas_clear(canvas); canvas_set_color(canvas, ColorBlack); - //draw the canvas(32x16) on screen(144x64) using 4x4 tiles - for(int y = 0; y < 16; y++) { - for(int x = 0; x < 32; x++) { + //draw the board(32x16) on screen(128x64) using 4x4 tiles + for(int y = 0; y < BOARD_HEIGHT; y++) { + for(int x = 0; x < BOARD_WIDTH; x++) { if(paint_state->board[x][y]) { - canvas_draw_box(canvas, x * 4, y * 4, 4, 4); + canvas_draw_box(canvas, x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); } } } - //draw cursor as a 4x4 black box with a 2x2 white box inside - canvas_set_color(canvas, ColorBlack); - canvas_draw_box(canvas, paint_state->selected.x * 4, paint_state->selected.y * 4, 4, 4); - canvas_set_color(canvas, ColorWhite); - canvas_draw_box( - canvas, paint_state->selected.x * 4 + 1, paint_state->selected.y * 4 + 1, 2, 2); + //the cursor is drawn in XOR so it stays visible on both painted and + //empty cells; its shape shows the active mode + int cursor_x = paint_state->selected.x * CELL_SIZE; + int cursor_y = paint_state->selected.y * CELL_SIZE; + canvas_set_color(canvas, ColorXOR); + switch(paint_state->mode) { + case PaintModeMove: //small dot: the pen is up + canvas_draw_box(canvas, cursor_x + 1, cursor_y + 1, 2, 2); + break; + case PaintModeDraw: //full cell: the pen is down + canvas_draw_box(canvas, cursor_x, cursor_y, CELL_SIZE, CELL_SIZE); + break; + case PaintModeErase: //hollow frame: the eraser is down + canvas_draw_frame(canvas, cursor_x, cursor_y, CELL_SIZE, CELL_SIZE); + break; + default: + break; + } - //release the mutex furi_mutex_release(paint_state->mutex); } @@ -51,16 +81,82 @@ void paint_input_callback(InputEvent* input_event, void* ctx) { furi_message_queue_put(event_queue, input_event, FuriWaitForever); } +//paint or erase the cell under the cursor according to the active mode +static void paint_apply_mode(PaintData* paint_state) { + if(paint_state->mode == PaintModeDraw) { + paint_state->board[paint_state->selected.x][paint_state->selected.y] = true; + } else if(paint_state->mode == PaintModeErase) { + paint_state->board[paint_state->selected.x][paint_state->selected.y] = false; + } +} + +static void paint_move_cursor(PaintData* paint_state, int dx, int dy) { + //apply the mode to the cell being left as well, so a stroke + //includes the cell it started on + paint_apply_mode(paint_state); + paint_state->selected.x = CLAMP(paint_state->selected.x + dx, BOARD_WIDTH - 1, 0); + paint_state->selected.y = CLAMP(paint_state->selected.y + dy, BOARD_HEIGHT - 1, 0); + paint_apply_mode(paint_state); +} + +//react to the initial press and keep moving while the key is held; +//InputTypeLong is included so movement does not stall between the +//press and the first repeat +static bool paint_is_move_event(InputType type) { + return type == InputTypePress || type == InputTypeLong || type == InputTypeRepeat; +} + +static void paint_load(PaintData* paint_state) { + //a missing or corrupt save just leaves the initialized empty canvas + if(!saved_struct_load( + PAINT_SAVE_PATH, + paint_state->board, + sizeof(paint_state->board), + PAINT_SAVE_MAGIC, + PAINT_SAVE_VERSION)) { + memset(paint_state->board, 0, sizeof(paint_state->board)); + } +} + +static bool paint_save(const PaintData* paint_state) { + //write to a temporary file first, then rename it over the real save, so a + //failed write cannot destroy the previously saved canvas + if(!saved_struct_save( + PAINT_SAVE_TMP_PATH, + paint_state->board, + sizeof(paint_state->board), + PAINT_SAVE_MAGIC, + PAINT_SAVE_VERSION)) { + FURI_LOG_E("paint", "cannot write the canvas"); + return false; + } + + Storage* storage = furi_record_open(RECORD_STORAGE); + FS_Error error = storage_common_rename(storage, PAINT_SAVE_TMP_PATH, PAINT_SAVE_PATH); + furi_record_close(RECORD_STORAGE); + if(error != FSE_OK) { + FURI_LOG_E("paint", "cannot save the canvas: %s", storage_error_get_desc(error)); + return false; + } + return true; +} + int32_t paint_app(void* p) { UNUSED(p); FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); PaintData* paint_state = malloc(sizeof(PaintData)); + memset(paint_state->board, 0, sizeof(paint_state->board)); + paint_state->selected.x = BOARD_WIDTH / 2; + paint_state->selected.y = BOARD_HEIGHT / 2; + paint_state->mode = PaintModeMove; + paint_load(paint_state); paint_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); if(!paint_state->mutex) { FURI_LOG_E("paint", "cannot create mutex\r\n"); free(paint_state); + furi_message_queue_free(event_queue); return -1; } @@ -73,80 +169,73 @@ int32_t paint_app(void* p) { Gui* gui = furi_record_open(RECORD_GUI); gui_add_view_port(gui, view_port, GuiLayerFullscreen); - //NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); - InputEvent event; + bool running = true; - while(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) { - //break out of the loop if the back key is pressed - if(event.type == InputTypeShort && event.key == InputKeyBack) { - break; - } + while(running && + furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) { + furi_mutex_acquire(paint_state->mutex, FuriWaitForever); - //check the key pressed and change x and y accordingly - if(event.type == InputTypeShort) { - switch(event.key) { - case InputKeyUp: - paint_state->selected.y -= 1; - break; - case InputKeyDown: - paint_state->selected.y += 1; - break; - case InputKeyLeft: - paint_state->selected.x -= 1; - break; - case InputKeyRight: - paint_state->selected.x += 1; - break; - case InputKeyOk: - paint_state->board[paint_state->selected.x][paint_state->selected.y] = - !paint_state->board[paint_state->selected.x][paint_state->selected.y]; - break; - - default: - break; - } - - //check if cursor position is out of bounds and reset it to the closest position - if(paint_state->selected.x < 0) { - paint_state->selected.x = 0; - } - if(paint_state->selected.x > 31) { - paint_state->selected.x = 31; - } - if(paint_state->selected.y < 0) { - paint_state->selected.y = 0; - } - if(paint_state->selected.y > 15) { - paint_state->selected.y = 15; - } - if(paint_state->isDrawing == true) { - paint_state->board[paint_state->selected.x][paint_state->selected.y] = true; - } - view_port_update(view_port); - } - if(event.key == InputKeyBack && event.type == InputTypeLong) { - paint_state->board[1][1] = true; - for(int y = 0; y < 16; y++) { - for(int x = 0; x < 32; x++) { - paint_state->board[x][y] = false; + switch(event.key) { + case InputKeyUp: + if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, 0, -1); + break; + case InputKeyDown: + if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, 0, 1); + break; + case InputKeyLeft: + if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, -1, 0); + break; + case InputKeyRight: + if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, 1, 0); + break; + case InputKeyOk: + if(event.type == InputTypeShort) { + if(paint_state->mode == PaintModeMove) { + //toggle the cell under the cursor + paint_state->board[paint_state->selected.x][paint_state->selected.y] = + !paint_state->board[paint_state->selected.x][paint_state->selected.y]; + } else { + //a short press lifts the pen or eraser + paint_state->mode = PaintModeMove; } + } else if(event.type == InputTypeLong) { + paint_state->mode = (PaintMode)((paint_state->mode + 1) % PaintModeCount); } - view_port_update(view_port); - } - if(event.key == InputKeyOk && event.type == InputTypeLong) { - paint_state->isDrawing = !paint_state->isDrawing; - paint_state->board[paint_state->selected.x][paint_state->selected.y] = true; - view_port_update(view_port); + break; + case InputKeyBack: + if(event.type == InputTypeLong) { + //start over with an empty canvas and the pen lifted + memset(paint_state->board, 0, sizeof(paint_state->board)); + paint_state->mode = PaintModeMove; + } else if(event.type == InputTypeShort) { + running = false; + } + break; + default: + break; } + + furi_mutex_release(paint_state->mutex); + view_port_update(view_port); } + //tear down the viewport before the (potentially slow) save so input + //events can no longer pile up in a queue nobody drains anymore gui_remove_view_port(gui, view_port); view_port_free(view_port); furi_message_queue_free(event_queue); - furi_mutex_free(paint_state->mutex); - furi_record_close(RECORD_NOTIFICATION); furi_record_close(RECORD_GUI); + + if(!paint_save(paint_state)) { + //the drawing could not be written out - tell the user instead of + //silently losing it + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + dialog_message_show_storage_error(dialogs, "Cannot save\nthe canvas"); + furi_record_close(RECORD_DIALOGS); + } + + furi_mutex_free(paint_state->mutex); free(paint_state); return 0;