diff --git a/doc/gettingStarted/Watchfaces.md b/doc/gettingStarted/Watchfaces.md index 9edff0bb51..67fc083c63 100644 --- a/doc/gettingStarted/Watchfaces.md +++ b/doc/gettingStarted/Watchfaces.md @@ -29,3 +29,8 @@ InfiniTime has 6 apps on the `main` branch at the time of writing. ### Casio G7710 ![Casio G7710 face](/doc/gettingStarted/Watchfaces/CasioG7710.png) + +### Garden +![Garden face](/doc/gettingStarted/Watchfaces/Garden.png) + - Flowers "grow" throughout the day + - Different flowers each day diff --git a/doc/gettingStarted/Watchfaces/Garden.png b/doc/gettingStarted/Watchfaces/Garden.png new file mode 100644 index 0000000000..70d7194e70 Binary files /dev/null and b/doc/gettingStarted/Watchfaces/Garden.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd2e656a4..0883615e34 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -403,6 +403,7 @@ list(APPEND SOURCE_FILES displayapp/widgets/PageIndicator.cpp displayapp/widgets/DotIndicator.cpp displayapp/widgets/StatusIcons.cpp + displayapp/widgets/Flower.cpp ## Settings displayapp/screens/settings/QuickSettings.cpp @@ -428,6 +429,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp displayapp/screens/WatchFacePrideFlag.cpp + displayapp/screens/WatchFaceGarden.cpp ## diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 8dc114429f..aeb2e0b63b 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -15,6 +15,7 @@ #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" #include "displayapp/screens/WatchFacePrideFlag.h" +#include "displayapp/screens/WatchFaceGarden.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index f6feeb7b6d..b5fd694b5d 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -54,6 +54,7 @@ namespace Pinetime { Infineat, CasioStyleG7710, PrideFlag, + Garden, }; template diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..6fe5c9bb3a 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -29,6 +29,7 @@ else() 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::PrideFlag") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Garden") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/fonts/AzeretMono-Regular.ttf b/src/displayapp/fonts/AzeretMono-Regular.ttf new file mode 100644 index 0000000000..cffa6ebc24 Binary files /dev/null and b/src/displayapp/fonts/AzeretMono-Regular.ttf differ diff --git a/src/displayapp/fonts/CMakeLists.txt b/src/displayapp/fonts/CMakeLists.txt index 562f0801ad..a3b8cbc338 100644 --- a/src/displayapp/fonts/CMakeLists.txt +++ b/src/displayapp/fonts/CMakeLists.txt @@ -1,6 +1,6 @@ set(FONTS jetbrains_mono_42 jetbrains_mono_76 jetbrains_mono_bold_20 jetbrains_mono_extrabold_compressed lv_font_sys_48 - open_sans_light fontawesome_weathericons) + open_sans_light fontawesome_weathericons azeret_mono) find_program(LV_FONT_CONV "lv_font_conv" NO_CACHE REQUIRED HINTS "${CMAKE_SOURCE_DIR}/node_modules/.bin") message(STATUS "Using ${LV_FONT_CONV} to generate font files") diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index fea3160572..7e40315a01 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -73,5 +73,15 @@ ], "bpp": 1, "size": 25 + }, + "azeret_mono": { + "sources": [ + { + "file": "AzeretMono-Regular.ttf", + "range": "0x30-0x3a" + } + ], + "bpp": 4, + "size": 82 } } diff --git a/src/displayapp/screens/WatchFaceGarden.cpp b/src/displayapp/screens/WatchFaceGarden.cpp new file mode 100644 index 0000000000..b35ecf5d52 --- /dev/null +++ b/src/displayapp/screens/WatchFaceGarden.cpp @@ -0,0 +1,258 @@ +#include "displayapp/screens/WatchFaceGarden.h" + +#include + +#include "displayapp/screens/NotificationIcon.h" +#include "displayapp/screens/Symbols.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/ble/NotificationManager.h" +#include "components/heartrate/HeartRateController.h" +#include "components/motion/MotionController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/settings/Settings.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceGarden::WatchFaceGarden(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + notificationManager {notificationManager}, + settingsController {settingsController}, + heartRateController {heartRateController}, + motionController {motionController}, + weatherService {weatherService}, + statusIcons(batteryController, bleController, alarmController) { + + statusIcons.Create(); + + notificationIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x5FFF5F)); + lv_label_set_text_static(notificationIcon, ""); + lv_obj_align(notificationIcon, statusIcons.GetObject(), LV_ALIGN_OUT_LEFT_MID, -5, 0); + + heartbeatIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(heartbeatIcon, Symbols::heartBeat); + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xFF5F5F)); + lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 0, 0); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xFF5F5F)); + lv_label_set_text_static(heartbeatValue, ""); + lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + + stepIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x5F8FFF)); + lv_label_set_text_static(stepIcon, Symbols::shoe); + lv_obj_align(stepIcon, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 70, 0); + + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x5F8FFF)); + lv_label_set_text_static(stepValue, "0"); + lv_obj_align(stepValue, stepIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + + garden = lv_cont_create(lv_scr_act(), nullptr); + lv_obj_set_size(garden, 240, 217); + lv_obj_set_style_local_radius(garden, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + lv_obj_set_style_local_bg_grad_dir(garden, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_GRAD_DIR_VER); + lv_obj_align(garden, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + + label_time_colon = lv_label_create(garden, nullptr); + lv_label_set_text(label_time_colon, ":"); + lv_obj_set_style_local_text_font(label_time_colon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &azeret_mono); + lv_obj_align(label_time_colon, garden, LV_ALIGN_IN_TOP_MID, 0, 10); + + label_time_hour = lv_label_create(garden, nullptr); + lv_obj_set_style_local_text_font(label_time_hour, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &azeret_mono); + lv_obj_align(label_time_hour, label_time_colon, LV_ALIGN_OUT_LEFT_MID, 12, 0); + + label_time_min = lv_label_create(garden, nullptr); + lv_obj_set_style_local_text_font(label_time_min, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &azeret_mono); + lv_obj_align(label_time_min, label_time_colon, LV_ALIGN_OUT_RIGHT_MID, -12, 0); + + label_time_ampm = lv_label_create(garden, nullptr); + lv_label_set_text(label_time_ampm, ""); + lv_obj_align(label_time_ampm, label_time_colon, LV_ALIGN_IN_TOP_MID, 0, -7); + + label_date = lv_label_create(garden, nullptr); + lv_obj_align(label_date, garden, LV_ALIGN_IN_TOP_LEFT, 4, 75); + + temperature = lv_label_create(garden, nullptr); + lv_label_set_text(temperature, ""); + lv_obj_align(temperature, garden, LV_ALIGN_IN_TOP_RIGHT, -4, 75); + + weatherIcon = lv_label_create(garden, nullptr); + lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text(weatherIcon, ""); + lv_obj_align(weatherIcon, temperature, LV_ALIGN_OUT_LEFT_MID, -3, 0); + + for (int k = 0; k < NUM_FLOWERS; k++) { + flowers[k].Create(garden); + lv_obj_align(flowers[k].GetObject(), garden, LV_ALIGN_IN_BOTTOM_MID, X_POS_OFFSET + (X_POS_STEP * k), 0); + } + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceGarden::~WatchFaceGarden() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceGarden::Refresh() { + bool refreshGarden = false; + bool realignNotification = statusIcons.Update(); + + notificationState = notificationManager.AreNewNotificationsAvailable(); + if (notificationState.IsUpdated()) { + lv_label_set_text(notificationIcon, (notificationState.Get() ? Symbols::lapsFlag : "")); + realignNotification = true; + } + + if (realignNotification) { + lv_obj_realign(notificationIcon); + } + + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + + if (currentDateTime.IsUpdated()) { + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + int offset = 0; + + if (hour < 6) { + stage = 0; + } else if (hour < 9) { + stage = 1; + } else if (hour < 11) { + stage = 2; + } else if (hour < 13) { + stage = 3; + } else if (hour < 20) { + stage = 4; + } else if (hour < 23) { + stage = 5; + } else { + stage = 0; + } + + if (stage.IsUpdated()) { + lv_obj_set_style_local_bg_color(garden, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(BG_COLORS[stage.Get() * 2])); + lv_obj_set_style_local_bg_grad_color(garden, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(BG_COLORS[stage.Get() * 2 + 1])); + refreshGarden = true; + } + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + lv_label_set_text(label_time_ampm, (hour >= 12 ? "PM" : "AM")); + + if (hour == 0) { + hour = 12; + } else if (hour > 12) { + hour = hour - 12; + } + lv_label_set_text_fmt(label_time_hour, "%2d", hour); + + if (hour < 10) { + offset = -24; // center time display + } + } else { + lv_label_set_text_fmt(label_time_hour, "%02d", hour); + } + lv_obj_align(label_time_colon, garden, LV_ALIGN_IN_TOP_MID, offset, 7); + lv_obj_realign(label_time_hour); + lv_obj_realign(label_time_ampm); + + lv_label_set_text_fmt(label_time_min, "%02d", minute); + lv_obj_realign(label_time_min); + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + uint8_t day = dateTimeController.Day(); + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H24) { + lv_label_set_text_fmt(label_date, + "%s %d %s", + dateTimeController.DayOfWeekShortToString(), + day, + dateTimeController.MonthShortToString()); + } else { + lv_label_set_text_fmt(label_date, + "%s %s %d", + dateTimeController.DayOfWeekShortToString(), + dateTimeController.MonthShortToString(), + day); + } + lv_obj_realign(label_date); + + refreshGarden = true; + // base seed is the date + seeds[0] = ((uint8_t) dateTimeController.Month() * 32U) + day; + for (int k = 1; k < NUM_FLOWERS; k++) { + // "sow" seeds using PRNG (xorshift32) + uint32_t s = seeds[k - 1]; + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + seeds[k] = s; + } + } + + if (refreshGarden) { + for (int k = 0; k < NUM_FLOWERS; k++) { + flowers[k].Update(seeds[k], stage.Get()); + lv_obj_realign(flowers[k].GetObject()); + } + } + } + + 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, lv_color_hex(0xFF5F5F)); + 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, lv_color_hex(0x5F5F5F)); + lv_label_set_text_static(heartbeatValue, ""); + } + + lv_obj_realign(heartbeatIcon); + lv_obj_realign(heartbeatValue); + } + + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + lv_obj_realign(stepIcon); + lv_obj_realign(stepValue); + } + + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature.Celsius(); + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.Fahrenheit(); + tempUnit = 'F'; + } + lv_label_set_text_fmt(temperature, "%d°%c", temp, tempUnit); + lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId)); + } else { + lv_label_set_text_static(temperature, ""); + lv_label_set_text(weatherIcon, ""); + } + lv_obj_realign(temperature); + lv_obj_realign(weatherIcon); + } +} diff --git a/src/displayapp/screens/WatchFaceGarden.h b/src/displayapp/screens/WatchFaceGarden.h new file mode 100644 index 0000000000..8057d55ff4 --- /dev/null +++ b/src/displayapp/screens/WatchFaceGarden.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/ble/BleController.h" +#include "displayapp/widgets/StatusIcons.h" +#include "displayapp/widgets/Flower.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class Ble; + class AlarmController; + class NotificationManager; + class HeartRateController; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceGarden : public Screen { + public: + WatchFaceGarden(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weather); + ~WatchFaceGarden() override; + + void Refresh() override; + + private: + static const int NUM_FLOWERS = 5; + static const int NUM_STAGES = 6; + static const int GARDEN_WIDTH = 180; + static constexpr int X_POS_OFFSET = -GARDEN_WIDTH / 2; + static constexpr int X_POS_STEP = GARDEN_WIDTH / (NUM_FLOWERS - 1); + + static constexpr uint32_t BG_COLORS[NUM_STAGES * 2] = + {0x000000, 0x16004C, 0x080065, 0xC6B431, 0x00539C, 0xABDFFF, 0x2D7DFF, 0x87BBFF, 0x2D7DFF, 0x87BBFF, 0x090463, 0xBB112D}; + + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentWeather {}; + Utility::DirtyValue stage {}; + Utility::DirtyValue seed {}; + Utility::DirtyValue> currentDate; + + lv_obj_t* garden; + lv_obj_t* label_time_hour; + lv_obj_t* label_time_min; + lv_obj_t* label_time_colon; + lv_obj_t* label_time_ampm; + lv_obj_t* label_date; + lv_obj_t* heartbeatIcon; + lv_obj_t* heartbeatValue; + lv_obj_t* stepIcon; + lv_obj_t* stepValue; + lv_obj_t* notificationIcon; + lv_obj_t* weatherIcon; + lv_obj_t* temperature; + + Controllers::DateTime& dateTimeController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; + Controllers::MotionController& motionController; + Controllers::SimpleWeatherService& weatherService; + + lv_task_t* taskRefresh; + Widgets::StatusIcons statusIcons; + Widgets::Flower flowers[NUM_FLOWERS]; + uint16_t seeds[NUM_FLOWERS]; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Garden; + static constexpr const char* name = "Garden"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceGarden(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.alarmController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + *controllers.weatherController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} diff --git a/src/displayapp/widgets/Flower.cpp b/src/displayapp/widgets/Flower.cpp new file mode 100644 index 0000000000..3810bc6e7e --- /dev/null +++ b/src/displayapp/widgets/Flower.cpp @@ -0,0 +1,107 @@ +#include "displayapp/widgets/Flower.h" +#include + +using namespace Pinetime::Applications::Widgets; + +Flower::Flower() { +} + +void Flower::Create(lv_obj_t* parent) { + container = lv_obj_create(parent, nullptr); + lv_obj_set_size(container, CONTAINER_WIDTH, CONTAINER_HEIGHT); + lv_obj_set_style_local_radius(container, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + lv_obj_set_style_local_bg_opa(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + + stem = lv_line_create(container, nullptr); + lv_obj_set_style_local_line_color(stem, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(STEM_COLOR)); + lv_obj_set_style_local_line_width(stem, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, LINE_WIDTH); + lv_line_set_y_invert(stem, true); + lv_obj_align(stem, container, LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + petals = lv_line_create(container, nullptr); + lv_obj_set_style_local_line_width(petals, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, LINE_WIDTH); + lv_obj_set_style_local_line_rounded(petals, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, LINE_WIDTH); + lv_line_set_y_invert(petals, true); + lv_obj_align(petals, container, LV_ALIGN_IN_BOTTOM_MID, 0, 0); +} + +void Flower::Update(uint16_t seed, int stage) { + uint32_t petalColor = PETAL_COLORS[COLOR_INDEX(seed)]; + int petalLength = MIN_PETAL_LENGTH + 2 * SIZE_INDEX(seed); + int maxStemHeight = MIN_STEM_HEIGHT + 8 * SIZE_INDEX(seed); + int numPetals = MIN_PETALS + PETAL_INDEX(seed); + int angleOffsIndex = ANGLE_OFFS_INDEX(seed); + int stemHeight = HEIGHT_FROM_STAGE(maxStemHeight, stage); + + lv_obj_set_hidden(stem, stage < 1); + if (stage >= 1) { + stemPoints[0].x = 0; + stemPoints[0].y = 0; + stemPoints[1].x = 0; + stemPoints[1].y = stemHeight; + lv_line_set_points(stem, stemPoints, 2); + lv_obj_realign(stem); + } + + lv_obj_set_hidden(petals, stage < 2); + lv_obj_set_style_local_line_color(petals, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(petalColor)); + if (stage == 2) { + int xOffs = 1; + petalPoints[0].x = 0 + xOffs; + petalPoints[0].y = stemHeight; + petalPoints[1].x = -1 + xOffs; + petalPoints[1].y = 1 + stemHeight; + petalPoints[2].x = 0 + xOffs; + petalPoints[2].y = petalLength / 2 + stemHeight; + petalPoints[3].x = 1 + xOffs; + petalPoints[3].y = 1 + stemHeight; + petalPoints[4].x = 0 + xOffs; + petalPoints[4].y = stemHeight; + lv_line_set_points(petals, petalPoints, 5); + lv_obj_realign(petals); + } else if (stage == 3) { + int xOffs = 2; + petalPoints[0].x = 0 + xOffs; + petalPoints[0].y = stemHeight; + petalPoints[1].x = -2 + xOffs; + petalPoints[1].y = 1 + stemHeight; + petalPoints[2].x = 0 + xOffs; + petalPoints[2].y = petalLength + stemHeight; + petalPoints[3].x = 2 + xOffs; + petalPoints[3].y = 1 + stemHeight; + petalPoints[4].x = 0 + xOffs; + petalPoints[4].y = stemHeight; + lv_line_set_points(petals, petalPoints, 5); + lv_obj_realign(petals); + } else if (stage == 4) { + int xOffs = petalLength; + float angleStep = 6.283185f / (numPetals * 2); + float angleOffs = (angleStep * 0.25f) * angleOffsIndex; + for (int k = 0; k < numPetals; k++) { + int k2 = k * 2; + petalPoints[k2].x = petalLength * sin((k2 * angleStep) + angleOffs) + xOffs; + petalPoints[k2].y = stemHeight + petalLength * cos((k2 * angleStep) + angleOffs); + petalPoints[k2 + 1].x = xOffs; + petalPoints[k2 + 1].y = stemHeight; + } + petalPoints[numPetals * 2].x = petalPoints[0].x; + petalPoints[numPetals * 2].y = petalPoints[0].y; + lv_line_set_points(petals, petalPoints, (numPetals * 2) + 1); + lv_obj_realign(petals); + } else if (stage == 5) { + int xOffs = petalLength / 2; + float angleStep = (6.283185f / ((numPetals * 2) - 1)) * 0.25f; + float angleOffs = 6.283185f * 3.0f / 8.0f; + for (int k = 0; k < numPetals; k++) { + int k2 = k * 2; + petalPoints[k2].x = petalLength * sin((k2 * angleStep) + angleOffs) + xOffs; + petalPoints[k2].y = stemHeight + petalLength * cos((k2 * angleStep) + angleOffs); + petalPoints[k2 + 1].x = xOffs; + petalPoints[k2 + 1].y = stemHeight; + } + petalPoints[numPetals * 2].x = petalPoints[0].x; + petalPoints[numPetals * 2].y = petalPoints[0].y; + lv_line_set_points(petals, petalPoints, (numPetals * 2) + 1); + lv_obj_realign(petals); + } +} diff --git a/src/displayapp/widgets/Flower.h b/src/displayapp/widgets/Flower.h new file mode 100644 index 0000000000..bcd6c54d2b --- /dev/null +++ b/src/displayapp/widgets/Flower.h @@ -0,0 +1,60 @@ +#pragma once +#include + +namespace Pinetime { + namespace Applications { + namespace Widgets { + class Flower { + public: + Flower(); + void Create(lv_obj_t* parent); + void Update(uint16_t seed, int stage); + + lv_obj_t* GetObject() { + return container; + } + + private: + static const int LINE_WIDTH = 3; + static const int CONTAINER_HEIGHT = 115; + static const int MIN_PETAL_LENGTH = 12; + static const int MAX_PETAL_LENGTH = 27; + static const int MIN_PETALS = 7; + static constexpr int MAX_STEM_HEIGHT = CONTAINER_HEIGHT - MAX_PETAL_LENGTH; + static constexpr int MIN_STEM_HEIGHT = MAX_PETAL_LENGTH + 5; + static const uint32_t STEM_COLOR = 0x135416; + static const int NUM_PETAL_COLORS = 8; + static constexpr int CONTAINER_WIDTH = MAX_PETAL_LENGTH * 2 + LINE_WIDTH * 2; + + static constexpr uint16_t PETAL_INDEX(uint16_t seed) { + return seed & 0x07UL; + } + + static constexpr uint16_t SIZE_INDEX(uint16_t seed) { + return (seed >> 3) & 0x07UL; + } + + static constexpr uint16_t ANGLE_OFFS_INDEX(uint16_t seed) { + return (seed >> 6) & 0x07UL; + } + + static constexpr uint16_t COLOR_INDEX(uint16_t seed) { + return (seed >> 9) % NUM_PETAL_COLORS; + } + + static constexpr int HEIGHT_FROM_STAGE(int max, int stage) { + return stage > 5 ? max * 3 / 4 : max * stage / 5; + } + + static constexpr uint32_t PETAL_COLORS[NUM_PETAL_COLORS] = + {0xf43838, 0xf48829, 0xf9f44d, 0x56efe2, 0x6f7afc, 0x8f68f9, 0xdb49fc, 0xfc83b5}; + + lv_obj_t* container; + lv_obj_t* stem; + lv_obj_t* petals; + lv_point_t stemPoints[2]; + lv_point_t petalPoints[29]; + }; + } + } +} diff --git a/src/displayapp/widgets/StatusIcons.cpp b/src/displayapp/widgets/StatusIcons.cpp index 777731a59f..f7c8b16c03 100644 --- a/src/displayapp/widgets/StatusIcons.cpp +++ b/src/displayapp/widgets/StatusIcons.cpp @@ -31,28 +31,38 @@ void StatusIcons::Create() { lv_obj_align(container, nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0); } -void StatusIcons::Update() { +bool StatusIcons::Update() { + bool updated = false; + powerPresent = batteryController.IsPowerPresent(); if (powerPresent.IsUpdated()) { lv_obj_set_hidden(batteryPlug, !powerPresent.Get()); + updated = true; } batteryPercentRemaining = batteryController.PercentRemaining(); if (batteryPercentRemaining.IsUpdated()) { auto batteryPercent = batteryPercentRemaining.Get(); batteryIcon.SetBatteryPercentage(batteryPercent); + updated = true; } alarmEnabled = alarmController.IsEnabled(); if (alarmEnabled.IsUpdated()) { lv_obj_set_hidden(alarmIcon, !alarmEnabled.Get()); + updated = true; } bleState = bleController.IsConnected(); bleRadioEnabled = bleController.IsRadioEnabled(); if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { lv_obj_set_hidden(bleIcon, !bleState.Get()); + updated = true; + } + + if (updated) { + lv_obj_realign(container); } - lv_obj_realign(container); + return updated; } diff --git a/src/displayapp/widgets/StatusIcons.h b/src/displayapp/widgets/StatusIcons.h index 5524e996c5..16c866de89 100644 --- a/src/displayapp/widgets/StatusIcons.h +++ b/src/displayapp/widgets/StatusIcons.h @@ -24,7 +24,7 @@ namespace Pinetime { return container; } - void Update(); + bool Update(); private: Screens::BatteryIcon batteryIcon; diff --git a/src/libs/lv_conf.h b/src/libs/lv_conf.h index c23647f2c0..6b93ff28f7 100644 --- a/src/libs/lv_conf.h +++ b/src/libs/lv_conf.h @@ -419,7 +419,8 @@ typedef void* lv_indev_drv_user_data_t; /*Type of user data in the in LV_FONT_DECLARE(jetbrains_mono_76) \ LV_FONT_DECLARE(open_sans_light) \ LV_FONT_DECLARE(fontawesome_weathericons) \ - LV_FONT_DECLARE(lv_font_sys_48) + LV_FONT_DECLARE(lv_font_sys_48) \ + LV_FONT_DECLARE(azeret_mono) /* Enable it if you have fonts with a lot of characters. * The limit depends on the font size, font face and bpp