Repository navigation
Expand file tree
/
Copy pathcalendar_ui.cpp
More file actions
209 lines (196 loc) · 8.59 KB
/
Copy pathcalendar_ui.cpp
File metadata and controls
209 lines (196 loc) · 8.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#include "calendar_ui.h"
#include <cstdio>
// 2026 calendar view and deferred month navigation.
namespace calendar {
// private calendar controls and rendering helpers.
namespace {
// calendar palette and fixed non-leap-year tables.
constexpr uint32_t White = 0xFFFFFF;
constexpr uint32_t Black = 0x000000;
constexpr uint32_t Red = 0xFF0000;
constexpr uint8_t MonthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
const char* const MonthNames[] = {"January", "February", "March", "April",
"May", "June", "July", "August", "September", "October", "November", "December"};
const char* const Weekdays[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
// borrowed LVGL objects and selected/pending month state.
lv_obj_t* parent = nullptr;
lv_obj_t* title = nullptr;
lv_obj_t* previous = nullptr;
lv_obj_t* next = nullptr;
lv_obj_t* dates[31] = {};
uint8_t month = 1;
uint8_t pendingMonth = 0;
int highlightedDay = -1;
// clear default styles and disable input/scrolling.
void plain(lv_obj_t* object) {
lv_obj_remove_style_all(object);
lv_obj_set_scrollable(object, false);
lv_obj_set_clickable(object, false);
}
// create a clipped, centered calendar label.
lv_obj_t* label(lv_obj_t* owner, const char* value, int x, int y,
int width, int height, const lv_font_t* font, uint32_t color) {
lv_obj_t* object = lv_label_create(owner);
plain(object);
lv_obj_set_pos(object, x, y);
lv_obj_set_size(object, width, height);
lv_obj_set_style_text_font(object, font, 0);
lv_obj_set_style_text_color(object, lv_color_hex(color), 0);
lv_obj_set_style_text_align(object, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_long_mode(object, LV_LABEL_LONG_MODE_CLIP);
lv_label_set_text(object, value);
return object;
}
// calculate the month start in the fixed 2026 calendar.
uint8_t firstWeekday(uint8_t value) {
// January 1, 2026 is Thursday; 2026 is not a leap year.
uint16_t elapsed = 4;
// accumulate preceding months from the Thursday year start.
for (uint8_t i = 1; i < value; ++i) elapsed += MonthDays[i - 1];
return static_cast<uint8_t>(elapsed % 7);
}
// queue month changes; update objects only after input dispatch.
void selectMonth(lv_event_t* event) {
// ignore input after the calendar is reset.
if (!parent) return;
const uint8_t current = pendingMonth ? pendingMonth : month;
const bool backwards = reinterpret_cast<uintptr_t>(lv_event_get_user_data(event)) == 1;
// queue the previous month without crossing January.
if (backwards && current > 1) pendingMonth = current - 1;
// queue the next month without crossing December.
if (!backwards && current < 12) pendingMonth = current + 1;
}
// create a month arrow with release activation and pressed feedback.
lv_obj_t* monthButton(int x, bool backwards) {
lv_obj_t* object = lv_obj_create(parent);
plain(object);
lv_obj_set_pos(object, x, 34);
lv_obj_set_size(object, 40, 38);
lv_obj_set_clickable(object, true);
lv_obj_set_press_lock(object, false); // sliding away cancels activation
// style normal, pressed and disabled arrow states.
lv_obj_set_style_radius(object, 10, 0);
lv_obj_set_style_bg_color(object, lv_color_hex(White), 0);
lv_obj_set_style_bg_opa(object, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(object, 1, 0);
lv_obj_set_style_border_color(object, lv_color_hex(Black), 0);
lv_obj_set_style_text_color(object, lv_color_hex(Black), 0);
lv_obj_set_style_bg_color(object, lv_color_hex(Red), LV_STATE_PRESSED);
lv_obj_set_style_border_color(object, lv_color_hex(Red), LV_STATE_PRESSED);
lv_obj_set_style_text_color(object, lv_color_hex(White), LV_STATE_PRESSED);
lv_obj_set_style_opa(object, LV_OPA_30, LV_STATE_DISABLED);
// route release clicks to the requested month direction.
lv_obj_add_event_cb(object, selectMonth, LV_EVENT_CLICKED,
reinterpret_cast<void*>(static_cast<uintptr_t>(backwards ? 1 : 2)));
lv_obj_t* arrow = label(object, backwards ? LV_SYMBOL_LEFT : LV_SYMBOL_RIGHT,
0, 0, 20, 16, &lv_font_montserrat_14, Black);
// inherit normal/pressed text colors for legible feedback.
lv_obj_remove_local_style_prop(arrow, LV_STYLE_TEXT_COLOR, 0);
lv_obj_center(arrow);
return object;
}
// highlight today only within the displayed 2026 month.
void refreshToday(const watch::WatchState& state) {
const watch::DateParts today = watch::localDateTime(state);
const int active = state.timeValid && today.year == 2026 && today.month == month &&
today.day >= 1 && today.day <= MonthDays[month - 1] ? today.day : 0;
// skip unchanged day highlighting.
if (active == highlightedDay) return;
highlightedDay = active;
const uint8_t first = firstWeekday(month);
// update date colors, preserving red Sundays outside today.
for (uint8_t day = 1; day <= 31; ++day) {
lv_obj_t* object = dates[day - 1];
const bool todayCell = active == day;
const uint32_t foreground = todayCell ? White : ((first + day - 1) % 7 == 0 ? Red : Black);
lv_obj_set_style_text_color(object, lv_color_hex(foreground), 0);
lv_obj_set_style_bg_color(object, lv_color_hex(Red), 0);
lv_obj_set_style_bg_opa(object, todayCell ? LV_OPA_COVER : LV_OPA_TRANSP, 0);
}
}
// position active month dates and bound month navigation.
void renderMonth(const watch::WatchState& state) {
lv_label_set_text(title, MonthNames[month - 1]);
// disable backwards navigation at January.
if (month == 1) lv_obj_add_state(previous, LV_STATE_DISABLED);
// enable backwards navigation after January.
else lv_obj_remove_state(previous, LV_STATE_DISABLED);
// disable forwards navigation at December.
if (month == 12) lv_obj_add_state(next, LV_STATE_DISABLED);
// enable forwards navigation before December.
else lv_obj_remove_state(next, LV_STATE_DISABLED);
const uint8_t first = firstWeekday(month);
// lay out existing dates in their weekday slots.
for (uint8_t day = 1; day <= 31; ++day) {
lv_obj_t* object = dates[day - 1];
// hide unused day labels in shorter months.
if (day > MonthDays[month - 1]) {
lv_obj_set_hidden(object, true);
continue;
}
lv_obj_set_hidden(object, false);
const uint8_t slot = first + day - 1;
// at most six rows, ending at y182 above the back key.
lv_obj_set_pos(object, 25 + (slot % 7) * 28, 92 + (slot / 7) * 15);
}
// reapply today highlighting and finalize the layout.
highlightedDay = -1;
refreshToday(state);
lv_obj_update_layout(parent);
}
} // namespace
// create the fixed-year calendar on a caller-owned screen.
void build(lv_obj_t* screen, const watch::WatchState& state) {
reset();
// stop when no destination screen is supplied.
if (!screen) return;
// use a non-scrollable white calendar background.
parent = screen;
lv_obj_set_style_bg_color(parent, lv_color_hex(White), 0);
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0);
lv_obj_set_scrollable(parent, false);
const watch::DateParts today = watch::localDateTime(state);
// open the current month when the clock falls within 2026.
if (state.timeValid && today.year == 2026 && today.month >= 1 && today.month <= 12)
month = today.month;
// create the year, month title and navigation arrows.
label(parent, "2026", 83, 14, 74, 17, &lv_font_montserrat_14, Red);
title = label(parent, "", 78, 45, 84, 19, &lv_font_montserrat_14, Black);
previous = monthButton(38, true);
next = monthButton(162, false);
// create weekday headings with Sunday in red.
for (uint8_t column = 0; column < 7; ++column)
label(parent, Weekdays[column], 25 + column * 28, 75, 22, 15,
&lv_font_montserrat_12, column == 0 ? Red : Black);
// allocate reusable date labels for any month.
for (uint8_t day = 1; day <= 31; ++day) {
char text[3];
std::snprintf(text, sizeof(text), "%u", static_cast<unsigned>(day));
dates[day - 1] = label(parent, text, 0, 0, 22, 16, &lv_font_montserrat_12, Black);
lv_obj_set_style_radius(dates[day - 1], 8, 0);
}
renderMonth(state);
}
// refresh today without rebuilding the calendar.
void update(const watch::WatchState& state) {
// update only while the calendar objects exist.
if (parent) refreshToday(state);
}
// apply pending months outside input-event dispatch.
void processNavigation(const watch::WatchState& state) {
// skip absent views and empty navigation queues.
if (!parent || !pendingMonth) return;
month = pendingMonth;
pendingMonth = 0;
renderMonth(state);
}
// release borrowed references before the owner deletes objects.
void reset() {
parent = title = previous = next = nullptr;
// clear every borrowed day-label pointer.
for (lv_obj_t*& object : dates) object = nullptr;
month = 1;
pendingMonth = 0;
highlightedDay = -1;
}
} // namespace calendar