mirror of
https://github.com/google/pebble.git
synced 2025-06-27 03:16:16 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
575
src/fw/popups/timeline/peek.c
Normal file
575
src/fw/popups/timeline/peek.c
Normal file
|
@ -0,0 +1,575 @@
|
|||
/*
|
||||
* 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 "peek_private.h"
|
||||
|
||||
#include "applib/ui/property_animation.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
#include "applib/unobstructed_area_service.h"
|
||||
#include "apps/system_apps/timeline/timeline_common.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "kernel/ui/modals/modal_manager.h"
|
||||
#include "shell/prefs.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
#include "util/struct.h"
|
||||
|
||||
#include <pebbleos/cron.h>
|
||||
|
||||
#define TIMELINE_PEEK_FRAME_HIDDEN GRect(0, DISP_ROWS, DISP_COLS, TIMELINE_PEEK_HEIGHT)
|
||||
#define TIMELINE_PEEK_OUTER_BORDER_WIDTH PBL_IF_RECT_ELSE(2, 1)
|
||||
#define TIMELINE_PEEK_MULTI_BORDER_WIDTH (1)
|
||||
#define TIMELINE_PEEK_MULTI_CONTENT_HEIGHT PBL_IF_RECT_ELSE(2, 1)
|
||||
#define TIMELINE_PEEK_MAX_CONCURRENT (2)
|
||||
|
||||
static TimelinePeek s_peek;
|
||||
|
||||
static unsigned int prv_get_concurrent_height(unsigned int num_concurrent) {
|
||||
// Height of the border and other concurrent contents
|
||||
return (TIMELINE_PEEK_OUTER_BORDER_WIDTH +
|
||||
(num_concurrent * (TIMELINE_PEEK_MULTI_BORDER_WIDTH +
|
||||
TIMELINE_PEEK_MULTI_CONTENT_HEIGHT)));
|
||||
}
|
||||
|
||||
unsigned int timeline_peek_get_concurrent_height(unsigned int num_concurrent) {
|
||||
return prv_get_concurrent_height(MIN(num_concurrent, TIMELINE_PEEK_MAX_CONCURRENT));
|
||||
}
|
||||
|
||||
static void prv_draw_background(GContext *ctx, const GRect *frame_orig,
|
||||
unsigned int num_concurrent) {
|
||||
GRect frame = *frame_orig;
|
||||
#if PBL_RECT
|
||||
// Fill all the way to the bottom of the screen
|
||||
frame.size.h = DISP_ROWS - frame.origin.y;
|
||||
#endif
|
||||
const GColor background_color = GColorWhite;
|
||||
graphics_context_set_fill_color(ctx, background_color);
|
||||
graphics_fill_rect(ctx, &frame);
|
||||
|
||||
// Draw the icon background
|
||||
frame.origin.x += DISP_COLS - TIMELINE_PEEK_ICON_BOX_WIDTH;
|
||||
frame.size.w = TIMELINE_PEEK_ICON_BOX_WIDTH;
|
||||
graphics_context_set_fill_color(ctx, TIMELINE_FUTURE_COLOR);
|
||||
graphics_fill_rect(ctx, &frame);
|
||||
|
||||
// Draw the top border and concurrent event indicators
|
||||
frame = *frame_orig;
|
||||
const GColor border_color = GColorBlack;
|
||||
for (unsigned int i = 0; i <= num_concurrent; i++) {
|
||||
const bool has_content = (i < num_concurrent);
|
||||
for (unsigned int type = 0; type < (has_content ? 2 : 1); type++) {
|
||||
const bool is_outer = (i == 0);
|
||||
const bool is_border = (type == 0);
|
||||
const int height = (is_outer && is_border) ? TIMELINE_PEEK_OUTER_BORDER_WIDTH :
|
||||
is_border ? TIMELINE_PEEK_MULTI_BORDER_WIDTH :
|
||||
TIMELINE_PEEK_MULTI_CONTENT_HEIGHT;
|
||||
frame.size.h = height;
|
||||
graphics_context_set_fill_color(ctx, is_border ? border_color : background_color);
|
||||
graphics_fill_rect(ctx, &frame);
|
||||
frame.origin.y += height;
|
||||
}
|
||||
}
|
||||
|
||||
#if PBL_ROUND
|
||||
// Draw the bottom border
|
||||
frame = *frame_orig;
|
||||
frame.origin.y += frame.size.h - TIMELINE_PEEK_OUTER_BORDER_WIDTH;
|
||||
frame.size.h = TIMELINE_PEEK_OUTER_BORDER_WIDTH;
|
||||
graphics_context_set_fill_color(ctx, border_color);
|
||||
graphics_fill_rect(ctx, &frame);
|
||||
#endif
|
||||
}
|
||||
|
||||
void timeline_peek_draw_background(GContext *ctx, const GRect *frame,
|
||||
unsigned int num_concurrent) {
|
||||
prv_draw_background(ctx, frame, num_concurrent);
|
||||
}
|
||||
|
||||
static void prv_timeline_peek_update_proc(Layer *layer, GContext *ctx) {
|
||||
TimelinePeek *peek = (TimelinePeek *)layer;
|
||||
const unsigned int num_concurrent = peek->peek_layout ?
|
||||
MIN(peek->peek_layout->info.num_concurrent, TIMELINE_PEEK_MAX_CONCURRENT) : 0;
|
||||
if (peek->removing_concurrent && (num_concurrent > 0)) {
|
||||
prv_draw_background(ctx, &TIMELINE_PEEK_FRAME_VISIBLE, num_concurrent - 1);
|
||||
}
|
||||
prv_draw_background(ctx, &peek->layout_layer.frame, num_concurrent);
|
||||
}
|
||||
|
||||
static void prv_redraw(void *UNUSED data) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
layer_mark_dirty(&peek->layout_layer);
|
||||
}
|
||||
|
||||
static void prv_cron_callback(CronJob *job, void *UNUSED data) {
|
||||
launcher_task_add_callback(prv_redraw, NULL);
|
||||
cron_job_schedule(job);
|
||||
}
|
||||
|
||||
static CronJob s_timeline_peek_job = {
|
||||
.minute = CRON_MINUTE_ANY,
|
||||
.hour = CRON_HOUR_ANY,
|
||||
.mday = CRON_MDAY_ANY,
|
||||
.month = CRON_MONTH_ANY,
|
||||
.cb = prv_cron_callback,
|
||||
};
|
||||
|
||||
static void prv_destroy_layout(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
if (!peek->peek_layout) {
|
||||
return;
|
||||
}
|
||||
|
||||
layout_destroy(&peek->peek_layout->timeline_layout->layout_layer);
|
||||
timeline_item_destroy(peek->peek_layout->item);
|
||||
task_free(peek->peek_layout);
|
||||
peek->peek_layout = NULL;
|
||||
}
|
||||
|
||||
static PeekLayout *prv_create_layout(TimelineItem *item, unsigned int num_concurrent) {
|
||||
PeekLayout *layout = task_zalloc_check(sizeof(PeekLayout));
|
||||
item = timeline_item_copy(item);
|
||||
layout->item = item;
|
||||
timeline_layout_init_info(&layout->info, item, time_util_get_midnight_of(rtc_get_time()));
|
||||
layout->info.num_concurrent = num_concurrent;
|
||||
const LayoutLayerConfig config = {
|
||||
.frame = &GRect(0, 0, DISP_COLS, TIMELINE_PEEK_HEIGHT),
|
||||
.attributes = &item->attr_list,
|
||||
.mode = LayoutLayerModePeek,
|
||||
.app_id = &item->header.parent_id,
|
||||
.context = &layout->info,
|
||||
};
|
||||
layout->timeline_layout = (TimelineLayout *)layout_create(item->header.layout, &config);
|
||||
return layout;
|
||||
}
|
||||
|
||||
static void prv_set_layout(PeekLayout *layout) {
|
||||
prv_destroy_layout();
|
||||
TimelinePeek *peek = &s_peek;
|
||||
peek->peek_layout = layout;
|
||||
layer_add_child(&peek->layout_layer, &peek->peek_layout->timeline_layout->layout_layer.layer);
|
||||
}
|
||||
|
||||
static void prv_unschedule_animation(TimelinePeek *peek) {
|
||||
animation_unschedule(peek->animation);
|
||||
peek->animation = NULL;
|
||||
}
|
||||
|
||||
static bool prv_should_use_unobstructed_area() {
|
||||
GSize app_framebuffer_size;
|
||||
app_manager_get_framebuffer_size(&app_framebuffer_size);
|
||||
return (DISP_ROWS - app_framebuffer_size.h) < TIMELINE_PEEK_HEIGHT;
|
||||
}
|
||||
|
||||
static void prv_peek_frame_setup(Animation *animation) {
|
||||
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
|
||||
TimelinePeek *peek;
|
||||
property_animation_subject(prop_anim, (void *)&peek, false /* set */);
|
||||
GRect from_frame;
|
||||
property_animation_get_from_grect(prop_anim, &from_frame);
|
||||
GRect to_frame;
|
||||
property_animation_get_to_grect(prop_anim, &to_frame);
|
||||
if (prv_should_use_unobstructed_area()) {
|
||||
unobstructed_area_service_will_change(from_frame.origin.y, to_frame.origin.y);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_peek_frame_update(Animation *animation, AnimationProgress progress) {
|
||||
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
|
||||
property_animation_update_grect(prop_anim, progress);
|
||||
TimelinePeek *peek;
|
||||
property_animation_subject(prop_anim, (void *)&peek, false /* set */);
|
||||
GRect to_frame;
|
||||
property_animation_get_to_grect(prop_anim, &to_frame);
|
||||
if (prv_should_use_unobstructed_area()) {
|
||||
unobstructed_area_service_change(peek->layout_layer.frame.origin.y, to_frame.origin.y,
|
||||
progress);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_peek_frame_teardown(Animation *animation) {
|
||||
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
|
||||
GRect to_frame;
|
||||
property_animation_get_to_grect(prop_anim, &to_frame);
|
||||
if (prv_should_use_unobstructed_area()) {
|
||||
unobstructed_area_service_did_change(to_frame.origin.y);
|
||||
}
|
||||
}
|
||||
|
||||
static GRect prv_peek_frame_getter(void *subject) {
|
||||
TimelinePeek *peek = subject;
|
||||
GRect frame;
|
||||
layer_get_frame(&peek->layout_layer, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
static void prv_peek_frame_setter(void *subject, GRect frame) {
|
||||
TimelinePeek *peek = subject;
|
||||
layer_set_frame(&peek->layout_layer, &frame);
|
||||
}
|
||||
|
||||
static const PropertyAnimationImplementation s_peek_prop_impl = {
|
||||
.base = {
|
||||
.setup = prv_peek_frame_setup,
|
||||
.update = prv_peek_frame_update,
|
||||
.teardown = prv_peek_frame_teardown,
|
||||
},
|
||||
.accessors = {
|
||||
.getter.grect = prv_peek_frame_getter,
|
||||
.setter.grect = prv_peek_frame_setter,
|
||||
},
|
||||
};
|
||||
|
||||
static void prv_peek_anim_stopped(Animation *animation, bool finished, void *context) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
if (context) {
|
||||
// Replace the previous item with the next item
|
||||
PeekLayout *layout = context;
|
||||
prv_set_layout(layout);
|
||||
// Reset the frame
|
||||
layer_set_frame(&peek->layout_layer, &TIMELINE_PEEK_FRAME_VISIBLE);
|
||||
} else if (!peek->visible) {
|
||||
// If the peek was becoming hidden, destroy the timeline layout
|
||||
prv_destroy_layout();
|
||||
}
|
||||
peek->removing_concurrent = false;
|
||||
}
|
||||
|
||||
static const AnimationHandlers s_peek_anim_handlers = {
|
||||
.stopped = prv_peek_anim_stopped,
|
||||
};
|
||||
|
||||
static void prv_transition_frame(TimelinePeek *peek, bool visible, bool animated) {
|
||||
prv_unschedule_animation(peek);
|
||||
|
||||
const bool last_visible = peek->visible;
|
||||
peek->visible = visible;
|
||||
GRect to_frame = visible ? TIMELINE_PEEK_FRAME_VISIBLE : TIMELINE_PEEK_FRAME_HIDDEN;
|
||||
if ((last_visible == visible) && grect_equal(&peek->layout_layer.frame, &to_frame)) {
|
||||
// No change
|
||||
return;
|
||||
}
|
||||
|
||||
if (!animated) {
|
||||
layer_set_frame(&peek->layout_layer, &to_frame);
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyAnimation *prop_anim = property_animation_create(&s_peek_prop_impl, peek, NULL, NULL);
|
||||
property_animation_set_from_grect(prop_anim, &peek->layout_layer.frame);
|
||||
property_animation_set_to_grect(prop_anim, &to_frame);
|
||||
Animation *animation = property_animation_get_animation(prop_anim);
|
||||
animation_set_duration(animation, interpolate_moook_duration());
|
||||
animation_set_custom_interpolation(animation, interpolate_moook);
|
||||
animation_set_handlers(animation, s_peek_anim_handlers, NULL);
|
||||
|
||||
peek->animation = animation;
|
||||
animation_schedule(animation);
|
||||
}
|
||||
|
||||
#define EXTENDED_BOUNCE_BACK (2 * INTERPOLATE_MOOOK_BOUNCE_BACK)
|
||||
|
||||
static const int32_t s_extended_moook_out[] =
|
||||
{EXTENDED_BOUNCE_BACK, INTERPOLATE_MOOOK_BOUNCE_BACK, 2, 1, 0};
|
||||
static const MoookConfig s_extended_moook_out_config = {
|
||||
.frames_out = s_extended_moook_out,
|
||||
.num_frames_out = ARRAY_LENGTH(s_extended_moook_out),
|
||||
.no_bounce_back = true,
|
||||
};
|
||||
|
||||
static int64_t prv_interpolate_extended_moook_out(AnimationProgress progress, int64_t from,
|
||||
int64_t to) {
|
||||
return interpolate_moook_custom(progress, from, to, &s_extended_moook_out_config);
|
||||
}
|
||||
|
||||
static Animation *prv_create_transition_adding_concurrent(
|
||||
TimelinePeek *peek, PeekLayout *layout) {
|
||||
const int height_shrink = 20;
|
||||
GRect frame_normal = TIMELINE_PEEK_FRAME_VISIBLE;
|
||||
GRect frame_shrink = grect_inset(frame_normal, GEdgeInsets(0, 0, height_shrink, 0));
|
||||
// Starting with shrink instead of ending with it will flash white
|
||||
PropertyAnimation *white_prop_anim = property_animation_create_layer_frame(
|
||||
&peek->layout_layer, &frame_shrink, &frame_normal);
|
||||
Animation *white_animation = property_animation_get_animation(white_prop_anim);
|
||||
animation_set_duration(white_animation, ANIMATION_TARGET_FRAME_INTERVAL_MS);
|
||||
animation_set_handlers(white_animation, s_peek_anim_handlers, layout);
|
||||
|
||||
GRect frame_bounce =
|
||||
grect_inset(frame_normal, GEdgeInsets(-EXTENDED_BOUNCE_BACK, 0, 0, 0));
|
||||
PropertyAnimation *bounce_prop_anim = property_animation_create_layer_frame(
|
||||
&peek->layout_layer, &frame_bounce, &frame_normal);
|
||||
Animation *bounce_animation = property_animation_get_animation(bounce_prop_anim);
|
||||
animation_set_duration(bounce_animation,
|
||||
interpolate_moook_custom_duration(&s_extended_moook_out_config));
|
||||
animation_set_custom_interpolation(bounce_animation, prv_interpolate_extended_moook_out);
|
||||
return animation_sequence_create(white_animation, bounce_animation, NULL);
|
||||
}
|
||||
|
||||
static const int32_t s_custom_moook_in[] = {0, 1, INTERPOLATE_MOOOK_BOUNCE_BACK};
|
||||
static const MoookConfig s_custom_moook_in_config = {
|
||||
.frames_in = s_custom_moook_in,
|
||||
.num_frames_in = ARRAY_LENGTH(s_custom_moook_in),
|
||||
};
|
||||
|
||||
static int64_t prv_interpolate_custom_moook_in(AnimationProgress progress, int64_t from,
|
||||
int64_t to) {
|
||||
return interpolate_moook_custom(progress, from, to, &s_custom_moook_in_config);
|
||||
}
|
||||
|
||||
static int64_t prv_interpolate_moook_out(AnimationProgress progress, int64_t from, int64_t to) {
|
||||
return interpolate_moook_out(progress, from, to, 0 /* num_frames_from */,
|
||||
false /* bounce_back */);
|
||||
}
|
||||
|
||||
static Animation *prv_create_transition_removing_concurrent(
|
||||
TimelinePeek *peek, PeekLayout *layout) {
|
||||
PropertyAnimation *remove_prop_anim = property_animation_create_layer_frame(
|
||||
&peek->layout_layer, &TIMELINE_PEEK_FRAME_VISIBLE, &TIMELINE_PEEK_FRAME_HIDDEN);
|
||||
Animation *remove_animation = property_animation_get_animation(remove_prop_anim);
|
||||
// Cut out the last frame
|
||||
animation_set_duration(remove_animation,
|
||||
interpolate_moook_custom_duration(&s_custom_moook_in_config));
|
||||
animation_set_custom_interpolation(remove_animation, prv_interpolate_custom_moook_in);
|
||||
animation_set_handlers(remove_animation, s_peek_anim_handlers, layout);
|
||||
|
||||
GRect bounds_normal = { .size = TIMELINE_PEEK_FRAME_VISIBLE.size };
|
||||
GRect bounds_bounce = { .origin.y = TIMELINE_PEEK_HEIGHT, .size = bounds_normal.size };
|
||||
PropertyAnimation *bounce_prop_anim = property_animation_create_layer_bounds(
|
||||
&peek->layout_layer, &bounds_bounce, &bounds_normal);
|
||||
Animation *bounce_animation = property_animation_get_animation(bounce_prop_anim);
|
||||
animation_set_duration(bounce_animation, interpolate_moook_out_duration());
|
||||
animation_set_custom_interpolation(bounce_animation, prv_interpolate_moook_out);
|
||||
return animation_sequence_create(remove_animation, bounce_animation, NULL);
|
||||
}
|
||||
|
||||
static void prv_transition_concurrent(TimelinePeek *peek, PeekLayout *layout) {
|
||||
const unsigned int old_num_concurrent = peek->peek_layout->info.num_concurrent;
|
||||
const unsigned int new_num_concurrent = layout->info.num_concurrent;
|
||||
if (uuid_equal(&peek->peek_layout->item->header.id, &layout->item->header.id) &&
|
||||
(old_num_concurrent == new_num_concurrent)) {
|
||||
// Either nothing changed or the item content changed, just set the layout
|
||||
prv_set_layout(layout);
|
||||
return;
|
||||
}
|
||||
|
||||
prv_unschedule_animation(peek);
|
||||
|
||||
Animation *animation = NULL;
|
||||
if (peek->peek_layout && (old_num_concurrent < new_num_concurrent)) {
|
||||
animation = prv_create_transition_adding_concurrent(peek, layout);
|
||||
} else {
|
||||
animation = prv_create_transition_removing_concurrent(peek, layout);
|
||||
peek->removing_concurrent = true;
|
||||
}
|
||||
|
||||
peek->animation = animation;
|
||||
animation_schedule(animation);
|
||||
}
|
||||
|
||||
static void prv_push_timeline_peek(void *unused) {
|
||||
timeline_peek_push();
|
||||
}
|
||||
|
||||
void timeline_peek_init(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
*peek = (TimelinePeek) {
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK && !SHELL_SDK
|
||||
.enabled = timeline_peek_prefs_get_enabled(),
|
||||
#endif
|
||||
};
|
||||
window_init(&peek->window, WINDOW_NAME("Timeline Peek"));
|
||||
window_set_focusable(&peek->window, false);
|
||||
window_set_transparent(&peek->window, true);
|
||||
layer_set_update_proc(&peek->window.layer, prv_timeline_peek_update_proc);
|
||||
layer_init(&peek->layout_layer, &TIMELINE_PEEK_FRAME_HIDDEN);
|
||||
layer_add_child(&peek->window.layer, &peek->layout_layer);
|
||||
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK
|
||||
timeline_peek_set_show_before_time(timeline_peek_prefs_get_before_time() * SECONDS_PER_MINUTE);
|
||||
#endif
|
||||
|
||||
// Wait one event loop to show the timeline peek
|
||||
launcher_task_add_callback(prv_push_timeline_peek, NULL);
|
||||
}
|
||||
|
||||
static void prv_set_visible(bool visible, bool animated) {
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK
|
||||
TimelinePeek *peek = &s_peek;
|
||||
if (!peek->started && visible) {
|
||||
cron_job_schedule(&s_timeline_peek_job);
|
||||
} else {
|
||||
cron_job_unschedule(&s_timeline_peek_job);
|
||||
}
|
||||
prv_transition_frame(peek, visible, animated);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool prv_can_animate(void) {
|
||||
return app_manager_is_watchface_running();
|
||||
}
|
||||
|
||||
void timeline_peek_set_visible(bool visible, bool animated) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
#if !SHELL_SDK
|
||||
if (!peek->exists) {
|
||||
visible = false;
|
||||
}
|
||||
#endif
|
||||
prv_set_visible((app_manager_is_watchface_running() && peek->enabled && visible),
|
||||
(prv_can_animate() && animated));
|
||||
}
|
||||
|
||||
void timeline_peek_set_item(TimelineItem *item, bool started, unsigned int num_concurrent,
|
||||
bool first, bool animated) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
animated = (prv_can_animate() && animated);
|
||||
if (!animated) {
|
||||
// We are not animating and thus don't need to retain the layout
|
||||
prv_destroy_layout();
|
||||
}
|
||||
|
||||
peek->exists = (item != NULL);
|
||||
peek->started = started;
|
||||
peek->first = first;
|
||||
timeline_peek_set_visible(peek->exists, animated);
|
||||
|
||||
PeekLayout *layout = item ? prv_create_layout(item, num_concurrent) : NULL;
|
||||
if (animated && !peek->animation && peek->visible) {
|
||||
// Swap the layout in an animation
|
||||
prv_transition_concurrent(peek, layout);
|
||||
} else if (layout) {
|
||||
// Immediately set the new layout
|
||||
prv_set_layout(layout);
|
||||
}
|
||||
}
|
||||
|
||||
void timeline_peek_dismiss(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
if (!peek->peek_layout) {
|
||||
return;
|
||||
}
|
||||
TimelineItem *item = peek->peek_layout->item;
|
||||
const status_t rv = pin_db_set_status_bits(&item->header.id, TimelineItemStatusDismissed);
|
||||
if (rv == S_SUCCESS) {
|
||||
timeline_event_refresh();
|
||||
} else {
|
||||
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(&item->header.id, uuid_buffer);
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Failed to dismiss Timeline Peek event %s (status: %"PRIi32")",
|
||||
uuid_buffer, rv);
|
||||
}
|
||||
}
|
||||
|
||||
int16_t timeline_peek_get_origin_y(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
return peek->layout_layer.frame.origin.y;
|
||||
}
|
||||
|
||||
int16_t timeline_peek_get_obstruction_origin_y(void) {
|
||||
return prv_should_use_unobstructed_area() ? timeline_peek_get_origin_y() : DISP_ROWS;
|
||||
}
|
||||
|
||||
void timeline_peek_get_item_id(TimelineItemId *item_id_out) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
*item_id_out = (peek->enabled && peek->visible && peek->exists && peek->peek_layout)
|
||||
? peek->peek_layout->item->header.id : UUID_INVALID;
|
||||
}
|
||||
|
||||
bool timeline_peek_is_first_event(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
return peek->first;
|
||||
}
|
||||
|
||||
bool timeline_peek_is_future_empty(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
return peek->future_empty;
|
||||
}
|
||||
|
||||
void timeline_peek_push(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
modal_window_push(&peek->window, ModalPriorityDiscreet, true);
|
||||
}
|
||||
|
||||
void timeline_peek_pop(void) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
window_stack_remove(&peek->window, true);
|
||||
}
|
||||
|
||||
void timeline_peek_set_enabled(bool enabled) {
|
||||
TimelinePeek *peek = &s_peek;
|
||||
peek->enabled = enabled;
|
||||
timeline_peek_set_visible(enabled, true /* animated */);
|
||||
}
|
||||
|
||||
void timeline_peek_handle_peek_event(PebbleTimelinePeekEvent *event) {
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK
|
||||
TimelinePeek *peek = &s_peek;
|
||||
peek->future_empty = event->is_future_empty;
|
||||
bool show = false;
|
||||
bool started = false;
|
||||
if (event->item_id != NULL) {
|
||||
switch (event->time_type) {
|
||||
case TimelinePeekTimeType_None:
|
||||
case TimelinePeekTimeType_SomeTimeNext:
|
||||
case TimelinePeekTimeType_WillEnd:
|
||||
break;
|
||||
case TimelinePeekTimeType_ShowWillStart:
|
||||
show = true;
|
||||
break;
|
||||
case TimelinePeekTimeType_ShowStarted:
|
||||
show = true;
|
||||
started = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
TimelineItem item = {};
|
||||
if (show) {
|
||||
const status_t rv = pin_db_get(event->item_id, &item);
|
||||
// We failed to read the pin since it may have been deleted immediately. We will probably
|
||||
// momentarily recover from another peek event resulting from the delete.
|
||||
show = (rv == S_SUCCESS);
|
||||
}
|
||||
if (show) {
|
||||
timeline_peek_set_item(&item, started, event->num_concurrent,
|
||||
event->is_first_event, true /* animated */);
|
||||
} else {
|
||||
timeline_peek_set_item(NULL, false /* started */, 0 /* num_concurrent */,
|
||||
false /* is_first_event */, true /* animated */);
|
||||
}
|
||||
timeline_item_free_allocated_buffer(&item);
|
||||
#endif
|
||||
}
|
||||
|
||||
void timeline_peek_handle_process_start(void) {
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK
|
||||
timeline_peek_set_visible(true, false /* animated */);
|
||||
#endif
|
||||
}
|
||||
|
||||
void timeline_peek_handle_process_kill(void) {
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK
|
||||
timeline_peek_set_visible(false, false /* animated */);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITTEST
|
||||
TimelinePeek *timeline_peek_get_peek(void) {
|
||||
return &s_peek;
|
||||
}
|
||||
#endif
|
120
src/fw/popups/timeline/peek.h
Normal file
120
src/fw/popups/timeline/peek.h
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/preferred_content_size.h"
|
||||
#include "applib/ui/animation.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "kernel/events.h"
|
||||
#include "services/normal/timeline/timeline.h"
|
||||
|
||||
#define TIMELINE_PEEK_HEIGHT \
|
||||
PREFERRED_CONTENT_SIZE_SWITCH(PreferredContentSizeDefault, \
|
||||
/* This is the same as Medium until Small is designed */ \
|
||||
/* small */ PBL_IF_RECT_ELSE(51, 45), \
|
||||
/* medium */ PBL_IF_RECT_ELSE(51, 45), \
|
||||
/* large */ 59, \
|
||||
/* This is the same as Large until ExtraLarge is designed */ \
|
||||
/* x-large */ 59 \
|
||||
)
|
||||
|
||||
#define TIMELINE_PEEK_ICON_BOX_WIDTH \
|
||||
PREFERRED_CONTENT_SIZE_SWITCH(PreferredContentSizeDefault, \
|
||||
/* This is the same as Medium until Small is designed */ \
|
||||
/* small */ PBL_IF_RECT_ELSE(30, 51), \
|
||||
/* medium */ PBL_IF_RECT_ELSE(30, 51), \
|
||||
/* large */ 34, \
|
||||
/* This is the same as Large until ExtraLarge is designed */ \
|
||||
/* x-large */ 34 \
|
||||
)
|
||||
|
||||
#define TIMELINE_PEEK_MARGIN (5)
|
||||
#define TIMELINE_PEEK_ORIGIN_Y_VISIBLE PBL_IF_RECT_ELSE(DISP_ROWS - TIMELINE_PEEK_HEIGHT, 112)
|
||||
#define TIMELINE_PEEK_FRAME_VISIBLE GRect(0, TIMELINE_PEEK_ORIGIN_Y_VISIBLE, DISP_COLS, \
|
||||
TIMELINE_PEEK_HEIGHT)
|
||||
|
||||
//! Gets the concurrent height needed to render for the number of concurrent events.
|
||||
//! @return The concurrent height
|
||||
unsigned int timeline_peek_get_concurrent_height(unsigned int num_concurrent);
|
||||
|
||||
//! Draws the timeline peek background.
|
||||
//! @param ctx Graphics context to draw with.
|
||||
//! @param frame The rectangle of the peek to draw.
|
||||
//! @param num_concurrent The number of events to indicate.
|
||||
void timeline_peek_draw_background(GContext *ctx, const GRect *frame,
|
||||
unsigned int num_concurrent);
|
||||
|
||||
//! Initializes a TimelinePeek overlay (transparent, unfocusable modal window)
|
||||
void timeline_peek_init(void);
|
||||
|
||||
//! Sets whether the peek is visible. The peek will animate in or out depending if it was
|
||||
//! previously visible or not.
|
||||
//! @param visible Whether to show the peek
|
||||
//! @param animated Whether the peek animates into its new visibility state
|
||||
void timeline_peek_set_visible(bool visible, bool animated);
|
||||
|
||||
//! Sets the pin information to display as well as the number of concurrent events
|
||||
//! @param item TimelineItem reference which is stored and expected to exist until replaced. If
|
||||
//! NULL, the peek will be emptied and no event information is displayed.
|
||||
//! @param started Whether the item has started or not.
|
||||
//! @param num_concurrent The number of concurrent events to indicate
|
||||
//! @param first Whether the item is the first event in Timeline.
|
||||
//! @param animated Whether the peek animates into its new visibility state
|
||||
void timeline_peek_set_item(TimelineItem *item, bool started, unsigned int num_concurrent,
|
||||
bool first, bool animated);
|
||||
|
||||
//! Returns whether the item in the peek is the first event in Timeline.
|
||||
//! @return true if the peek is showing the first time, false otherwise.
|
||||
bool timeline_peek_is_first_event(void);
|
||||
|
||||
//! Returns whether Timeline future is empty upon entering it.
|
||||
//! @return true if Timeline future is empty, false otherwise.
|
||||
bool timeline_peek_is_future_empty(void);
|
||||
|
||||
//! Dismisses the current TimelinePeek Timeline item.
|
||||
void timeline_peek_dismiss(void);
|
||||
|
||||
//! Gets the current y of the peek
|
||||
int16_t timeline_peek_get_origin_y(void);
|
||||
|
||||
//! Gets the current obstruction y from which the unobstructed area can be derived from
|
||||
int16_t timeline_peek_get_obstruction_origin_y(void);
|
||||
|
||||
//! Gets the current timeline item id. If there is no item, UUID_INVALID is given instead.
|
||||
//! @param item_id_out Pointer to the item id buffer to write to.
|
||||
void timeline_peek_get_item_id(TimelineItemId *item_id_out);
|
||||
|
||||
//! Pushes the TimelinePeek window
|
||||
void timeline_peek_push(void);
|
||||
|
||||
//! Pops the TimelinePeek window
|
||||
void timeline_peek_pop(void);
|
||||
|
||||
//! Toggles whether TimelinePeek is enabled. Used by the qemu serial protocol for the SDK.
|
||||
void timeline_peek_set_enabled(bool enabled);
|
||||
|
||||
//! Handles timeline peek events
|
||||
void timeline_peek_handle_peek_event(PebbleTimelinePeekEvent *event);
|
||||
|
||||
//! Handles process start synchronously. This is synchronous because the app manager needs to know
|
||||
//! the new unobstructed area that would result from process start in order to prepare the app
|
||||
//! state initialization parameters with the new obstruction position.
|
||||
void timeline_peek_handle_process_start(void);
|
||||
|
||||
//! Handles process kill synchronously. This is synchronous because process start is handled
|
||||
//! synchronously -- a processing being killed and another process starting happen in sequence.
|
||||
void timeline_peek_handle_process_kill(void);
|
68
src/fw/popups/timeline/peek_animations.c
Normal file
68
src/fw/popups/timeline/peek_animations.c
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 "peek_animations.h"
|
||||
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#define LINE_WIDTH (2)
|
||||
#define LINE_SPACING (10)
|
||||
|
||||
static void prv_draw_vertical_lines(GContext *ctx, unsigned int num_lines,
|
||||
const uint16_t *offsets_y, const uint16_t *heights,
|
||||
unsigned int width, unsigned int spacing, GPoint offset) {
|
||||
for (int i = 0; i < (int)num_lines; i++) {
|
||||
GRect box = {
|
||||
.origin = gpoint_add(GPoint((spacing + width) * i - width,
|
||||
offsets_y ? offsets_y[i] : 0), offset),
|
||||
.size = { width, heights[i] },
|
||||
};
|
||||
graphics_fill_rect(ctx, &box);
|
||||
}
|
||||
}
|
||||
|
||||
void peek_animations_draw_compositor_foreground_speed_lines(GContext *ctx, GPoint offset) {
|
||||
static const uint16_t s_upper_heights[] = { 48, 73, 78, 48, 48, 48, 61, 48 };
|
||||
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_upper_heights), NULL /* offsets_y */,
|
||||
s_upper_heights, LINE_WIDTH, LINE_SPACING, offset);
|
||||
|
||||
static const uint16_t s_lower_offsets_y[] = { 24, 24, 0, 19, 7, 0, 0, 24 };
|
||||
static const uint16_t s_lower_heights[] = { 48, 48, 72, 53, 65, 72, 72, 48 };
|
||||
offset.y += 90;
|
||||
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_lower_heights), s_lower_offsets_y, s_lower_heights,
|
||||
LINE_WIDTH, LINE_SPACING, offset);
|
||||
}
|
||||
|
||||
void peek_animations_draw_compositor_background_speed_lines(GContext *ctx, GPoint offset) {
|
||||
static const uint16_t s_heights[] = { 0, DISP_ROWS, DISP_ROWS, 0, 0, 0, DISP_ROWS };
|
||||
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_heights), NULL /* offsets_y */, s_heights,
|
||||
LINE_WIDTH, LINE_SPACING, offset);
|
||||
}
|
||||
|
||||
void peek_animations_draw_timeline_speed_lines(GContext *ctx, GPoint offset) {
|
||||
static const uint16_t s_upper_offsets_y[] = { 12, 0, 0, 12, 12, 12, 12, 12 };
|
||||
static const uint16_t s_upper_heights[] = { 53, 65, 65, 53, 53, 53, 53, 53 };
|
||||
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_upper_heights), s_upper_offsets_y, s_upper_heights,
|
||||
LINE_WIDTH, LINE_SPACING, offset);
|
||||
|
||||
static const uint16_t s_lower_offsets_y[] = { 5, 5, 0, 0, 5, 5, 5, 5 };
|
||||
static const uint16_t s_lower_heights[] = { 53, 87, 87, 53, 53, 53, 53, 53 };
|
||||
offset.y += 65;
|
||||
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_lower_heights), s_lower_offsets_y, s_lower_heights,
|
||||
LINE_WIDTH, LINE_SPACING, offset);
|
||||
}
|
27
src/fw/popups/timeline/peek_animations.h
Normal file
27
src/fw/popups/timeline/peek_animations.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/graphics/gtypes.h"
|
||||
|
||||
#define PEEK_ANIMATIONS_SPEED_LINES_OFFSET_X (7)
|
||||
|
||||
void peek_animations_draw_compositor_foreground_speed_lines(GContext *ctx, GPoint offset);
|
||||
|
||||
void peek_animations_draw_compositor_background_speed_lines(GContext *ctx, GPoint offset);
|
||||
|
||||
void peek_animations_draw_timeline_speed_lines(GContext *ctx, GPoint offset);
|
47
src/fw/popups/timeline/peek_private.h
Normal file
47
src/fw/popups/timeline/peek_private.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "peek.h"
|
||||
|
||||
#include "applib/ui/animation.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "services/normal/timeline/timeline_layout.h"
|
||||
|
||||
typedef struct PeekLayout {
|
||||
TimelineLayoutInfo info;
|
||||
TimelineLayout *timeline_layout;
|
||||
TimelineItem *item;
|
||||
} PeekLayout;
|
||||
|
||||
typedef struct TimelinePeek {
|
||||
Window window;
|
||||
Layer layout_layer;
|
||||
PeekLayout *peek_layout;
|
||||
Animation *animation; //!< Currently running animation
|
||||
bool exists; //!< Whether there exists an item to show in peek.
|
||||
bool started; //!< Whether the item has started.
|
||||
bool enabled; //!< Whether to persistently show or hide the peek.
|
||||
bool visible; //!< Whether the peek is visible or not.
|
||||
bool first; //!< Whether the item is the first item in Timeline.
|
||||
bool removing_concurrent; //!< Whether the removing concurrent animation is occurring.
|
||||
bool future_empty; //!< Whether Timeline future is empty.
|
||||
} TimelinePeek;
|
||||
|
||||
#if UNITTEST
|
||||
TimelinePeek *timeline_peek_get_peek(void);
|
||||
#endif
|
292
src/fw/popups/timeline/timeline_item_layer.c
Normal file
292
src/fw/popups/timeline/timeline_item_layer.c
Normal file
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* 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 "timeline_item_layer.h"
|
||||
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/ui/action_menu_window.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/ui/window_manager.h"
|
||||
#include "apps/system_apps/timeline/timeline.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "kernel/ui/modals/modal_manager.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "services/normal/timeline/actions_endpoint.h"
|
||||
#include "services/normal/timeline/layout_layer.h"
|
||||
#include "services/normal/timeline/timeline.h"
|
||||
#include "services/normal/timeline/timeline_actions.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/math.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Drawing functions
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
static GSize prv_get_frame_size(TimelineItemLayer *item_layer) {
|
||||
return item_layer->layer.bounds.size;
|
||||
}
|
||||
|
||||
static int16_t prv_get_height(TimelineItemLayer *item_layer) {
|
||||
if (item_layer->timeline_layout) {
|
||||
GSize size = layout_get_size(graphics_context_get_current_context(),
|
||||
(LayoutLayer *)item_layer->timeline_layout);
|
||||
return size.h;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_update_item(GContext *ctx, TimelineItemLayer *item_layer) {
|
||||
if (item_layer->timeline_layout) {
|
||||
GRect bounds = item_layer->timeline_layout->layout_layer.layer.bounds;
|
||||
bounds.origin.y = 0 - item_layer->scroll_offset_pixels;
|
||||
layer_set_bounds((Layer *)item_layer->timeline_layout, &bounds);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Scrolling related functions
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
static int16_t prv_get_first_scroll_offset(TimelineItemLayer *item_layer) {
|
||||
if (!item_layer->timeline_layout->has_page_break) {
|
||||
return 0;
|
||||
}
|
||||
return MAX(prv_get_frame_size(item_layer).h, 0);
|
||||
}
|
||||
|
||||
static int16_t prv_get_min_scroll_offset(TimelineItemLayer *item_layer) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int16_t prv_get_max_scroll_offset(TimelineItemLayer *item_layer) {
|
||||
int16_t max_scroll = prv_get_height(item_layer) - prv_get_frame_size(item_layer).h;
|
||||
if (max_scroll > 0) {
|
||||
int16_t first_scroll = prv_get_first_scroll_offset(item_layer);
|
||||
return MAX(first_scroll, max_scroll);
|
||||
} else {
|
||||
return MAX(max_scroll, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_scroll_offset_setter(TimelineItemLayer *item_layer, int16_t value) {
|
||||
item_layer->scroll_offset_pixels = value;
|
||||
layer_mark_dirty(&item_layer->layer);
|
||||
}
|
||||
|
||||
static int16_t prv_scroll_offset_getter(TimelineItemLayer *item_layer) {
|
||||
return item_layer->scroll_offset_pixels;
|
||||
}
|
||||
|
||||
static void prv_update_scroll_offset(TimelineItemLayer *item_layer, int16_t new_offset,
|
||||
bool is_first_scroll) {
|
||||
static const PropertyAnimationImplementation implementation = {
|
||||
.base = {
|
||||
.update = (AnimationUpdateImplementation) property_animation_update_int16,
|
||||
},
|
||||
.accessors = {
|
||||
.setter = { .int16 = (const Int16Setter) prv_scroll_offset_setter, },
|
||||
.getter = { .int16 = (const Int16Getter) prv_scroll_offset_getter},
|
||||
},
|
||||
};
|
||||
|
||||
// If we're already at that position, don't bother scheduling an animation
|
||||
if (item_layer->scroll_offset_pixels == new_offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item_layer->animation
|
||||
&& animation_is_scheduled(property_animation_get_animation(item_layer->animation))) {
|
||||
// Don't do anything if we're already animating to this position from our current position
|
||||
int16_t offset;
|
||||
property_animation_get_to_int16(item_layer->animation, &offset);
|
||||
if (offset == new_offset) {
|
||||
return;
|
||||
}
|
||||
animation_unschedule(property_animation_get_animation(item_layer->animation));
|
||||
}
|
||||
|
||||
if (item_layer->animation) {
|
||||
property_animation_init(item_layer->animation, &implementation, item_layer, NULL, &new_offset);
|
||||
} else {
|
||||
item_layer->animation = property_animation_create(&implementation,
|
||||
item_layer, NULL, &new_offset);
|
||||
PBL_ASSERTN(item_layer->animation);
|
||||
animation_set_auto_destroy(property_animation_get_animation(item_layer->animation), false);
|
||||
}
|
||||
Animation *animation = property_animation_get_animation(item_layer->animation);
|
||||
if (is_first_scroll) {
|
||||
animation_set_duration(animation, interpolate_moook_duration());
|
||||
animation_set_custom_interpolation(animation, interpolate_moook);
|
||||
} else {
|
||||
animation_set_curve(animation, AnimationCurveEaseOut);
|
||||
}
|
||||
animation_schedule(animation);
|
||||
}
|
||||
|
||||
//! Maybe make this part of the style and smaller for smaller text sizes?
|
||||
static const int SCROLL_AMOUNT = PBL_IF_RECT_ELSE(48, DISP_ROWS - STATUS_BAR_LAYER_HEIGHT);
|
||||
static const int SCROLL_FUDGE_AMOUNT = PBL_IF_RECT_ELSE(10, 0);
|
||||
|
||||
/////////////////////////////////////////
|
||||
// Click Config
|
||||
/////////////////////////////////////////
|
||||
|
||||
T_STATIC void prv_handle_down_click(ClickRecognizerRef recognizer, void *context) {
|
||||
TimelineItemLayer *item_layer = (TimelineItemLayer *)context;
|
||||
int16_t max_scroll = prv_get_max_scroll_offset(item_layer);
|
||||
const int16_t first_scroll = prv_get_first_scroll_offset(item_layer);
|
||||
int16_t current_scroll = item_layer->scroll_offset_pixels;
|
||||
#if PBL_ROUND // align current_scroll with paging for text flow
|
||||
current_scroll = ROUND_TO_MOD_CEIL(current_scroll, SCROLL_AMOUNT);
|
||||
#endif
|
||||
|
||||
if (max_scroll >= first_scroll && current_scroll < first_scroll) {
|
||||
prv_update_scroll_offset(item_layer, first_scroll, true);
|
||||
} else if (current_scroll + SCROLL_AMOUNT + SCROLL_FUDGE_AMOUNT >= max_scroll) {
|
||||
#if PBL_ROUND
|
||||
// scroll down to page aligned end of content
|
||||
max_scroll = ROUND_TO_MOD_CEIL(max_scroll, DISP_ROWS - STATUS_BAR_LAYER_HEIGHT);
|
||||
#endif
|
||||
prv_update_scroll_offset(item_layer, max_scroll, false);
|
||||
} else {
|
||||
prv_update_scroll_offset(item_layer, current_scroll + SCROLL_AMOUNT, false);
|
||||
}
|
||||
layer_mark_dirty(&item_layer->layer);
|
||||
}
|
||||
|
||||
static void prv_handle_select_click(ClickRecognizerRef recognizer, void *context) {
|
||||
TimelineItemLayer *item_layer = context;
|
||||
TimelineItemActionGroup *action_group = &item_layer->item->action_group;
|
||||
const uint8_t num_actions = action_group->num_actions;
|
||||
ActionMenuLevel *root_level = timeline_actions_create_action_menu_root_level(
|
||||
num_actions, 0, TimelineItemActionSourceTimeline);
|
||||
for (int i = 0; i < num_actions; i++) {
|
||||
timeline_actions_add_action_to_root_level(&action_group->actions[i], root_level);
|
||||
}
|
||||
const LayoutColors *colors = layout_get_colors((LayoutLayer *)item_layer->timeline_layout);
|
||||
ActionMenuConfig config = {
|
||||
.root_level = root_level,
|
||||
.context = item_layer->item,
|
||||
.colors.background = colors->bg_color,
|
||||
.colors.foreground = colors->primary_color,
|
||||
};
|
||||
timeline_actions_push_action_menu(
|
||||
&config, window_manager_get_window_stack(ModalPriorityNotification));
|
||||
}
|
||||
|
||||
static void prv_handle_up_click(ClickRecognizerRef recognizer, void *context) {
|
||||
TimelineItemLayer *item_layer = (TimelineItemLayer *)context;
|
||||
const int16_t min_scroll = prv_get_min_scroll_offset(item_layer);
|
||||
const int16_t first_scroll = prv_get_first_scroll_offset(item_layer);
|
||||
int16_t current_scroll = item_layer->scroll_offset_pixels;
|
||||
#if PBL_ROUND // align current_scroll with paging for text flow
|
||||
current_scroll = ROUND_TO_MOD_CEIL(current_scroll, SCROLL_AMOUNT);
|
||||
#endif
|
||||
|
||||
if (current_scroll <= first_scroll) {
|
||||
prv_update_scroll_offset(item_layer, min_scroll, true);
|
||||
#if PBL_RECT // fudge breaks ROUND display paging
|
||||
} else if (current_scroll - (SCROLL_AMOUNT + SCROLL_FUDGE_AMOUNT) < first_scroll) {
|
||||
prv_update_scroll_offset(item_layer, first_scroll, false);
|
||||
#endif
|
||||
} else {
|
||||
prv_update_scroll_offset(item_layer, current_scroll - SCROLL_AMOUNT, false);
|
||||
}
|
||||
layer_mark_dirty(&item_layer->layer);
|
||||
}
|
||||
|
||||
static void prv_handle_back_click(ClickRecognizerRef recognizer, void *context) {
|
||||
timeline_animate_back_from_card();
|
||||
}
|
||||
|
||||
static void timeline_item_layer_click_config_provider(void *context) {
|
||||
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, prv_handle_up_click);
|
||||
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100, prv_handle_down_click);
|
||||
window_single_click_subscribe(BUTTON_ID_SELECT, prv_handle_select_click);
|
||||
window_set_click_context(BUTTON_ID_UP, context);
|
||||
window_set_click_context(BUTTON_ID_DOWN, context);
|
||||
window_set_click_context(BUTTON_ID_SELECT, context);
|
||||
window_set_click_context(BUTTON_ID_BACK, context);
|
||||
|
||||
if (pebble_task_get_current() == PebbleTask_App) {
|
||||
// only override the back button when we're in the app
|
||||
window_single_click_subscribe(BUTTON_ID_BACK, prv_handle_back_click);
|
||||
}
|
||||
}
|
||||
|
||||
void timeline_item_layer_set_click_config_onto_window(TimelineItemLayer *item_layer,
|
||||
struct Window *window) {
|
||||
window_set_click_config_provider_with_context(window,
|
||||
timeline_item_layer_click_config_provider,
|
||||
item_layer);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// Public functions
|
||||
/////////////////////////////////////////
|
||||
|
||||
void timeline_item_layer_update_proc(Layer* layer, GContext* ctx) {
|
||||
//! Fill background with white to hide layers below
|
||||
TimelineItemLayer* item_layer = (TimelineItemLayer *)layer;
|
||||
const LayoutColors *colors = layout_get_colors((LayoutLayer *)item_layer->timeline_layout);
|
||||
graphics_context_set_fill_color(ctx, colors->bg_color);
|
||||
graphics_fill_rect(ctx, &layer->bounds);
|
||||
prv_update_item(ctx, item_layer);
|
||||
}
|
||||
|
||||
void timeline_item_layer_init(TimelineItemLayer *item_layer, const GRect *frame) {
|
||||
*item_layer = (TimelineItemLayer){};
|
||||
layer_init(&item_layer->layer, frame);
|
||||
layer_set_update_proc(&item_layer->layer, timeline_item_layer_update_proc);
|
||||
layer_set_clips(&item_layer->layer, false);
|
||||
}
|
||||
|
||||
void timeline_item_layer_deinit(TimelineItemLayer *item_layer) {
|
||||
property_animation_destroy(item_layer->animation);
|
||||
layer_deinit(&item_layer->layer);
|
||||
if (item_layer->timeline_layout) {
|
||||
layout_destroy((LayoutLayer *)item_layer->timeline_layout);
|
||||
item_layer->timeline_layout = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void timeline_item_layer_set_item(TimelineItemLayer *item_layer, TimelineItem *item,
|
||||
TimelineLayoutInfo *info) {
|
||||
item_layer->item = item;
|
||||
if (item_layer->timeline_layout) {
|
||||
layer_remove_from_parent((Layer *)item_layer->timeline_layout);
|
||||
layout_destroy((LayoutLayer *)item_layer->timeline_layout);
|
||||
}
|
||||
const LayoutLayerConfig config = {
|
||||
.frame = &(GRect) { GPointZero, item_layer->layer.frame.size },
|
||||
.attributes = &item_layer->item->attr_list,
|
||||
.mode = LayoutLayerModeCard,
|
||||
.app_id = &item->header.parent_id,
|
||||
.context = info,
|
||||
};
|
||||
item_layer->timeline_layout =
|
||||
(TimelineLayout *)layout_create(item_layer->item->header.layout, &config);
|
||||
layer_add_child(&item_layer->layer, (Layer *)item_layer->timeline_layout);
|
||||
}
|
67
src/fw/popups/timeline/timeline_item_layer.h
Normal file
67
src/fw/popups/timeline/timeline_item_layer.h
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "applib/graphics/text.h"
|
||||
#include "applib/ui/layer.h"
|
||||
#include "applib/ui/property_animation_private.h"
|
||||
#include "kernel/events.h"
|
||||
#include "services/normal/timeline/item.h"
|
||||
#include "services/normal/timeline/timeline_layout.h"
|
||||
|
||||
//! The timeline item layer is a mock UI used to display timeline items
|
||||
//! until actual layouts are implemented. It is somewhat related to the
|
||||
//! notification layer, although it does not swap between items.
|
||||
|
||||
typedef struct {
|
||||
Layer layer;
|
||||
//!< The line that's currently at the top of the frame.
|
||||
int16_t scroll_offset_pixels;
|
||||
PropertyAnimation *animation;
|
||||
TimelineItem *item;
|
||||
TimelineLayout *timeline_layout;
|
||||
} TimelineItemLayer;
|
||||
|
||||
//! The layer update proc for the TimelineItemLayer
|
||||
void timeline_item_layer_update_proc(Layer* layer, GContext* ctx);
|
||||
|
||||
//! Initialize a timeline item layer
|
||||
//! @param layer a pointer to the TimelineItemLayer to initialize
|
||||
//! @param frame the frame with which to initialize the layer
|
||||
void timeline_item_layer_init(TimelineItemLayer *item_layer, const GRect *frame);
|
||||
|
||||
//! Deinitialize a timeline item layer. Currently a no-op
|
||||
void timeline_item_layer_deinit(TimelineItemLayer *item_layer);
|
||||
|
||||
//! Set the timeline item displayed by the TimelineItemLayer
|
||||
//! @param layer a pointer to the TimelineItemLayer
|
||||
//! @param item a pointer to the item to use
|
||||
void timeline_item_layer_set_item(TimelineItemLayer *item_layer, TimelineItem *item,
|
||||
TimelineLayoutInfo *info);
|
||||
|
||||
//! Down click handler for the TimelineItemLayer
|
||||
void timeline_item_layer_down_click_handler(ClickRecognizerRef recognizer, void *context);
|
||||
|
||||
//! Up click handler for the TimelineItemLayer
|
||||
void timeline_item_layer_up_click_handler(ClickRecognizerRef recognizer, void *context);
|
||||
|
||||
//! Convenience function to set the \ref ClickConfigProvider callback on the
|
||||
//! given window to menu layer's internal click config provider. This internal
|
||||
//! click configuration provider, will set up the default UP & DOWN handlers
|
||||
void timeline_item_layer_set_click_config_onto_window(TimelineItemLayer *item_layer,
|
||||
struct Window *window);
|
Loading…
Add table
Add a link
Reference in a new issue