Skip to content

Commit 64a638d

Browse files
committed
Add date/time input module
1 parent d1c3b0b commit 64a638d

File tree

4 files changed

+424
-0
lines changed

4 files changed

+424
-0
lines changed

applications/services/gui/application.fam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ App(
3535
"modules/submenu.h",
3636
"modules/widget_elements/widget_element.h",
3737
"modules/empty_screen.h",
38+
"modules/date_time_input.h",
3839
],
3940
)
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
#include "date_time_input.h"
2+
#include <furi.h>
3+
#include <assets_icons.h>
4+
5+
#define get_state(m, r, c) \
6+
((m)->row == (r) && (m)->column == (c) ? \
7+
((m)->editing ? EditStateActiveEditing : EditStateActive) : \
8+
EditStateNone)
9+
10+
#define ROW_0_Y (10)
11+
#define ROW_0_H (20)
12+
13+
#define ROW_1_Y (40)
14+
#define ROW_1_H (20)
15+
16+
#define ROW_COUNT 2
17+
#define COLUMN_COUNT 3
18+
19+
struct DateTimeInput {
20+
View* view;
21+
};
22+
23+
typedef struct {
24+
const char* header;
25+
DateTime* datetime;
26+
27+
uint8_t row;
28+
uint8_t column;
29+
bool editing;
30+
31+
DateTimeChangedCallback changed_callback;
32+
DateTimeDoneCallback done_callback;
33+
void* callback_context;
34+
} DateTimeInputModel;
35+
36+
typedef enum {
37+
EditStateNone,
38+
EditStateActive,
39+
EditStateActiveEditing,
40+
} EditState;
41+
42+
static inline void date_time_input_cleanup_date(DateTime* dt) {
43+
uint8_t day_per_month =
44+
datetime_get_days_per_month(datetime_is_leap_year(dt->year), dt->month);
45+
if(dt->day > day_per_month) {
46+
dt->day = day_per_month;
47+
}
48+
}
49+
static inline void date_time_input_draw_block(
50+
Canvas* canvas,
51+
int32_t x,
52+
int32_t y,
53+
size_t w,
54+
size_t h,
55+
Font font,
56+
EditState state,
57+
const char* text) {
58+
furi_assert(canvas);
59+
furi_assert(text);
60+
61+
canvas_set_color(canvas, ColorBlack);
62+
if(state != EditStateNone) {
63+
if(state == EditStateActiveEditing) {
64+
canvas_draw_icon(canvas, x + w / 2 - 2, y - 1 - 3, &I_SmallArrowUp_3x5);
65+
canvas_draw_icon(canvas, x + w / 2 - 2, y + h + 1, &I_SmallArrowDown_3x5);
66+
}
67+
canvas_draw_rbox(canvas, x, y, w, h, 1);
68+
canvas_set_color(canvas, ColorWhite);
69+
} else {
70+
canvas_draw_rframe(canvas, x, y, w, h, 1);
71+
}
72+
73+
canvas_set_font(canvas, font);
74+
canvas_draw_str_aligned(canvas, x + w / 2, y + h / 2, AlignCenter, AlignCenter, text);
75+
if(state != EditStateNone) {
76+
canvas_set_color(canvas, ColorBlack);
77+
}
78+
}
79+
80+
static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputModel* model) {
81+
furi_check(model->datetime);
82+
83+
char buffer[64];
84+
85+
canvas_set_font(canvas, FontSecondary);
86+
canvas_draw_str(canvas, 0, ROW_1_Y - 2, " H H M M S S");
87+
canvas_set_font(canvas, FontPrimary);
88+
89+
snprintf(buffer, sizeof(buffer), "%02u", model->datetime->hour);
90+
date_time_input_draw_block(
91+
canvas, 30, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 0), buffer);
92+
canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7, 2, 2);
93+
canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2);
94+
95+
snprintf(buffer, sizeof(buffer), "%02u", model->datetime->minute);
96+
date_time_input_draw_block(
97+
canvas, 64, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 1), buffer);
98+
canvas_draw_box(canvas, 94, ROW_1_Y + ROW_1_H - 7, 2, 2);
99+
canvas_draw_box(canvas, 94, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2);
100+
101+
snprintf(buffer, sizeof(buffer), "%02u", model->datetime->second);
102+
date_time_input_draw_block(
103+
canvas, 98, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 2), buffer);
104+
}
105+
106+
static void date_time_input_draw_date_callback(Canvas* canvas, DateTimeInputModel* model) {
107+
furi_check(model->datetime);
108+
109+
char buffer[64];
110+
111+
canvas_set_font(canvas, FontSecondary);
112+
canvas_draw_str(canvas, 0, ROW_0_Y - 2, " Y Y Y Y M M D D");
113+
canvas_set_font(canvas, FontPrimary);
114+
snprintf(buffer, sizeof(buffer), "%04u", model->datetime->year);
115+
date_time_input_draw_block(
116+
canvas, 2, ROW_0_Y, 56, ROW_0_H, FontBigNumbers, get_state(model, 0, 0), buffer);
117+
snprintf(buffer, sizeof(buffer), "%02u", model->datetime->month);
118+
date_time_input_draw_block(
119+
canvas, 64, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 1), buffer);
120+
canvas_draw_box(canvas, 64 - 5, ROW_0_Y + (ROW_0_H / 2), 4, 2);
121+
snprintf(buffer, sizeof(buffer), "%02u", model->datetime->day);
122+
date_time_input_draw_block(
123+
canvas, 98, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 2), buffer);
124+
canvas_draw_box(canvas, 98 - 5, ROW_0_Y + (ROW_0_H / 2), 4, 2);
125+
}
126+
127+
static void date_time_input_view_draw_callback(Canvas* canvas, void* _model) {
128+
DateTimeInputModel* model = _model;
129+
canvas_clear(canvas);
130+
date_time_input_draw_time_callback(canvas, model);
131+
date_time_input_draw_date_callback(canvas, model);
132+
}
133+
134+
static bool date_time_input_navigation_callback(InputEvent* event, DateTimeInputModel* model) {
135+
if(event->key == InputKeyUp) {
136+
if(model->row > 0) model->row--;
137+
} else if(event->key == InputKeyDown) {
138+
if(model->row < ROW_COUNT - 1) model->row++;
139+
} else if(event->key == InputKeyOk) {
140+
model->editing = !model->editing;
141+
} else if(event->key == InputKeyRight) {
142+
if(model->column < COLUMN_COUNT - 1) model->column++;
143+
} else if(event->key == InputKeyLeft) {
144+
if(model->column > 0) model->column--;
145+
} else if(event->key == InputKeyBack && model->editing) {
146+
model->editing = false;
147+
} else if(event->key == InputKeyBack && model->done_callback) {
148+
model->done_callback(model->callback_context);
149+
} else {
150+
return false;
151+
}
152+
153+
return true;
154+
}
155+
156+
static bool date_time_input_time_callback(InputEvent* event, DateTimeInputModel* model) {
157+
furi_check(model->datetime);
158+
159+
if(event->key == InputKeyUp) {
160+
if(model->column == 0) {
161+
model->datetime->hour++;
162+
model->datetime->hour = model->datetime->hour % 24;
163+
} else if(model->column == 1) {
164+
model->datetime->minute++;
165+
model->datetime->minute = model->datetime->minute % 60;
166+
} else if(model->column == 2) {
167+
model->datetime->second++;
168+
model->datetime->second = model->datetime->second % 60;
169+
} else {
170+
furi_crash();
171+
}
172+
} else if(event->key == InputKeyDown) {
173+
if(model->column == 0) {
174+
if(model->datetime->hour > 0) {
175+
model->datetime->hour--;
176+
} else {
177+
model->datetime->hour = 23;
178+
}
179+
model->datetime->hour = model->datetime->hour % 24;
180+
} else if(model->column == 1) {
181+
if(model->datetime->minute > 0) {
182+
model->datetime->minute--;
183+
} else {
184+
model->datetime->minute = 59;
185+
}
186+
model->datetime->minute = model->datetime->minute % 60;
187+
} else if(model->column == 2) {
188+
if(model->datetime->second > 0) {
189+
model->datetime->second--;
190+
} else {
191+
model->datetime->second = 59;
192+
}
193+
model->datetime->second = model->datetime->second % 60;
194+
} else {
195+
furi_crash();
196+
}
197+
} else {
198+
return date_time_input_navigation_callback(event, model);
199+
}
200+
201+
return true;
202+
}
203+
204+
static bool date_time_input_date_callback(InputEvent* event, DateTimeInputModel* model) {
205+
furi_check(model->datetime);
206+
207+
if(event->key == InputKeyUp) {
208+
if(model->column == 0) {
209+
if(model->datetime->year < 2099) {
210+
model->datetime->year++;
211+
}
212+
} else if(model->column == 1) {
213+
if(model->datetime->month < 12) {
214+
model->datetime->month++;
215+
}
216+
} else if(model->column == 2) {
217+
if(model->datetime->day < 31) model->datetime->day++;
218+
} else {
219+
furi_crash();
220+
}
221+
} else if(event->key == InputKeyDown) {
222+
if(model->column == 0) {
223+
if(model->datetime->year > 1980) {
224+
model->datetime->year--;
225+
}
226+
} else if(model->column == 1) {
227+
if(model->datetime->month > 1) {
228+
model->datetime->month--;
229+
}
230+
} else if(model->column == 2) {
231+
if(model->datetime->day > 1) {
232+
model->datetime->day--;
233+
}
234+
} else {
235+
furi_crash();
236+
}
237+
} else {
238+
return date_time_input_navigation_callback(event, model);
239+
}
240+
241+
date_time_input_cleanup_date(model->datetime);
242+
243+
return true;
244+
}
245+
246+
static bool date_time_input_view_input_callback(InputEvent* event, void* context) {
247+
DateTimeInput* instance = context;
248+
bool consumed = false;
249+
250+
with_view_model(
251+
instance->view,
252+
DateTimeInputModel * model,
253+
{
254+
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
255+
if(model->editing) {
256+
if(model->row == 0) {
257+
consumed = date_time_input_date_callback(event, model);
258+
} else if(model->row == 1) {
259+
consumed = date_time_input_time_callback(event, model);
260+
} else {
261+
furi_crash();
262+
}
263+
264+
if(model->changed_callback) {
265+
model->changed_callback(model->callback_context);
266+
}
267+
} else {
268+
consumed = date_time_input_navigation_callback(event, model);
269+
}
270+
}
271+
},
272+
true);
273+
274+
return consumed;
275+
}
276+
277+
/** Reset all input-related data in model
278+
*
279+
* @param model The model
280+
*/
281+
static void date_time_input_reset_model_input_data(DateTimeInputModel* model) {
282+
model->row = 0;
283+
model->column = 0;
284+
285+
model->datetime = NULL;
286+
}
287+
288+
DateTimeInput* date_time_input_alloc(void) {
289+
DateTimeInput* date_time_input = malloc(sizeof(DateTimeInput));
290+
date_time_input->view = view_alloc();
291+
view_allocate_model(date_time_input->view, ViewModelTypeLocking, sizeof(DateTimeInputModel));
292+
view_set_context(date_time_input->view, date_time_input);
293+
view_set_draw_callback(date_time_input->view, date_time_input_view_draw_callback);
294+
view_set_input_callback(date_time_input->view, date_time_input_view_input_callback);
295+
296+
with_view_model(
297+
date_time_input->view,
298+
DateTimeInputModel * model,
299+
{
300+
model->header = "";
301+
model->changed_callback = NULL;
302+
model->callback_context = NULL;
303+
date_time_input_reset_model_input_data(model);
304+
},
305+
true);
306+
307+
return date_time_input;
308+
}
309+
310+
void date_time_input_free(DateTimeInput* date_time_input) {
311+
furi_check(date_time_input);
312+
view_free(date_time_input->view);
313+
free(date_time_input);
314+
}
315+
316+
View* date_time_input_get_view(DateTimeInput* date_time_input) {
317+
furi_check(date_time_input);
318+
return date_time_input->view;
319+
}
320+
321+
void date_time_input_set_result_callback(
322+
DateTimeInput* date_time_input,
323+
DateTimeChangedCallback changed_callback,
324+
DateTimeDoneCallback done_callback,
325+
void* callback_context,
326+
DateTime* current_datetime) {
327+
furi_check(date_time_input);
328+
329+
with_view_model(
330+
date_time_input->view,
331+
DateTimeInputModel * model,
332+
{
333+
date_time_input_reset_model_input_data(model);
334+
model->changed_callback = changed_callback;
335+
model->done_callback = done_callback;
336+
model->callback_context = callback_context;
337+
model->datetime = current_datetime;
338+
},
339+
true);
340+
}
341+
342+
void date_time_input_set_header_text(DateTimeInput* date_time_input, const char* text) {
343+
furi_check(date_time_input);
344+
345+
with_view_model(
346+
date_time_input->view, DateTimeInputModel * model, { model->header = text; }, true);
347+
}

0 commit comments

Comments
 (0)