mirror of
https://github.com/google/pebble.git
synced 2025-06-10 20:13:11 +00:00
292 lines
11 KiB
C
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);
|
|
}
|