Skip to content

Commit db9d562

Browse files
committed
Add Legend watch face
An elegant and very practical digital watch face. Uses only fonts already in InfiniTime and no external resources. Has the standard functionality: * shows time, date (without year) and day of week * supports 24 and 12 hour time formats * BLE, new notification and alarm clock icons * battery percentage (green when charging) * steps and heart rate * temperature and weather icon Additionally: * visual indication (amber dot) for any notification(s) * visual indication (red or blue line) when notifications are muted * battery percentage turns red when 20% or less * stopwatch and timer icons (when active) * colorful sun, snow and thunderstorm weather icons * generally uses brighter colors for more urgent things
1 parent b66c5de commit db9d562

6 files changed

Lines changed: 497 additions & 0 deletions

File tree

‎src/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ list(APPEND SOURCE_FILES
430430
displayapp/screens/WatchFacePineTimeStyle.cpp
431431
displayapp/screens/WatchFaceCasioStyleG7710.cpp
432432
displayapp/screens/WatchFacePrideFlag.cpp
433+
displayapp/screens/WatchFaceLegend.cpp
433434

434435
##
435436

‎src/displayapp/UserApps.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "displayapp/screens/WatchFacePineTimeStyle.h"
1616
#include "displayapp/screens/WatchFaceTerminal.h"
1717
#include "displayapp/screens/WatchFacePrideFlag.h"
18+
#include "displayapp/screens/WatchFaceLegend.h"
1819

1920
namespace Pinetime {
2021
namespace Applications {

‎src/displayapp/apps/Apps.h.in‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ namespace Pinetime {
5656
Infineat,
5757
CasioStyleG7710,
5858
PrideFlag,
59+
Legend,
5960
};
6061

6162
template <Apps>

‎src/displayapp/apps/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ else()
2929
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat")
3030
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710")
3131
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PrideFlag")
32+
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Legend")
3233
set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware")
3334
endif()
3435

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
#include "displayapp/screens/WatchFaceLegend.h"
2+
3+
#include <lvgl/lvgl.h>
4+
#include "displayapp/screens/BleIcon.h"
5+
#include "displayapp/screens/NotificationIcon.h"
6+
#include "displayapp/screens/Symbols.h"
7+
#include "displayapp/screens/WeatherSymbols.h"
8+
#include "components/battery/BatteryController.h"
9+
#include "components/ble/BleController.h"
10+
#include "components/ble/NotificationManager.h"
11+
#include "components/heartrate/HeartRateController.h"
12+
#include "components/motion/MotionController.h"
13+
#include "components/settings/Settings.h"
14+
15+
using namespace Pinetime::Applications::Screens;
16+
17+
WatchFaceLegend::WatchFaceLegend(Controllers::DateTime& dateTimeController,
18+
const Controllers::Battery& batteryController,
19+
const Controllers::Ble& bleController,
20+
const Controllers::AlarmController& alarmController,
21+
Controllers::Timer& timerController,
22+
Controllers::StopWatchController& stopWatchController,
23+
Controllers::NotificationManager& notificationManager,
24+
Controllers::Settings& settingsController,
25+
Controllers::HeartRateController& heartRateController,
26+
Controllers::MotionController& motionController,
27+
Controllers::SimpleWeatherService& weatherService)
28+
: dateTimeController {dateTimeController},
29+
batteryController {batteryController},
30+
bleController {bleController},
31+
alarmController {alarmController},
32+
timerController {timerController},
33+
stopWatchController {stopWatchController},
34+
notificationManager {notificationManager},
35+
settingsController {settingsController},
36+
heartRateController {heartRateController},
37+
motionController {motionController},
38+
weatherService {weatherService} {
39+
40+
// ---- Top status strip -------------------------------------------------
41+
// Any notification indicator (top left). Hidden when notifications are empty.
42+
notificationAnyDot = lv_obj_create(lv_scr_act(), nullptr);
43+
lv_obj_set_size(notificationAnyDot, 7, 7);
44+
lv_obj_set_style_local_bg_color(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, colorAnyNotification);
45+
lv_obj_set_style_local_radius(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
46+
lv_obj_set_style_local_border_width(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0);
47+
lv_obj_align(notificationAnyDot, nullptr, LV_ALIGN_IN_TOP_LEFT, 18, 16);
48+
lv_obj_set_hidden(notificationAnyDot, true); // hidden by default
49+
50+
// Notification indicator. Empty string when nothing is unread,
51+
// so the label costs no pixels and no redraws in the common case.
52+
notificationNewIcon = lv_label_create(lv_scr_act(), nullptr);
53+
lv_obj_set_style_local_text_color(notificationNewIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorRed);
54+
lv_label_set_text_static(notificationNewIcon, "");
55+
lv_obj_align(notificationNewIcon, notificationAnyDot, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
56+
lv_obj_set_auto_realign(notificationNewIcon, true);
57+
58+
// Services icons. Empty string when nothing is active,
59+
// so the label costs no pixels and no redraws in the common case.
60+
servicesIcons = lv_label_create(lv_scr_act(), nullptr);
61+
lv_obj_set_style_local_text_color(servicesIcons, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
62+
lv_label_set_text_static(servicesIcons, "");
63+
lv_obj_align(servicesIcons, notificationNewIcon, LV_ALIGN_OUT_RIGHT_MID, 12, 1);
64+
lv_obj_set_auto_realign(servicesIcons, true);
65+
66+
// Numeric battery (top right). Right-anchored with auto realign so the
67+
// invalidated region stays pinned to the corner when the width changes
68+
// (e.g. 100% -> 99%).
69+
batteryValue = lv_label_create(lv_scr_act(), nullptr);
70+
lv_obj_set_style_local_text_color(batteryValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
71+
lv_label_set_text_static(batteryValue, "--%");
72+
lv_obj_align(batteryValue, nullptr, LV_ALIGN_IN_TOP_RIGHT, -5, 10);
73+
lv_obj_set_auto_realign(batteryValue, true);
74+
75+
// BLE icon, left of the battery percentage.
76+
bleIcon = lv_label_create(lv_scr_act(), nullptr);
77+
lv_obj_set_style_local_text_color(bleIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorBlue);
78+
lv_label_set_text_static(bleIcon, "");
79+
lv_obj_align(bleIcon, batteryValue, LV_ALIGN_OUT_LEFT_MID, -10, 0);
80+
lv_obj_set_auto_realign(bleIcon, true);
81+
82+
// Upper divider - indicator of silence
83+
upperDivider = lv_line_create(lv_scr_act(), nullptr);
84+
lv_line_set_points(upperDivider, dividerPoints, 2);
85+
lv_obj_set_style_local_line_width(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 1);
86+
lv_obj_align(upperDivider, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 45);
87+
lv_obj_set_hidden(upperDivider, true); // hidden by default
88+
89+
// ---- Center block ------------------------------------------------------
90+
labelDate = lv_label_create(lv_scr_act(), nullptr);
91+
lv_obj_set_style_local_text_color(labelDate, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
92+
lv_label_set_text_static(labelDate, "");
93+
lv_obj_align(labelDate, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 64);
94+
95+
labelTime = lv_label_create(lv_scr_act(), nullptr);
96+
lv_obj_set_style_local_text_font(labelTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_extrabold_compressed);
97+
lv_obj_set_style_local_text_color(labelTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorForeground);
98+
lv_label_set_text_static(labelTime, "");
99+
lv_obj_align(labelTime, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 94);
100+
101+
labelAmPm = lv_label_create(lv_scr_act(), nullptr);
102+
lv_obj_set_style_local_text_color(labelAmPm, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorForeground);
103+
lv_label_set_text_static(labelAmPm, "");
104+
lv_obj_align(labelAmPm, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 64);
105+
lv_obj_set_auto_realign(labelAmPm, true);
106+
107+
divider = lv_line_create(lv_scr_act(), nullptr);
108+
lv_line_set_points(divider, dividerPoints, 2);
109+
lv_obj_set_style_local_line_color(divider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorDivider);
110+
lv_obj_set_style_local_line_width(divider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 1);
111+
lv_obj_align(divider, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 177);
112+
113+
// ---- Bottom row --------------------------------------------------
114+
stepValue = lv_label_create(lv_scr_act(), nullptr);
115+
lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
116+
lv_label_set_text_static(stepValue, "0");
117+
lv_obj_align(stepValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 186);
118+
119+
stepCaption = lv_label_create(lv_scr_act(), nullptr);
120+
lv_obj_set_style_local_text_color(stepCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption);
121+
lv_label_set_text_static(stepCaption, "steps");
122+
lv_obj_align(stepCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 210);
123+
124+
heartbeatValue = lv_label_create(lv_scr_act(), nullptr);
125+
lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
126+
lv_label_set_text_static(heartbeatValue, "--");
127+
lv_obj_align(heartbeatValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 100, 186);
128+
129+
heartbeatCaption = lv_label_create(lv_scr_act(), nullptr);
130+
lv_obj_set_style_local_text_color(heartbeatCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption);
131+
lv_label_set_text_static(heartbeatCaption, "BPM");
132+
lv_obj_align(heartbeatCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 100, 210);
133+
134+
weatherValue = lv_label_create(lv_scr_act(), nullptr);
135+
lv_obj_set_style_local_text_color(weatherValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
136+
lv_label_set_text_static(weatherValue, "--");
137+
lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 186);
138+
139+
weatherCaption = lv_label_create(lv_scr_act(), nullptr);
140+
lv_obj_set_style_local_text_color(weatherCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption);
141+
lv_label_set_text_static(weatherCaption, "\xC2\xB0");
142+
lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 210);
143+
144+
weatherIcon = lv_label_create(lv_scr_act(), nullptr);
145+
lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
146+
lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons);
147+
lv_label_set_text_static(weatherIcon, "");
148+
lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12);
149+
lv_obj_set_auto_realign(weatherIcon, true);
150+
151+
// Use the standard periodic refresh task: no extra timers.
152+
taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
153+
Refresh();
154+
}
155+
156+
WatchFaceLegend::~WatchFaceLegend() {
157+
lv_task_del(taskRefresh);
158+
lv_obj_clean(lv_scr_act());
159+
}
160+
161+
void WatchFaceLegend::Refresh() {
162+
// ---- Notification indicators ----
163+
notificationState = notificationManager.AreNewNotificationsAvailable();
164+
notificationEmpty = notificationManager.IsEmpty();
165+
if (notificationState.IsUpdated() || notificationEmpty.IsUpdated()) {
166+
bool state = notificationState.Get();
167+
bool empty = notificationEmpty.Get();
168+
lv_obj_set_hidden(notificationAnyDot, empty && !state);
169+
lv_label_set_text_static(notificationNewIcon, NotificationIcon::GetIcon(state));
170+
lv_obj_align(servicesIcons, state ? notificationNewIcon : notificationAnyDot, LV_ALIGN_OUT_RIGHT_MID, empty && !state ? -18 : 12, 1);
171+
}
172+
173+
// ---- Services (Alarm, Timer, StopWatch) ----
174+
alarmEnabled = alarmController.IsEnabled();
175+
timerRunning = timerController.IsRunning();
176+
stopWatchRunning = stopWatchController.IsRunning();
177+
if (alarmEnabled.IsUpdated() || timerRunning.IsUpdated() || stopWatchRunning.IsUpdated()) {
178+
lv_label_set_text_fmt(servicesIcons,
179+
"%s%s%s%s%s",
180+
stopWatchRunning.Get() ? Symbols::stopWatch : "",
181+
stopWatchRunning.Get() ? " " : "",
182+
alarmEnabled.Get() ? Symbols::bell : "",
183+
alarmEnabled.Get() ? " " : "",
184+
timerRunning.Get() ? Symbols::hourGlass : "");
185+
}
186+
187+
// ---- BLE ----
188+
bleState = bleController.IsConnected();
189+
bleRadioEnabled = bleController.IsRadioEnabled();
190+
if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) {
191+
lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get()));
192+
}
193+
194+
// ---- Battery ----
195+
batteryPercentRemaining = batteryController.PercentRemaining();
196+
powerPresent = batteryController.IsPowerPresent();
197+
if (batteryPercentRemaining.IsUpdated() || powerPresent.IsUpdated()) {
198+
uint8_t remaining = batteryPercentRemaining.Get();
199+
lv_label_set_text_fmt(batteryValue, "%d%%", remaining);
200+
lv_obj_set_style_local_text_color(batteryValue,
201+
LV_LABEL_PART_MAIN,
202+
LV_STATE_DEFAULT,
203+
powerPresent.Get() ? colorCharging
204+
: remaining > 20 ? colorSecondary
205+
: colorRed);
206+
lv_obj_align(bleIcon, batteryValue, LV_ALIGN_OUT_LEFT_MID, -10, 0);
207+
}
208+
209+
// ---- Upper divider, based on notification config ----
210+
notificationConfig = settingsController.GetNotificationStatus();
211+
if (notificationConfig.IsUpdated()) {
212+
switch (notificationConfig.Get()) {
213+
case Controllers::Settings::Notification::Off:
214+
lv_obj_set_style_local_line_color(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorRed);
215+
lv_obj_set_hidden(upperDivider, false);
216+
break;
217+
case Controllers::Settings::Notification::Sleep:
218+
lv_obj_set_style_local_line_color(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorBlue);
219+
lv_obj_set_hidden(upperDivider, false);
220+
break;
221+
default:
222+
lv_obj_set_hidden(upperDivider, true);
223+
break;
224+
}
225+
}
226+
227+
// ---- Time and date ----
228+
currentDateTime = std::chrono::time_point_cast<std::chrono::minutes>(dateTimeController.CurrentDateTime());
229+
if (currentDateTime.IsUpdated()) {
230+
uint8_t hour = dateTimeController.Hours();
231+
uint8_t minute = dateTimeController.Minutes();
232+
233+
if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) {
234+
const char* ampm = "AM";
235+
if (hour == 0) {
236+
hour = 12;
237+
} else if (hour == 12) {
238+
ampm = "PM";
239+
} else if (hour > 12) {
240+
hour = hour - 12;
241+
ampm = "PM";
242+
}
243+
lv_label_set_text_static(labelAmPm, ampm);
244+
} else {
245+
lv_label_set_text_static(labelAmPm, "");
246+
}
247+
lv_label_set_text_fmt(labelTime, "%02d:%02d", hour, minute);
248+
249+
currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get());
250+
if (currentDate.IsUpdated()) {
251+
lv_label_set_text_fmt(labelDate,
252+
"%s %02d %s",
253+
dateTimeController.DayOfWeekShortToString(),
254+
dateTimeController.Day(),
255+
dateTimeController.MonthShortToString());
256+
}
257+
}
258+
259+
// ---- Steps ----
260+
stepCount = motionController.NbSteps();
261+
if (stepCount.IsUpdated()) {
262+
const uint32_t steps = stepCount.Get();
263+
if (steps >= 1000) {
264+
// "6.4k" style: compact, fixed maximum width
265+
lv_label_set_text_fmt(stepValue, "%lu.%luk", steps / 1000, (steps % 1000) / 100);
266+
} else {
267+
lv_label_set_text_fmt(stepValue, "%lu", steps);
268+
}
269+
}
270+
271+
// ---- Heart rate ----
272+
heartbeat = heartRateController.HeartRate();
273+
heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped;
274+
if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) {
275+
if (heartbeatRunning.Get()) {
276+
lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get());
277+
} else {
278+
lv_label_set_text_static(heartbeatValue, "--");
279+
}
280+
}
281+
282+
// ---- Weather ----
283+
currentWeather = weatherService.Current();
284+
if (currentWeather.IsUpdated()) {
285+
if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) {
286+
lv_label_set_text_static(weatherCaption,
287+
"\xC2\xB0"
288+
"F");
289+
} else {
290+
lv_label_set_text_static(weatherCaption,
291+
"\xC2\xB0"
292+
"C");
293+
}
294+
295+
auto optCurrentWeather = currentWeather.Get();
296+
if (optCurrentWeather) {
297+
int16_t temp;
298+
if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) {
299+
temp = optCurrentWeather->temperature.Fahrenheit();
300+
} else {
301+
temp = optCurrentWeather->temperature.Celsius();
302+
}
303+
if (temp > -100) {
304+
lv_label_set_text_fmt(weatherValue, "%d", temp);
305+
} else {
306+
lv_label_set_text_static(weatherValue, "--");
307+
}
308+
309+
lv_color_t color;
310+
switch (optCurrentWeather->iconId) {
311+
case Controllers::SimpleWeatherService::Icons::Sun:
312+
color = colorWeatherYellow;
313+
break;
314+
case Controllers::SimpleWeatherService::Icons::Snow:
315+
case Controllers::SimpleWeatherService::Icons::Thunderstorm:
316+
color = colorWeatherLightBlue;
317+
break;
318+
default:
319+
color = colorWeatherDefault;
320+
break;
321+
}
322+
lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color);
323+
lv_label_set_text_static(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId, weatherService.IsNight()));
324+
325+
if (!weatherDisplayed) { // adjust the layout when necessary
326+
lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 200, 186);
327+
lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 200, 210);
328+
lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12);
329+
weatherDisplayed = true;
330+
}
331+
} else {
332+
lv_label_set_text_static(weatherValue, "--");
333+
lv_label_set_text_static(weatherIcon, "");
334+
335+
if (weatherDisplayed) { // adjust the layout when necessary
336+
lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 186);
337+
lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 210);
338+
lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12);
339+
weatherDisplayed = false;
340+
}
341+
}
342+
}
343+
}

0 commit comments

Comments
 (0)