From 64a638d7da4a79ef4beac8da812b5d3feeeb57bf Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Wed, 13 Aug 2025 00:13:26 -0700 Subject: [PATCH] Add date/time input module --- applications/services/gui/application.fam | 1 + .../services/gui/modules/date_time_input.c | 347 ++++++++++++++++++ .../services/gui/modules/date_time_input.h | 70 ++++ targets/f7/api_symbols.csv | 6 + 4 files changed, 424 insertions(+) create mode 100644 applications/services/gui/modules/date_time_input.c create mode 100644 applications/services/gui/modules/date_time_input.h diff --git a/applications/services/gui/application.fam b/applications/services/gui/application.fam index b24f5bbb6..4eb09ff25 100644 --- a/applications/services/gui/application.fam +++ b/applications/services/gui/application.fam @@ -35,5 +35,6 @@ App( "modules/submenu.h", "modules/widget_elements/widget_element.h", "modules/empty_screen.h", + "modules/date_time_input.h", ], ) diff --git a/applications/services/gui/modules/date_time_input.c b/applications/services/gui/modules/date_time_input.c new file mode 100644 index 000000000..63f648edb --- /dev/null +++ b/applications/services/gui/modules/date_time_input.c @@ -0,0 +1,347 @@ +#include "date_time_input.h" +#include +#include + +#define get_state(m, r, c) \ + ((m)->row == (r) && (m)->column == (c) ? \ + ((m)->editing ? EditStateActiveEditing : EditStateActive) : \ + EditStateNone) + +#define ROW_0_Y (10) +#define ROW_0_H (20) + +#define ROW_1_Y (40) +#define ROW_1_H (20) + +#define ROW_COUNT 2 +#define COLUMN_COUNT 3 + +struct DateTimeInput { + View* view; +}; + +typedef struct { + const char* header; + DateTime* datetime; + + uint8_t row; + uint8_t column; + bool editing; + + DateTimeChangedCallback changed_callback; + DateTimeDoneCallback done_callback; + void* callback_context; +} DateTimeInputModel; + +typedef enum { + EditStateNone, + EditStateActive, + EditStateActiveEditing, +} EditState; + +static inline void date_time_input_cleanup_date(DateTime* dt) { + uint8_t day_per_month = + datetime_get_days_per_month(datetime_is_leap_year(dt->year), dt->month); + if(dt->day > day_per_month) { + dt->day = day_per_month; + } +} +static inline void date_time_input_draw_block( + Canvas* canvas, + int32_t x, + int32_t y, + size_t w, + size_t h, + Font font, + EditState state, + const char* text) { + furi_assert(canvas); + furi_assert(text); + + canvas_set_color(canvas, ColorBlack); + if(state != EditStateNone) { + if(state == EditStateActiveEditing) { + canvas_draw_icon(canvas, x + w / 2 - 2, y - 1 - 3, &I_SmallArrowUp_3x5); + canvas_draw_icon(canvas, x + w / 2 - 2, y + h + 1, &I_SmallArrowDown_3x5); + } + canvas_draw_rbox(canvas, x, y, w, h, 1); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_draw_rframe(canvas, x, y, w, h, 1); + } + + canvas_set_font(canvas, font); + canvas_draw_str_aligned(canvas, x + w / 2, y + h / 2, AlignCenter, AlignCenter, text); + if(state != EditStateNone) { + canvas_set_color(canvas, ColorBlack); + } +} + +static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputModel* model) { + furi_check(model->datetime); + + char buffer[64]; + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 0, ROW_1_Y - 2, " H H M M S S"); + canvas_set_font(canvas, FontPrimary); + + snprintf(buffer, sizeof(buffer), "%02u", model->datetime->hour); + date_time_input_draw_block( + canvas, 30, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 0), buffer); + canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7, 2, 2); + canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->datetime->minute); + date_time_input_draw_block( + canvas, 64, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 1), buffer); + canvas_draw_box(canvas, 94, ROW_1_Y + ROW_1_H - 7, 2, 2); + canvas_draw_box(canvas, 94, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->datetime->second); + date_time_input_draw_block( + canvas, 98, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 2), buffer); +} + +static void date_time_input_draw_date_callback(Canvas* canvas, DateTimeInputModel* model) { + furi_check(model->datetime); + + char buffer[64]; + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 0, ROW_0_Y - 2, " Y Y Y Y M M D D"); + canvas_set_font(canvas, FontPrimary); + snprintf(buffer, sizeof(buffer), "%04u", model->datetime->year); + date_time_input_draw_block( + canvas, 2, ROW_0_Y, 56, ROW_0_H, FontBigNumbers, get_state(model, 0, 0), buffer); + snprintf(buffer, sizeof(buffer), "%02u", model->datetime->month); + date_time_input_draw_block( + canvas, 64, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 1), buffer); + canvas_draw_box(canvas, 64 - 5, ROW_0_Y + (ROW_0_H / 2), 4, 2); + snprintf(buffer, sizeof(buffer), "%02u", model->datetime->day); + date_time_input_draw_block( + canvas, 98, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 2), buffer); + canvas_draw_box(canvas, 98 - 5, ROW_0_Y + (ROW_0_H / 2), 4, 2); +} + +static void date_time_input_view_draw_callback(Canvas* canvas, void* _model) { + DateTimeInputModel* model = _model; + canvas_clear(canvas); + date_time_input_draw_time_callback(canvas, model); + date_time_input_draw_date_callback(canvas, model); +} + +static bool date_time_input_navigation_callback(InputEvent* event, DateTimeInputModel* model) { + if(event->key == InputKeyUp) { + if(model->row > 0) model->row--; + } else if(event->key == InputKeyDown) { + if(model->row < ROW_COUNT - 1) model->row++; + } else if(event->key == InputKeyOk) { + model->editing = !model->editing; + } else if(event->key == InputKeyRight) { + if(model->column < COLUMN_COUNT - 1) model->column++; + } else if(event->key == InputKeyLeft) { + if(model->column > 0) model->column--; + } else if(event->key == InputKeyBack && model->editing) { + model->editing = false; + } else if(event->key == InputKeyBack && model->done_callback) { + model->done_callback(model->callback_context); + } else { + return false; + } + + return true; +} + +static bool date_time_input_time_callback(InputEvent* event, DateTimeInputModel* model) { + furi_check(model->datetime); + + if(event->key == InputKeyUp) { + if(model->column == 0) { + model->datetime->hour++; + model->datetime->hour = model->datetime->hour % 24; + } else if(model->column == 1) { + model->datetime->minute++; + model->datetime->minute = model->datetime->minute % 60; + } else if(model->column == 2) { + model->datetime->second++; + model->datetime->second = model->datetime->second % 60; + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->datetime->hour > 0) { + model->datetime->hour--; + } else { + model->datetime->hour = 23; + } + model->datetime->hour = model->datetime->hour % 24; + } else if(model->column == 1) { + if(model->datetime->minute > 0) { + model->datetime->minute--; + } else { + model->datetime->minute = 59; + } + model->datetime->minute = model->datetime->minute % 60; + } else if(model->column == 2) { + if(model->datetime->second > 0) { + model->datetime->second--; + } else { + model->datetime->second = 59; + } + model->datetime->second = model->datetime->second % 60; + } else { + furi_crash(); + } + } else { + return date_time_input_navigation_callback(event, model); + } + + return true; +} + +static bool date_time_input_date_callback(InputEvent* event, DateTimeInputModel* model) { + furi_check(model->datetime); + + if(event->key == InputKeyUp) { + if(model->column == 0) { + if(model->datetime->year < 2099) { + model->datetime->year++; + } + } else if(model->column == 1) { + if(model->datetime->month < 12) { + model->datetime->month++; + } + } else if(model->column == 2) { + if(model->datetime->day < 31) model->datetime->day++; + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->datetime->year > 1980) { + model->datetime->year--; + } + } else if(model->column == 1) { + if(model->datetime->month > 1) { + model->datetime->month--; + } + } else if(model->column == 2) { + if(model->datetime->day > 1) { + model->datetime->day--; + } + } else { + furi_crash(); + } + } else { + return date_time_input_navigation_callback(event, model); + } + + date_time_input_cleanup_date(model->datetime); + + return true; +} + +static bool date_time_input_view_input_callback(InputEvent* event, void* context) { + DateTimeInput* instance = context; + bool consumed = false; + + with_view_model( + instance->view, + DateTimeInputModel * model, + { + if(event->type == InputTypeShort || event->type == InputTypeRepeat) { + if(model->editing) { + if(model->row == 0) { + consumed = date_time_input_date_callback(event, model); + } else if(model->row == 1) { + consumed = date_time_input_time_callback(event, model); + } else { + furi_crash(); + } + + if(model->changed_callback) { + model->changed_callback(model->callback_context); + } + } else { + consumed = date_time_input_navigation_callback(event, model); + } + } + }, + true); + + return consumed; +} + +/** Reset all input-related data in model + * + * @param model The model + */ +static void date_time_input_reset_model_input_data(DateTimeInputModel* model) { + model->row = 0; + model->column = 0; + + model->datetime = NULL; +} + +DateTimeInput* date_time_input_alloc(void) { + DateTimeInput* date_time_input = malloc(sizeof(DateTimeInput)); + date_time_input->view = view_alloc(); + view_allocate_model(date_time_input->view, ViewModelTypeLocking, sizeof(DateTimeInputModel)); + view_set_context(date_time_input->view, date_time_input); + view_set_draw_callback(date_time_input->view, date_time_input_view_draw_callback); + view_set_input_callback(date_time_input->view, date_time_input_view_input_callback); + + with_view_model( + date_time_input->view, + DateTimeInputModel * model, + { + model->header = ""; + model->changed_callback = NULL; + model->callback_context = NULL; + date_time_input_reset_model_input_data(model); + }, + true); + + return date_time_input; +} + +void date_time_input_free(DateTimeInput* date_time_input) { + furi_check(date_time_input); + view_free(date_time_input->view); + free(date_time_input); +} + +View* date_time_input_get_view(DateTimeInput* date_time_input) { + furi_check(date_time_input); + return date_time_input->view; +} + +void date_time_input_set_result_callback( + DateTimeInput* date_time_input, + DateTimeChangedCallback changed_callback, + DateTimeDoneCallback done_callback, + void* callback_context, + DateTime* current_datetime) { + furi_check(date_time_input); + + with_view_model( + date_time_input->view, + DateTimeInputModel * model, + { + date_time_input_reset_model_input_data(model); + model->changed_callback = changed_callback; + model->done_callback = done_callback; + model->callback_context = callback_context; + model->datetime = current_datetime; + }, + true); +} + +void date_time_input_set_header_text(DateTimeInput* date_time_input, const char* text) { + furi_check(date_time_input); + + with_view_model( + date_time_input->view, DateTimeInputModel * model, { model->header = text; }, true); +} diff --git a/applications/services/gui/modules/date_time_input.h b/applications/services/gui/modules/date_time_input.h new file mode 100644 index 000000000..3efc6c7a1 --- /dev/null +++ b/applications/services/gui/modules/date_time_input.h @@ -0,0 +1,70 @@ +/** + * @file date_time_input.h + * GUI: DateTimeInput view module API + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Date/time input anonymous structure */ +typedef struct DateTimeInput DateTimeInput; + +/** callback that is executed on value change */ +typedef void (*DateTimeChangedCallback)(void* context); + +/** callback that is executed on back button press */ +typedef void (*DateTimeDoneCallback)(void* context); + +/** Allocate and initialize date/time input + * + * This screen used to input a date and time + * + * @return DateTimeInput instance + */ +DateTimeInput* date_time_input_alloc(void); + +/** Deinitialize and free date/time input + * + * @param date_time_input Date/time input instance + */ +void date_time_input_free(DateTimeInput* date_time_input); + +/** Get date/time input view + * + * @param date_time_input Date/time input instance + * + * @return View instance that can be used for embedding + */ +View* date_time_input_get_view(DateTimeInput* date_time_input); + +/** Set date/time input result callback + * + * @param date_time_input date/time input instance + * @param changed_callback changed callback fn + * @param done_callback finished callback fn + * @param callback_context callback context + * @param datetime date/time value + */ +void date_time_input_set_result_callback( + DateTimeInput* date_time_input, + DateTimeChangedCallback changed_callback, + DateTimeDoneCallback done_callback, + void* callback_context, + DateTime* datetime); + +/** Set date/time input header text + * + * @param date_time_input date/time input instance + * @param text text to be shown + */ +void date_time_input_set_header_text(DateTimeInput* date_time_input, const char* text); + +#ifdef __cplusplus +} +#endif diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index d142a6374..1ea105e00 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -14,6 +14,7 @@ Header,+,applications/services/gui/icon_i.h,, Header,+,applications/services/gui/modules/button_menu.h,, Header,+,applications/services/gui/modules/button_panel.h,, Header,+,applications/services/gui/modules/byte_input.h,, +Header,+,applications/services/gui/modules/date_time_input.h,, Header,+,applications/services/gui/modules/dialog_ex.h,, Header,+,applications/services/gui/modules/empty_screen.h,, Header,+,applications/services/gui/modules/file_browser.h,, @@ -925,6 +926,11 @@ Function,+,crypto1_reset,void,Crypto1* Function,+,crypto1_word,uint32_t,"Crypto1*, uint32_t, int" Function,-,ctermid,char*,char* Function,-,cuserid,char*,char* +Function,+,date_time_input_alloc,DateTimeInput*, +Function,+,date_time_input_free,void,DateTimeInput* +Function,+,date_time_input_get_view,View*,DateTimeInput* +Function,+,date_time_input_set_header_text,void,"DateTimeInput*, const char*" +Function,+,date_time_input_set_result_callback,void,"DateTimeInput*, DateTimeChangedCallback, DateTimeDoneCallback, void*, DateTime*" Function,+,datetime_datetime_to_timestamp,uint32_t,DateTime* Function,+,datetime_get_days_per_month,uint8_t,"_Bool, uint8_t" Function,+,datetime_get_days_per_year,uint16_t,uint16_t