diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 90be1febe6..c8e2b23e51 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -28,7 +28,7 @@ "sources": [ { "file": "JetBrainsMono-Light.ttf", - "range": "0x25, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0" + "range": "0x25, 0x2B, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0" } ], "bpp": 1, diff --git a/src/displayapp/screens/Timer.cpp b/src/displayapp/screens/Timer.cpp index 749d985933..1cee62ae52 100644 --- a/src/displayapp/screens/Timer.cpp +++ b/src/displayapp/screens/Timer.cpp @@ -6,9 +6,14 @@ using namespace Pinetime::Applications::Screens; +// Initialize static member with default timer durations (5min, 10min, 15min) +uint32_t Timer::timerDurations[Timer::numRecentTimers] = {300000, 600000, 900000}; + static void btnEventHandler(lv_obj_t* obj, lv_event_t event) { auto* screen = static_cast(obj->user_data); - if (event == LV_EVENT_PRESSED) { + if (screen->launcherMode && event == LV_EVENT_CLICKED) { + screen->OnLauncherButtonClicked(obj); + } else if (event == LV_EVENT_PRESSED) { screen->ButtonPressed(); } else if (event == LV_EVENT_RELEASED || event == LV_EVENT_PRESS_LOST) { screen->MaskReset(); @@ -20,57 +25,17 @@ static void btnEventHandler(lv_obj_t* obj, lv_event_t event) { Timer::Timer(Controllers::Timer& timerController, Controllers::MotorController& motorController, System::SystemTask& systemTask) : timer {timerController}, motorController {motorController}, wakeLock(systemTask) { - lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr); - lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); - lv_obj_set_style_local_text_color(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); - lv_label_set_text_static(colonLabel, ":"); - lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29); - - minuteCounter.Create(); - secondCounter.Create(); - lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); - lv_obj_align(secondCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0); - - highlightObjectMask = lv_objmask_create(lv_scr_act(), nullptr); - lv_obj_set_size(highlightObjectMask, 240, 50); - lv_obj_align(highlightObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); - - lv_draw_mask_line_param_t tmpMaskLine; - - lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_LEFT); - highlightMask = lv_objmask_add_mask(highlightObjectMask, &tmpMaskLine); - - lv_obj_t* btnHighlight = lv_obj_create(highlightObjectMask, nullptr); - lv_obj_set_style_local_radius(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); - lv_obj_set_style_local_bg_color(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE); - lv_obj_set_size(btnHighlight, LV_HOR_RES, 50); - lv_obj_align(btnHighlight, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); - - btnObjectMask = lv_objmask_create(lv_scr_act(), nullptr); - lv_obj_set_size(btnObjectMask, 240, 50); - lv_obj_align(btnObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); - - lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_RIGHT); - btnMask = lv_objmask_add_mask(btnObjectMask, &tmpMaskLine); - - btnPlayPause = lv_btn_create(btnObjectMask, nullptr); - btnPlayPause->user_data = this; - lv_obj_set_style_local_radius(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); - lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt); - lv_obj_set_event_cb(btnPlayPause, btnEventHandler); - lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50); - - // Create the label as a child of the button so it stays centered by default - txtPlayPause = lv_label_create(btnPlayPause, nullptr); - auto timerStatus = timer.GetTimerState(); if (timerStatus && timerStatus->expired) { - SetTimerRinging(); + // If timer has expired, go directly to timer UI with 0 duration + CreateTimerUI(0, false); } else if (timer.IsRunning()) { - SetTimerRunning(); + // If timer is already running, skip launcher and go directly to timer UI + uint32_t durationMs = GetTimerDuration(0); + CreateTimerUI(durationMs, false); } else { - SetTimerStopped(); + CreateLauncherUI(); } taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); @@ -86,6 +51,10 @@ Timer::~Timer() { timer.ResetExpiredTime(); } + if (launcherMode) { + lv_style_reset(&btnStyle); + } + lv_obj_clean(lv_scr_act()); } @@ -116,6 +85,18 @@ void Timer::UpdateMask() { } void Timer::Refresh() { + // Don't try to update timer display if we're in launcher mode + if (launcherMode) { + // If timer starts while in launcher, transition to timer UI + if (timer.IsRunning()) { + uint32_t durationMs = GetTimerDuration(0); + lv_style_reset(&btnStyle); + lv_obj_clean(lv_scr_act()); + CreateTimerUI(durationMs, false); + } + return; + } + auto timerStatus = timer.GetTimerState(); if (timerStatus && timerStatus->expired) { @@ -156,6 +137,9 @@ void Timer::DisplayTime() { } void Timer::SetTimerRunning() { + if (launcherMode) { + return; + } minuteCounter.HideControls(); secondCounter.HideControls(); lv_label_set_text_static(txtPlayPause, "Pause"); @@ -163,6 +147,9 @@ void Timer::SetTimerRunning() { } void Timer::SetTimerStopped() { + if (launcherMode) { + return; + } minuteCounter.ShowControls(); secondCounter.ShowControls(); lv_label_set_text_static(txtPlayPause, "Start"); @@ -170,6 +157,10 @@ void Timer::SetTimerStopped() { } void Timer::SetTimerRinging() { + if (launcherMode) { + // Timer expired while in launcher mode - transition will happen in Refresh() + return; + } motorController.StartRinging(); wakeLock.Lock(); minuteCounter.HideControls(); @@ -190,6 +181,11 @@ void Timer::ToggleRunning() { } else if (secondCounter.GetValue() + minuteCounter.GetValue() > 0) { auto timerDuration = std::chrono::minutes(minuteCounter.GetValue()) + std::chrono::seconds(secondCounter.GetValue()); timer.StartTimer(timerDuration); + + // Add the timer duration to MRU list + uint32_t durationMs = (minuteCounter.GetValue() * 60 + secondCounter.GetValue()) * 1000; + AddTimerDuration(durationMs); + Refresh(); SetTimerRunning(); } @@ -200,3 +196,195 @@ void Timer::Reset() { DisplayTime(); SetTimerStopped(); } + +void Timer::AddTimerDuration(uint32_t duration) { + // If already at front, nothing to do + if (duration == timerDurations[0]) { + return; + } + + // Shift elements down, stopping after we find the duration + uint32_t prev = timerDurations[0]; + for (int i = 1; i < numRecentTimers; i++) { + uint32_t temp = timerDurations[i]; + timerDurations[i] = prev; + prev = temp; + if (temp == duration) { + // Found it - stop after this shift + break; + } + } + + // Insert duration at front + timerDurations[0] = duration; +} + +uint32_t Timer::GetTimerDuration(uint8_t index) const { + if (index >= numRecentTimers) { + return timerDurations[0]; + } + return timerDurations[index]; +} + +void Timer::CreateLauncherUI() { + static constexpr uint8_t innerDistance = 10; + static constexpr uint8_t buttonHeight = (LV_VER_RES_MAX - innerDistance) / 2; + static constexpr uint8_t buttonWidth = (LV_HOR_RES_MAX - innerDistance) / 2; + + lv_style_init(&btnStyle); + lv_style_set_radius(&btnStyle, LV_STATE_DEFAULT, buttonHeight / 4); + lv_style_set_bg_color(&btnStyle, LV_STATE_DEFAULT, Colors::bgAlt); + + // Layout positions for the 3 recent timer buttons + static constexpr lv_align_t buttonAlignments[numRecentTimers] = { + LV_ALIGN_IN_TOP_LEFT, // Button 0: Top-left + LV_ALIGN_IN_TOP_RIGHT, // Button 1: Top-right + LV_ALIGN_IN_BOTTOM_LEFT // Button 2: Bottom-left + }; + + // Create each of the recent timer buttons + for (int i = 0; i < numRecentTimers; i++) { + btnRecent[i] = lv_btn_create(lv_scr_act(), nullptr); + btnRecent[i]->user_data = this; + lv_obj_set_event_cb(btnRecent[i], btnEventHandler); + lv_obj_add_style(btnRecent[i], LV_BTN_PART_MAIN, &btnStyle); + lv_obj_set_size(btnRecent[i], buttonWidth, buttonHeight); + lv_obj_align(btnRecent[i], nullptr, buttonAlignments[i], 0, 0); + + uint32_t duration = GetTimerDuration(i); + uint32_t minutes = duration / 60000; + uint32_t seconds = (duration % 60000) / 1000; + + labelRecent[i] = lv_label_create(btnRecent[i], nullptr); + lv_obj_t* labelIcon = lv_label_create(btnRecent[i], nullptr); + + // Show the minutes + lv_obj_set_style_local_text_font(labelRecent[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + lv_label_set_text_fmt(labelRecent[i], "%lu", minutes); + lv_obj_align(labelRecent[i], btnRecent[i], LV_ALIGN_CENTER, 0, -20); + + // Show the seconds, or "min" below + lv_obj_set_style_local_text_font(labelIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + if (seconds == 0) { + lv_label_set_text_static(labelIcon, "min"); + } else { + lv_label_set_text_fmt(labelIcon, ":%02lu", seconds); + } + lv_obj_align(labelIcon, btnRecent[i], LV_ALIGN_CENTER, 0, 20); + } + + // Bottom-right: New timer + btnCustom = lv_btn_create(lv_scr_act(), nullptr); + btnCustom->user_data = this; + lv_obj_set_event_cb(btnCustom, btnEventHandler); + lv_obj_add_style(btnCustom, LV_BTN_PART_MAIN, &btnStyle); + lv_obj_set_size(btnCustom, buttonWidth, buttonHeight); + lv_obj_align(btnCustom, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0); + + labelCustom = lv_label_create(btnCustom, nullptr); + lv_obj_set_style_local_text_font(labelCustom, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + lv_label_set_text_static(labelCustom, "+"); +} + +void Timer::CreateTimerUI(uint32_t startDurationMs, bool autoStart) { + launcherMode = false; + + lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + lv_obj_set_style_local_text_color(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_label_set_text_static(colonLabel, ":"); + lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29); + + minuteCounter.Create(); + secondCounter.Create(); + lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); + lv_obj_align(secondCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0); + + highlightObjectMask = lv_objmask_create(lv_scr_act(), nullptr); + lv_obj_set_size(highlightObjectMask, 240, 50); + lv_obj_align(highlightObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + lv_draw_mask_line_param_t tmpMaskLine; + + lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_LEFT); + highlightMask = lv_objmask_add_mask(highlightObjectMask, &tmpMaskLine); + + lv_obj_t* btnHighlight = lv_obj_create(highlightObjectMask, nullptr); + lv_obj_set_style_local_radius(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); + lv_obj_set_style_local_bg_color(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE); + lv_obj_set_size(btnHighlight, LV_HOR_RES, 50); + lv_obj_align(btnHighlight, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + btnObjectMask = lv_objmask_create(lv_scr_act(), nullptr); + lv_obj_set_size(btnObjectMask, 240, 50); + lv_obj_align(btnObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_RIGHT); + btnMask = lv_objmask_add_mask(btnObjectMask, &tmpMaskLine); + + btnPlayPause = lv_btn_create(btnObjectMask, nullptr); + btnPlayPause->user_data = this; + lv_obj_set_style_local_radius(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); + lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt); + lv_obj_set_event_cb(btnPlayPause, btnEventHandler); + lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50); + + // Create the label as a child of the button so it stays centered by default + txtPlayPause = lv_label_create(btnPlayPause, nullptr); + + // Reset button press state + buttonPressing = false; + pressTime = 0; + + if (timer.IsRunning()) { + SetTimerRunning(); + DisplayTime(); + } else if (autoStart) { + auto timerDuration = std::chrono::milliseconds(startDurationMs); + timer.StartTimer(timerDuration); + AddTimerDuration(startDurationMs); + SetTimerRunning(); + DisplayTime(); + } else { + // Set the initial duration only when timer is stopped + uint32_t minutes = startDurationMs / 60000; + uint32_t seconds = (startDurationMs % 60000) / 1000; + minuteCounter.SetValue(minutes); + secondCounter.SetValue(seconds); + SetTimerStopped(); + } +} + +void Timer::OnLauncherButtonClicked(lv_obj_t* obj) { + uint32_t durationMs; + bool autoStart; + + // Check if it's one of the recent timer buttons + bool found = false; + for (int i = 0; i < numRecentTimers; i++) { + if (obj == btnRecent[i]) { + durationMs = GetTimerDuration(i); + autoStart = true; + found = true; + break; + } + } + + // Check if it's the custom timer button + if (!found) { + if (obj == btnCustom) { + durationMs = 0; + autoStart = false; + } else { + return; + } + } + + lv_style_reset(&btnStyle); + lv_obj_clean(lv_scr_act()); + + CreateTimerUI(durationMs, autoStart); + + // Wait for button release to prevent the press state from carrying over to the new UI + lv_indev_wait_release(lv_indev_get_act()); +} diff --git a/src/displayapp/screens/Timer.h b/src/displayapp/screens/Timer.h index 651c7f0d57..25199c5e2a 100644 --- a/src/displayapp/screens/Timer.h +++ b/src/displayapp/screens/Timer.h @@ -24,16 +24,32 @@ namespace Pinetime::Applications { void ButtonPressed(); void MaskReset(); void SetTimerRinging(); + void OnLauncherButtonClicked(lv_obj_t* obj); + + bool launcherMode = true; private: void SetTimerRunning(); void SetTimerStopped(); void UpdateMask(); void DisplayTime(); + void CreateLauncherUI(); + void CreateTimerUI(uint32_t startDurationMs, bool autoStart); + void AddTimerDuration(uint32_t duration); + uint32_t GetTimerDuration(uint8_t index) const; + Pinetime::Controllers::Timer& timer; Pinetime::Controllers::MotorController& motorController; Pinetime::System::WakeLock wakeLock; + // Launcher UI elements + static constexpr int numRecentTimers = 3; + static uint32_t timerDurations[numRecentTimers]; + lv_obj_t* btnRecent[numRecentTimers] = {nullptr}; + lv_obj_t* btnCustom = nullptr; + lv_obj_t* labelRecent[numRecentTimers] = {nullptr}; + lv_obj_t* labelCustom = nullptr; + lv_style_t btnStyle; lv_obj_t* btnPlayPause; lv_obj_t* txtPlayPause;