pebble/src/fw/applib/ui/content_indicator.c
2025-01-27 11:38:16 -08:00

381 lines
14 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 "content_indicator.h"
#include "content_indicator_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/app_timer.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/graphics.h"
#include "kernel/ui/kernel_ui.h"
#include "system/passert.h"
#include "util/buffer.h"
#include "util/size.h"
//! Signature for callbacks provided to prv_content_indicator_iterate()
//! @param content_indicator The current ContentIndicator in the iteration.
//! @param buffer_offset_bytes The offset of the ContentIndicator in the buffer's storage.
//! @param input_context An input context.
//! @param output_context An output context.
//! @return `true` if iteration should continue, `false` otherwise.
typedef bool (*ContentIndicatorIteratorCb)(ContentIndicator *content_indicator,
size_t buffer_offset_bytes,
void *input_context,
void *output_context);
bool prv_content_indicator_init(ContentIndicator *content_indicator) {
if (!content_indicator) {
return false;
}
*content_indicator = (ContentIndicator){};
// Add the content indicator to the appropriate buffer
ContentIndicatorsBuffer *content_indicators_buffer = content_indicator_get_current_buffer();
Buffer *buffer = &content_indicators_buffer->buffer;
size_t bytes_written = buffer_add(buffer,
(uint8_t *)&content_indicator,
sizeof(ContentIndicator *));
// Return whether or not the content indicator was successfully written to the buffer
return (bytes_written == sizeof(ContentIndicator *));
}
void content_indicator_init(ContentIndicator *content_indicator) {
bool success = prv_content_indicator_init(content_indicator);
PBL_ASSERTN(success);
}
//! Returns `true` if `iterator_cb` signaled iteration to end, `false` otherwise.
static bool prv_content_indicator_iterate(ContentIndicatorIteratorCb iterator_cb,
void *input_context,
void *output_context) {
if (!iterator_cb) {
return false;
}
ContentIndicatorsBuffer *content_indicators_buffer = content_indicator_get_current_buffer();
Buffer *buffer = &content_indicators_buffer->buffer;
for (size_t offset = 0; offset < buffer->bytes_written; offset += sizeof(ContentIndicator *)) {
// We have to break up the access into two parts, otherwise we get a strict-aliasing error
ContentIndicator **content_indicator_address = (ContentIndicator **)(buffer->data + offset);
ContentIndicator *content_indicator = *content_indicator_address;
if (!iterator_cb(content_indicator, offset, input_context, output_context)) {
return true;
}
}
return false;
}
ContentIndicator *content_indicator_create(void) {
ContentIndicator *content_indicator = applib_type_zalloc(ContentIndicator);
if (!content_indicator) {
return NULL;
}
if (!prv_content_indicator_init(content_indicator)) {
applib_free(content_indicator);
return NULL;
}
return content_indicator;
}
static bool prv_content_indicator_find_for_scroll_layer_cb(ContentIndicator *content_indicator,
size_t buffer_offset_bytes,
void *input_context,
void *output_context) {
ScrollLayer *target_scroll_layer = input_context;
if (content_indicator->scroll_layer == target_scroll_layer) {
*((ContentIndicator **)output_context) = content_indicator;
return false;
}
return true;
}
ContentIndicator *content_indicator_get_for_scroll_layer(ScrollLayer *scroll_layer) {
if (!scroll_layer) {
return NULL;
}
ContentIndicator *content_indicator = NULL;
prv_content_indicator_iterate(prv_content_indicator_find_for_scroll_layer_cb,
scroll_layer,
&content_indicator);
return content_indicator;
}
ContentIndicator *content_indicator_get_or_create_for_scroll_layer(ScrollLayer *scroll_layer) {
if (!scroll_layer) {
return NULL;
}
ContentIndicator *content_indicator = content_indicator_get_for_scroll_layer(scroll_layer);
if (!content_indicator) {
content_indicator = content_indicator_create();
if (content_indicator) {
content_indicator->scroll_layer = scroll_layer;
}
}
return content_indicator;
}
static bool prv_content_indicator_find_buffer_offset_bytes_cb(ContentIndicator *content_indicator,
size_t buffer_offset_bytes,
void *input_context,
void *output_context) {
ContentIndicator *target_content_indicator = input_context;
if (content_indicator == target_content_indicator) {
*((size_t *)output_context) = buffer_offset_bytes;
return false;
}
return true;
}
static void prv_content_indicator_reset_direction(ContentIndicatorDirectionData *direction_data) {
// Cancel the timeout timer, if necessary
if (direction_data->timeout_timer) {
app_timer_cancel(direction_data->timeout_timer);
direction_data->timeout_timer = NULL;
}
ContentIndicatorConfig *config = &direction_data->config;
if (config->layer) {
// Set the layer's update proc to be the layer's original update proc
config->layer->update_proc = direction_data->original_update_proc;
layer_mark_dirty(config->layer);
}
}
void content_indicator_deinit(ContentIndicator *content_indicator) {
if (!content_indicator) {
return;
}
// Deinit the data for each of the directions
for (size_t i = 0; i < ARRAY_LENGTH(content_indicator->direction_data); i++) {
ContentIndicatorDirectionData *direction_data = &content_indicator->direction_data[i];
prv_content_indicator_reset_direction(direction_data);
}
// Find the offset of the content indicator in the buffer
size_t buffer_offset_bytes;
if (!prv_content_indicator_iterate(prv_content_indicator_find_buffer_offset_bytes_cb,
content_indicator,
&buffer_offset_bytes)) {
return;
}
// Remove the content indicator from the appropriate buffer
ContentIndicatorsBuffer *content_indicators_buffer = content_indicator_get_current_buffer();
Buffer *buffer = &content_indicators_buffer->buffer;
buffer_remove(buffer, buffer_offset_bytes, sizeof(ContentIndicator *));
}
void content_indicator_destroy(ContentIndicator *content_indicator) {
if (!content_indicator) {
return;
}
content_indicator_deinit(content_indicator);
applib_free(content_indicator);
}
void content_indicator_destroy_for_scroll_layer(ScrollLayer *scroll_layer) {
if (!scroll_layer) {
return;
}
ContentIndicator *content_indicator;
if (prv_content_indicator_iterate(prv_content_indicator_find_for_scroll_layer_cb,
scroll_layer,
&content_indicator)) {
content_indicator_destroy(content_indicator);
}
}
bool content_indicator_configure_direction(ContentIndicator *content_indicator,
ContentIndicatorDirection direction,
const ContentIndicatorConfig *config) {
if (!content_indicator) {
return false;
}
// If NULL is passed for config, reset the data for this direction.
if (!config) {
ContentIndicatorDirectionData *direction_data = &content_indicator->direction_data[direction];
prv_content_indicator_reset_direction(direction_data);
*direction_data = (ContentIndicatorDirectionData){};
return true;
}
if (!config->layer) {
return false;
}
// Fail if any of the other directions have already been configured with this config's layer
for (ContentIndicatorDirection dir = 0; dir < NumContentIndicatorDirections; dir++) {
if (dir == direction) {
continue;
}
ContentIndicatorDirectionData *direction_data = &content_indicator->direction_data[dir];
if (direction_data->config.layer == config->layer) {
return false;
}
}
ContentIndicatorDirectionData *direction_data = &content_indicator->direction_data[direction];
prv_content_indicator_reset_direction(direction_data);
*direction_data = (ContentIndicatorDirectionData){
.direction = direction,
.config = *config,
.original_update_proc = config->layer->update_proc,
};
return true;
}
static bool prv_content_indicator_find_direction_data_cb(ContentIndicator *content_indicator,
size_t buffer_offset_bytes,
void *input_context,
void *output_context) {
Layer *target_layer = input_context;
for (ContentIndicatorDirection dir = 0; dir < NumContentIndicatorDirections; dir++) {
ContentIndicatorDirectionData *direction_data = &content_indicator->direction_data[dir];
if (direction_data->config.layer == target_layer) {
*((ContentIndicatorDirectionData **)output_context) = direction_data;
return false;
}
}
return true;
}
void content_indicator_draw_arrow(GContext *ctx,
const GRect *frame,
ContentIndicatorDirection direction,
GColor fg_color,
GColor bg_color,
GAlign alignment) {
// Fill the background color
graphics_context_set_fill_color(ctx, bg_color);
graphics_fill_rect(ctx, frame);
// Pick the arrow to draw
const int16_t arrow_height = 6;
const GPathInfo arrow_up_path_info = {
.num_points = 3,
.points = (GPoint[]) {{0, arrow_height}, {(arrow_height + 1), 0},
{((arrow_height * 2) + 1), arrow_height}}
};
const GPathInfo arrow_down_path_info = {
.num_points = 3,
.points = (GPoint[]) {{0, 0}, {(arrow_height + 1), arrow_height},
{((arrow_height * 2) + 1), 0}}
};
const GPathInfo *arrow_path_info;
switch (direction) {
case ContentIndicatorDirectionUp:
arrow_path_info = &arrow_up_path_info;
break;
case ContentIndicatorDirectionDown:
arrow_path_info = &arrow_down_path_info;
break;
default:
WTF;
}
// Draw the arrow
GPath arrow_path;
gpath_init(&arrow_path, arrow_path_info);
// Align the arrow within the provided bounds
GRect arrow_box = gpath_outer_rect(&arrow_path);
grect_align(&arrow_box, frame, alignment, true /* clip */);
gpath_move_to(&arrow_path, arrow_box.origin);
const bool prev_antialiased = graphics_context_get_antialiased(ctx);
graphics_context_set_antialiased(ctx, false);
graphics_context_set_fill_color(ctx, fg_color);
gpath_draw_filled(ctx, &arrow_path);
graphics_context_set_antialiased(ctx, prev_antialiased);
}
T_STATIC void prv_content_indicator_update_proc(Layer *layer, GContext *ctx) {
// Find the direction data corresponding to the layer that should be updated
ContentIndicatorDirectionData *direction_data;
if (!prv_content_indicator_iterate(prv_content_indicator_find_direction_data_cb,
layer,
&direction_data)) {
return;
}
ContentIndicatorConfig *config = &direction_data->config;
content_indicator_draw_arrow(ctx,
&layer->bounds,
direction_data->direction,
config->colors.foreground,
config->colors.background,
config->alignment);
}
bool content_indicator_get_content_available(ContentIndicator *content_indicator,
ContentIndicatorDirection direction) {
if (!content_indicator) {
return false;
}
ContentIndicatorDirectionData *direction_data = &content_indicator->direction_data[direction];
return direction_data->content_available;
}
void content_indicator_set_content_available(ContentIndicator *content_indicator,
ContentIndicatorDirection direction,
bool available) {
if (!content_indicator) {
return;
}
ContentIndicatorDirectionData *direction_data = &content_indicator->direction_data[direction];
direction_data->content_available = available;
ContentIndicatorConfig *config = &direction_data->config;
if (!config->layer) {
return;
}
// Cleans potentially scheduled timer, resets update_proc, marks dirty
prv_content_indicator_reset_direction(direction_data);
if (available) {
// Set the layer's update proc to be the arrow-drawing update proc and mark it as dirty
config->layer->update_proc = prv_content_indicator_update_proc;
layer_mark_dirty(config->layer);
// If the arrow should time out and a timer isn't already scheduled, register a timeout timer
if (config->times_out && !direction_data->timeout_timer) {
direction_data->timeout_timer = app_timer_register(
CONTENT_INDICATOR_TIMEOUT_MS,
(AppTimerCallback)prv_content_indicator_reset_direction,
direction_data);
}
}
}
void content_indicator_init_buffer(ContentIndicatorsBuffer *content_indicators_buffer) {
if (!content_indicators_buffer) {
return;
}
buffer_init(&content_indicators_buffer->buffer, CONTENT_INDICATOR_BUFFER_SIZE_BYTES);
}