mirror of
https://github.com/google/pebble.git
synced 2025-05-24 12:14:53 +00:00
340 lines
12 KiB
C
340 lines
12 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "progress_window.h"
|
|
|
|
#include "applib/applib_malloc.auto.h"
|
|
#include "applib/fonts/fonts.h"
|
|
#include "applib/ui/app_window_stack.h"
|
|
#include "applib/ui/window_private.h"
|
|
#include "applib/ui/window_stack.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "services/common/compositor/compositor_transitions.h"
|
|
#include "services/normal/timeline/timeline_resources.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/math.h"
|
|
|
|
#define SCROLL_OUT_MS 250
|
|
#define BAR_HEIGHT PROGRESS_SUGGESTED_HEIGHT
|
|
#define BAR_WIDTH 80
|
|
#define BAR_TO_TRANS_MS 160
|
|
#define TRANS_TO_DOT_MS 90
|
|
#define DOT_TRANSITION_RADIUS 13
|
|
#define DOT_COMPOSITOR_RADIUS 7
|
|
#define DOT_OFFSET 25
|
|
|
|
#define FAKE_PROGRESS_UPDATE_INTERVAL 200
|
|
#define FAKE_PROGRESS_UPDATE_AMOUNT 2
|
|
|
|
#define INITIAL_PERCENT 0
|
|
|
|
static void prv_finished(ProgressWindow *data, bool success) {
|
|
if (data->callbacks.finished) {
|
|
data->callbacks.finished(data, success, data->context);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////
|
|
// Animation Related Functions
|
|
///////////////////////////////
|
|
|
|
static void prv_animation_stopped_success(Animation *animation, bool finished, void *context) {
|
|
ProgressWindow *data = context;
|
|
|
|
const bool success = true;
|
|
prv_finished(data, success);
|
|
}
|
|
|
|
static void prv_finished_failure_callback(void *data) {
|
|
const bool success = false;
|
|
prv_finished(data, success);
|
|
}
|
|
|
|
static void prv_show_peek_layer(ProgressWindow *data) {
|
|
if (data->is_peek_layer_used) {
|
|
Layer *root_layer = window_get_root_layer(&data->window);
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
peek_layer_play(peek_layer);
|
|
layer_add_child(root_layer, (Layer *)peek_layer);
|
|
|
|
const int standing_ms = 1 * MS_PER_SECOND;
|
|
data->peek_layer_timer = evented_timer_register(PEEK_LAYER_UNFOLD_DURATION + standing_ms,
|
|
false, prv_finished_failure_callback, data);
|
|
} else {
|
|
prv_finished_failure_callback(data);
|
|
}
|
|
}
|
|
|
|
static void prv_animation_stopped_failure(Animation *animation, bool finished, void *context) {
|
|
ProgressWindow *data = context;
|
|
prv_show_peek_layer(data);
|
|
}
|
|
|
|
static void prv_schedule_progress_success_animation(ProgressWindow *data) {
|
|
#if !PLATFORM_TINTIN
|
|
GRect beg = data->progress_layer.layer.bounds;
|
|
GRect mid = beg;
|
|
GRect end = beg;
|
|
|
|
// Morph from progress_layer to a large transition dot to the compositor dot
|
|
// by changing the bounds of the progress_layer using 2 animations
|
|
layer_set_clips((Layer *)&data->progress_layer, false); // Extending bounds to grow the dot
|
|
progress_layer_set_corner_radius(&data->progress_layer, DOT_TRANSITION_RADIUS);
|
|
|
|
mid.size.w = DOT_TRANSITION_RADIUS * 2;
|
|
mid.size.h = DOT_TRANSITION_RADIUS * 2;
|
|
mid.origin.x = DOT_OFFSET - DOT_TRANSITION_RADIUS + 2;
|
|
mid.origin.y = BAR_HEIGHT - DOT_TRANSITION_RADIUS + 1; // shift to accommodate growing radius
|
|
|
|
end.size.w = DOT_COMPOSITOR_RADIUS * 2;
|
|
end.size.h = DOT_COMPOSITOR_RADIUS * 2;
|
|
end.origin.x = DOT_OFFSET - DOT_COMPOSITOR_RADIUS - 1;
|
|
end.origin.y = BAR_HEIGHT - DOT_COMPOSITOR_RADIUS - 2; // shift to accommodate growing radius
|
|
|
|
PropertyAnimation *prop_anim =
|
|
property_animation_create_layer_bounds((Layer *)&data->progress_layer, &beg, &mid);
|
|
|
|
Animation *animation1 = property_animation_get_animation(prop_anim);
|
|
animation_set_duration(animation1, BAR_TO_TRANS_MS);
|
|
animation_set_curve(animation1, AnimationCurveEaseIn);
|
|
|
|
prop_anim = property_animation_create_layer_bounds((Layer *)&data->progress_layer, &mid, &end);
|
|
|
|
Animation *animation2 = property_animation_get_animation(prop_anim);
|
|
animation_set_duration(animation2, TRANS_TO_DOT_MS);
|
|
animation_set_curve(animation2, AnimationCurveLinear);
|
|
animation_set_handlers(animation2, (AnimationHandlers) {
|
|
.stopped = prv_animation_stopped_success,
|
|
}, data);
|
|
|
|
Animation *animation = animation_sequence_create(animation1, animation2, NULL);
|
|
|
|
data->result_animation = animation;
|
|
animation_schedule(animation);
|
|
#else
|
|
// Don't animate to a dot on old platforms, just finish immediately.
|
|
static const bool success = true;
|
|
prv_finished(data, success);
|
|
#endif
|
|
}
|
|
|
|
static void prv_schedule_progress_failure_animation(ProgressWindow *data, uint32_t timeline_res_id,
|
|
const char *message, uint32_t delay) {
|
|
// Initialize the peek layer
|
|
if (timeline_res_id || message) {
|
|
Layer *root_layer = window_get_root_layer(&data->window);
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
peek_layer_init(peek_layer, &root_layer->frame);
|
|
peek_layer_set_title_font(peek_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
|
|
|
TimelineResourceInfo timeline_res = {
|
|
.res_id = timeline_res_id,
|
|
};
|
|
peek_layer_set_icon(peek_layer, &timeline_res);
|
|
peek_layer_set_title(peek_layer, message);
|
|
peek_layer_set_background_color(peek_layer, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
|
|
data->is_peek_layer_used = true;
|
|
}
|
|
|
|
#if !PLATFORM_TINTIN
|
|
// Animate the progress bar out, by shrinking it's width from it's current size down to 0.
|
|
// When this completes, prv_animation_stopped_failure will show the peek layer.
|
|
GRect *start = &data->progress_layer.layer.frame;
|
|
GRect stop = *start;
|
|
stop.size.w = 0;
|
|
|
|
PropertyAnimation *prop_anim =
|
|
property_animation_create_layer_frame((Layer *)&data->progress_layer, start, &stop);
|
|
|
|
Animation *animation = property_animation_get_animation(prop_anim);
|
|
// If we failed, pause on the screen for a little.
|
|
animation_set_delay(animation, delay);
|
|
animation_set_duration(animation, SCROLL_OUT_MS);
|
|
animation_set_curve(animation, AnimationCurveEaseOut);
|
|
animation_set_handlers(animation, (AnimationHandlers) {
|
|
.stopped = prv_animation_stopped_failure,
|
|
}, data);
|
|
|
|
data->result_animation = animation;
|
|
animation_schedule(animation);
|
|
|
|
#else
|
|
// Don't animate, just show the peek layer immediately.
|
|
prv_show_peek_layer(data);
|
|
#endif
|
|
}
|
|
|
|
////////////////////////////
|
|
// Internal Helper Functions
|
|
////////////////////////////
|
|
|
|
//! Used to clean up the application's data before exiting
|
|
static void prv_cancel_fake_progress_timer(ProgressWindow *data) {
|
|
if (data->fake_progress_timer != EVENTED_TIMER_INVALID_ID) {
|
|
evented_timer_cancel(data->fake_progress_timer);
|
|
data->fake_progress_timer = EVENTED_TIMER_INVALID_ID;
|
|
}
|
|
}
|
|
|
|
static void prv_set_progress(ProgressWindow *data, int16_t progress) {
|
|
data->progress_percent = CLIP(progress, data->progress_percent, MAX_PROGRESS_PERCENT);
|
|
progress_layer_set_progress(&data->progress_layer, data->progress_percent);
|
|
}
|
|
|
|
static void prv_fake_update_progress(void *context) {
|
|
ProgressWindow *data = context;
|
|
|
|
prv_set_progress(data, data->progress_percent + FAKE_PROGRESS_UPDATE_AMOUNT);
|
|
|
|
if (data->progress_percent >= data->max_fake_progress_percent) {
|
|
// Hit the max, we're done
|
|
data->fake_progress_timer = EVENTED_TIMER_INVALID_ID;
|
|
} else {
|
|
data->fake_progress_timer = evented_timer_register(FAKE_PROGRESS_UPDATE_INTERVAL, false,
|
|
prv_fake_update_progress, data);
|
|
}
|
|
}
|
|
|
|
////////////////////////////
|
|
// Public API
|
|
////////////////////////////
|
|
|
|
void progress_window_set_max_fake_progress(ProgressWindow *window,
|
|
int16_t max_fake_progress_percent) {
|
|
window->max_fake_progress_percent = CLIP(max_fake_progress_percent, 0, MAX_PROGRESS_PERCENT);
|
|
}
|
|
|
|
void progress_window_set_progress(ProgressWindow *window, int16_t progress) {
|
|
if (window->state == ProgressWindowState_FakeProgress) {
|
|
// We've seen our first bit of real progress, stop faking it.
|
|
prv_cancel_fake_progress_timer(window);
|
|
window->state = ProgressWindowState_RealProgress;
|
|
}
|
|
|
|
prv_set_progress(window, progress);
|
|
}
|
|
|
|
void progress_window_set_result_success(ProgressWindow *window) {
|
|
if (window->state == ProgressWindowState_Result) {
|
|
// Ignore requests to change the result once we already have one
|
|
return;
|
|
}
|
|
|
|
window->state = ProgressWindowState_Result;
|
|
prv_cancel_fake_progress_timer(window);
|
|
prv_set_progress(window, MAX_PROGRESS_PERCENT);
|
|
prv_schedule_progress_success_animation(window);
|
|
}
|
|
|
|
void progress_window_set_result_failure(ProgressWindow *window, uint32_t timeline_res,
|
|
const char *message, uint32_t delay) {
|
|
if (window->state == ProgressWindowState_Result) {
|
|
// Ignore requests to change the result once we already have one
|
|
return;
|
|
}
|
|
|
|
window->state = ProgressWindowState_Result;
|
|
prv_cancel_fake_progress_timer(window);
|
|
prv_schedule_progress_failure_animation(window, timeline_res, message, delay);
|
|
}
|
|
|
|
void progress_window_set_callbacks(ProgressWindow *window, ProgressWindowCallbacks callbacks,
|
|
void *context) {
|
|
window->context = context;
|
|
window->callbacks = callbacks;
|
|
}
|
|
|
|
void progress_window_set_back_disabled(ProgressWindow *window, bool disabled) {
|
|
window_set_overrides_back_button(&window->window, disabled);
|
|
}
|
|
|
|
void progress_window_push(ProgressWindow *window, WindowStack *window_stack) {
|
|
const bool animated = true;
|
|
window_stack_push(window_stack, (Window *)window, animated);
|
|
}
|
|
|
|
void app_progress_window_push(ProgressWindow *window) {
|
|
const bool animated = true;
|
|
app_window_stack_push((Window *)window, animated);
|
|
}
|
|
|
|
void progress_window_pop(ProgressWindow *window) {
|
|
const bool animated = true;
|
|
window_stack_remove((Window *)window, animated);
|
|
}
|
|
|
|
void progress_window_init(ProgressWindow *data) {
|
|
// Create and set up the window
|
|
Window *window = &data->window;
|
|
window_init(window, WINDOW_NAME("Progress Window"));
|
|
window_set_background_color(window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
|
|
|
|
const GRect *bounds = &window->layer.bounds;
|
|
GPoint center = grect_center_point(bounds);
|
|
const GRect progress_bounds = (GRect) {
|
|
.origin = { center.x - (BAR_WIDTH / 2), center.y - (BAR_HEIGHT / 2) },
|
|
.size = { BAR_WIDTH, BAR_HEIGHT },
|
|
};
|
|
|
|
ProgressLayer *progress_layer = &data->progress_layer;
|
|
progress_layer_init(progress_layer, &progress_bounds);
|
|
#if PBL_COLOR
|
|
progress_layer_set_foreground_color(progress_layer, GColorWhite);
|
|
progress_layer_set_background_color(progress_layer, GColorBlack);
|
|
#endif
|
|
progress_layer_set_corner_radius(progress_layer, PROGRESS_SUGGESTED_CORNER_RADIUS);
|
|
layer_add_child(&window->layer, (Layer *)progress_layer);
|
|
|
|
data->max_fake_progress_percent = PROGRESS_WINDOW_DEFAULT_FAKE_PERCENT;
|
|
data->state = ProgressWindowState_FakeProgress;
|
|
data->is_peek_layer_used = false;
|
|
|
|
data->fake_progress_timer = evented_timer_register(FAKE_PROGRESS_UPDATE_INTERVAL, false,
|
|
prv_fake_update_progress, data);
|
|
prv_set_progress(data, INITIAL_PERCENT);
|
|
}
|
|
|
|
void progress_window_deinit(ProgressWindow *data) {
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
animation_unschedule(data->result_animation);
|
|
peek_layer_deinit(&data->peek_layer);
|
|
data->is_peek_layer_used = false;
|
|
|
|
prv_cancel_fake_progress_timer(data);
|
|
|
|
if (data->peek_layer_timer != EVENTED_TIMER_INVALID_ID) {
|
|
evented_timer_cancel(data->peek_layer_timer);
|
|
data->peek_layer_timer = EVENTED_TIMER_INVALID_ID;
|
|
}
|
|
}
|
|
|
|
ProgressWindow *progress_window_create(void) {
|
|
ProgressWindow *window = applib_zalloc(sizeof(ProgressWindow));
|
|
progress_window_init(window);
|
|
return window;
|
|
}
|
|
|
|
void progress_window_destroy(ProgressWindow *window) {
|
|
if (window) {
|
|
progress_window_pop(window);
|
|
}
|
|
progress_window_deinit(window);
|
|
applib_free(window);
|
|
}
|