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

408 lines
17 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 "status_bar_layer.h"
#include "animation_timing.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/graphics_line.h"
#include "applib/graphics/text.h"
#include "applib/ui/window_stack.h"
#include "kernel/ui/kernel_ui.h"
#include "process_state/app_state/app_state.h"
#include "services/common/clock.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/string.h"
typedef struct StatusBarTextFormat {
GTextOverflowMode overflow_mode;
GTextAlignment text_alignment;
GFont font;
} StatusBarTextFormat;
static ALWAYS_INLINE StatusBarTextFormat prv_get_text_format(void) {
const PlatformType platform = process_manager_current_platform();
const char *font_key = PBL_PLATFORM_SWITCH(platform,
/*aplite*/ FONT_KEY_GOTHIC_14,
/*basalt*/ FONT_KEY_GOTHIC_14,
/*chalk*/ FONT_KEY_GOTHIC_14,
/*diorite*/ FONT_KEY_GOTHIC_14,
/*emery*/ FONT_KEY_GOTHIC_18);
return (StatusBarTextFormat) {
.overflow_mode = GTextOverflowModeTrailingEllipsis,
.text_alignment = GTextAlignmentCenter,
.font = fonts_get_system_font(font_key),
};
}
static int prv_height(void) {
const PlatformType platform = process_manager_current_platform();
return _STATUS_BAR_LAYER_HEIGHT(platform);
}
// Function prototypes
static void prv_status_bar_layer_update_clock(StatusBarLayer *status_bar_layer);
static void prv_tick_timer_handler_cb(PebbleEvent *e, void *cb_data);
static void prv_status_bar_property_changed(struct Layer *layer) {
const int16_t height = prv_height();
if (layer->frame.size.h != height) {
layer->frame.size.h = height;
}
if (layer->bounds.size.h != height) {
layer->bounds.size.h = height;
}
}
static void prv_status_bar_layer_render(Layer *layer, GContext *ctx) {
StatusBarLayer *status_bar_layer = (StatusBarLayer *)layer;
// During a window transition with fixed status bars, ignore horizontal offset of the window.
// For two windows with a status bar at (0,0), this will make sure that both status bars share the
// same screen coordinates despite the window movement - the clip_box prevents overdrawing.
// This is a first step towards a general purpose system for static status bars.
const int16_t stored_drawing_box_x = ctx->draw_state.drawing_box.origin.x;
if (window_stack_is_animating_with_fixed_status_bar(app_state_get_window_stack())) {
ctx->draw_state.drawing_box.origin.x -= status_bar_layer->layer.window->layer.frame.origin.x;
}
status_bar_layer_render(ctx, &status_bar_layer->layer.bounds, &status_bar_layer->config);
ctx->draw_state.drawing_box.origin.x = stored_drawing_box_x;
}
void status_bar_layer_init(StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
*status_bar_layer = (StatusBarLayer){
.previous_min_of_day = -1,
};
// The status bar needs to be as wide as the framebuffer we will render it into, which may be less
// wide than the display e.g. if an app is running in bezel mode. The current graphics context's
// contains the appropriate size.
GContext *ctx = graphics_context_get_current_context();
const GSize current_framebuffer_size = graphics_context_get_framebuffer_size(ctx);
layer_init(&status_bar_layer->layer, &GRect(0, 0, current_framebuffer_size.w, prv_height()));
status_bar_layer->layer.update_proc = prv_status_bar_layer_render;
status_bar_layer->layer.property_changed_proc = prv_status_bar_property_changed;
// tick event to callback which checks every second if the time is correct
status_bar_layer->tick_event = (EventServiceInfo){
.type = PEBBLE_TICK_EVENT,
.handler = prv_tick_timer_handler_cb,
.context = status_bar_layer};
event_service_client_subscribe(&(status_bar_layer->tick_event));
status_bar_layer->config = (StatusBarLayerConfig){
.foreground_color = GColorWhite,
.background_color = GColorBlack,
.separator.mode = StatusBarLayerSeparatorModeNone,
};
status_bar_layer->title_timer_id = TIMER_INVALID_ID;
}
StatusBarLayer *status_bar_layer_create(void) {
StatusBarLayer *layer = applib_type_zalloc(StatusBarLayer);
if (layer) {
status_bar_layer_init(layer);
}
return layer;
}
void status_bar_layer_destroy(StatusBarLayer *status_bar_layer) {
if (!status_bar_layer) {
return;
}
status_bar_layer_deinit(status_bar_layer);
applib_free(status_bar_layer);
}
void status_bar_layer_deinit(StatusBarLayer *status_bar_layer) {
layer_deinit(&status_bar_layer->layer);
if (status_bar_layer->title_timer_id != TIMER_INVALID_ID) {
app_timer_cancel(status_bar_layer->title_timer_id);
}
event_service_client_unsubscribe(&(status_bar_layer->tick_event));
}
Layer *status_bar_layer_get_layer(StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
return &status_bar_layer->layer;
}
void status_bar_layer_set_colors(StatusBarLayer *status_bar_layer, GColor background,
GColor foreground) {
PBL_ASSERTN(status_bar_layer);
if (gcolor_equal(status_bar_layer->config.background_color, background) &&
gcolor_equal(status_bar_layer->config.foreground_color, foreground)) {
return;
}
status_bar_layer->config.background_color = background;
status_bar_layer->config.foreground_color = foreground;
layer_mark_dirty(&(status_bar_layer->layer));
}
GColor status_bar_layer_get_background_color(const StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
return status_bar_layer->config.background_color;
}
GColor status_bar_layer_get_foreground_color(const StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
return status_bar_layer->config.foreground_color;
}
void status_bar_layer_set_title(StatusBarLayer *status_bar_layer,
const char *text,
bool revert,
bool animated) {
// copies the contents at text into title_text_buffer for display
strncpy(status_bar_layer->config.title_text_buffer, text, TITLE_TEXT_BUFFER_SIZE);
if (revert) { // revert title text back to clock time after STATUS_BAR_LAYER_TITLE_TIMEOUT
if (status_bar_layer->title_timer_id != TIMER_INVALID_ID) {
app_timer_cancel(status_bar_layer->title_timer_id);
}
status_bar_layer->title_timer_id = app_timer_register(STATUS_BAR_LAYER_TITLE_TIMEOUT,
status_bar_layer_reset_title,
status_bar_layer);
}
status_bar_layer->config.mode = StatusBarLayerModeLoading;
layer_mark_dirty(&(status_bar_layer->layer));
}
const char *status_bar_layer_get_title(const StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
return status_bar_layer->config.title_text_buffer;
}
void status_bar_layer_reset_title(void *cb_data) {
StatusBarLayer *status_bar_layer = (StatusBarLayer *)cb_data;
// set title text mode to 'clock', update text, and setup timer to allow clock text to update
status_bar_layer->config.mode = StatusBarLayerModeClock;
prv_status_bar_layer_update_clock(status_bar_layer);
}
void status_bar_layer_set_info_text(StatusBarLayer *status_bar_layer, const char *text) {
PBL_ASSERTN(status_bar_layer);
strncpy(status_bar_layer->config.info_text_buffer, text, INFO_TEXT_BUFFER_SIZE);
layer_mark_dirty(&(status_bar_layer->layer));
}
// Sets info text either ot X/Y or percentage if total is larger than MAX_INFO_TOTAL
void status_bar_layer_set_info_progress(StatusBarLayer *status_bar_layer,
uint16_t current,
uint16_t total) {
PBL_ASSERTN(status_bar_layer);
if (current > total) {
// do not display
return;
} else {
char *str = status_bar_layer->config.info_text_buffer;
memset(str, 0 , INFO_TEXT_BUFFER_SIZE);
if (total > MAX_INFO_TOTAL) { // total is large; display as a percentage
itoa_int(current * 100 / total, str, 10);
strcat(str, "%");
} else { // display as an X/Y
itoa_int(current, str, 10);
strcat(str, "/");
char buffer[(INFO_TEXT_BUFFER_SIZE - 1) / 2];
itoa_int(total, buffer, 10);
strcat(str, buffer);
}
}
layer_mark_dirty(&(status_bar_layer->layer));
}
const char *status_bar_layer_get_info_text(const StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
return status_bar_layer->config.info_text_buffer;
}
void status_bar_layer_reset_info(StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
memset(status_bar_layer->config.info_text_buffer, 0,
sizeof(status_bar_layer->config.info_text_buffer));
layer_mark_dirty(&status_bar_layer->layer);
}
void status_bar_layer_set_separator_mode(StatusBarLayer *status_bar_layer,
StatusBarLayerSeparatorMode mode) {
PBL_ASSERTN(status_bar_layer);
status_bar_layer->config.separator.mode = mode;
layer_mark_dirty(&(status_bar_layer->layer));
}
void status_bar_layer_set_separator_load_percentage(StatusBarLayer *status_bar_layer,
int16_t percentage) {
PBL_ASSERTN(status_bar_layer);
// TODO: animation related function
layer_mark_dirty(&(status_bar_layer->layer));
}
StatusBarLayerSeparatorMode status_bar_layer_get_separator_mode(
const StatusBarLayer *status_bar_layer) {
PBL_ASSERTN(status_bar_layer);
return status_bar_layer->config.separator.mode;
}
//*****************************************************************************
//* INTERNAL FUNCTIONS
//*****************************************************************************
// Manual internal function to refresh title text clock and mark dirty
static void prv_status_bar_layer_update_clock(StatusBarLayer *status_bar_layer) {
clock_copy_time_string(status_bar_layer->config.title_text_buffer,
sizeof(status_bar_layer->config.title_text_buffer));
layer_mark_dirty(&(status_bar_layer->layer));
}
// Callback for updating title text clock's time
static void prv_tick_timer_handler_cb(PebbleEvent *e, void *cb_data) {
StatusBarLayer *status_bar_layer = (StatusBarLayer *)cb_data;
if (status_bar_layer->config.mode != StatusBarLayerModeClock) {
return;
}
struct tm currtime;
sys_localtime_r(&e->clock_tick.tick_time, &currtime);
const int min_of_day = (currtime.tm_hour * 60) + currtime.tm_min;
if (status_bar_layer->previous_min_of_day != min_of_day) {
prv_status_bar_layer_update_clock(status_bar_layer); // update clock text and mark dirty
status_bar_layer->previous_min_of_day = min_of_day;
}
}
// Calculate position and renders text
static void prv_status_bar_layer_render_text(GContext *ctx,
int16_t min_x,
int16_t max_x,
int16_t min_y,
int16_t max_y,
char *data) {
const StatusBarTextFormat text_format = prv_get_text_format();
const GFont font = text_format.font;
const uint8_t font_height = fonts_get_font_height(font);
const int16_t center = (max_x + min_x) / 2;
const int16_t left_width = center - min_x;
const int16_t right_width = max_x - center;
// Use larger distance from the center to min_x or max_x as half of the width (odd num pixels)
const int16_t width = 2 * MAX(left_width, right_width);
// starting point of text needs to be half width left of the center.
const int16_t x_start = center - width / 2;
// Position the text vertically offset from the bottom of the status bar
const int16_t y = min_y + max_y - (2 * STATUS_BAR_LAYER_SEPARATOR_Y_OFFSET) - font_height;
graphics_draw_text(ctx,
data,
font,
GRect(x_start, y, width, font_height),
text_format.overflow_mode,
text_format.text_alignment, NULL);
}
// Renders all of StatusBarLayer when layer_mark_dirty triggers LayerUpdateProc
void status_bar_layer_render(GContext *ctx, const GRect *bounds, StatusBarLayerConfig *config) {
// define x and y coords of status_bar_layer
int16_t x_offset_l = bounds->origin.x;
int16_t x_offset_r = x_offset_l + bounds->size.w;
int16_t y_offset_top = bounds->origin.y;
int16_t y_offset_bottom = y_offset_top + bounds->size.h;
// TODO: x offset for ICON_MARGINs
// for (uint8_t i = 0; i < ARRAY_LENGTH(s_left_icons); ++i) {
// x_offset_l += s_left_icons[i](ctx, x_offset_l, GAlignLeft);
// }
// Fill background of status layer using bounds, if color is not transparent
// TODO: use of gcolor_is_transparent to determine whether or
if (!gcolor_is_transparent(config->background_color)) {
graphics_context_set_fill_color(ctx, config->background_color);
graphics_fill_rect(ctx, bounds);
}
// Set context text color and compositing mode
graphics_context_set_text_color(ctx, config->foreground_color);
graphics_context_set_compositing_mode(ctx, GCompOpSet);
// update title buffer with time if in StatusBarLayerModeClock
if (config->mode == StatusBarLayerModeClock) {
clock_copy_time_string(config->title_text_buffer,
sizeof(config->title_text_buffer));
}
if (config->mode != StatusBarLayerModeCustomText) { // draw center text
graphics_context_set_compositing_mode(ctx, GCompOpAssign);
prv_status_bar_layer_render_text(ctx, x_offset_l, x_offset_r, y_offset_top, y_offset_bottom,
config->title_text_buffer);
} else { // TODO: here goes center text animations
}
// render info text
GFont info_font = prv_get_text_format().font;
// find width of info text
GSize max_used_size = graphics_text_layout_get_max_used_size(ctx, config->info_text_buffer,
info_font,
GRect(0, 0, 100, prv_height()),
GTextOverflowModeTrailingEllipsis,
GTextAlignmentCenter,
NULL);
// use the width found to render the info text
int16_t info_text_left_offset = (int16_t) (x_offset_r - max_used_size.w -
STATUS_BAR_LAYER_INFO_PADDING);
int16_t info_text_right_offset = (int16_t) (x_offset_r - STATUS_BAR_LAYER_INFO_PADDING);
prv_status_bar_layer_render_text(ctx,
info_text_left_offset,
info_text_right_offset,
y_offset_top,
y_offset_bottom,
config->info_text_buffer);
// draw the separator
if (config->separator.mode != StatusBarLayerSeparatorModeNone) {
graphics_context_set_stroke_color(ctx, config->foreground_color);
GPoint origin = {x_offset_l, (int16_t) (y_offset_bottom - STATUS_BAR_LAYER_SEPARATOR_Y_OFFSET)};
graphics_draw_horizontal_line_dotted(ctx, origin, (uint16_t) x_offset_r);
}
}
bool layer_is_status_bar_layer(Layer *layer) {
return layer && layer->update_proc == prv_status_bar_layer_render;
}
int16_t status_layer_get_title_text_width(StatusBarLayer *status_bar_layer) {
// other modes not supported
PBL_ASSERTN(status_bar_layer->config.mode == StatusBarLayerModeClock);
const StatusBarTextFormat text_format = prv_get_text_format();
char time_text_buffer[TITLE_TEXT_BUFFER_SIZE];
clock_copy_time_string(time_text_buffer, sizeof(time_text_buffer));
GContext *ctx = graphics_context_get_current_context();
return graphics_text_layout_get_max_used_size(ctx,
time_text_buffer,
text_format.font,
status_bar_layer->layer.bounds,
text_format.overflow_mode,
text_format.text_alignment,
NULL).w;
}