pebble/src/fw/popups/timeline/timeline_item_layer.c
2025-01-27 11:38:16 -08:00

292 lines
11 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 "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);
}