From c388c8a5886b0f89376c978aab8e7d001bd66d47 Mon Sep 17 00:00:00 2001 From: ljahn Date: Mon, 23 Dec 2024 20:49:45 +0100 Subject: [PATCH 1/4] Provide callback for watch face to react to LCD state changes --- src/displayapp/DisplayApp.cpp | 7 +++++++ src/displayapp/screens/Screen.h | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 6671ac9e51..cfd16c90dd 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -324,6 +324,9 @@ void DisplayApp::Refresh() { // display activity timer causing the screen to never sleep after timeout lvgl.ClearTouchState(); if (msg == Messages::GoToAOD) { + currentScreen->OnLCDSleep(true); // prepare for low power while still fast + lv_task_handler(); + vTaskDelay(100); lcd.LowPowerOn(); // Record idle entry time alwaysOnFrameCount = 0; @@ -331,6 +334,9 @@ void DisplayApp::Refresh() { PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskAOD); state = States::AOD; } else { + currentScreen->OnLCDSleep(false); + lv_task_handler(); // call to update display, will not be called again in Idle mode + vTaskDelay(100); // give time for display refresh lcd.Sleep(); PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskSleeping); state = States::Idle; @@ -354,6 +360,7 @@ void DisplayApp::Refresh() { lv_disp_trig_activity(nullptr); ApplyBrightness(); state = States::Running; + currentScreen->OnLCDWakeup(settingsController.GetAlwaysOnDisplay()); break; case Messages::UpdateBleConnection: // Only used for recovery firmware diff --git a/src/displayapp/screens/Screen.h b/src/displayapp/screens/Screen.h index 9f6e0edefb..02cf056b36 100644 --- a/src/displayapp/screens/Screen.h +++ b/src/displayapp/screens/Screen.h @@ -40,6 +40,12 @@ namespace Pinetime { return false; } + virtual void OnLCDWakeup([[maybe_unused]] bool aodMode) { + } + + virtual void OnLCDSleep([[maybe_unused]] bool aodMode) { + } + protected: bool running = true; }; From 115d9192441387cae0690394c762d57d8bb6c6e1 Mon Sep 17 00:00:00 2001 From: ljahn Date: Mon, 23 Dec 2024 20:43:53 +0100 Subject: [PATCH 2/4] BatteryIcon: fix set color when using color on low battery --- src/displayapp/screens/BatteryIcon.cpp | 18 ++++++++++++++---- src/displayapp/screens/BatteryIcon.h | 4 ++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/displayapp/screens/BatteryIcon.cpp b/src/displayapp/screens/BatteryIcon.cpp index 6194807d32..518d5f5e20 100644 --- a/src/displayapp/screens/BatteryIcon.cpp +++ b/src/displayapp/screens/BatteryIcon.cpp @@ -11,7 +11,7 @@ BatteryIcon::BatteryIcon(bool colorOnLowBattery) : colorOnLowBattery {colorOnLow void BatteryIcon::Create(lv_obj_t* parent) { batteryImg = lv_img_create(parent, nullptr); lv_img_set_src(batteryImg, &batteryicon); - lv_obj_set_style_local_image_recolor(batteryImg, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_image_recolor(batteryImg, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, baseColor); batteryJuice = lv_obj_create(batteryImg, nullptr); lv_obj_set_width(batteryJuice, 8); @@ -30,21 +30,31 @@ void BatteryIcon::SetBatteryPercentage(uint8_t percentage) { static constexpr int lowBatteryThreshold = 15; static constexpr int criticalBatteryThreshold = 5; if (percentage > lowBatteryThreshold) { - SetColor(LV_COLOR_WHITE); + _SetColor(baseColor); } else if (percentage > criticalBatteryThreshold) { - SetColor(LV_COLOR_ORANGE); + _SetColor(LV_COLOR_ORANGE); } else { - SetColor(Colors::deepOrange); + _SetColor(Colors::deepOrange); } } } void BatteryIcon::SetColor(lv_color_t color) { + baseColor = color; + _SetColor(color); +} + +void BatteryIcon::_SetColor(lv_color_t color) { lv_obj_set_style_local_image_recolor(batteryImg, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, color); lv_obj_set_style_local_image_recolor_opa(batteryImg, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_COVER); lv_obj_set_style_local_bg_color(batteryJuice, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, color); } +void BatteryIcon::SetVisible(bool visible) { + lv_obj_set_hidden(batteryImg, !visible); + lv_obj_set_hidden(batteryJuice, !visible); +} + const char* BatteryIcon::GetPlugIcon(bool isCharging) { if (isCharging) return Symbols::plug; diff --git a/src/displayapp/screens/BatteryIcon.h b/src/displayapp/screens/BatteryIcon.h index 19fea967d9..84fc2092f4 100644 --- a/src/displayapp/screens/BatteryIcon.h +++ b/src/displayapp/screens/BatteryIcon.h @@ -9,6 +9,7 @@ namespace Pinetime { public: explicit BatteryIcon(bool colorOnLowBattery); void Create(lv_obj_t* parent); + void SetVisible(bool visible); void SetColor(lv_color_t); void SetBatteryPercentage(uint8_t percentage); @@ -21,6 +22,9 @@ namespace Pinetime { lv_obj_t* batteryImg; lv_obj_t* batteryJuice; bool colorOnLowBattery = false; + + lv_color_t baseColor = LV_COLOR_WHITE; + void _SetColor(lv_color_t color); }; } } From 32d801eae64d190c74f4e8a161fba9f0ee2a3f3c Mon Sep 17 00:00:00 2001 From: ljahn Date: Mon, 23 Dec 2024 20:33:57 +0100 Subject: [PATCH 3/4] add possibility to mark DirtyValue updated --- src/utility/DirtyValue.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utility/DirtyValue.h b/src/utility/DirtyValue.h index 8d5147aa33..2663b4d7dd 100644 --- a/src/utility/DirtyValue.h +++ b/src/utility/DirtyValue.h @@ -18,6 +18,10 @@ namespace Pinetime { return false; } + void MarkUpdated() { + this->isUpdated = true; + } + T const& Get() { this->isUpdated = false; return value; From f0dc1134f6420de5de3b8641cefdac4448a26847 Mon Sep 17 00:00:00 2001 From: ljahn Date: Tue, 7 Jan 2025 18:44:20 +0100 Subject: [PATCH 4/4] StarTrek watchface --- src/CMakeLists.txt | 1 + src/components/settings/Settings.h | 56 +- src/displayapp/UserApps.h | 1 + src/displayapp/apps/Apps.h.in | 1 + src/displayapp/apps/CMakeLists.txt | 1 + src/displayapp/fonts/fonts.json | 4 + .../icons/watchfacestartrek/bracket_left.c | 26 + .../icons/watchfacestartrek/bracket_left.png | Bin 0 -> 4574 bytes .../icons/watchfacestartrek/bracket_right.c | 26 + .../icons/watchfacestartrek/bracket_right.png | Bin 0 -> 4597 bytes src/displayapp/screens/WatchFaceStarTrek.cpp | 980 ++++++++++++++++++ src/displayapp/screens/WatchFaceStarTrek.h | 214 ++++ .../screens/settings/SettingWatchFace.h | 1 + src/resources/fonts.json | 12 + .../fonts/EdgeOfTheGalaxyRegular.ttf | Bin 0 -> 26744 bytes 15 files changed, 1322 insertions(+), 1 deletion(-) create mode 100644 src/displayapp/icons/watchfacestartrek/bracket_left.c create mode 100644 src/displayapp/icons/watchfacestartrek/bracket_left.png create mode 100644 src/displayapp/icons/watchfacestartrek/bracket_right.c create mode 100644 src/displayapp/icons/watchfacestartrek/bracket_right.png create mode 100644 src/displayapp/screens/WatchFaceStarTrek.cpp create mode 100644 src/displayapp/screens/WatchFaceStarTrek.h create mode 100644 src/resources/fonts/EdgeOfTheGalaxyRegular.ttf diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2b69b8b02..f78088f7d5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -426,6 +426,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFaceTerminal.cpp displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp + displayapp/screens/WatchFaceStarTrek.cpp ## diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index 602de3a585..5a864b472e 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -36,6 +36,7 @@ namespace Pinetime { }; enum class PTSGaugeStyle : uint8_t { Full, Half, Numeric }; enum class PTSWeather : uint8_t { On, Off }; + enum class StarTrekAnimateType { None, Start, Continuous, All }; struct PineTimeStyle { Colors ColorTime = Colors::Teal; @@ -50,6 +51,13 @@ namespace Pinetime { int colorIndex = 0; }; + struct WatchFaceStarTrek { + bool useSystemFont = false; + StarTrekAnimateType animate = StarTrekAnimateType::All; + bool displaySeconds = false; + bool weather = true; + }; + Settings(Pinetime::Controllers::FS& fs); Settings(const Settings&) = delete; @@ -154,6 +162,50 @@ namespace Pinetime { return settings.PTS.weatherEnable; }; + bool GetStarTrekUseSystemFont() const { + return settings.watchFaceStarTrek.useSystemFont; + }; + + void SetStarTrekUseSystemFont(bool useSystemFont) { + if (useSystemFont != settings.watchFaceStarTrek.useSystemFont) { + settings.watchFaceStarTrek.useSystemFont = useSystemFont; + settingsChanged = true; + } + }; + + StarTrekAnimateType GetStarTrekAnimate() const { + return settings.watchFaceStarTrek.animate; + }; + + void SetStarTrekAnimate(StarTrekAnimateType animate) { + if (animate != settings.watchFaceStarTrek.animate) { + settings.watchFaceStarTrek.animate = animate; + settingsChanged = true; + } + }; + + bool GetStarTrekDisplaySeconds() const { + return settings.watchFaceStarTrek.displaySeconds; + }; + + void SetStarTrekDisplaySeconds(bool displaySeconds) { + if (displaySeconds != settings.watchFaceStarTrek.displaySeconds) { + settings.watchFaceStarTrek.displaySeconds = displaySeconds; + settingsChanged = true; + } + }; + + bool GetStarTrekWeather() const { + return settings.watchFaceStarTrek.weather; + }; + + void SetStarTrekWeather(bool weather) { + if (weather != settings.watchFaceStarTrek.weather) { + settings.watchFaceStarTrek.weather = weather; + settingsChanged = true; + } + }; + void SetAppMenu(uint8_t menu) { appMenu = menu; }; @@ -301,7 +353,7 @@ namespace Pinetime { private: Pinetime::Controllers::FS& fs; - static constexpr uint32_t settingsVersion = 0x0008; + static constexpr uint32_t settingsVersion = 0x0009; struct SettingsData { uint32_t version = settingsVersion; @@ -321,6 +373,8 @@ namespace Pinetime { WatchFaceInfineat watchFaceInfineat; + WatchFaceStarTrek watchFaceStarTrek; + std::bitset<5> wakeUpMode {0}; uint16_t shakeWakeThreshold = 150; diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 67bbfa7d41..1b4de9f00e 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -14,6 +14,7 @@ #include "displayapp/screens/WatchFaceInfineat.h" #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" +#include "displayapp/screens/WatchFaceStarTrek.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 2104a267c0..b2f7556fda 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -52,6 +52,7 @@ namespace Pinetime { Terminal, Infineat, CasioStyleG7710, + StarTrek, }; template diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index d78587609e..31d741f160 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -27,6 +27,7 @@ else() set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Terminal") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::StarTrek") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 41c383c0d4..651f0ed1a1 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -8,6 +8,10 @@ { "file": "FontAwesome5-Solid+Brands+Regular.woff", "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743" + }, + { + "file": "material-design-icons/MaterialIcons-Regular.ttf", + "range": "0xe7f6, 0xef44" } ], "bpp": 1, diff --git a/src/displayapp/icons/watchfacestartrek/bracket_left.c b/src/displayapp/icons/watchfacestartrek/bracket_left.c new file mode 100644 index 0000000000..34c770ac4e --- /dev/null +++ b/src/displayapp/icons/watchfacestartrek/bracket_left.c @@ -0,0 +1,26 @@ +#include "lvgl/lvgl.h" + +#ifndef LV_ATTRIBUTE_MEM_ALIGN + #define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef LV_ATTRIBUTE_IMG_BRACKET_LEFT + #define LV_ATTRIBUTE_IMG_BRACKET_LEFT +#endif + +const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMG_BRACKET_LEFT uint8_t bracket_left_map[] = { + 0x00, 0x00, 0x00, 0xff, /*Color of index 0*/ + 0x10, 0x5d, 0xd5, 0xff, /*Color of index 1*/ + 0xfe, 0xdd, 0x82, 0xff, /*Color of index 2*/ + 0x6b, 0x61, 0x4a, 0xff, /*Color of index 3*/ + + 0x00, 0xaa, 0xaa, 0xaa, 0x02, 0xaa, 0xaa, 0xaa, 0x02, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0xaa, 0xa8, 0x00, 0x00, 0xaa, 0xa8, + 0x00, 0x00, 0xaa, 0xa8, 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xfc, 0x00, 0x00, 0x03, 0xfc, 0x00, 0x00, 0x03, 0xfc, 0x00, 0x00, 0x03, 0xfc, 0x00, 0x00, 0x03, 0xfc, + 0x00, 0x00, 0x03, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x54, 0x00, 0x00, 0x01, 0x54, 0x00, 0x00, 0x01, 0x54, 0x00, 0x00, + 0x01, 0x54, 0x00, 0x00, 0x01, 0x54, 0x00, 0x00, 0x01, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, 0x02, 0xa8, + 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, 0x02, 0xa8, 0x00, 0x00, 0xaa, 0xa8, 0x00, 0x00, 0xaa, 0xa8, 0x00, 0x00, + 0xaa, 0xa8, 0x00, 0x00, 0xaa, 0xaa, 0x00, 0x00, 0x02, 0xaa, 0xaa, 0xaa, 0x02, 0xaa, 0xaa, 0xaa, 0x00, 0xaa, 0xaa, 0xaa, +}; + +const lv_img_dsc_t bracket_left = {{LV_IMG_CF_INDEXED_2BIT, 0, 0, 16, 38}, 168, bracket_left_map}; diff --git a/src/displayapp/icons/watchfacestartrek/bracket_left.png b/src/displayapp/icons/watchfacestartrek/bracket_left.png new file mode 100644 index 0000000000000000000000000000000000000000..66d5aeae9f200c03861706fe14ac514ebf6468ed GIT binary patch literal 4574 zcmeHKe{dA_72gX6Vl**DEGda{Y%n0Qw?C8HTke3EJIEmy(r^u-IGFG4e)qC+zp}UC zE)=TJiedm2uplx5Dx+1Z%rHt*Dh3L~(o$##>nLhd#<9~XjenI}Ee1aef{59Tsn2TVP=Z%Hm(`QTm2-8{I=vV#z z8AKjDfTis^!UmFQXUodvAm`Y|5My9o4`V#Yrj0o!*>bIUBgiFLc^k;nvo@v=Ys(=D zVBQ6T19<|>FT&6s%ItLnWb>0}eVsXkWic?%_Xq0ioP%W?ya4_T$4e|Dal9RVSwZ4l zu)l53)rd@_>h+xqA-S}vJkE>b)6GVE~n#VmgznNQl;@)NZ_6194&baW{ zUg;g5uXM#NkJL>$m;VcC$32&G1CzE)m^lCFtKHpQp|_5nSh{4~lB%LBL`!Q+z47?? z{_PWfcs!kW@6ps3x21ZB`wyN99-L--VJBAV{ZqEzd&|04^5ncJUS>MGc=ydG1b5fl zp4F>gZ~ynqp5*Supw+by0twdJnWwQh7acf;fk$Kfj{ zJ3c(HJ3MbwM15LZFmv0l8b1j&R0c0-7j`^z-?pAJ9s3g>T-e`masQe{eaC95mM#5A zI{p3U4m^DO!$tOIH|M{2py>G@l%4Rd|9tChec$P#x7=O6BzI%!4KGwZ_~E6a{oScU zYRi3p3Oq7(?FSRj_Z5#>cX;kg==@*0UVC4%<(%n!;?JMGy|eS5)Be6`^LJ~G9^3X=ou6)Xd~ZuxZu8}KUw7}uBX`_%<9j)OtvGb(^VzkNADG$t#1UIz;o8r} z<5zz4>sjaCKmSJf-QHfJ;Vu2C8z+AZC0Dpi3pN^!frXMBk2;VN4`D|-nlKNHaL-96 zkh}sL_7DzhF%Q{u_zlvoDIRi3MSuw;s&TVcvr5N{SJejPRV!qdLe80;?@mhq5XAbQ>Wf&iB>&grK89+j^HSmCSg~0*p}vv8}u+K2y4-Vw~ zREQT4CCg6U#VIPTR9!=${IR5gVlpAx5TLTqRGjm{ThR zRaKl7%n*o0x&~E=B15Axp%fEJ389e4@sx}ZM+r{Aa|x9q#k*t?RS1HNDpU(fk)?TY zJ&NEswI~W>IuQ$73nt-GRh{2Ma*oWdmZmO(3>6$a%hVGFq*8#ou6{Voc7_$sQlo(PJNkH8g_r^S_hx^+-@47W}-WAhY%Fp6a;jtjzH zEue~bW(lLOeTuRm@QN(EC_zvwD4{Y0CCYOZ5Ko9#)R0rGN_8o8LXJKfe_;*C%S0 zCJEw(o6TV(cJ3+$-Mvo~>l zPWb{sE}mQ1bToXizrVluAKRY%ZK~?omkGT5(tkhZZ#}lBVM9xkjJgY&yo*jv9Y=Cq zcd{iu{rr`mmrW^pXqm8llkfDAciZP|JbCPF`@H{<1s%UE*_6v&Jh}FZoRWz&9((Sd z%blHXzFD(+!PMK`eZ9TE>pcGZg+DH4SYBSWx1oKj@)Kf4$BYf{yp(E&7zw|x*8B2Z HP3!&#S_u^m literal 0 HcmV?d00001 diff --git a/src/displayapp/icons/watchfacestartrek/bracket_right.c b/src/displayapp/icons/watchfacestartrek/bracket_right.c new file mode 100644 index 0000000000..941d5faf77 --- /dev/null +++ b/src/displayapp/icons/watchfacestartrek/bracket_right.c @@ -0,0 +1,26 @@ +#include "lvgl/lvgl.h" + +#ifndef LV_ATTRIBUTE_MEM_ALIGN + #define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef LV_ATTRIBUTE_IMG_BRACKET_RIGHT + #define LV_ATTRIBUTE_IMG_BRACKET_RIGHT +#endif + +const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMG_BRACKET_RIGHT uint8_t bracket_right_map[] = { + 0x00, 0x00, 0x00, 0xff, /*Color of index 0*/ + 0x10, 0x5d, 0xd5, 0xff, /*Color of index 1*/ + 0xfe, 0xdd, 0x82, 0xff, /*Color of index 2*/ + 0x6b, 0x61, 0x4a, 0xff, /*Color of index 3*/ + + 0xaa, 0xaa, 0xaa, 0x00, 0xaa, 0xaa, 0xaa, 0x80, 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x00, 0xaa, 0xaa, 0x00, 0x00, 0x2a, 0xaa, 0x00, 0x00, + 0x2a, 0xaa, 0x00, 0x00, 0x2a, 0xaa, 0x00, 0x00, 0x2a, 0x80, 0x00, 0x00, 0x2a, 0x80, 0x00, 0x00, 0x2a, 0x80, 0x00, 0x00, 0x2a, 0x80, + 0x00, 0x00, 0x2a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x40, 0x00, 0x00, 0x15, 0x40, 0x00, 0x00, 0x15, 0x40, 0x00, 0x00, + 0x15, 0x40, 0x00, 0x00, 0x15, 0x40, 0x00, 0x00, 0x15, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x3f, 0xc0, + 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2a, 0x80, 0x00, 0x00, 0x2a, 0x80, 0x00, 0x00, 0x2a, 0x80, 0x00, 0x00, 0x2a, 0x80, 0x00, 0x00, 0x2a, 0xaa, 0x00, 0x00, 0x2a, 0xaa, + 0x00, 0x00, 0x2a, 0xaa, 0x00, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x80, 0xaa, 0xaa, 0xaa, 0x80, 0xaa, 0xaa, 0xaa, 0x00, +}; + +const lv_img_dsc_t bracket_right = {{LV_IMG_CF_INDEXED_2BIT, 0, 0, 16, 38}, 168, bracket_right_map}; diff --git a/src/displayapp/icons/watchfacestartrek/bracket_right.png b/src/displayapp/icons/watchfacestartrek/bracket_right.png new file mode 100644 index 0000000000000000000000000000000000000000..9b20745df43dc0d911a7c3eb95f3ad8e0aba2ac9 GIT binary patch literal 4597 zcmeHKeQ*=U6+fBU7-5J@NKL^L4$&BraJo-Ry2CO!mW|aQ;~LeNk_NBtR`P+qkj_YU zpu`LbAqKa672+_(Y0HqNq|hO$O9@{o;2CDpxSgb#6avOWlVND$GMRx61>AQhS+?8E zbUZEpS(??p+x_it-~Qg)XWy2Z>P3^x)6E1yOmbJbYN5}ER+O6qzwhZxZdq| zyPqI(;Q^4QuMsA&bet(|7lEB+8b^$S{zYh0z}7K(n{LWB`mJEkNZY%>o}0$#Ygkhj zF$wy8&^WNCLjMLdb#rR3lVIzgT;prcA`HY}zsT*WAvr6{Sa|`~GaN6mjKuLI{IWKQ zFNXb1okJe!OnrCX>;eBaspaK0?(%Xn8jJYVASQ_9zV+LkRli?Ycy8s20|&f?^PcGY z(QDGj6%~c6A9=ZE)(7S{rM=JG$@Wa&F|}aPnYVj;yL}&?Ik$Y-_+@4JcZoGkYihOa zQwH`-{odJR^wJL#w;xUP6B~|R@E)CGdc6ZHweDGaRz9-vaITy?%f-xPm+pV~oKVvJ zk#pU;6K!A4>yK|}FFnw}ot~3FXK%Rv?2NPgt3Uoj^M1#L3p<`#S6za7vbW4UW<7rQ z{IP3C_6I7r1^qv9e0$!m-_?KNtFw9g)V{qxUA3$K{bPrsm-`OY-8i&<$^Cb$%2q7@ zRC@0_KR@!)V=v{CKWjI?aU}oXcW0k-y?ATqqxZkwP49TRXj%5w!ile!y>RUxX9ju` zUH&zz{^WW2TdkK1F5aIuZsYNVzeE?WbpPfP$&~f}>D_<6`BBH|zsNo9~fBNq`t>4@+JG=2tTSf1^ttTIUDDP6%)#9$MTcy=ApP$#X`=n{|q@is}I)N){xe zNM4OK(uV_T*lFoMe#%0siqo>J*u!|D<+xF;YKh^cE!AGRWwk6SmIbBelB5I#A*>-X z848BuQqpNLcqOp)VcJ3(5N);7QV)4l9*JR6unJa&sz|D9IZLUTEQu+8sn%7Qrhq4> zrBTzO5=|!(32TD4Mq&Y)6-AL|IGW=qKv41Ku!fRUI6hmaNO8DuT#l(xO^t*}ofG*Y zO`6kUfq8P!zfjcU$ZI*Fn*Yh~zAh#tNou2rlBlC(o#xgzduj$*nN$0JQK z8CR^uVQuzs3PsMWk2b}E#&Hyx#z7ncXdHHBM{KFzxXcP&f`A%|8mmC;5lBt-4~aFx zH+{x9&TvOyd4_icdhqN97*HOMlh0&=SgJRECth&G2nA>L-E1VQE~hu>aI*%+Sl zi7dylSQ$pKB&I@EqTq0^bSed?1jUDZ4vwc}gg8pD`x#0UYz~SSWd|w-YES|sL;M1oFBrs`&< zJ#d{Aq#+m5Fpy^Iuy?QyKgnnTCn^jDS!?4Iu~-p#VWiXl(-HZ>ocOmR zme6`x91Lm+{U2dZ0fyC4kR~+-^%=@NovHU}%^)NX@{>-t2e}1Fj%_k3eaGY)lWSB8 zj0!xKU1M^MN`X;<$Fl4HCYO2WjS+|8AH4*;5BiE%-hg+>9N*K6T*QF>?L2XCCyY#p zRxOVc#Ked7W+FQFO@qPln%h$`{xfqyZvJCSO3_gmI_!3pd8@kR;K|zq3&u_8tZUoZ zdbRr@$Nnv?uAX(D&o_h7+IscMl~Z}wvMO_Oa`rvjpeRb?JNGuXZlBOqezWE3z`#Hr z)smI`{z4}22kZa2_!ZM8VxriwYWK8Xe6|ra5SKT(-md@c^tO(@Zxw8qw(W)23bsAF z<>R?`Lth?jKmTq|+xbtQ8`vCwrK;}4^Myrk-Ol0*Xnei%`WGv6Dl0GDI?~yFv-j|f g?V&(JLjyt7ZFut6^k45k1FjS9ifY%do@&_mFZnVeA^-pY literal 0 HcmV?d00001 diff --git a/src/displayapp/screens/WatchFaceStarTrek.cpp b/src/displayapp/screens/WatchFaceStarTrek.cpp new file mode 100644 index 0000000000..3e6d77a98a --- /dev/null +++ b/src/displayapp/screens/WatchFaceStarTrek.cpp @@ -0,0 +1,980 @@ +#include "displayapp/screens/WatchFaceStarTrek.h" + +#include +#include "displayapp/screens/BatteryIcon.h" +#include "displayapp/screens/BleIcon.h" +#include "displayapp/screens/Symbols.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/heartrate/HeartRateController.h" +#include "components/motion/MotionController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/settings/Settings.h" +#include "displayapp/icons/watchfacestartrek/bracket_left.c" +#include "displayapp/icons/watchfacestartrek/bracket_right.c" +using namespace Pinetime::Applications::Screens; +using namespace Pinetime::Controllers; + +// ########## Definitions of gaps between and sizes of objects +// with the future vision to make this all canvas size independent :) +// -- list for date and stuff +constexpr uint8_t gap = 3; +constexpr uint8_t labelgap = 4; +constexpr uint8_t cellheight = 26; +constexpr uint8_t cellwidth = 72; +constexpr uint8_t cells_x = 34; +constexpr uint8_t cpg = cellheight + gap; +// -- end of upper stuff (all magic numbers at the moment) +constexpr uint8_t upperend = 80; // the upper arch shape portion ends at this y +constexpr uint8_t upg = upperend + gap; +// -- icon spaces +constexpr uint8_t iconrectwidth = 17; // icon space consists of a rect and a circ +constexpr uint8_t iconrect_x = 14; +constexpr uint8_t icon_x = 0; +// -- decorative bars +constexpr uint8_t bar_y = upperend - cellheight; +constexpr uint8_t barwidth = 4; +constexpr uint8_t bargap = 7; +// -- precomputed distances +constexpr uint8_t listdistance2 = upg + cpg; +constexpr uint8_t listdistance3 = upg + 2 * cpg; +constexpr uint8_t listdistance4 = upg + 3 * cpg; +constexpr uint8_t bar1_x = cells_x - 2 * bargap; +constexpr uint8_t bar2_x = cells_x - bargap; +// -- end of list part (back to magic numbers below this) +constexpr uint8_t listend = upperend + 4 * cellheight + 5 * gap; + +// ########## Color scheme +constexpr lv_color_t COLOR_LIGHTBLUE = LV_COLOR_MAKE(0x83, 0xdd, 0xff); +constexpr lv_color_t COLOR_DARKBLUE = LV_COLOR_MAKE(0x09, 0x46, 0xee); +constexpr lv_color_t COLOR_BLUE = LV_COLOR_MAKE(0x37, 0x86, 0xff); +constexpr lv_color_t COLOR_ORANGE = LV_COLOR_MAKE(0xd4, 0x5f, 0x10); +constexpr lv_color_t COLOR_DARKGRAY = LV_COLOR_MAKE(0x48, 0x60, 0x6c); +constexpr lv_color_t COLOR_LIGHTGRAY = LV_COLOR_MAKE(0xdd, 0xdd, 0xdd); +constexpr lv_color_t COLOR_BEIGE = LV_COLOR_MAKE(0xad, 0xa8, 0x8b); +constexpr lv_color_t COLOR_BROWN = LV_COLOR_MAKE(0x64, 0x44, 0x00); + +constexpr lv_color_t COLOR_DATE = LV_COLOR_MAKE(0x22, 0x22, 0x22); +constexpr lv_color_t COLOR_ICONS = LV_COLOR_MAKE(0x11, 0x11, 0x11); +constexpr lv_color_t COLOR_HEARTBEAT_ON = LV_COLOR_MAKE(0xff, 0x4f, 0x10); +constexpr lv_color_t COLOR_HEARTBEAT_OFF = COLOR_BLUE; +constexpr lv_color_t COLOR_STEPS = COLOR_BEIGE; +constexpr lv_color_t COLOR_BG = LV_COLOR_BLACK; + +// ### order of colors +// 0: time color, 1-14: shape colors in rows from top left to bottom right, +// 15: heartbeat off +// 16: heartbeat on +// no text and further icon colors yet +constexpr lv_color_t COLOR_THEME_NORM[] = {COLOR_LIGHTBLUE, + COLOR_ORANGE, + COLOR_DARKGRAY, + COLOR_BLUE, + COLOR_DARKGRAY, + COLOR_LIGHTBLUE, + COLOR_BEIGE, + COLOR_BEIGE, + COLOR_ORANGE, + COLOR_BROWN, + COLOR_LIGHTBLUE, + COLOR_DARKBLUE, + COLOR_DARKGRAY, + COLOR_BLUE, + COLOR_ORANGE, + COLOR_HEARTBEAT_OFF, + COLOR_HEARTBEAT_ON}; + +constexpr lv_color_t COLOR_THEME_AOD[] = {LV_COLOR_WHITE, + LV_COLOR_RED, + LV_COLOR_RED, + LV_COLOR_BLUE, + LV_COLOR_RED, + LV_COLOR_CYAN, + LV_COLOR_CYAN, + LV_COLOR_BLUE, + LV_COLOR_CYAN, + LV_COLOR_CYAN, + LV_COLOR_CYAN, + LV_COLOR_BLUE, + LV_COLOR_CYAN, + LV_COLOR_BLUE, + LV_COLOR_RED, + LV_COLOR_BLUE, + LV_COLOR_RED}; + +// ########### Config strings +constexpr const char* WANT_SYSTEM_FONT = "System font"; +constexpr const char* WANT_ST_FONT = "Star Trek font"; +constexpr const char* WANT_ST_FONT_BUT_NO = "Not installed"; +constexpr const char* WANT_STATIC = "Static"; +constexpr const char* WANT_ANIMATE_START = "Startup Animation"; +constexpr const char* WANT_ANIMATE_CONTINUOUS = "Continuous Anim."; +constexpr const char* WANT_ANIMATE_ALL = "All Animations"; +constexpr const char* WANT_WEATHER = "Weather"; +constexpr const char* WANT_NO_WEATHER = "No Weather"; +constexpr const char* WANT_SECONDS = "Seconds"; +constexpr const char* WANT_MINUTES = "Minutes"; +constexpr const char* TEXT_ERROR = "Error"; + +// ########## Timing constants +constexpr uint16_t SETTINGS_AUTO_CLOSE_TICKS = 5000; +constexpr uint16_t ANIMATOR_START_TICKS = 50; +constexpr uint16_t ANIMATOR_CONTINUOUS_TICKS = 500; + +// ########## Touch handler +namespace { + void event_handler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + screen->UpdateSelected(obj, event); + } +} + +WatchFaceStarTrek::WatchFaceStarTrek(const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::DateTime& dateTimeController, + Controllers::HeartRateController& heartRateController, + Controllers::Settings& settingsController, + Controllers::MotionController& motionController, + Controllers::AlarmController& alarmController, + Controllers::SimpleWeatherService& weatherService, + Controllers::FS& filesystem, + Controllers::Timer& timer) + + : batteryController {batteryController}, + bleController {bleController}, + dateTimeController {dateTimeController}, + heartRateController {heartRateController}, + settingsController {settingsController}, + motionController {motionController}, + alarmController {alarmController}, + weatherService {weatherService}, + filesystem {filesystem}, + timer {timer}, + currentDateTime {{}}, + batteryIcon(true) { + + lfs_file f = {}; + if (filesystem.FileOpen(&f, "/fonts/edge_of_the_galaxy.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + starTrekFontAvailable = true; + } + if (settingsController.GetStarTrekUseSystemFont()) { + font_time = &jetbrains_mono_extrabold_compressed; + } else { + if (starTrekFontAvailable) { + font_StarTrek = lv_font_load("F:/fonts/edge_of_the_galaxy.bin"); + font_time = font_StarTrek; + } else { + font_time = &jetbrains_mono_extrabold_compressed; + } + } + + animatorContinuousTick = lv_tick_get(); + Settings::StarTrekAnimateType animateType = settingsController.GetStarTrekAnimate(); + if (animateType == Settings::StarTrekAnimateType::All || animateType == Settings::StarTrekAnimateType::Start) { + drawWatchFace(false); + startStartAnimation(); + } else { + drawWatchFace(); + } + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceStarTrek::~WatchFaceStarTrek() { + lv_task_del(taskRefresh); + + if (font_StarTrek != nullptr) { + lv_font_free(font_StarTrek); + } + + lv_obj_clean(lv_scr_act()); +} + +// ########## Watch face drawing ############################################### + +void WatchFaceStarTrek::drawWatchFace(bool visible) { + + // preparation for future color theme selection + const lv_color_t* colors = COLOR_THEME_NORM; + + topRightRect = rect(visible, 84, 11, 156, 0, colors[4]); + upperShape[0] = rect(visible, 85, 11, 68, 0, colors[3]); + upperShape[1] = rect(visible, 72, 46, 34, 34, colors[3]); + upperShape[2] = rect(visible, 36, 80, 70, 0, colors[3]); + upperShape[3] = rect(visible, 14, 14, 106, 11, colors[3]); + upperShape[4] = circ(visible, 68, 34, 0, colors[3]); + upperShape[5] = circ(visible, 28, 106, 11, COLOR_BG); + lowerShape[0] = circ(visible, 68, 34, 172, colors[13]); // draw these two first, because circle is to big + lowerShape[1] = rect(visible, 68, 34, 34, 172, COLOR_BG); // and has to be occluded by this + lowerShape[2] = rect(visible, 85, 11, 68, 229, colors[13]); + lowerShape[3] = rect(visible, 72, 240 - listend - 34, 34, listend, colors[13]); + lowerShape[4] = rect(visible, 36, 240 - listend, 70, listend, colors[13]); + lowerShape[5] = rect(visible, 14, 14, 106, 215, colors[13]); + lowerShape[6] = circ(visible, 28, 106, 201, COLOR_BG); + bottomRightRect = rect(visible, 84, 11, 156, 229, colors[14]); + + listItem1[0] = rect(visible, cellwidth, cellheight, cells_x, upg, colors[6]); + listItem2[0] = rect(visible, cellwidth, cellheight, cells_x, listdistance2, colors[8]); + listItem3[0] = rect(visible, cellwidth, cellheight, cells_x, listdistance3, colors[10]); + listItem4[0] = rect(visible, cellwidth, cellheight, cells_x, listdistance4, colors[12]); + listItem1[1] = rect(visible, iconrectwidth, cellheight, iconrect_x, upg, colors[5]); + listItem2[1] = rect(visible, iconrectwidth, cellheight, iconrect_x, listdistance2, colors[7]); + listItem3[1] = rect(visible, iconrectwidth, cellheight, iconrect_x, listdistance3, colors[9]); + listItem4[1] = rect(visible, iconrectwidth, cellheight, iconrect_x, listdistance4, colors[11]); + listItem1[2] = circ(visible, cellheight, icon_x, upg, colors[5]); + listItem2[2] = circ(visible, cellheight, icon_x, listdistance2, colors[7]); + listItem3[2] = circ(visible, cellheight, icon_x, listdistance3, colors[9]); + listItem4[2] = circ(visible, cellheight, icon_x, listdistance4, colors[11]); + + bar1 = rect(visible, barwidth, cellheight, bar1_x, bar_y, colors[1]); + bar2 = rect(visible, barwidth, cellheight, bar2_x, bar_y, colors[2]); + + // small brackets + constexpr uint8_t bracket_y = 183; + constexpr uint8_t leftbracket_x = 110; + constexpr uint8_t rightbracket_x = 220; + constexpr uint8_t bracket_rect_w = 3; + constexpr uint8_t bracket_rect_h = 22; + constexpr uint8_t bracket_rect_y = bracket_y + 8; + constexpr uint8_t leftbracket_rect_x = leftbracket_x - 1; + constexpr uint8_t rightbracket_rect_x = rightbracket_x + 14; + imgBracketLeft = lv_img_create(lv_scr_act(), nullptr); + lv_obj_set_hidden(imgBracketLeft, !visible); + lv_img_set_src(imgBracketLeft, &bracket_left); + lv_obj_set_pos(imgBracketLeft, leftbracket_x, bracket_y); + rectBracketLeft = rect(visible, bracket_rect_w, bracket_rect_h, leftbracket_rect_x, bracket_rect_y, COLOR_DARKBLUE); + imgBracketRight = lv_img_create(lv_scr_act(), nullptr); + lv_obj_set_hidden(imgBracketRight, !visible); + lv_img_set_src(imgBracketRight, &bracket_right); + lv_obj_set_pos(imgBracketRight, rightbracket_x, bracket_y); + rectBracketRight = rect(visible, bracket_rect_w, bracket_rect_h, rightbracket_rect_x, bracket_rect_y, COLOR_DARKBLUE); + + batteryIcon.Create(lv_scr_act()); + batteryIcon.SetColor(COLOR_ICONS); + batteryIcon.SetVisible(visible); + lv_obj_align(batteryIcon.GetObject(), listItem1[1], LV_ALIGN_CENTER, -gap, 0); + timerOrAlarmSetIcon = label(visible, COLOR_ICONS, listItem2[1], LV_ALIGN_CENTER, -gap, 0, ""); + bleIcon = label(visible, COLOR_ICONS, listItem3[1], LV_ALIGN_CENTER, -gap, 0, Symbols::bluetooth); + batteryPlugOrNotificationStatus = label(visible, COLOR_ICONS, listItem4[1], LV_ALIGN_CENTER, -gap, 0, Symbols::plug); + + label_dayname = label(visible, COLOR_DATE, listItem1[0], LV_ALIGN_IN_LEFT_MID, labelgap); + label_day = label(visible, COLOR_DATE, listItem2[0], LV_ALIGN_IN_LEFT_MID, labelgap); + label_month = label(visible, COLOR_DATE, listItem3[0], LV_ALIGN_IN_LEFT_MID, labelgap); + label_year = label(visible, COLOR_DATE, listItem4[0], LV_ALIGN_IN_LEFT_MID, labelgap); + + hourAnchor = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(hourAnchor, ""); + minuteAnchor = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(minuteAnchor, ""); + setTimeAnchorForDisplaySeconds(settingsController.GetStarTrekDisplaySeconds()); + label_time_hour_1 = label(true, colors[0], hourAnchor, LV_ALIGN_OUT_RIGHT_MID, 2); + lv_obj_set_style_local_text_font(label_time_hour_1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + label_time_hour_10 = label(true, colors[0], hourAnchor, LV_ALIGN_OUT_LEFT_MID, -2); + lv_obj_set_style_local_text_font(label_time_hour_10, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + label_time_min_1 = label(true, colors[0], minuteAnchor, LV_ALIGN_OUT_RIGHT_MID, 2); + lv_obj_set_style_local_text_font(label_time_min_1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + label_time_min_10 = label(true, colors[0], minuteAnchor, LV_ALIGN_OUT_LEFT_MID, -2); + lv_obj_set_style_local_text_font(label_time_min_10, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + label_time_seconds = label(settingsController.GetStarTrekDisplaySeconds(), colors[0], minuteAnchor, LV_ALIGN_CENTER, 0, 46, "00"); + label_time_ampm = label(visible, COLOR_DATE, upperShape[2], LV_ALIGN_IN_BOTTOM_RIGHT, -30, -30); + + temperature = label(visible, COLOR_DATE, upperShape[1], LV_ALIGN_IN_BOTTOM_LEFT, labelgap, -2); + weatherIcon = label(visible, COLOR_ICONS, temperature, LV_ALIGN_OUT_TOP_LEFT, gap, -gap); + lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + heartbeatIcon = label(visible, COLOR_HEARTBEAT_OFF, lowerShape[3], LV_ALIGN_IN_TOP_LEFT, 6, gap, Symbols::heartBeat); + heartbeatValue = label(visible, COLOR_ICONS, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5); + stepIcon = label(visible, COLOR_STEPS, imgBracketLeft, LV_ALIGN_OUT_RIGHT_MID, -2, 0, Symbols::shoe); + stepValue = label(visible, COLOR_STEPS, stepIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0, "0"); + stepBar = lv_bar_create(lv_scr_act(), nullptr); + lv_obj_set_hidden(stepBar, !visible); + lv_obj_set_style_local_bg_color(stepBar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, COLOR_STEPS); + lv_obj_set_size(stepBar, 92, 3); + lv_obj_align(stepBar, imgBracketLeft, LV_ALIGN_OUT_RIGHT_BOTTOM, 1, 0); +} + +void WatchFaceStarTrek::setTimeAnchorForDisplaySeconds(bool displaySeconds) { + constexpr uint8_t TA_X = 175; + constexpr uint8_t TA_NOSEC_H_Y = 47; + constexpr uint8_t TA_NOSEC_M_Y = 122; + constexpr int8_t TA_SEC_SHIFT_Y = -6; + if (displaySeconds) { + lv_obj_set_pos(hourAnchor, TA_X, TA_NOSEC_H_Y + TA_SEC_SHIFT_Y); + lv_obj_set_pos(minuteAnchor, TA_X, TA_NOSEC_M_Y + TA_SEC_SHIFT_Y); + } else { + lv_obj_set_pos(hourAnchor, TA_X, TA_NOSEC_H_Y); + lv_obj_set_pos(minuteAnchor, TA_X, TA_NOSEC_M_Y); + } +} + +void WatchFaceStarTrek::realignTime() { + lv_obj_realign(label_time_hour_1); + lv_obj_realign(label_time_hour_10); + lv_obj_realign(label_time_min_1); + lv_obj_realign(label_time_min_10); + lv_obj_realign(label_time_seconds); +} + +lv_obj_t* WatchFaceStarTrek::rect(bool visible, uint8_t w, uint8_t h, uint8_t x, uint8_t y, lv_color_t color) { + lv_obj_t* rect = _base(visible, w, h, x, y, color); + lv_obj_set_style_local_radius(rect, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, 0); + return rect; +} + +lv_obj_t* WatchFaceStarTrek::circ(bool visible, uint8_t d, uint8_t x, uint8_t y, lv_color_t color) { + lv_obj_t* circ = _base(visible, d, d, x, y, color); + lv_obj_set_style_local_radius(circ, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); + return circ; +} + +lv_obj_t* WatchFaceStarTrek::_base(bool visible, uint8_t w, uint8_t h, uint8_t x, uint8_t y, lv_color_t color) { + lv_obj_t* base = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_hidden(base, !visible); + lv_obj_set_style_local_bg_color(base, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); + lv_obj_set_size(base, w, h); + lv_obj_set_pos(base, x, y); + return base; +} + +lv_obj_t* WatchFaceStarTrek::label(bool visible, + lv_color_t color, + lv_obj_t* alignto, + lv_align_t alignmode, + int16_t gapx, + int16_t gapy, + const char* text, + lv_obj_t* base) { + lv_obj_t* label = lv_label_create(base, nullptr); + lv_obj_set_hidden(label, !visible); + lv_obj_set_style_local_text_color(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); + lv_label_set_text_static(label, text); + lv_obj_align(label, alignto, alignmode, gapx, gapy); + return label; +} + +lv_obj_t* WatchFaceStarTrek::button(bool visible, + uint16_t sizex, + uint16_t sizey, + lv_obj_t* alignto, + lv_align_t alignmode, + int16_t gapx, + int16_t gapy) { + lv_obj_t* btn = lv_btn_create(lv_scr_act(), nullptr); + lv_obj_set_hidden(btn, !visible); + btn->user_data = this; + lv_obj_set_size(btn, sizex, sizey); + lv_obj_align(btn, alignto, alignmode, gapx, gapy); + lv_obj_set_style_local_bg_opa(btn, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_set_event_cb(btn, event_handler); + return btn; +} + +// ########## Watch face content updates ####################################### + +void WatchFaceStarTrek::Refresh() { + powerPresent = batteryController.IsPowerPresent(); + if (powerPresent.IsUpdated()) { + lv_label_set_text_static(batteryPlugOrNotificationStatus, BatteryIcon::GetPlugIcon(powerPresent.Get())); + // fontawesome and material icons have slightly different y alignment. + lv_obj_align(batteryPlugOrNotificationStatus, listItem4[1], LV_ALIGN_CENTER, -gap, 0); + // if charge off, possibly display status again + notificationStatus.MarkUpdated(); + } + // TODO figure out the logic here + if (!powerPresent.Get()) { + notificationStatus = settingsController.GetNotificationStatus(); + if (notificationStatus.IsUpdated()) { + if (notificationStatus.Get() == Controllers::Settings::Notification::Off) { + lv_label_set_text_static(batteryPlugOrNotificationStatus, Symbols::notificationsOff); + } else if (notificationStatus.Get() == Controllers::Settings::Notification::Sleep) { + lv_label_set_text_static(batteryPlugOrNotificationStatus, Symbols::sleep); + } else { + lv_label_set_text_static(batteryPlugOrNotificationStatus, ""); + } + lv_obj_align(batteryPlugOrNotificationStatus, listItem4[1], LV_ALIGN_CENTER, -gap, 1); + } + } + + batteryPercentRemaining = batteryController.PercentRemaining(); + if (batteryPercentRemaining.IsUpdated()) { + auto batteryPercent = batteryPercentRemaining.Get(); + batteryIcon.SetBatteryPercentage(batteryPercent); + } + + bleState = bleController.IsConnected(); + bleRadioEnabled = bleController.IsRadioEnabled(); + if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { + lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get())); + } + + alarmEnabled = alarmController.IsEnabled(); + timerEnabled = timer.IsRunning(); + if (alarmEnabled.IsUpdated() || timerEnabled.IsUpdated()) { + if (timerEnabled.Get()) { + lv_label_set_text_static(timerOrAlarmSetIcon, Symbols::hourGlass); + } else if (alarmEnabled.Get()) { + lv_label_set_text_static(timerOrAlarmSetIcon, Symbols::bell); + } else { + lv_label_set_text_static(timerOrAlarmSetIcon, ""); + } + lv_obj_realign(timerOrAlarmSetIcon); + } + + currentDateTime = dateTimeController.CurrentDateTime(); + + if (currentDateTime.IsUpdated()) { + auto hour = dateTimeController.Hours(); + auto minute = dateTimeController.Minutes(); + auto second = dateTimeController.Seconds(); + auto year = dateTimeController.Year(); + auto month = dateTimeController.Month(); + auto dayOfWeek = dateTimeController.DayOfWeek(); + auto day = dateTimeController.Day(); + + if (settingsController.GetStarTrekDisplaySeconds()) { + if (displayedSecond != second) { + lv_label_set_text_fmt(label_time_seconds, "%02d", second); + } + } + + if (displayedMinute != minute || displayedHour != hour) { + displayedHour = hour; + displayedMinute = minute; + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + char ampmChar[3] = "AM"; + if (hour == 0) { + hour = 12; + } else if (hour == 12) { + ampmChar[0] = 'P'; + } else if (hour > 12) { + hour = hour - 12; + ampmChar[0] = 'P'; + } + lv_label_set_text(label_time_ampm, ampmChar); + } + lv_label_set_text_fmt(label_time_hour_1, "%d", hour % 10); + lv_label_set_text_fmt(label_time_hour_10, "%d", hour / 10); + lv_label_set_text_fmt(label_time_min_1, "%d", minute % 10); + lv_label_set_text_fmt(label_time_min_10, "%d", minute / 10); + realignTime(); + } + + if ((day != currentDay) || (dayOfWeek != currentDayOfWeek) || (month != currentMonth) || (year != currentYear)) { + lv_label_set_text_fmt(label_dayname, "%s", dateTimeController.DayOfWeekShortToString()); + lv_label_set_text_fmt(label_day, "%02d", day); + lv_label_set_text_fmt(label_month, "%s", dateTimeController.MonthShortToString()); + lv_label_set_text_fmt(label_year, "%d", year); + currentYear = year; + currentMonth = month; + currentDayOfWeek = dayOfWeek; + currentDay = day; + } + } + + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get()) { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_HEARTBEAT_ON); + lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get()); + } else { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_HEARTBEAT_OFF); + lv_label_set_text_static(heartbeatValue, ""); + } + lv_obj_realign(heartbeatValue); + } + + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + lv_obj_realign(stepValue); + lv_bar_set_value(stepBar, stepCount.Get() * 100 / settingsController.GetStepsGoal(), LV_ANIM_OFF); + } + + if (settingsController.GetStarTrekWeather()) { + currentWeather = weatherService.Current(); + bool isUpdated = currentWeather.IsUpdated(); + auto optCurrentWeather = currentWeather.Get(); + + if (optCurrentWeather) { + if (isUpdated) { + int16_t temp = optCurrentWeather->temperature.Celsius(); + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.Fahrenheit(); + } + lv_label_set_text_fmt(temperature, "%d°", temp); + lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId)); + lv_obj_realign(temperature); + lv_obj_realign(weatherIcon); + } + } else { + lv_label_set_text_static(temperature, "--"); + lv_label_set_text(weatherIcon, ""); + lv_obj_realign(temperature); + lv_obj_realign(weatherIcon); + } + } + + if (showingMenu) { + if ((settingsAutoCloseTick > 0) && (lv_tick_get() - settingsAutoCloseTick > SETTINGS_AUTO_CLOSE_TICKS)) { + destroyMenu(); + settingsAutoCloseTick = 0; + } + } + + Settings::StarTrekAnimateType animateType = settingsController.GetStarTrekAnimate(); + if (animateType != Settings::StarTrekAnimateType::None && !aodNoAnimations) { + if (animateType == Settings::StarTrekAnimateType::All || animateType == Settings::StarTrekAnimateType::Start) { + if (!startAnimationFinished && lv_tick_get() - animatorStartTick > ANIMATOR_START_TICKS) { + animateStartStep(); + } + } + if (animateType == Settings::StarTrekAnimateType::All || animateType == Settings::StarTrekAnimateType::Continuous) { + if (lv_tick_get() - animatorContinuousTick > ANIMATOR_CONTINUOUS_TICKS) { + animateContinuousStep(); + } + } + } +} + +// ########## Style and animations ############################################# + +void WatchFaceStarTrek::updateFontTime() { + lv_obj_set_style_local_text_font(label_time_hour_1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + lv_obj_align(label_time_hour_1, hourAnchor, LV_ALIGN_OUT_RIGHT_MID, 2, 0); + lv_obj_set_style_local_text_font(label_time_hour_10, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + lv_obj_align(label_time_hour_10, hourAnchor, LV_ALIGN_OUT_LEFT_MID, -2, 0); + lv_obj_set_style_local_text_font(label_time_min_1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + lv_obj_align(label_time_min_1, minuteAnchor, LV_ALIGN_OUT_RIGHT_MID, 2, 0); + lv_obj_set_style_local_text_font(label_time_min_10, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_time); + lv_obj_align(label_time_min_10, minuteAnchor, LV_ALIGN_OUT_LEFT_MID, -2, 0); +} + +void WatchFaceStarTrek::animateStartStep() { + switch (animatorStartStage) { + case 0: + lv_obj_set_hidden(topRightRect, false); + setShapeVisible(upperShape, PART_COUNT_UPPER_SHAPE, true); + lv_obj_set_hidden(weatherIcon, false); + lv_obj_set_hidden(temperature, false); + lv_obj_set_hidden(label_time_ampm, false); + break; + case 1: + setShapeVisible(listItem1, PART_COUNT_LIST_ITEM, true); + lv_obj_set_hidden(label_dayname, false); + batteryIcon.SetVisible(true); + break; + case 2: + setShapeVisible(listItem2, PART_COUNT_LIST_ITEM, true); + lv_obj_set_hidden(label_day, false); + lv_obj_set_hidden(timerOrAlarmSetIcon, false); + break; + case 3: + setShapeVisible(listItem3, PART_COUNT_LIST_ITEM, true); + lv_obj_set_hidden(label_month, false); + lv_obj_set_hidden(bleIcon, false); + break; + case 4: + setShapeVisible(listItem4, PART_COUNT_LIST_ITEM, true); + lv_obj_set_hidden(label_year, false); + lv_obj_set_hidden(batteryPlugOrNotificationStatus, false); + break; + case 5: + setShapeVisible(lowerShape, PART_COUNT_LOWER_SHAPE, true); + lv_obj_set_hidden(bottomRightRect, false); + break; + case 6: + lv_obj_set_hidden(bar1, false); + lv_obj_set_hidden(bar2, false); + lv_obj_set_hidden(imgBracketLeft, false); + lv_obj_set_hidden(rectBracketLeft, false); + lv_obj_set_hidden(imgBracketRight, false); + lv_obj_set_hidden(rectBracketRight, false); + lv_obj_set_hidden(heartbeatIcon, false); + lv_obj_set_hidden(heartbeatValue, false); + lv_obj_set_hidden(stepIcon, false); + lv_obj_set_hidden(stepValue, false); + lv_obj_set_hidden(stepBar, false); + startAnimationFinished = true; + break; + } + animatorStartStage++; + animatorStartTick = lv_tick_get(); +} + +void WatchFaceStarTrek::animateContinuousStep() { + // if Start animation is also active, only act if it is already finished + if (settingsController.GetStarTrekAnimate() == Settings::StarTrekAnimateType::All && !startAnimationFinished) + return; + + // flash the brackets + bool hidden = animatorContinuousStage == 7; + if (animatorContinuousStage > 6 && animatorContinuousStage < 9) { + lv_obj_set_hidden(imgBracketLeft, hidden); + lv_obj_set_hidden(imgBracketRight, hidden); + lv_obj_set_hidden(stepBar, hidden); + } + + // walk down list with color change, change some panel colors + switch (animatorContinuousStage) { + case 0: + lv_obj_set_style_local_text_color(label_dayname, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_LIGHTGRAY); + break; + case 1: + lv_obj_set_hidden(bar1, true); + break; + case 2: + lv_obj_set_style_local_text_color(label_dayname, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + lv_obj_set_style_local_text_color(label_day, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_LIGHTGRAY); + lv_obj_set_hidden(bar2, true); + break; + case 3: + lv_obj_set_hidden(bar1, false); + lv_obj_set_style_local_bg_color(listItem3[0], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DARKBLUE); + break; + case 4: + lv_obj_set_style_local_text_color(label_day, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + lv_obj_set_style_local_text_color(label_month, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_LIGHTGRAY); + lv_obj_set_hidden(bar2, false); + break; + case 5: + lv_obj_set_style_local_bg_color(topRightRect, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_BROWN); + break; + case 6: + lv_obj_set_style_local_text_color(label_month, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + lv_obj_set_style_local_text_color(label_year, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_LIGHTGRAY); + lv_obj_set_hidden(rectBracketLeft, true); + lv_obj_set_hidden(rectBracketRight, true); + break; + case 7: + lv_obj_set_style_local_bg_color(listItem3[0], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_LIGHTBLUE); + break; + case 8: + lv_obj_set_style_local_text_color(label_year, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + break; + case 9: + lv_obj_set_hidden(rectBracketLeft, false); + lv_obj_set_hidden(rectBracketRight, false); + break; + case 10: + lv_obj_set_style_local_bg_color(topRightRect, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DARKGRAY); + break; + case 11: + animatorContinuousStage = 255; // overflows to 0 below + break; + } + animatorContinuousStage++; + animatorContinuousTick = lv_tick_get(); +} + +void WatchFaceStarTrek::resetAnimateColors() { + lv_obj_set_style_local_text_color(label_dayname, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + lv_obj_set_style_local_text_color(label_day, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + lv_obj_set_style_local_text_color(label_month, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + lv_obj_set_style_local_text_color(label_year, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_DATE); + lv_obj_set_style_local_bg_color(listItem3[0], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_THEME_NORM[10]); + lv_obj_set_style_local_bg_color(topRightRect, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_THEME_NORM[4]); +} + +void WatchFaceStarTrek::resetAnimateVisibility() { + lv_obj_set_hidden(imgBracketLeft, false); + lv_obj_set_hidden(imgBracketRight, false); + lv_obj_set_hidden(stepBar, false); + lv_obj_set_hidden(rectBracketLeft, false); + lv_obj_set_hidden(rectBracketRight, false); + lv_obj_set_hidden(bar1, false); + lv_obj_set_hidden(bar2, false); +} + +void WatchFaceStarTrek::setShapeVisible(lv_obj_t** shape, uint8_t partcount, bool visible) { + for (uint8_t i = 0; i < partcount; i++) { + lv_obj_set_hidden(shape[i], !visible); + } +} + +void WatchFaceStarTrek::setVisible(bool visible) { + lv_obj_set_hidden(topRightRect, !visible); + setShapeVisible(upperShape, PART_COUNT_UPPER_SHAPE, visible); + lv_obj_set_hidden(label_time_ampm, !visible); + setShapeVisible(listItem1, PART_COUNT_LIST_ITEM, visible); + lv_obj_set_hidden(label_dayname, !visible); + batteryIcon.SetVisible(visible); + setShapeVisible(listItem2, PART_COUNT_LIST_ITEM, visible); + lv_obj_set_hidden(label_day, !visible); + lv_obj_set_hidden(timerOrAlarmSetIcon, !visible); + setShapeVisible(listItem3, PART_COUNT_LIST_ITEM, visible); + lv_obj_set_hidden(label_month, !visible); + lv_obj_set_hidden(bleIcon, !visible); + setShapeVisible(listItem4, PART_COUNT_LIST_ITEM, visible); + lv_obj_set_hidden(label_year, !visible); + lv_obj_set_hidden(batteryPlugOrNotificationStatus, !visible); + setShapeVisible(lowerShape, PART_COUNT_LOWER_SHAPE, visible); + lv_obj_set_hidden(bottomRightRect, !visible); + lv_obj_set_hidden(bar1, !visible); + lv_obj_set_hidden(bar2, !visible); + lv_obj_set_hidden(heartbeatIcon, !visible); + lv_obj_set_hidden(heartbeatValue, !visible); + lv_obj_set_hidden(stepIcon, !visible); + lv_obj_set_hidden(stepValue, !visible); + lv_obj_set_hidden(stepBar, !visible); + lv_obj_set_hidden(weatherIcon, !visible); + lv_obj_set_hidden(temperature, !visible); + lv_obj_set_hidden(imgBracketLeft, !visible); + lv_obj_set_hidden(rectBracketLeft, !visible); + lv_obj_set_hidden(imgBracketRight, !visible); + lv_obj_set_hidden(rectBracketRight, !visible); +} + +void WatchFaceStarTrek::aodColors(bool aodMode) { + if (aodMode) { + setColorTheme(COLOR_THEME_AOD); + } else { + setColorTheme(COLOR_THEME_NORM); + } +} + +void WatchFaceStarTrek::setShapeColor(lv_obj_t** shape, uint8_t partcount, lv_color_t color) { + for (uint8_t i = 0; i < partcount; i++) { + lv_obj_set_style_local_bg_color(shape[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); + } +} + +void WatchFaceStarTrek::setColorTheme(const lv_color_t* colors) { + lv_obj_set_style_local_bg_color(topRightRect, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[4]); + setShapeColor(upperShape, PART_COUNT_UPPER_SHAPE, colors[3]); + lv_obj_set_style_local_bg_color(upperShape[5], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_BG); + setShapeColor(lowerShape, PART_COUNT_LOWER_SHAPE, colors[13]); + lv_obj_set_style_local_bg_color(lowerShape[1], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_BG); + lv_obj_set_style_local_bg_color(lowerShape[6], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, COLOR_BG); + lv_obj_set_style_local_bg_color(bottomRightRect, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[14]); + + lv_obj_set_style_local_bg_color(listItem1[0], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[6]); + lv_obj_set_style_local_bg_color(listItem2[0], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[8]); + lv_obj_set_style_local_bg_color(listItem3[0], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[10]); + lv_obj_set_style_local_bg_color(listItem4[0], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[12]); + setShapeColor(&listItem1[1], PART_COUNT_LIST_ICON, colors[5]); + setShapeColor(&listItem2[1], PART_COUNT_LIST_ICON, colors[7]); + setShapeColor(&listItem3[1], PART_COUNT_LIST_ICON, colors[9]); + setShapeColor(&listItem4[1], PART_COUNT_LIST_ICON, colors[11]); + + lv_obj_set_style_local_bg_color(bar1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[1]); + lv_obj_set_style_local_bg_color(bar2, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[2]); + + lv_obj_set_style_local_text_color(label_time_hour_1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[0]); + lv_obj_set_style_local_text_color(label_time_hour_10, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[0]); + lv_obj_set_style_local_text_color(label_time_min_1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[0]); + lv_obj_set_style_local_text_color(label_time_min_10, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[0]); + lv_obj_set_style_local_text_color(label_time_seconds, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[0]); + + if (heartbeatRunning.Get()) { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[16]); + } else { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colors[15]); + } +} + +void WatchFaceStarTrek::startStartAnimation() { + startAnimationFinished = false; + animatorStartTick = lv_tick_get(); + animatorStartStage = 0; +} + +void WatchFaceStarTrek::startContinuousAnimation() { + animatorContinuousTick = lv_tick_get(); + animatorContinuousStage = 0; +} + +// ########## Parent overrrides ################################################ + +bool WatchFaceStarTrek::OnTouchEvent(Pinetime::Applications::TouchEvents event) { + if ((event == Pinetime::Applications::TouchEvents::LongTap) && !showingMenu) { + createMenu(); + settingsAutoCloseTick = lv_tick_get(); + return true; + } + if ((event == Pinetime::Applications::TouchEvents::DoubleTap) && showingMenu) { + return true; + } + return false; +} + +bool WatchFaceStarTrek::OnButtonPushed() { + if (showingMenu) { + destroyMenu(); + return true; + } + return false; +} + +void WatchFaceStarTrek::OnLCDWakeup(bool aodMode) { + Settings::StarTrekAnimateType animateType = settingsController.GetStarTrekAnimate(); + if (aodMode) { + aodNoAnimations = false; + aodColors(false); + } + if (!aodMode && (animateType == Settings::StarTrekAnimateType::All || animateType == Settings::StarTrekAnimateType::Start)) { + startStartAnimation(); + } + if (animateType == Settings::StarTrekAnimateType::All || animateType == Settings::StarTrekAnimateType::Continuous) { + startContinuousAnimation(); + } +} + +void WatchFaceStarTrek::OnLCDSleep(bool aodMode) { + Settings::StarTrekAnimateType animateType = settingsController.GetStarTrekAnimate(); + if (animateType == Settings::StarTrekAnimateType::All || animateType == Settings::StarTrekAnimateType::Continuous) { + resetAnimateVisibility(); + resetAnimateColors(); + } + if (animateType == Settings::StarTrekAnimateType::All || animateType == Settings::StarTrekAnimateType::Start) { + if (aodMode) { + setVisible(true); + } else { + setVisible(false); + } + } + if (aodMode) { + aodNoAnimations = true; + aodColors(true); + } +} + +// ########## Config handling ################################################## + +void WatchFaceStarTrek::createMenu() { + btnSetUseSystemFont = button(true, 224, 50, lv_scr_act(), LV_ALIGN_IN_TOP_MID, 0, 8); + const char* label_sysfont = + settingsController.GetStarTrekUseSystemFont() ? WANT_SYSTEM_FONT : (starTrekFontAvailable ? WANT_ST_FONT : WANT_ST_FONT_BUT_NO); + lblSetUseSystemFont = label(true, LV_COLOR_WHITE, btnSetUseSystemFont, LV_ALIGN_CENTER, 0, 0, label_sysfont, btnSetUseSystemFont); + btnSetAnimate = button(true, 224, 50, btnSetUseSystemFont, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 8); + lblSetAnimate = label(true, LV_COLOR_WHITE, btnSetAnimate, LV_ALIGN_CENTER, 0, 0, animateMenuButtonText(), btnSetAnimate); + btnSetWeather = button(true, 224, 50, btnSetAnimate, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 8); + const char* label_weather = settingsController.GetStarTrekWeather() ? WANT_WEATHER : WANT_NO_WEATHER; + lblSetWeather = label(true, LV_COLOR_WHITE, btnSetWeather, LV_ALIGN_CENTER, 0, 0, label_weather, btnSetWeather); + btnSetDisplaySeconds = button(true, 132, 50, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 8, -8); + const char* label_displaySeconds = settingsController.GetStarTrekDisplaySeconds() ? WANT_SECONDS : WANT_MINUTES; + lblSetDisplaySeconds = + label(true, LV_COLOR_WHITE, btnSetDisplaySeconds, LV_ALIGN_CENTER, 0, 0, label_displaySeconds, btnSetDisplaySeconds); + btnClose = button(true, 80, 50, btnSetDisplaySeconds, LV_ALIGN_OUT_RIGHT_MID, 8); + lblClose = label(true, LV_COLOR_WHITE, btnClose, LV_ALIGN_CENTER, 0, 0, "X", btnClose); + showingMenu = true; +} + +void WatchFaceStarTrek::destroyMenu() { + settingsController.SaveSettings(); + lv_obj_del(btnSetUseSystemFont); + lv_obj_del(btnSetAnimate); + lv_obj_del(btnSetDisplaySeconds); + lv_obj_del(btnSetWeather); + lv_obj_del(btnClose); + showingMenu = false; +} + +void WatchFaceStarTrek::UpdateSelected(lv_obj_t* object, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + + settingsAutoCloseTick = lv_tick_get(); + + if (object == btnClose) { + destroyMenu(); + } + + if (object == btnSetUseSystemFont) { + bool usedSystem = settingsController.GetStarTrekUseSystemFont(); + // ST font was not used and shall be used now + if (starTrekFontAvailable && usedSystem) { + lv_label_set_text_static(lblSetUseSystemFont, WANT_ST_FONT); + if (font_StarTrek == nullptr) { + font_StarTrek = lv_font_load("F:/fonts/edge_of_the_galaxy.bin"); + } + font_time = font_StarTrek; + updateFontTime(); + usedSystem = false; + } + // ST font was used and gets deactivated + else if (starTrekFontAvailable && !usedSystem) { + lv_label_set_text_static(lblSetUseSystemFont, WANT_SYSTEM_FONT); + font_time = &jetbrains_mono_extrabold_compressed; + updateFontTime(); + usedSystem = true; + } + // ST font was not used, shall be used now, but is not available + // ST font should be used by default but is not installed + else { + lv_label_set_text_static(lblSetUseSystemFont, WANT_ST_FONT_BUT_NO); + } + settingsController.SetStarTrekUseSystemFont(usedSystem); + } + + if (object == btnSetAnimate) { + Settings::StarTrekAnimateType next = animateStateCycler(settingsController.GetStarTrekAnimate()); + if (next == Settings::StarTrekAnimateType::All || next == Settings::StarTrekAnimateType::Start) { + setVisible(false); + startStartAnimation(); + } else { + setVisible(); + } + resetAnimateColors(); + settingsController.SetStarTrekAnimate(next); + lv_label_set_text_static(lblSetAnimate, animateMenuButtonText()); + } + + if (object == btnSetDisplaySeconds) { + if (settingsController.GetStarTrekDisplaySeconds()) { + settingsController.SetStarTrekDisplaySeconds(false); + setTimeAnchorForDisplaySeconds(false); + lv_obj_set_hidden(label_time_seconds, true); + } else { + settingsController.SetStarTrekDisplaySeconds(true); + setTimeAnchorForDisplaySeconds(true); + lv_obj_set_hidden(label_time_seconds, false); + } + realignTime(); + lv_label_set_text_static(lblSetDisplaySeconds, settingsController.GetStarTrekDisplaySeconds() ? WANT_SECONDS : WANT_MINUTES); + } + + if (object == btnSetWeather) { + if (settingsController.GetStarTrekWeather()) { + settingsController.SetStarTrekWeather(false); + lv_label_set_text_static(weatherIcon, ""); + lv_label_set_text_static(temperature, ""); + lv_label_set_text_static(lblSetWeather, WANT_NO_WEATHER); + } else { + settingsController.SetStarTrekWeather(true); + currentWeather.MarkUpdated(); + lv_label_set_text_static(lblSetWeather, WANT_WEATHER); + } + } + } +} + +const char* WatchFaceStarTrek::animateMenuButtonText() { + switch (settingsController.GetStarTrekAnimate()) { + case Settings::StarTrekAnimateType::All: + return WANT_ANIMATE_ALL; + case Settings::StarTrekAnimateType::Continuous: + return WANT_ANIMATE_CONTINUOUS; + case Settings::StarTrekAnimateType::Start: + return WANT_ANIMATE_START; + case Settings::StarTrekAnimateType::None: + return WANT_STATIC; + } + return TEXT_ERROR; +} + +Settings::StarTrekAnimateType WatchFaceStarTrek::animateStateCycler(Settings::StarTrekAnimateType previous) { + switch (previous) { + case Settings::StarTrekAnimateType::All: + return Settings::StarTrekAnimateType::Start; + case Settings::StarTrekAnimateType::Start: + return Settings::StarTrekAnimateType::Continuous; + case Settings::StarTrekAnimateType::Continuous: + return Settings::StarTrekAnimateType::None; + case Settings::StarTrekAnimateType::None: + return Settings::StarTrekAnimateType::All; + } + return Settings::StarTrekAnimateType::None; +} \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceStarTrek.h b/src/displayapp/screens/WatchFaceStarTrek.h new file mode 100644 index 0000000000..363806f780 --- /dev/null +++ b/src/displayapp/screens/WatchFaceStarTrek.h @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include +#include +#include "displayapp/Controllers.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/BatteryIcon.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/ble/BleController.h" +#include "components/alarm/AlarmController.h" +#include "utility/DirtyValue.h" + +constexpr uint8_t PART_COUNT_LIST_ITEM = 3; +constexpr uint8_t PART_COUNT_LIST_ICON = 2; +constexpr uint8_t PART_COUNT_UPPER_SHAPE = 6; +constexpr uint8_t PART_COUNT_LOWER_SHAPE = 7; + +namespace Pinetime { + namespace Applications { + namespace Screens { + + class WatchFaceStarTrek : public Screen { + public: + WatchFaceStarTrek(const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::DateTime& dateTimeController, + Controllers::HeartRateController& heartRateController, + Controllers::Settings& settingsController, + Controllers::MotionController& motionController, + Controllers::AlarmController& alarmController, + Controllers::SimpleWeatherService& weatherService, + Controllers::FS& filesystem, + Controllers::Timer& timer); + ~WatchFaceStarTrek() override; + + bool OnTouchEvent(TouchEvents event) override; + bool OnButtonPushed() override; + void UpdateSelected(lv_obj_t* object, lv_event_t event); + + void Refresh() override; + void OnLCDWakeup(bool aodMode) override; + void OnLCDSleep(bool aodMode) override; + + private: + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; + Controllers::DateTime& dateTimeController; + Controllers::HeartRateController& heartRateController; + Controllers::Settings& settingsController; + Controllers::MotionController& motionController; + Controllers::AlarmController& alarmController; + Controllers::SimpleWeatherService& weatherService; + Controllers::FS& filesystem; + Controllers::Timer& timer; + + lv_task_t* taskRefresh; + + uint8_t displayedHour = -1; + uint8_t displayedMinute = -1; + uint8_t displayedSecond = -1; + uint16_t currentYear = 1970; + Controllers::DateTime::Months currentMonth = Pinetime::Controllers::DateTime::Months::Unknown; + Controllers::DateTime::Days currentDayOfWeek = Pinetime::Controllers::DateTime::Days::Unknown; + uint8_t currentDay = 0; + + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue powerPresent {}; + Utility::DirtyValue notificationStatus {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue bleRadioEnabled {}; + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue alarmEnabled {}; + Utility::DirtyValue timerEnabled {}; + Utility::DirtyValue> currentWeather {}; + + lv_obj_t* label_time_hour_1; + lv_obj_t* label_time_hour_10; + lv_obj_t* label_time_min_1; + lv_obj_t* label_time_min_10; + lv_obj_t* label_time_seconds; + lv_obj_t* label_time_ampm; + lv_obj_t* label_dayname; + lv_obj_t* label_day; + lv_obj_t* label_month; + lv_obj_t* label_year; + lv_obj_t* bleIcon; + lv_obj_t* batteryPlugOrNotificationStatus; + lv_obj_t* heartbeatIcon; + lv_obj_t* heartbeatValue; + lv_obj_t* stepIcon; + lv_obj_t* stepValue; + lv_obj_t* stepBar; + BatteryIcon batteryIcon; + lv_obj_t* weatherIcon; + lv_obj_t* temperature; + lv_obj_t* timerOrAlarmSetIcon; + + // background + lv_obj_t *topRightRect, *bottomRightRect; + lv_obj_t *bar1, *bar2; + lv_obj_t* listItem1[PART_COUNT_LIST_ITEM]; + lv_obj_t* listItem2[PART_COUNT_LIST_ITEM]; + lv_obj_t* listItem3[PART_COUNT_LIST_ITEM]; + lv_obj_t* listItem4[PART_COUNT_LIST_ITEM]; + lv_obj_t* upperShape[PART_COUNT_UPPER_SHAPE]; + lv_obj_t* lowerShape[PART_COUNT_LOWER_SHAPE]; + lv_obj_t* imgBracketLeft; + lv_obj_t* rectBracketLeft; + lv_obj_t* imgBracketRight; + lv_obj_t* rectBracketRight; + lv_obj_t *minuteAnchor, *hourAnchor; + + // config menu + lv_obj_t* btnSetUseSystemFont; + lv_obj_t* lblSetUseSystemFont; + lv_obj_t* btnSetAnimate; + lv_obj_t* lblSetAnimate; + lv_obj_t* btnSetWeather; + lv_obj_t* lblSetWeather; + lv_obj_t* btnSetDisplaySeconds; + lv_obj_t* lblSetDisplaySeconds; + lv_obj_t* btnClose; + lv_obj_t* lblClose; + uint32_t settingsAutoCloseTick = 0; + bool showingMenu = false; + void createMenu(); + void destroyMenu(); + + // ### visibility functions affect everything BUT the time digits + // ### a watch should always tell the time ;) + + void drawWatchFace(bool visible = true); + void setTimeAnchorForDisplaySeconds(bool displaySeconds); + void realignTime(); + lv_obj_t* rect(bool visible, uint8_t w, uint8_t h, uint8_t x, uint8_t y, lv_color_t color); + lv_obj_t* circ(bool visible, uint8_t d, uint8_t x, uint8_t y, lv_color_t color); + lv_obj_t* _base(bool visible, uint8_t w, uint8_t h, uint8_t x, uint8_t y, lv_color_t color); + lv_obj_t* label(bool visible, + lv_color_t color, + lv_obj_t* alignto = lv_scr_act(), + lv_align_t alignmode = LV_ALIGN_CENTER, + int16_t gapx = 0, + int16_t gapy = 0, + const char* text = "", + lv_obj_t* base = lv_scr_act()); + lv_obj_t* button(bool visible, + uint16_t sizex, + uint16_t sizey, + lv_obj_t* alignto = lv_scr_act(), + lv_align_t alignmode = LV_ALIGN_CENTER, + int16_t gapx = 0, + int16_t gapy = 0); + + lv_font_t* font_time = nullptr; + lv_font_t* font_StarTrek = nullptr; + bool starTrekFontAvailable = false; + void updateFontTime(); + + bool weatherNeedsRefresh = false; + + uint32_t animatorStartTick = 0; + uint32_t animatorContinuousTick = 0; + uint8_t animatorStartStage = 0; + uint8_t animatorContinuousStage = 0; + bool startAnimationFinished = false; + const char* animateMenuButtonText(); + void setVisible(bool visible = true); + void setShapeVisible(lv_obj_t** shape, uint8_t partcount, bool visible); + void animateStartStep(); + void startStartAnimation(); + void animateContinuousStep(); + void startContinuousAnimation(); + void resetAnimateColors(); + void resetAnimateVisibility(); + Controllers::Settings::StarTrekAnimateType animateStateCycler(Controllers::Settings::StarTrekAnimateType previous); + + bool aodNoAnimations = false; + void aodColors(bool aodMode); + + void setColorTheme(const lv_color_t* colors); + void setShapeColor(lv_obj_t** shape, uint8_t partcount, lv_color_t color); + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::StarTrek; + static constexpr const char* name = "Star Trek"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceStarTrek(controllers.batteryController, + controllers.bleController, + controllers.dateTimeController, + controllers.heartRateController, + controllers.settingsController, + controllers.motionController, + controllers.alarmController, + *controllers.weatherController, + controllers.filesystem, + controllers.timer); + }; + + static bool IsAvailable(Pinetime::Controllers::FS&) { + return true; + } + }; + } +} \ No newline at end of file diff --git a/src/displayapp/screens/settings/SettingWatchFace.h b/src/displayapp/screens/settings/SettingWatchFace.h index 9edc1f7ac8..9f71f37eb1 100644 --- a/src/displayapp/screens/settings/SettingWatchFace.h +++ b/src/displayapp/screens/settings/SettingWatchFace.h @@ -11,6 +11,7 @@ #include "displayapp/screens/CheckboxList.h" #include "displayapp/screens/WatchFaceInfineat.h" #include "displayapp/screens/WatchFaceCasioStyleG7710.h" +#include "displayapp/screens/WatchFaceStarTrek.h" namespace Pinetime { diff --git a/src/resources/fonts.json b/src/resources/fonts.json index c4a63349a1..cddfe1ff0b 100644 --- a/src/resources/fonts.json +++ b/src/resources/fonts.json @@ -58,5 +58,17 @@ "size": 115, "format": "bin", "target_path": "/fonts/" + }, + "edge_of_the_galaxy" : { + "sources": [ + { + "file": "fonts/EdgeOfTheGalaxyRegular.ttf", + "symbols": "1234567890" + } + ], + "bpp": 1, + "size": 80, + "format": "bin", + "target_path": "/fonts/" } } diff --git a/src/resources/fonts/EdgeOfTheGalaxyRegular.ttf b/src/resources/fonts/EdgeOfTheGalaxyRegular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bd09fda02f1c6e5b75e39533ce90517b0df5baee GIT binary patch literal 26744 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 NFX11%Jami5{{jtdpIiU{ literal 0 HcmV?d00001