Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2024-12-12 16:43:03 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View File

@@ -0,0 +1,396 @@
/*
* 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 "action_bar_layer.h"
#include "animation.h"
#include "animation_timing.h"
#include "applib/app_timer.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/window_private.h"
#include "process_management/process_manager.h"
#include "system/passert.h"
#include "util/trig.h"
const int16_t MAX_ICON_HEIGHT = 18;
const int64_t PRESS_ANIMATION_DURATION_MS = 144;
const int64_t ICON_CHANGE_ANIMATION_DURATION_MS = 144;
const int16_t ICON_CHANGE_OFFSET[NUM_ACTION_BAR_ITEMS] = {-5, 0, 5};
const uint32_t MILLISECONDS_PER_FRAME = 1000 / 30;
static int prv_width(void) {
const PlatformType platform = process_manager_current_platform();
return _ACTION_BAR_WIDTH(platform);
}
static int prv_vertical_icon_margin(void) {
const PlatformType platform = process_manager_current_platform();
return PBL_PLATFORM_SWITCH(platform,
/*aplite*/ 24,
/*basalt*/ 24,
/*chalk*/ 53,
/*diorite*/ 24,
/*emery*/ 45);
}
static int prv_press_animation_offset(void) {
const PlatformType platform = process_manager_current_platform();
return PBL_PLATFORM_SWITCH(platform,
/*aplite*/ 5,
/*basalt*/ 5,
/*chalk*/ 4,
/*diorite*/ 5,
/*emery*/ 5);
}
// TODO: Once PBL-16032 is implemented, use that instead.
static int64_t prv_get_precise_time(void) {
time_t seconds;
uint16_t milliseconds;
time_ms(&seconds, &milliseconds);
return (seconds * 1000) + milliseconds;
}
inline static bool action_bar_is_highlighted(ActionBarLayer *action_bar, uint8_t index) {
PBL_ASSERTN(index < NUM_ACTION_BAR_ITEMS);
return (bool) (action_bar->is_highlighted & (1 << index));
}
static void prv_register_redraw_timer(ActionBarLayer *layer);
static void prv_timed_redraw(void *context) {
ActionBarLayer *action_bar = context;
layer_mark_dirty(&action_bar->layer);
action_bar->redraw_timer = NULL;
int64_t now = prv_get_precise_time();
for (int i = 0; i < NUM_ACTION_BAR_ITEMS; ++i) {
if ((action_bar->state_change_times[i] != 0 &&
(now - action_bar->state_change_times[i]) <= PRESS_ANIMATION_DURATION_MS) ||
(action_bar->icon_change_times[i] != 0 &&
(now - action_bar->icon_change_times[i]) <= ICON_CHANGE_ANIMATION_DURATION_MS)) {
prv_register_redraw_timer(action_bar);
return;
}
}
}
static void prv_register_redraw_timer(ActionBarLayer *action_bar) {
if (!action_bar->redraw_timer) {
action_bar->redraw_timer = app_timer_register(MILLISECONDS_PER_FRAME, prv_timed_redraw,
action_bar);
}
}
inline static void action_bar_set_highlighted(ActionBarLayer *action_bar, uint8_t index, bool highlighted) {
PBL_ASSERT(index < NUM_ACTION_BAR_ITEMS, "Index: %"PRIu8, index);
const uint8_t bit = (1 << index);
if (action_bar_is_highlighted(action_bar, index) == highlighted) {
return;
}
if (highlighted) {
action_bar->is_highlighted |= bit;
} else {
action_bar->is_highlighted &= ~bit;
prv_register_redraw_timer(action_bar);
}
action_bar->state_change_times[index] = prv_get_precise_time();
layer_mark_dirty(&action_bar->layer);
}
void action_bar_changed_proc(ActionBarLayer *action_bar, GContext* ctx) {
if (action_bar->layer.window && action_bar->layer.window->on_screen == false) {
// clear first, fixes issue of returning from other page while highlighted
for (int i = 0; i < NUM_ACTION_BAR_ITEMS; i++) {
action_bar_set_highlighted(action_bar, i, false);
}
}
}
static GPoint prv_offset_since_time(int64_t time_ms, int64_t duration_ms,
GPoint max_offset) {
if (time_ms == 0) {
return GPointZero;
}
const int64_t delta_ms = prv_get_precise_time() - time_ms;
if (delta_ms >= duration_ms) {
return GPointZero;
}
const uint32_t normalized_time = (delta_ms * ANIMATION_NORMALIZED_MAX) / duration_ms;
const uint32_t normalized_distance = animation_timing_curve(normalized_time,
AnimationCurveEaseOut);
const GPoint real_offset = GPoint(
max_offset.x - ((normalized_distance * max_offset.x) / ANIMATION_NORMALIZED_MAX),
max_offset.y - ((normalized_distance * max_offset.y) / ANIMATION_NORMALIZED_MAX));
return real_offset;
}
static GPoint prv_get_button_press_offset(ActionBarLayer *action_bar, uint8_t button_index) {
const int16_t animation_offset = prv_press_animation_offset();
const GPoint offset[5] = {
GPointZero,
GPoint(-animation_offset, 0),
GPoint(0, -animation_offset),
GPoint(animation_offset, 0),
GPoint(0, animation_offset),
};
return offset[action_bar->animation[button_index]];
}
static void prv_draw_background_rect(ActionBarLayer *action_bar, GContext *ctx, GColor bg_color) {
graphics_fill_rect(ctx, &action_bar->layer.bounds);
}
void prv_draw_background_round(ActionBarLayer *action_bar, GContext *ctx, GColor bg_color) {
const uint32_t action_bar_circle_diameter = DISP_ROWS * 19 / 9;
GRect action_bar_circle_frame = (GRect) {
.size = GSize(action_bar_circle_diameter, action_bar_circle_diameter)
};
grect_align(&action_bar_circle_frame, &action_bar->layer.bounds, GAlignLeft, false /* clips */);
graphics_fill_oval(ctx, action_bar_circle_frame, GOvalScaleModeFitCircle);
}
void action_bar_update_proc(ActionBarLayer *action_bar, GContext* ctx) {
const GColor bg_color = action_bar->background_color;
if (!gcolor_is_transparent(bg_color)) {
graphics_context_set_fill_color(ctx, bg_color);
PBL_IF_RECT_ELSE(prv_draw_background_rect,
prv_draw_background_round)(action_bar, ctx, bg_color);
}
for (unsigned int index = 0; index < NUM_ACTION_BAR_ITEMS; ++index) {
const GBitmap *icon = action_bar->icons[index];
if (icon) {
GRect rect = GRect(1, 0, prv_width(), MAX_ICON_HEIGHT);
const int button_id = index + 1;
const int vertical_icon_margin = prv_vertical_icon_margin();
switch (button_id) {
case BUTTON_ID_UP:
rect.origin.y = vertical_icon_margin;
break;
case BUTTON_ID_SELECT:
rect.origin.y = (action_bar->layer.bounds.size.h / 2) - (rect.size.h / 2);
break;
case BUTTON_ID_DOWN:
rect.origin.y = action_bar->layer.bounds.size.h - vertical_icon_margin -
rect.size.h;
break;
default:
WTF;
}
// In order to avoid creating relatively heavy animations, we instead just base our drawing
// directly on time. The time is set when the animation should start; we convert the delta
// since then into an offset and apply that to our rendering.
GPoint offset;
if (action_bar_is_highlighted(action_bar, index)) {
offset = prv_get_button_press_offset(action_bar, index);
} else {
const int64_t state_change_time = action_bar->state_change_times[index];
offset = prv_offset_since_time(state_change_time, PRESS_ANIMATION_DURATION_MS,
prv_get_button_press_offset(action_bar, index));
}
const int64_t icon_change_time = action_bar->icon_change_times[index];
offset = gpoint_add(offset, prv_offset_since_time(icon_change_time,
ICON_CHANGE_ANIMATION_DURATION_MS,
GPoint(0, ICON_CHANGE_OFFSET[index])));
GRect icon_rect = icon->bounds;
const bool clip = true;
grect_align(&icon_rect, &rect, GAlignCenter, clip);
#if PBL_ROUND
// Offset needed because the new curvature of the action bar makes the icons look off-center
const int32_t icon_horizontal_offset = -2;
icon_rect.origin.x += icon_horizontal_offset;
#endif
icon_rect.origin.x += offset.x;
icon_rect.origin.y += offset.y;
// We use GCompOpAssign on 1-bit images, because they still support the old operations.
// We use GCompOpSet otherwise to ensure we support transparency.
if (gbitmap_get_format(icon) == GBitmapFormat1Bit) {
graphics_context_set_compositing_mode(ctx, GCompOpAssign);
} else {
graphics_context_set_compositing_mode(ctx, GCompOpSet);
}
graphics_draw_bitmap_in_rect(ctx, (GBitmap*)icon, &icon_rect);
}
}
}
void action_bar_layer_init(ActionBarLayer *action_bar) {
*action_bar = (ActionBarLayer){};
layer_set_clips(&action_bar->layer, true);
action_bar->layer.update_proc = (LayerUpdateProc) action_bar_update_proc;
action_bar->layer.property_changed_proc =
(PropertyChangedProc) action_bar_changed_proc;
action_bar->background_color = GColorBlack;
for (unsigned int i = 0; i < NUM_ACTION_BAR_ITEMS; ++i) {
action_bar->animation[i] = ActionBarLayerIconPressAnimationMoveLeft;
}
}
ActionBarLayer* action_bar_layer_create(void) {
ActionBarLayer* layer = applib_type_malloc(ActionBarLayer);
if (layer) {
action_bar_layer_init(layer);
}
return layer;
}
void action_bar_layer_deinit(ActionBarLayer *action_bar_layer) {
if (action_bar_layer->redraw_timer) {
app_timer_cancel(action_bar_layer->redraw_timer);
}
layer_deinit(&action_bar_layer->layer);
}
void action_bar_layer_destroy(ActionBarLayer *action_bar_layer) {
if (action_bar_layer == NULL) {
return;
}
action_bar_layer_deinit(action_bar_layer);
applib_free(action_bar_layer);
}
Layer* action_bar_layer_get_layer(ActionBarLayer *action_bar_layer) {
return &action_bar_layer->layer;
}
inline static void* action_bar_get_context(ActionBarLayer *action_bar) {
return action_bar->context ? action_bar->context : action_bar;
}
void action_bar_layer_set_context(ActionBarLayer *action_bar, void *context) {
action_bar->context = context;
}
static void action_bar_raw_up_down_handler(ClickRecognizerRef recognizer, ActionBarLayer *action_bar, bool is_highlighted) {
const ButtonId button_id = click_recognizer_get_button_id(recognizer);
const uint8_t index = button_id - 1;
// is_highlighted will cause the icon in the action bar to render normal or inverted:
action_bar_set_highlighted(action_bar, index, is_highlighted);
}
static void action_bar_raw_up_handler(ClickRecognizerRef recognizer, void *context) {
ActionBarLayer *action_bar = (ActionBarLayer *)context;
action_bar_raw_up_down_handler(recognizer, action_bar, false);
}
static void action_bar_raw_down_handler(ClickRecognizerRef recognizer, void *context) {
ActionBarLayer *action_bar = (ActionBarLayer *)context;
action_bar_raw_up_down_handler(recognizer, action_bar, true);
}
static void action_bar_click_config_provider(void *config_context) {
ActionBarLayer *action_bar = config_context;
void *context = action_bar_get_context(action_bar);
// For UP, SELECT and DOWN, setup the raw handler and assign the user specified context:
for (ButtonId button_id = BUTTON_ID_UP; button_id < NUM_BUTTONS; ++button_id) {
window_raw_click_subscribe(button_id, action_bar_raw_down_handler, action_bar_raw_up_handler, action_bar);
window_set_click_context(button_id, context);
}
// If back button is overridden, set context of BACK click recognizer as well:
if (action_bar->window && action_bar->window->overrides_back_button) {
window_set_click_context(BUTTON_ID_BACK, context);
}
if (action_bar->click_config_provider) {
action_bar->click_config_provider(context);
}
}
inline static void action_bar_update_click_config_provider(ActionBarLayer *action_bar) {
if (action_bar->window) {
window_set_click_config_provider_with_context(action_bar->window,
action_bar_click_config_provider, action_bar);
}
}
void action_bar_layer_set_click_config_provider(ActionBarLayer *action_bar, ClickConfigProvider click_config_provider) {
action_bar->click_config_provider = click_config_provider;
action_bar_update_click_config_provider(action_bar);
}
void action_bar_layer_set_icon_animated(ActionBarLayer *action_bar, ButtonId button_id,
const GBitmap *icon, bool animated) {
if (button_id < BUTTON_ID_UP || button_id >= NUM_BUTTONS) {
return;
}
const uint8_t index = button_id - 1;
if (action_bar->icons[index] == icon) {
return;
}
action_bar->icons[index] = icon;
if (animated) {
action_bar->icon_change_times[index] = prv_get_precise_time();
prv_register_redraw_timer(action_bar);
} else {
action_bar->icon_change_times[index] = 0;
}
layer_mark_dirty(&action_bar->layer);
}
void action_bar_layer_set_icon(ActionBarLayer *action_bar, ButtonId button_id,
const GBitmap *icon) {
action_bar_layer_set_icon_animated(action_bar, button_id, icon, false);
}
void action_bar_layer_clear_icon(ActionBarLayer *action_bar, ButtonId button_id) {
action_bar_layer_set_icon(action_bar, button_id, NULL);
}
void action_bar_layer_set_icon_press_animation(ActionBarLayer *action_bar, ButtonId button_id,
ActionBarLayerIconPressAnimation animation) {
if (button_id < BUTTON_ID_UP || button_id >= NUM_BUTTONS) {
return;
}
action_bar->animation[button_id - 1] = animation;
}
void action_bar_layer_add_to_window(ActionBarLayer *action_bar, struct Window *window) {
const GRect *window_bounds = &window->layer.bounds;
const int16_t width = prv_width();
GRect rect = GRect(0, 0, width, window_bounds->size.h);
layer_set_bounds(&action_bar->layer, &rect);
rect.origin.x = window_bounds->size.w - width;
layer_set_frame(&action_bar->layer, &rect);
layer_add_child(&window->layer, &action_bar->layer);
action_bar->window = window;
action_bar_update_click_config_provider(action_bar);
}
void action_bar_layer_remove_from_window(ActionBarLayer *action_bar) {
if (action_bar == NULL || action_bar->window == NULL) {
return;
}
layer_remove_from_parent(&action_bar->layer);
window_set_click_config_provider_with_context(action_bar->window, NULL, NULL);
action_bar->window = NULL;
}
void action_bar_layer_set_background_color(ActionBarLayer *action_bar, GColor background_color) {
if (gcolor_equal(background_color, action_bar->background_color)) {
return;
}
action_bar->background_color = background_color;
layer_mark_dirty(&(action_bar->layer));
}

View File

@@ -0,0 +1,289 @@
/*
* 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/platform.h"
#include "layer.h"
#include "click.h"
//! @file action_bar_layer.h
//! @addtogroup UI
//! @{
//! @addtogroup Layer Layers
//! @{
//! @addtogroup ActionBarLayer
//! \brief Vertical, bar-shaped control widget on the right edge of the window
//!
//! ![](action_bar_layer.png)
//! ActionBarLayer is a Layer that displays a bar on the right edge of the
//! window. The bar can contain up to 3 icons, each corresponding with one of
//! the buttons on the right side of the watch. The purpose of each icon is
//! to provide a hint (feed-forward) to what action a click on the respective
//! button will cause.
//!
//! The action bar is useful when there are a few (up to 3) main actions that
//! are desirable to be able to take quickly, literally with one press of a
//! button.
//!
//! <h3>More actions</h3>
//! If there are more than 3 actions the user might want to take:
//! * Try assigning the top and bottom icons of the action bar to the two most
//! immediate actions and use the middle icon to push a Window with a MenuLayer
//! with less immediate actions.
//! * Secondary actions that are not vital, can be "hidden" under a long click.
//! Try to group similar actions to one button. For example, in a Music app,
//! a single click on the top button is tied to the action to jump to the
//! previous track. Holding that same button means seek backwards.
//!
//! <h3>Directionality mapping</h3>
//! When the top and bottom buttons are used to control navigating through
//! a (potentially virtual, non-visible) list of items, follow this guideline:
//! * Tie the top button to the action that goes to the _previous_ item in the
//! list, for example "jump to previous track" in a Music app.
//! * Tie the bottom button to the action that goes to the _next_ item in the
//! list, for example "jump to next track" in a Music app.
//!
//! <h3>Geometry</h3>
//! * The action bar is 30 pixels wide. Use the \ref ACTION_BAR_WIDTH define.
//! * Icons should not be wider than 28 pixels, or taller than 18 pixels.
//! It is recommended to use a size of around 15 x 15 pixels for the "visual core" of the icon,
//! and extending or contracting where needed.
//! <h3>Example Code</h3>
//! The code example below shows how to do the initial setup of the action bar
//! in a window's `.load` handler.
//! Configuring the button actions is similar to the process when using
//! \ref window_set_click_config_provider(). See \ref Clicks for more
//! information.
//!
//! \code{.c}
//! ActionBarLayer *action_bar;
//!
//! // The implementation of my_next_click_handler and my_previous_click_handler
//! // is omitted for the sake of brevity. See the Clicks reference docs.
//!
//! void click_config_provider(void *context) {
//! window_single_click_subscribe(BUTTON_ID_DOWN, (ClickHandler) my_next_click_handler);
//! window_single_click_subscribe(BUTTON_ID_UP, (ClickHandler) my_previous_click_handler);
//! }
//!
//! void window_load(Window *window) {
//! ...
//! // Initialize the action bar:
//! action_bar = action_bar_layer_create();
//! // Associate the action bar with the window:
//! action_bar_layer_add_to_window(action_bar, window);
//! // Set the click config provider:
//! action_bar_layer_set_click_config_provider(action_bar,
//! click_config_provider);
//!
//! // Set the icons:
//! // The loading of the icons is omitted for brevity... See gbitmap_create_with_resource()
//! action_bar_layer_set_icon_animated(action_bar, BUTTON_ID_UP, my_icon_previous, true);
//! action_bar_layer_set_icon_animated(action_bar, BUTTON_ID_DOWN, my_icon_next, true);
//! }
//! \endcode
//! @{
//! The width of the action bar in pixels, for all platforms.
#define _ACTION_BAR_WIDTH(plat) PBL_PLATFORM_SWITCH(plat, \
/*aplite*/ 30, \
/*basalt*/ 30, \
/*chalk*/ 40, \
/*diorite*/ 30, \
/*emery*/ 34)
//! The width of the action bar in pixels.
#define ACTION_BAR_WIDTH _ACTION_BAR_WIDTH(PBL_PLATFORM_TYPE_CURRENT)
//! The maximum number of action bar items.
#define NUM_ACTION_BAR_ITEMS 3
struct Window;
struct GBitmap;
struct AppTimer;
typedef enum {
ActionBarLayerIconPressAnimationNone = 0,
ActionBarLayerIconPressAnimationMoveLeft,
ActionBarLayerIconPressAnimationMoveUp,
ActionBarLayerIconPressAnimationMoveRight,
ActionBarLayerIconPressAnimationMoveDown,
} ActionBarLayerIconPressAnimation;
//! Data structure of an action bar.
//! @note an `ActionBarLayer *` can safely be casted to a `Layer *` and can
//! thus be used with all other functions that take a `Layer *` as an argument.
//! <br/>For example, the following is legal:
//! \code{.c}
//! ActionBarLayer action_bar;
//! ...
//! layer_set_hidden((Layer *)&action_bar, true);
//! \endcode
typedef struct {
Layer layer;
const struct GBitmap *icons[NUM_ACTION_BAR_ITEMS];
struct Window *window;
void *context;
ClickConfigProvider click_config_provider;
unsigned is_highlighted:NUM_ACTION_BAR_ITEMS;
struct AppTimer *redraw_timer;
GColor8 background_color;
ActionBarLayerIconPressAnimation animation[NUM_ACTION_BAR_ITEMS];
int64_t state_change_times[NUM_ACTION_BAR_ITEMS];
int64_t icon_change_times[NUM_ACTION_BAR_ITEMS];
} ActionBarLayer;
//! Initializes the action bar and reverts any state back to the default state:
//! * Background color: \ref GColorBlack
//! * No click configuration provider (`NULL`)
//! * No icons
//! * Not added to / associated with any window, thus not catching any button input yet.
//! @note Do not call this function on an action bar that is still or already added to a window.
//! @param action_bar The action bar to initialize
void action_bar_layer_init(ActionBarLayer *action_bar);
//! Creates a new ActionBarLayer on the heap and initalizes it with the default values.
//! * Background color: \ref GColorBlack
//! * No click configuration provider (`NULL`)
//! * No icons
//! * Not added to / associated with any window, thus not catching any button input yet.
//! @return A pointer to the ActionBarLayer. `NULL` if the ActionBarLayer could not
//! be created
ActionBarLayer* action_bar_layer_create(void);
void action_bar_layer_deinit(ActionBarLayer *action_bar_layer);
//! Destroys a ActionBarLayer previously created by action_bar_layer_create
void action_bar_layer_destroy(ActionBarLayer *action_bar_layer);
//! Gets the "root" Layer of the action bar layer, which is the parent for the sub-
//! layers used for its implementation.
//! @param action_bar_layer Pointer to the ActionBarLayer for which to get the "root" Layer
//! @return The "root" Layer of the action bar layer.
//! @internal
//! @note The result is always equal to `(Layer *) action_bar_layer`.
Layer* action_bar_layer_get_layer(ActionBarLayer *action_bar_layer);
//! Sets the context parameter, which will be passed in to \ref ClickHandler
//! callbacks and the \ref ClickConfigProvider callback of the action bar.
//! @note By default, a pointer to the action bar itself is passed in, if the
//! context has not been set or if it has been set to `NULL`.
//! @param action_bar The action bar for which to assign the new context
//! @param context The new context
//! @see action_bar_layer_set_click_config_provider()
//! @see \ref Clicks
void action_bar_layer_set_context(ActionBarLayer *action_bar, void *context);
//! Sets the click configuration provider callback of the action bar.
//! In this callback your application can associate handlers to the different
//! types of click events for each of the buttons, see \ref Clicks.
//! @note If the action bar had already been added to a window and the window
//! is currently on-screen, the click configuration provider will be called
//! before this function returns. Otherwise, it will be called by the system
//! when the window becomes on-screen.
//! @note The `.raw` handlers cannot be used without breaking the automatic
//! highlighting of the segment of the action bar that for which a button is
//! @see action_bar_layer_set_icon()
//! @param action_bar The action bar for which to assign a new click
//! configuration provider
//! @param click_config_provider The new click configuration provider
void action_bar_layer_set_click_config_provider(ActionBarLayer *action_bar, ClickConfigProvider click_config_provider);
//! Sets an action bar icon onto one of the 3 slots as identified by `button_id`.
//! Only \ref BUTTON_ID_UP, \ref BUTTON_ID_SELECT and \ref BUTTON_ID_DOWN can be
//! used. The transition will not be animated.
//! Whenever an icon is set, the click configuration provider will be
//! called, to give the application the opportunity to reconfigure the button
//! interaction.
//! @param action_bar The action bar for which to set the new icon
//! @param button_id The identifier of the button for which to set the icon
//! @param icon Pointer to the \ref GBitmap icon
//! @see action_bar_layer_set_icon_animated()
//! @see action_bar_layer_set_icon_press_animation()
//! @see action_bar_layer_set_click_config_provider()
void action_bar_layer_set_icon(ActionBarLayer *action_bar, ButtonId button_id, const GBitmap *icon);
//! Sets an action bar icon onto one of the 3 slots as identified by `button_id`.
//! Only \ref BUTTON_ID_UP, \ref BUTTON_ID_SELECT and \ref BUTTON_ID_DOWN can be
//! used. Optionally, if `animated` is `true`, the transition will be animated.
//! Whenever an icon is set, the click configuration provider will be called,
//! to give the application the opportunity to reconfigure the button interaction.
//! @param action_bar The action bar for which to set the new icon
//! @param button_id The identifier of the button for which to set the icon
//! @param icon Pointer to the \ref GBitmap icon
//! @param animated True = animate the transition, False = do not animate the transition
//! @see action_bar_layer_set_icon()
//! @see action_bar_layer_set_icon_press_animation()
//! @see action_bar_layer_set_click_config_provider()
void action_bar_layer_set_icon_animated(ActionBarLayer *action_bar, ButtonId button_id,
const GBitmap *icon, bool animated);
//! Sets the animation to use while a button is pressed on an ActionBarLayer.
//! By default we use ActionBarLayerIconPressAnimationMoveLeft
//! @param action_bar The action bar for which to set the press animation
//! @param button_id The button for which to set the press animation
//! @param animation The animation to use.
//! @see action_bar_layer_set_icon_animated()
//! @see action_bar_layer_set_click_config_provider()
void action_bar_layer_set_icon_press_animation(ActionBarLayer *action_bar, ButtonId button_id,
ActionBarLayerIconPressAnimation animation);
//! Convenience function to clear out an existing icon.
//! All it does is call `action_bar_layer_set_icon(action_bar, button_id, NULL)`
//! @param action_bar The action bar for which to clear an icon
//! @param button_id The identifier of the button for which to clear the icon
//! @see action_bar_layer_set_icon()
void action_bar_layer_clear_icon(ActionBarLayer *action_bar, ButtonId button_id);
//! Adds the action bar's layer on top of the window's root layer. It also
//! adjusts the layout of the action bar to match the geometry of the window it
//! gets added to.
//! Lastly, it calls \ref window_set_click_config_provider_with_context() on
//! the window to set it up to work with the internal callback and raw click
//! handlers of the action bar, to enable the highlighting of the section of the
//! action bar when the user presses a button.
//! @note After this call, do not use
//! \ref window_set_click_config_provider_with_context() with the window that
//! the action bar has been added to (this would de-associate the action bar's
//! click config provider and context). Instead use
//! \ref action_bar_layer_set_click_config_provider() and
//! \ref action_bar_layer_set_context() to register the click configuration
//! provider to configure the buttons actions.
//! @note It is advised to call this is in the window's `.load` or `.appear`
//! handler. Make sure to call \ref action_bar_layer_remove_from_window() in the
//! window's `.unload` or `.disappear` handler.
//! @note Adding additional layers to the window's root layer after this calll
//! can occlude the action bar.
//! @param action_bar The action bar to associate with the window
//! @param window The window with which the action bar is to be associated
void action_bar_layer_add_to_window(ActionBarLayer *action_bar, struct Window *window);
//! Removes the action bar from the window and unconfigures the window's
//! click configuration provider. `NULL` is set as the window's new click config
//! provider and also as its callback context. If it has not been added to a
//! window before, this function is a no-op.
//! @param action_bar The action bar to de-associate from its current window
void action_bar_layer_remove_from_window(ActionBarLayer *action_bar);
//! Sets the background color of the action bar. Defaults to \ref GColorBlack.
//! The action bar's layer is automatically marked dirty.
//! @param action_bar The action bar of which to set the background color
//! @param background_color The new background color
void action_bar_layer_set_background_color(ActionBarLayer *action_bar, GColor background_color);
//! @} // end addtogroup ActionBarLayer
//! @} // end addtogroup Layer
//! @} // end addtogroup UI

View File

@@ -0,0 +1,51 @@
/*
* 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 "action_button.h"
#include "applib/graphics/graphics.h"
#include "applib/preferred_content_size.h"
void action_button_draw(GContext *ctx, Layer *layer, GColor fill_color) {
// This should match the window bounds
const GRect bounds = layer->bounds;
// Glue button to the right side of the window
const int radius = PBL_IF_ROUND_ELSE(12, 13);
GRect rect = { .size = { radius * 2, radius * 2 } };
grect_align(&rect, &bounds, GAlignRight, false);
// Offset the button halfway off-screen
rect.origin.x += radius;
// Further offset the button on a per-default-content-size basis
// Note that this will need to be updated if we ever want ActionButton to adapt to the user's
// preferred content size
rect.origin.x += PREFERRED_CONTENT_SIZE_SWITCH(PreferredContentSizeDefault,
//! @note this is the same as Medium until Small is designed
/* small */ PBL_IF_ROUND_ELSE(1, 8),
/* medium */ PBL_IF_ROUND_ELSE(1, 8),
/* large */ 4,
//! @note this is the same as Large until ExtraLarge is designed
/* extralarge */ 4);
graphics_context_set_fill_color(ctx, fill_color);
graphics_fill_oval(ctx, rect, GOvalScaleModeFitCircle);
}
void action_button_update_proc(Layer *action_button_layer, GContext *ctx) {
action_button_draw(ctx, action_button_layer, GColorBlack);
}

View File

@@ -0,0 +1,30 @@
/*
* 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/ui/layer.h"
//! Action button is the actionable affordance for windows that display actionable content.
//! Action button only provides an update proc instead of entire layer.
//! Draws the action button on the layer with the fill color specified.
//! This expects a layer with a frame and bounds that spans the entire window.
void action_button_draw(GContext *ctx, Layer *layer, GColor fill_color);
//! Update proc of the action button.
//! This expects a layer with a frame and bounds that spans the entire window.
void action_button_update_proc(Layer *action_button_layer, GContext *ctx);

View File

@@ -0,0 +1,125 @@
/*
* 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 "action_menu_hierarchy.h"
#include "action_menu_window_private.h"
#include "applib/applib_malloc.auto.h"
#include "kernel/pbl_malloc.h"
// Item
/////////////////////////////////
char *action_menu_item_get_label(const ActionMenuItem *item) {
if (item == NULL) {
return NULL;
}
return (char *)item->label;
}
void *action_menu_item_get_action_data(const ActionMenuItem *item) {
if (item == NULL || !item->is_leaf) {
return NULL;
}
return (void *)item->action_data;
}
// Level
/////////////////////////////////
ActionMenuLevel *action_menu_level_create(uint16_t max_items) {
// TODO add applib-malloc padding
ActionMenuLevel *level = applib_malloc(applib_type_size(ActionMenuLevel) +
max_items * applib_type_size(ActionMenuItem));
if (!level) return NULL;
*level = (ActionMenuLevel){
.max_items = max_items,
.display_mode = ActionMenuLevelDisplayModeWide,
};
return level;
}
void action_menu_level_set_display_mode(ActionMenuLevel *level,
ActionMenuLevelDisplayMode display_mode) {
if (!level) return;
level->display_mode = display_mode;
}
ActionMenuItem *action_menu_level_add_action(ActionMenuLevel *level,
const char *label,
ActionMenuPerformActionCb cb,
void *action_data) {
if (!level || !label || !cb ||
(level->num_items >= level->max_items)) {
return NULL;
}
ActionMenuItem *item = &level->items[level->num_items];
*item = (ActionMenuItem) {
.label = label,
.perform_action = cb,
.action_data = action_data,
};
++level->num_items;
return item;
}
ActionMenuItem *action_menu_level_add_child(ActionMenuLevel *level,
ActionMenuLevel *child,
const char *label) {
if (!level || !child || !label ||
(level->num_items >= level->max_items)) {
return NULL;
}
child->parent_level = level;
ActionMenuItem *item = &level->items[level->num_items];
*item = (ActionMenuItem) {
.label = label,
.next_level = child,
};
++level->num_items;
return item;
}
// Hierarchy
/////////////////////////////////
static void prv_cleanup_helper(const ActionMenuLevel *level,
ActionMenuEachItemCb each_cb,
void *context) {
for (int i = 0; i < level->num_items; ++i) {
const ActionMenuItem *item = &level->items[i];
if (!item->is_leaf && item->next_level) {
prv_cleanup_helper(item->next_level, each_cb, context);
}
if (each_cb) {
each_cb(item, context);
}
}
applib_free((void *)level);
}
void action_menu_hierarchy_destroy(const ActionMenuLevel *root,
ActionMenuEachItemCb each_cb,
void *context) {
if (root) {
prv_cleanup_helper(root, each_cb, context);
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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 "action_menu_window.h"
//! @file action_menu_hierarchy.h
//! @addtogroup UI
//! @{
//! @addtogroup ActionMenu
//!
//! @{
//! Callback executed when a given action is selected
//! @param action_menu the action menu currently on screen
//! @param action the action that was triggered
//! @param context the context passed to the action menu
//! @note the action menu is closed immediately after an action is performed,
//! unless it is frozen in the ActionMenuPerformActionCb
typedef void (*ActionMenuPerformActionCb)(ActionMenu *action_menu,
const ActionMenuItem *action,
void *context);
//! Callback invoked for each item in an action menu hierarchy.
//! @param item the current action menu item
//! @param a caller-provided context callback
typedef void (*ActionMenuEachItemCb)(const ActionMenuItem *item, void *context);
//! enum value that controls whether menu items are displayed in a grid
//! (similarly to the emoji replies) or in a single column (reminiscent of \ref MenuLayer)
typedef enum {
ActionMenuLevelDisplayModeWide, //!< Each item gets its own row
ActionMenuLevelDisplayModeThin, //!< Grid view: multiple items per row
} ActionMenuLevelDisplayMode;
//! Getter for the label of a given \ref ActionMenuItem
//! @param item the \ref ActionMenuItem of interest
//! @return a pointer to the string label. NULL if invalid.
char *action_menu_item_get_label(const ActionMenuItem *item);
//! Getter for the action_data pointer of a given \ref ActionMenuitem.
//! @see action_menu_level_add_action
//! @param item the \ref ActionMenuItem of interest
//! @return a pointer to the data. NULL if invalid.
void *action_menu_item_get_action_data(const ActionMenuItem *item);
//! Create a new action menu level with storage allocated for a given number of items
//! @param max_items the max number of items that will be displayed at that level
//! @note levels are freed alongside the whole hierarchy so no destroy API is provided.
//! @note by default, levels are using ActionMenuLevelDisplayModeWide.
//! Use \ref action_menu_level_set_display_mode to change it.
//! @see action_menu_hierarchy_destroy
ActionMenuLevel *action_menu_level_create(uint16_t max_items);
//! Set the action menu display mode
//! @param level The ActionMenuLevel whose display mode you want to change
//! @param display_mode The display mode for the action menu (3 vs. 1 item per row)
void action_menu_level_set_display_mode(ActionMenuLevel *level,
ActionMenuLevelDisplayMode display_mode);
//! Add an action to an ActionLevel
//! @param level the level to add the action to
//! @param label the text to display for the action in the menu
//! @param cb the callback that will be triggered when this action is actuated
//! @param action_data data to pass to the callback for this action
//! @return a reference to the new \ref ActionMenuItem on success, NULL if the level is full
ActionMenuItem *action_menu_level_add_action(ActionMenuLevel *level,
const char *label,
ActionMenuPerformActionCb cb,
void *action_data);
//! Add a child to this ActionMenuLevel
//! @param level the parent level
//! @param child the child level
//! @param label the text to display in the action menu for this level
//! @return a reference to the new \ref ActionMenuItem on success, NULL if the level is full
ActionMenuItem *action_menu_level_add_child(ActionMenuLevel *level,
ActionMenuLevel *child,
const char *label);
//! Destroy a hierarchy of ActionMenuLevels
//! @param root the root level in the hierarchy
//! @param each_cb a callback to call on every \ref ActionMenuItem in every level
//! @param context a context pointer to pass to each_cb on invocation
//! @note Typical implementations will cleanup memory allocated for the item label/data
//! associated with each item in the callback
//! @note Hierarchy is traversed in post-order.
//! In other words, all children items are freed before their parent is freed.
void action_menu_hierarchy_destroy(const ActionMenuLevel *root,
ActionMenuEachItemCb each_cb,
void *context);
//! @} // end addtogroup ActionMenu
//! @} // end addtogroup UI

View File

@@ -0,0 +1,847 @@
/*
* 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 "action_menu_layer.h"
#include "action_menu_window_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/text.h"
#include "applib/ui/animation.h"
#include "applib/ui/menu_layer.h"
#include "applib/ui/property_animation.h"
#include "applib/ui/window_private.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/kernel_ui.h"
#include "resource/resource_ids.auto.h"
#include "shell/system_theme.h"
#include "system/passert.h"
#include "util/math.h"
#include <string.h>
#define INDICATOR "»"
static const int VERTICAL_PADDING = PBL_IF_COLOR_ELSE(2, 4);
static const int EXTRA_PADDING_1_BIT = 2;
static const int SHORT_COL_COUNT = 3;
static const int MAX_NUM_VISIBLE_LINES = 2;
static const int SHORT_ITEM_MAX_ROWS_SPALDING = 3;
static GFont prv_get_item_font(void) {
return system_theme_get_font(TextStyleFont_MenuCellTitle);
}
//! Only used on round displays to achieve a fish-eye effect
static GFont prv_get_unfocused_item_font(void) {
return system_theme_get_font(TextStyleFont_Header);
}
static uint16_t prv_get_num_rows(MenuLayer *menu_layer, uint16_t section_index,
void *callback_context) {
ActionMenuLayer *aml = callback_context;
return (uint16_t)(aml->num_items +
(aml->num_short_items + SHORT_COL_COUNT - 1) / SHORT_COL_COUNT);
}
static void prv_cell_column_draw(GContext *ctx, struct Layer const *cell_layer,
ActionMenuLayer *aml, ActionMenuItem *items,
int num_items, int sel_idx) {
const GFont font = aml->layout_cache.font;
const int16_t font_height = fonts_get_font_height(font);
const GRect *layer_bounds = &cell_layer->bounds;
GRect r = *layer_bounds;
#if PBL_ROUND
// more narrow on round
r = grect_inset_internal(r, 25, 0);
// center the columns horizontally if there's only one row
const bool is_single_short_row = aml->num_short_items <= SHORT_COL_COUNT;
r.size.w /= is_single_short_row ? num_items : SHORT_COL_COUNT;
#else
r.size.w /= SHORT_COL_COUNT;
#endif
r.origin.y += (r.size.h - font_height) / 2 - 4;
for (int i = 0; i < num_items; i++) {
if (!items[i].label) {
break;
}
if (sel_idx == i) {
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack));
#if SCREEN_COLOR_DEPTH_BITS == 1
// We only want to have a background on non-color platforms, while leaving this in with
// a PBL_IF_COLOR_ELSE makes this a no-op, we'll save some cycles and code space just
// skipping it.
graphics_context_set_fill_color(ctx, GColorWhite);
const int16_t y_offset = 1;
const int16_t padding = r.size.w / 6;
const uint16_t corner_radius = 4;
GRect bg_rect = r;
bg_rect.origin.y = layer_bounds->origin.y;
bg_rect.size.h = layer_bounds->size.h;
bg_rect = grect_inset_internal(bg_rect, padding, y_offset);
graphics_fill_round_rect(ctx, &bg_rect, corner_radius, GCornersAll);
#endif
} else {
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite));
}
graphics_draw_text(ctx, items[i].label, font, r, GTextOverflowModeTrailingEllipsis,
GTextAlignmentCenter, NULL);
r.origin.x += r.size.w;
}
}
static const ActionMenuItem *prv_get_item_for_index(ActionMenuLayer *aml, int idx) {
if (!aml->num_items && !aml->num_short_items) {
return NULL;
}
PBL_ASSERTN(idx >= 0);
if (idx < aml->num_items) {
return &aml->items[idx];
} else {
const int short_items_idx = idx - aml->num_items;
PBL_ASSERTN(short_items_idx < aml->num_short_items);
return &aml->short_items[short_items_idx];
}
}
static int16_t prv_get_item_line_height(ActionMenuLayer *aml, int idx) {
const GFont font = aml->layout_cache.font;
const ActionMenuItem *item = prv_get_item_for_index(aml, idx);
GRect box = menu_layer_get_layer(&aml->menu_layer)->bounds;
// In calculating the item line height for round displays, we need to horizontally inset by the
// standard focused cell inset since that's the horizontal inset of the cells where we show
// the vertical scrolling animation of long text cells (where the height is crucial to be correct)
const int inset = PBL_IF_ROUND_ELSE(MENU_CELL_ROUND_FOCUSED_HORIZONTAL_INSET,
menu_cell_basic_horizontal_inset());
// Tintin has a rounded rectangle highlight
box = grect_inset_internal(box, PBL_IF_COLOR_ELSE(inset, 2 * inset), 0);
// We offset the text 5 pixels from the left of the cell. If the indicator is
// present, the indicator also will be offset, so we add 5 pixels more spacing
// between the text and the indicator. This extra padding isn't needed for round.
const int nudge = PBL_IF_ROUND_ELSE(0, menu_cell_basic_horizontal_inset());
GContext *ctx = graphics_context_get_current_context();
// On rectangular displays, if the indicator is present, the indicator also will be offset,
// so we add another nudge between the text and the indicator.
#if PBL_RECT
if (!item->is_leaf) {
const GSize indicator_size = graphics_text_layout_get_max_used_size(ctx, INDICATOR,
font, box,
GTextOverflowModeWordWrap,
GTextAlignmentRight, NULL);
box.size.w -= (indicator_size.w + nudge);
}
#endif
return graphics_text_layout_get_text_height(ctx, item->label, font, box.size.w,
GTextOverflowModeWordWrap, PBL_IF_ROUND_ELSE(GTextAlignmentCenter, GTextAlignmentLeft));
}
// Item Scroll Animation
///////////////////////////////////
static int16_t prv_get_cell_offset(void *subject) {
ActionMenuLayer *aml = subject;
return aml->item_animation.current_offset_y;
}
T_STATIC void prv_set_cell_offset(void *subject, int16_t value) {
ActionMenuLayer *aml = subject;
aml->item_animation.current_offset_y = value;
layer_mark_dirty(&aml->layer);
}
static void prv_cell_animation_stopped_handler(Animation *animation, bool finished, void *context) {
ActionMenuLayer *aml = context;
if (finished) {
prv_set_cell_offset(aml, aml->item_animation.bottom_offset_y);
}
}
static const PropertyAnimationImplementation s_item_animation_implementation = {
.base = {
.update = (AnimationUpdateImplementation)property_animation_update_int16
},
.accessors = {
.setter = { .int16 = prv_set_cell_offset },
.getter = { .int16 = prv_get_cell_offset }
}
};
static void prv_unschedule_item_animation(ActionMenuLayer *aml) {
animation_unschedule(aml->item_animation.animation);
aml->item_animation.animation = NULL;
}
static void prv_animate_cell(ActionMenuLayer *aml, GRect *label_text_frame, bool *draw_top_shading,
bool *draw_bottom_shading) {
// Check to see if this item spans more than max number of visible lines,
// in which case we want to make it scroll.
const int16_t item_height = aml->layout_cache.item_heights[aml->selected_index];
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
#if SCREEN_COLOR_DEPTH_BITS == 1
// We need to force it to scroll a little extra for 1 bit
label_text_frame->origin.y -= EXTRA_PADDING_1_BIT;
#endif
// On rect displays, calculate the visible item height based on a desired number of visible lines
// On round displays, use the height of the provided box since it might be inset for the indicator
const int16_t max_visible_item_height = PBL_IF_RECT_ELSE(MAX_NUM_VISIBLE_LINES * line_height,
label_text_frame->size.h);
if (item_height > max_visible_item_height) {
// Compute the limit at which we should bounce back to the top of the layer. Since
// there are at most MAX_NUM_VISIBLE_LINES shown at a given time, we want to stop
// when there are that number of lines in view and no more lines remaining below.
const int16_t max_scroll_distance = item_height - max_visible_item_height;
ActionMenuItemAnimation *item_animation = &aml->item_animation;
if (item_animation->animation == NULL) {
const int16_t DELAY_PER_LINE = 600; /* milliseconds to delay per line */
// Top offset represents when the text has scrolled to its minimum y value so the last line of
// text is visible. Bottom offset represents when the text has scrolled all the way to its
// maximum y so the first line of text is visible.
item_animation->top_offset_y = -max_scroll_distance;
item_animation->bottom_offset_y = 0;
item_animation->current_offset_y = 0;
// Create the animation that will scroll us up in the cell
PropertyAnimation *animation = property_animation_create(&s_item_animation_implementation,
(void *)aml, NULL, &item_animation->top_offset_y);
animation_set_duration((Animation *)animation, DELAY_PER_LINE * (item_height / line_height));
animation_set_curve((Animation *)animation, AnimationCurveLinear);
animation_set_handlers((Animation *)animation, (AnimationHandlers){0}, aml);
// Create the animation that stalls when we have auto-scrolled up completely
PropertyAnimation *s_animation = property_animation_create(&s_item_animation_implementation,
(void *)aml, &item_animation->top_offset_y, &item_animation->top_offset_y);
animation_set_duration((Animation *)s_animation, DELAY_PER_LINE /* ms to wait */);
animation_set_handlers((Animation *)s_animation, (AnimationHandlers){0}, aml);
// Create the reverse animation that takes us from the scrolled up position back down
PropertyAnimation *r_animation = property_animation_create(&s_item_animation_implementation,
(void *)aml, &item_animation->top_offset_y, &item_animation->bottom_offset_y);
animation_set_duration((Animation *)r_animation,
(DELAY_PER_LINE / 4) * (item_height / line_height));
animation_set_curve((Animation *)r_animation, AnimationCurveEaseInOut);
animation_set_handlers((Animation *)r_animation, (AnimationHandlers){0}, aml);
item_animation->animation = animation_sequence_create((Animation *)animation,
(Animation *)s_animation, (Animation *)r_animation);
animation_set_handlers(item_animation->animation,
(AnimationHandlers){ .stopped = prv_cell_animation_stopped_handler },
aml);
animation_set_play_count(item_animation->animation, PLAY_COUNT_INFINITE);
animation_set_delay(item_animation->animation, DELAY_PER_LINE /* ms */);
animation_schedule(item_animation->animation);
}
*draw_top_shading = (item_animation->current_offset_y != item_animation->bottom_offset_y);
*draw_bottom_shading = (item_animation->current_offset_y != item_animation->top_offset_y);
// update the rect height and offset based on the current animation state
label_text_frame->origin.y += item_animation->current_offset_y;
label_text_frame->size.h = item_height;
}
}
// Menu Layer Drawing Routines
///////////////////////////////
static bool prv_should_center(ActionMenuLayer *aml) {
// We only center an ActionMenuLayer's items if the user has specified to
// center the items or there is only one item in the ActionMenuLayer.
if (aml->num_items == 1 || aml->layout_cache.align == ActionMenuAlignCenter) {
return true;
}
return false;
}
static void prv_cell_item_content_draw_rect(GContext *ctx, const Layer *cell_layer,
const ActionMenuLayer *aml, const ActionMenuItem *item,
bool selected, GRect *content_box) {
char *indicator = NULL;
const int16_t horizontal_padding = menu_cell_basic_horizontal_inset();
const GFont font = aml->layout_cache.font;
if (!item->is_leaf) {
// If an item is not a leaf, then there would be an indicator when it is focused. Either
// we draw the indicator or we force the box to be smaller to force the text to render as
// if the indicator was present in case it would line wrap.
if (selected) {
indicator = INDICATOR;
} else {
const GSize indicator_size = graphics_text_layout_get_max_used_size(
ctx, INDICATOR, font, *content_box, GTextOverflowModeWordWrap, GTextAlignmentRight, NULL);
content_box->size.w -= (indicator_size.w + (2 * horizontal_padding));
}
} else {
content_box->size.w -= horizontal_padding;
}
#if SCREEN_COLOR_DEPTH_BITS == 1
// Fill in the background layer. This effectively does nothing on watches where we have the
// ability to draw with color, but on others, it will render a background behind the selected
// cell.
const int x_offset = horizontal_padding;
const int y_padding = EXTRA_PADDING_1_BIT;
const uint16_t corner_radius = 4;
GRect bg_box = grect_inset_internal(cell_layer->bounds, x_offset, 0);
bg_box.size.h -= y_padding;
graphics_fill_round_rect(ctx, &bg_box, corner_radius, GCornersAll);
// We have to adjust the box to compensate for the padding we added. Note that we can't call
// inset as it will discard our offset when it standardizes.
content_box->origin.x += x_offset;
content_box->size.w -= (2 * x_offset);
content_box->size.h -= (2 * y_padding);
#endif
// Cast the cell layer so we can briefly modify its bounds. We do this because we're
// desperate for stack space and we understand the call hierarchy. We'll restore the state below.
Layer *mutable_cell_layer = (Layer *)cell_layer;
const GRect saved_bounds = mutable_cell_layer->bounds;
mutable_cell_layer->bounds = *content_box;
// Draw the menu cell specifying that we're allowing word wrapping
const GTextOverflowMode overflow_mode = GTextOverflowModeWordWrap;
menu_cell_basic_draw_custom(ctx, mutable_cell_layer, font, item->label, font, indicator, font,
NULL, NULL, false, overflow_mode);
// Restore the cell layer's bounds
mutable_cell_layer->bounds = saved_bounds;
}
static void prv_cell_item_content_draw_round(GContext *ctx, const Layer *cell_layer,
const ActionMenuLayer *aml, const ActionMenuItem *item,
bool selected, GRect *content_box) {
const int16_t horizontal_inset = selected ? MENU_CELL_ROUND_FOCUSED_HORIZONTAL_INSET :
MENU_CELL_ROUND_UNFOCUSED_HORIZONTAL_INSET;
*content_box = grect_inset(*content_box, GEdgeInsets(0, horizontal_inset));
// Use a smaller font for the unfocused cells to achieve a fish-eye effect
const GFont font = selected ? aml->layout_cache.font : prv_get_unfocused_item_font();
const GTextOverflowMode overflow_mode = selected ? GTextOverflowModeWordWrap :
GTextOverflowModeTrailingEllipsis;
const GTextAlignment text_alignment = GTextAlignmentCenter;
const GSize text_size = graphics_text_layout_get_max_used_size(ctx, item->label, font,
*content_box, overflow_mode,
text_alignment, NULL);
GRect text_box = (GRect) { .size = text_size };
const GAlign item_label_text_alignment = GAlignCenter;
grect_align(&text_box, content_box, item_label_text_alignment, true /* clip */);
text_box.origin.y -= fonts_get_font_cap_offset(font);
graphics_draw_text(ctx, item->label, font, text_box, overflow_mode, text_alignment, NULL);
}
static int16_t prv_get_indicator_height(const ActionMenuLayer *aml) {
// This magic factor is an approximation of the indicator height in relation to the font line
// height; it Just Works(tm)
return fonts_get_font_height(aml->layout_cache.font) * 40 / 100;
}
static void prv_draw_indicator_round(GContext *ctx, const ActionMenuLayer *aml,
const GRect *label_text_container) {
const int indicator_height = fonts_get_font_height(aml->layout_cache.font);
const int text_height = aml->layout_cache.item_heights[aml->selected_index];
const int content_height = MIN(label_text_container->size.h, text_height + indicator_height);
GRect content_frame = (GRect) {
.size = GSize(label_text_container->size.w, content_height)
};
GRect indicator_frame = (GRect) {
.size = GSize(label_text_container->size.w, indicator_height)
};
grect_align(&content_frame, label_text_container, GAlignCenter, true);
grect_align(&indicator_frame, &content_frame, GAlignBottom, true);
graphics_draw_text(ctx, INDICATOR, aml->layout_cache.font, indicator_frame,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
}
static void prv_cell_item_draw(GContext *ctx, const Layer *cell_layer,
ActionMenuLayer *aml, const ActionMenuItem *item,
bool selected) {
GRect label_text_container = cell_layer->bounds;
// bottom_inset won't be used on black and white, using UNUSED here quiets the linter
UNUSED int16_t bottom_inset = 0;
#if PBL_ROUND
// On round displays, inset the box from the bottom to account for drawing the indicator at the
// bottom center, and then draw the indicator
const bool selected_with_indicator = (selected && !item->is_leaf);
if (selected_with_indicator) {
prv_draw_indicator_round(ctx, aml, &label_text_container);
const int16_t indicator_text_margin = 7;
bottom_inset = prv_get_indicator_height(aml) + indicator_text_margin;
label_text_container.size.h -= bottom_inset;
}
#endif
GRect label_text_frame = label_text_container;
bool draw_top_shading = false;
bool draw_bottom_shading = false;
// If we are the selected index, check to see if we have started scrolling.
// If we have, use our internal box to draw the layer, otherwise use the
// layer box.
if (selected) {
prv_animate_cell(aml, &label_text_frame, &draw_top_shading, &draw_bottom_shading);
#if !defined(RECOVERY_FW) && SCREEN_COLOR_DEPTH_BITS == 8
// Replace the clip box with a clip box that will render the item in the right place with the
// right size, without menu layer's selection clipping. Menu layer will responsible for cleaning
// up the changes made to this clip box.
ctx->draw_state.clip_box.origin = ctx->draw_state.drawing_box.origin;
ctx->draw_state.clip_box.size = cell_layer->bounds.size;
// We have to update the clip box of the drawing state to account for text padding to
// force it to clip around the shadow.
if (draw_top_shading) {
ctx->draw_state.clip_box.origin.y += VERTICAL_PADDING;
ctx->draw_state.clip_box.size.h -= VERTICAL_PADDING;
}
if (draw_bottom_shading) {
ctx->draw_state.clip_box.size.h -= VERTICAL_PADDING + bottom_inset;
}
// Prevent drawing outside of the context bitmap
grect_clip(&ctx->draw_state.clip_box, &ctx->dest_bitmap.bounds);
#endif
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack));
graphics_context_set_fill_color(ctx, PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite));
}
PBL_IF_RECT_ELSE(prv_cell_item_content_draw_rect,
prv_cell_item_content_draw_round)(ctx, cell_layer, aml, item, selected,
&label_text_frame);
#if !defined(RECOVERY_FW) && SCREEN_COLOR_DEPTH_BITS == 8
const int16_t fade_height = 10;
graphics_context_set_compositing_mode(ctx, GCompOpSet);
if (draw_top_shading) {
GRect top_bounds = label_text_container;
top_bounds.origin.y += VERTICAL_PADDING;
top_bounds.size.h = fade_height;
graphics_draw_bitmap_in_rect(ctx, &aml->item_animation.fade_top, &top_bounds);
}
if (draw_bottom_shading) {
GRect bottom_bounds = label_text_container;
bottom_bounds.size.h = fade_height;
bottom_bounds.origin.y = grect_get_max_y(&label_text_container) -
(fade_height + VERTICAL_PADDING);
graphics_draw_bitmap_in_rect(ctx, &aml->item_animation.fade_bottom, &bottom_bounds);
}
#endif
}
static void prv_draw_row(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index,
void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (cell_index->row < aml->num_items) {
const ActionMenuItem *item = prv_get_item_for_index(aml, cell_index->row);
const bool selected = menu_layer_is_index_selected(&aml->menu_layer, cell_index);
prv_cell_item_draw(ctx, cell_layer, aml, item, selected);
} else {
const int base_idx = (cell_index->row - aml->num_items) * SHORT_COL_COUNT;
const int sel_idx = aml->selected_index - (base_idx + aml->num_items);
const int num_items = CLIP(aml->num_short_items - base_idx, 0, SHORT_COL_COUNT);
prv_cell_column_draw(ctx, cell_layer, aml, (ActionMenuItem *)&aml->short_items[base_idx],
num_items, sel_idx);
}
}
static int prv_get_menu_layer_row(ActionMenuLayer *aml, int item_index) {
if (item_index < aml->num_items) {
return item_index;
} else {
return aml->num_items + (item_index - aml->num_items) / SHORT_COL_COUNT;
}
}
T_STATIC void prv_set_selected_index(ActionMenuLayer *aml, int new_selected_index, bool animated) {
new_selected_index = CLIP(new_selected_index, 0, aml->num_items + aml->num_short_items - 1);
if (new_selected_index != aml->selected_index) {
// Unschedule any running item animation but don't NULL the pointer, to prevent another
// animation from being accidentally re-scheduled.
animation_unschedule(aml->item_animation.animation);
}
if (new_selected_index >= aml->num_items) {
// For short columns, aml->selected_index needs to be updated here, because the column index
// will be lost in the menu layer selection changed callback. Otherwise, it will be updated
// in prv_selection_changed_cb() to ensure the correct index is used by the draw functions.
aml->selected_index = new_selected_index;
}
const int menu_layer_index = prv_get_menu_layer_row(aml, new_selected_index);
menu_layer_set_selected_index(&aml->menu_layer, MenuIndex(0, menu_layer_index),
MenuRowAlignCenter, animated);
}
static void prv_scroll_handler(ClickRecognizerRef recognizer, void *context) {
ActionMenuLayer *aml = context;
const bool up = (click_recognizer_get_button_id(recognizer) == BUTTON_ID_UP);
const int new_idx = aml->selected_index + (up ? -1 : 1);
prv_set_selected_index(aml, new_idx, true /* animated */);
}
static void prv_select_handler(ClickRecognizerRef recognizer, void *context) {
ActionMenuLayer *aml = context;
const ActionMenuItem *item = prv_get_item_for_index(aml, aml->selected_index);
if (item && aml->cb) {
aml->cb(item, aml->context);
}
}
static bool prv_aml_is_short(ActionMenuLayer *aml) {
return (aml->num_short_items != 0 || aml->num_items == 0);
}
static int16_t prv_get_cell_padding(ActionMenuLayer *aml) {
const int16_t default_sep_height = 10;
#if PBL_ROUND
// when showing columns, set cells further apart
return prv_aml_is_short(aml) ? default_sep_height : 1;
#elif SCREEN_COLOR_DEPTH_BITS == 1
return default_sep_height;
#else
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
const int16_t sep_height = MAX(menu_cell_small_cell_height() - line_height,
default_sep_height) + 1;
return sep_height;
#endif
}
static int16_t prv_get_cell_height_cb(struct MenuLayer *menu_layer,
MenuIndex *cell_index,
void *context) {
ActionMenuLayer *aml = (ActionMenuLayer *)context;
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
// If we have short items, just return the line height.
if (prv_aml_is_short(aml)) {
return line_height;
}
#if PBL_ROUND
return menu_layer_is_index_selected(menu_layer, cell_index) ?
MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT :
MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT;
#else
const int16_t max_visible_height = line_height * MAX_NUM_VISIBLE_LINES;
const int16_t actual_height = aml->layout_cache.item_heights[cell_index->row];
return (VERTICAL_PADDING * 2) + MIN(max_visible_height, actual_height);
#endif
}
static int16_t prv_get_separator_height_cb(struct MenuLayer *menu_layer, MenuIndex *cell_index,
void *callback_context) {
// We use the separator to pad the cells (insert spacing), so we compute the height
// needed for each separator here.
ActionMenuLayer *aml = callback_context;
return prv_get_cell_padding(aml);
}
typedef struct ActionMenuSeparatorConfig {
GSize separator;
} ActionMenuSeparatorConfig;
static const ActionMenuSeparatorConfig s_separator_configs[NumPreferredContentSizes] = {
[PreferredContentSizeSmall] = {
.separator = {100, 1},
},
[PreferredContentSizeMedium] = {
.separator = {100, 1},
},
[PreferredContentSizeLarge] = {
.separator = {162, 2},
},
[PreferredContentSizeExtraLarge] = {
.separator = {162, 2},
},
};
static void prv_draw_separator_cb(GContext *ctx, const Layer *cell_layer,
MenuIndex *cell_index, void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (aml->separator_index && cell_index->row == aml->separator_index) {
const PreferredContentSize runtime_platform_default_size =
system_theme_get_default_content_size_for_runtime_platform();
const ActionMenuSeparatorConfig *config = &s_separator_configs[runtime_platform_default_size];
// If this index is the seperator index, we want to draw the separator line
// in the vertical center of the separator
const int16_t nudge_down = PBL_IF_RECT_ELSE(3, 0);
const int16_t nudge_right = menu_cell_basic_horizontal_inset() + 1;
const int16_t separator_width = config->separator.w;
const GRect *cell_layer_bounds = &cell_layer->bounds;
const int16_t offset_x = PBL_IF_RECT_ELSE(nudge_right,
(cell_layer->bounds.size.w - separator_width) / 2);
const int16_t offset_y = (cell_layer_bounds->size.h / 2) + nudge_down;
GPoint separator_start_point = gpoint_add(cell_layer_bounds->origin,
GPoint(offset_x, offset_y));
graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite));
separator_start_point.y += config->separator.h;
for (int i = 0; i < config->separator.h; i++) {
// First point from bottom will be +0, second +1, third +0, etc.
separator_start_point.y--;
separator_start_point.x += i & 1;
graphics_draw_horizontal_line_dotted(ctx, separator_start_point, separator_width);
separator_start_point.x -= i & 1;
}
}
}
static int16_t prv_get_header_height_cb(struct MenuLayer *menu_layer, uint16_t second_index,
void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (!prv_should_center(aml) || prv_aml_is_short(aml) || aml->num_items == 0) {
return 0;
}
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
const int16_t padding = prv_get_cell_padding(aml);
const int16_t max_visible_height = line_height * MAX_NUM_VISIBLE_LINES;
const GRect *bounds = &aml->layer.bounds;
int16_t total_h = 0;
for (int16_t idx = 0; idx < aml->num_items; idx++) {
int16_t item_height = aml->layout_cache.item_heights[idx];
total_h += MIN(max_visible_height, item_height);
}
const int16_t header_padding = 6 * aml->num_items;
const int16_t header_height = ((bounds->size.h - total_h) / 2) - padding;
return MAX(header_height - header_padding, 0);
}
static void prv_draw_header_cb(GContext *ctx, const Layer *cell_layer, uint16_t section_index,
void *callback_context) {
// The header here is just being used for padding, so we don't actually need to draw anything.
return;
}
static void prv_selection_changed_cb(struct MenuLayer *menu_layer, MenuIndex new_index,
MenuIndex old_index, void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (new_index.row < aml->num_items) {
// Enable a new item animation to be scheduled
prv_unschedule_item_animation(aml);
aml->selected_index = new_index.row;
}
}
static void prv_changed_proc(Layer *layer) {
ActionMenuLayer *aml = (ActionMenuLayer *)layer;
const GRect *aml_bounds = &layer->bounds;
GRect menu_layer_frame = *aml_bounds;
#if PBL_ROUND
if (prv_aml_is_short(aml)) {
// clip the menu layer to show exactly SHORT_ITEM_MAX_ROWS_SPALDING lines at a time
const int16_t font_height = fonts_get_font_height(aml->layout_cache.font);
const int16_t cell_padding = prv_get_cell_padding(aml);
const int num_visible_rows = MIN(prv_get_num_rows(&aml->menu_layer, 0, aml),
SHORT_ITEM_MAX_ROWS_SPALDING);
menu_layer_frame.size.h = (font_height * num_visible_rows) +
(cell_padding * (num_visible_rows - 1));
grect_align(&menu_layer_frame, aml_bounds, GAlignCenter, true /* clip */);
}
#endif
layer_set_frame(menu_layer_get_layer(&aml->menu_layer), &menu_layer_frame);
}
static void prv_update_proc(Layer *layer, GContext *ctx) {
#if PBL_ROUND
ActionMenuLayer *aml = (ActionMenuLayer *)layer;
const int num_rows = prv_get_num_rows(&aml->menu_layer, 0, aml);
if (prv_aml_is_short(aml) && (num_rows > SHORT_ITEM_MAX_ROWS_SPALDING)) {
// draw some "content indicator" arrows
const GRect *aml_bounds = &layer->bounds;
const GRect *menu_layer_frame = &menu_layer_get_layer(&aml->menu_layer)->frame;
const int16_t arrow_layer_height = (aml_bounds->size.h - menu_layer_frame->size.h) / 2;
const int row = prv_get_menu_layer_row(aml, aml->selected_index);
const GColor bg_color = GColorBlack;
const GColor fg_color = PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite);
GRect arrow_rect = (GRect) { .size = GSize(aml_bounds->size.w, arrow_layer_height) };
if (row >= SHORT_ITEM_MAX_ROWS_SPALDING - 1) {
grect_align(&arrow_rect, aml_bounds, GAlignTop, true /* clip */);
content_indicator_draw_arrow(ctx, &arrow_rect, ContentIndicatorDirectionUp, fg_color,
bg_color, GAlignTop);
}
if (num_rows - row >= SHORT_ITEM_MAX_ROWS_SPALDING) {
grect_align(&arrow_rect, aml_bounds, GAlignBottom, true /* clip */);
content_indicator_draw_arrow(ctx, &arrow_rect, ContentIndicatorDirectionDown, fg_color,
bg_color, GAlignBottom);
}
}
#endif
}
static void prv_update_aml_cache(ActionMenuLayer *aml, int selected_index) {
prv_unschedule_item_animation(aml);
if (aml->layout_cache.item_heights != NULL) {
applib_free(aml->layout_cache.item_heights);
aml->layout_cache.item_heights = NULL;
}
if (aml->num_items > 0) {
// Update the cache of heights. We do this here to avoid recomputing the same
// values repeatedly when we call the menu layer height callback.
aml->layout_cache.item_heights = applib_zalloc(aml->num_items * sizeof(int16_t));
for (int idx = 0; idx < aml->num_items; idx++) {
aml->layout_cache.item_heights[idx] = prv_get_item_line_height(aml, idx);
}
}
#if PBL_ROUND
const bool center_focused = !prv_aml_is_short(aml);
menu_layer_set_center_focused(&aml->menu_layer, center_focused);
#endif
layer_mark_dirty(&aml->layer);
menu_layer_reload_data(&aml->menu_layer);
prv_set_selected_index(aml, selected_index, false /* animated */);
}
// Public API
/////////////////////
void action_menu_layer_click_config_provider(ActionMenuLayer *aml) {
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, prv_scroll_handler);
window_set_click_context(BUTTON_ID_UP, aml);
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100, prv_scroll_handler);
window_set_click_context(BUTTON_ID_DOWN, aml);
window_single_click_subscribe(BUTTON_ID_SELECT, prv_select_handler);
window_set_click_context(BUTTON_ID_SELECT, aml);
}
void action_menu_layer_set_callback(ActionMenuLayer *aml,
ActionMenuLayerCallback cb,
void *context) {
aml->cb = cb;
aml->context = context;
}
void action_menu_layer_init(ActionMenuLayer *aml, const GRect *frame) {
layer_init(&aml->layer, frame);
// Since menu_layer_set_callbacks() will call the menu functions, we need to initialize
// the ActionMenuLayer attributes before setting the callbacks onto the menu.
aml->item_animation = (ActionMenuItemAnimation){};
aml->layout_cache = (ActionMenuLayoutCache){
.font = prv_get_item_font()
};
aml->layer.property_changed_proc = prv_changed_proc;
aml->layer.update_proc = prv_update_proc;
menu_layer_init(&aml->menu_layer, &aml->layer.bounds);
menu_layer_set_normal_colors(&aml->menu_layer, GColorBlack,
PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite));
#if PBL_ROUND
menu_layer_pad_bottom_enable(&aml->menu_layer, false);
#endif
menu_layer_set_callbacks(&aml->menu_layer, aml, &(MenuLayerCallbacks){
.get_num_rows = prv_get_num_rows,
.draw_row = prv_draw_row,
.get_cell_height = prv_get_cell_height_cb,
.get_separator_height = prv_get_separator_height_cb,
.draw_separator = prv_draw_separator_cb,
.get_header_height = prv_get_header_height_cb,
.draw_header = prv_draw_header_cb,
.selection_changed = prv_selection_changed_cb
});
#if !defined(RECOVERY_FW)
gbitmap_init_with_resource_system(&aml->item_animation.fade_top, SYSTEM_APP,
RESOURCE_ID_ACTION_MENU_FADE_TOP);
gbitmap_init_with_resource_system(&aml->item_animation.fade_bottom, SYSTEM_APP,
RESOURCE_ID_ACTION_MENU_FADE_BOTTOM);
#endif
layer_add_child(&aml->layer, menu_layer_get_layer(&aml->menu_layer));
layer_set_hidden((Layer *)&aml->menu_layer.inverter, true);
aml->menu_layer.selection_animation_disabled = true;
}
void action_menu_layer_deinit(ActionMenuLayer *aml) {
if (aml->layout_cache.item_heights) {
applib_free(aml->layout_cache.item_heights);
}
prv_unschedule_item_animation(aml);
#ifndef RECOVERY_FW
gbitmap_deinit(&aml->item_animation.fade_top);
gbitmap_deinit(&aml->item_animation.fade_bottom);
#endif
menu_layer_deinit(&aml->menu_layer);
}
ActionMenuLayer *action_menu_layer_create(GRect frame) {
ActionMenuLayer *aml = applib_zalloc(sizeof(ActionMenuLayer));
if (!aml) {
return NULL;
}
action_menu_layer_init(aml, &frame);
return aml;
}
void action_menu_layer_destroy(ActionMenuLayer *aml) {
if (!aml) {
return;
}
action_menu_layer_deinit(aml);
applib_free(aml);
}
void action_menu_layer_set_align(ActionMenuLayer *aml, ActionMenuAlign align) {
if (!aml) {
return;
}
aml->layout_cache.align = align;
}
void action_menu_layer_set_items(ActionMenuLayer *aml, const ActionMenuItem* items, int num_items,
unsigned default_selected_item, unsigned separator_index) {
aml->items = items;
aml->num_items = num_items;
aml->separator_index = separator_index;
prv_update_aml_cache(aml, default_selected_item);
}
void action_menu_layer_set_short_items(ActionMenuLayer *aml, const ActionMenuItem* items,
int num_items, unsigned default_selected_item) {
aml->short_items = items;
aml->separator_index = 0;
aml->num_short_items = num_items;
prv_update_aml_cache(aml, default_selected_item);
}

View File

@@ -0,0 +1,98 @@
/*
* 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 "action_menu_window.h"
#include "click.h"
#include "inverter_layer.h"
#include "layer.h"
#include "menu_layer.h"
#include "scroll_layer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/animation.h"
#include "applib/ui/window_private.h"
#include "system/passert.h"
#include <string.h>
typedef void (*ActionMenuLayerCallback)(const ActionMenuItem *item, void *context);
typedef struct {
ActionMenuAlign align;
GFont font;
int16_t *item_heights;
} ActionMenuLayoutCache;
typedef struct {
struct Animation *animation;
int16_t top_offset_y;
int16_t bottom_offset_y;
int16_t current_offset_y;
GBitmap fade_top;
GBitmap fade_bottom;
} ActionMenuItemAnimation;
typedef struct {
Layer layer;
MenuLayer menu_layer;
int selected_index;
unsigned separator_index;
ActionMenuLayerCallback cb;
const ActionMenuItem* items;
int num_items;
//! @internal
ActionMenuLayoutCache layout_cache;
//! @internal
ActionMenuItemAnimation item_animation;
const ActionMenuItem* short_items;
int num_short_items;
void *context;
} ActionMenuLayer;
ActionMenuLayer *action_menu_layer_create(GRect frame);
void action_menu_layer_set_callback(ActionMenuLayer *aml,
ActionMenuLayerCallback cb,
void *context);
void action_menu_layer_set_align(ActionMenuLayer *aml,
ActionMenuAlign align);
void action_menu_layer_set_items(ActionMenuLayer *aml,
const ActionMenuItem *items,
int num_items,
unsigned default_selected_item,
unsigned separator_index);
void action_menu_layer_click_config_provider(ActionMenuLayer *aml);
void action_menu_layer_destroy(ActionMenuLayer *aml);
void action_menu_layer_set_short_items(ActionMenuLayer *aml,
const ActionMenuItem *items,
int num_items,
unsigned default_selected_item);
void action_menu_layer_init(ActionMenuLayer *aml, const GRect *frame);
void action_menu_layer_deinit(ActionMenuLayer *aml);

View File

@@ -0,0 +1,359 @@
/*
* 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 "action_menu_window.h"
#include "action_menu_window_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/ui/status_bar_layer.h"
#include "applib/ui/window.h"
#include "applib/ui/window_stack.h"
#include "process_state/app_state/app_state.h"
#include "services/normal/timeline/timeline.h"
#include "services/common/i18n/i18n.h"
#define ACTION_MENU_DEFAULT_BACKGROUND_COLOR GColorWhite
static const int IN_OUT_ANIMATION_DURATION = 200;
static void prv_invoke_will_close(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (data->config.will_close) {
data->config.will_close(action_menu,
data->performed_item,
data->config.context);
}
}
static void prv_invoke_did_close(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (data->config.did_close) {
data->config.did_close(action_menu,
data->performed_item,
data->config.context);
}
}
static void prv_action_window_push(WindowStack *window_stack, ActionMenu *action_menu,
bool animated) {
window_stack_push(window_stack, &action_menu->window, animated);
}
static void prv_action_window_pop(bool animated, ActionMenu *action_menu) {
prv_invoke_will_close(action_menu);
window_stack_remove(&action_menu->window, animated);
}
static void prv_action_window_insert_below(ActionMenu *action_menu, Window *window) {
window_stack_insert_next(action_menu->window.parent_window_stack, window);
}
static void prv_remove_window(Window *window) {
window_stack_remove(window, false /* animated */);
}
static void prv_view_model_did_change(ActionMenuData *data) {
ActionMenuViewModel *vm = &data->view_model;
const ActionMenuLevel *cur_level = vm->cur_level;
GRect frame = grect_inset(data->action_menu.window.layer.frame, vm->menu_insets);
layer_set_frame(&data->action_menu_layer.layer, &frame);
if (cur_level->display_mode == ActionMenuLevelDisplayModeThin) {
action_menu_layer_set_items(&data->action_menu_layer, NULL, 0, 0, 0);
action_menu_layer_set_short_items(&data->action_menu_layer,
cur_level->items,
cur_level->num_items,
cur_level->default_selected_item);
} else {
action_menu_layer_set_short_items(&data->action_menu_layer, NULL, 0, 0);
action_menu_layer_set_items(&data->action_menu_layer, cur_level->items, cur_level->num_items,
cur_level->default_selected_item, cur_level->separator_index);
}
crumbs_layer_set_level(&data->crumbs_layer, vm->num_dots);
}
static void prv_next_level_anim_stopped(Animation *anim, bool finished, void *context) {
// update the view model
AnimationContext *anim_ctx = (AnimationContext*) context;
ActionMenuData *data = window_get_user_data((Window*) anim_ctx->window);
if (!data || !finished) {
// We could have gotten cleaned up in the middle of an animation, bail
applib_free(anim_ctx);
return;
}
if (data->view_model.cur_level->parent_level == anim_ctx->next_level) {
--data->view_model.num_dots;
} else {
++data->view_model.num_dots;
}
data->view_model.cur_level = anim_ctx->next_level;
// update the view
prv_view_model_did_change(data);
applib_free(anim_ctx);
}
static GEdgeInsets prv_action_menu_insets(Window *window) {
const int crumbs_width = crumbs_layer_width();
return (GEdgeInsets) {
.top = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT),
.right = PBL_IF_RECT_ELSE(0, crumbs_width),
.bottom = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT),
.left = crumbs_width,
};
}
static Animation* prv_create_content_in_animation(ActionMenuData *data,
const ActionMenuLevel *level) {
// animate the ease in of the new level
const GRect window_frame = data->action_menu.window.layer.frame;
const GEdgeInsets insets = prv_action_menu_insets(&data->action_menu.window);
GRect stop = grect_inset(window_frame, insets);
GRect start = stop;
start.origin.x -= crumbs_layer_width();
PropertyAnimation *prop_anim =
property_animation_create_layer_frame((Layer *)&data->action_menu_layer,
&start,
&stop);
Animation *content_in = property_animation_get_animation(prop_anim);
animation_set_duration(content_in, IN_OUT_ANIMATION_DURATION);
#if !defined(PLATFORM_TINTIN)
// animate the dots
Animation *crumbs_anim = crumbs_layer_get_animation(&data->crumbs_layer);
animation_set_duration(crumbs_anim, IN_OUT_ANIMATION_DURATION);
// combine the two
Animation *spawn_anim = animation_spawn_create(content_in, crumbs_anim, NULL);
return spawn_anim;
#else
return content_in;
#endif
}
static Animation* prv_create_content_out_animation(ActionMenuData *data,
const ActionMenuLevel *level) {
// animate the ease out of the current level
GRect *start = &data->action_menu_layer.layer.frame;
GRect stop = *start;
stop.origin.x = crumbs_layer_width() - start->size.w;
PropertyAnimation *prop_anim =
property_animation_create_layer_frame((Layer *)&data->action_menu_layer, start, &stop);
Animation *content_out = property_animation_get_animation(prop_anim);
animation_set_duration(content_out, IN_OUT_ANIMATION_DURATION);
AnimationHandlers anim_handlers = {
.started = NULL,
.stopped = prv_next_level_anim_stopped,
};
AnimationContext *anim_ctx = applib_type_malloc(AnimationContext);
*anim_ctx = (AnimationContext) {
.window = &data->action_menu.window,
.next_level = level,
};
animation_set_handlers(content_out, anim_handlers, anim_ctx);
return content_out;
}
static void prv_set_level(ActionMenuData *data, const ActionMenuLevel *level) {
if (animation_is_scheduled(data->level_change_anim)) {
// We are already animating.
return;
}
Animation *content_out = prv_create_content_out_animation(data, level);
Animation *content_in = prv_create_content_in_animation(data, level);
data->level_change_anim = animation_sequence_create(content_out, content_in, NULL);
animation_schedule(data->level_change_anim);
}
static void prv_action_callback(const ActionMenuItem *item, void *context) {
ActionMenu *action_menu = context;
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (item->is_leaf && item->perform_action) {
item->perform_action(action_menu, item, data->config.context);
data->performed_item = item;
if (!data->frozen) {
prv_action_window_pop(true /*animated*/, action_menu);
}
} else if (item->next_level) {
prv_set_level(data, item->next_level);
}
}
static void prv_back_click_handler(ClickRecognizerRef recognizer, void *context) {
ActionMenuData *data = context;
if (animation_is_scheduled(data->level_change_anim)) {
animation_set_elapsed(data->level_change_anim,
animation_get_duration(data->level_change_anim, true, true));
}
ActionMenuLevel *parent_level = data->view_model.cur_level->parent_level;
if (parent_level) {
prv_set_level(data, parent_level);
} else {
prv_action_window_pop(true, &data->action_menu);
}
}
static void prv_click_config_provider(void *context) {
ActionMenuData *data = context;
action_menu_layer_click_config_provider(&data->action_menu_layer);
window_single_click_subscribe(BUTTON_ID_BACK, prv_back_click_handler);
window_set_click_context(BUTTON_ID_BACK, data);
}
static void prv_action_window_load(Window *window) {
ActionMenuData *data = window_get_user_data(window);
// Init action menu layer
ActionMenuLayer *action_menu_layer = &data->action_menu_layer;
action_menu_layer_init(action_menu_layer, &GRectZero);
action_menu_layer_set_callback(action_menu_layer, prv_action_callback, (void *)window);
action_menu_layer_set_align(action_menu_layer, data->config.align);
// Init crumbs layer
CrumbsLayer *crumbs_layer = &data->crumbs_layer;
GRect frame = window_get_root_layer(window)->frame;
#if PBL_RECT
// on round display, the layer fills a full circle
// here (on rect) it's just a small vertical stripe on the left
frame.size.w = crumbs_layer_width();
#endif
crumbs_layer_init(&data->crumbs_layer, &frame, data->config.colors.background,
data->config.colors.foreground);
// Add them to the tree
layer_add_child(window_get_root_layer(window), (Layer *)action_menu_layer);
layer_add_child(window_get_root_layer(window), (Layer *)crumbs_layer);
// Click config
window_set_click_config_provider_with_context(window, prv_click_config_provider, data);
// Init the view model
data->view_model = (ActionMenuViewModel) {
.cur_level = data->config.root_level,
.menu_insets = prv_action_menu_insets(window),
.num_dots = 1,
};
prv_view_model_did_change(data);
}
static void prv_action_window_unload(Window *window) {
ActionMenuData *data = window_get_user_data(window);
// call did close callback so user can cleanup
prv_invoke_did_close((ActionMenu *)window);
// cleanup
animation_unschedule(data->level_change_anim);
action_menu_layer_deinit(&data->action_menu_layer);
crumbs_layer_deinit(&data->crumbs_layer);
applib_free(data);
}
static void prv_dummy_click_config(void *data) {
}
ActionMenuLevel *action_menu_get_root_level(ActionMenu *action_menu) {
if (!action_menu) return NULL;
ActionMenuData *data = window_get_user_data(&action_menu->window);
return (ActionMenuLevel *)data->config.root_level;
}
void *action_menu_get_context(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
return data->config.context;
}
void action_menu_freeze(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
window_set_click_config_provider(&action_menu->window, prv_dummy_click_config);
data->frozen = true;
}
void action_menu_unfreeze(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
window_set_click_config_provider_with_context(&action_menu->window,
prv_click_config_provider, data);
data->frozen = false;
}
bool action_menu_is_frozen(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
return data->frozen;
}
void action_menu_close(ActionMenu *action_menu, bool animated) {
prv_action_window_pop(animated, action_menu);
}
void action_menu_set_result_window(ActionMenu *action_menu, Window *result_window) {
if (!action_menu) return;
// remove existing result window
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (data->result_window) {
prv_remove_window(data->result_window);
}
// insert new result window
if (result_window) {
prv_action_window_insert_below(action_menu, result_window);
}
data->result_window = result_window;
}
void action_menu_set_align(ActionMenuConfig *config, ActionMenuAlign align) {
if (!config) {
return;
}
config->align = align;
}
ActionMenu *action_menu_open(WindowStack *window_stack, ActionMenuConfig *config) {
ActionMenuData *data = applib_type_zalloc(ActionMenuData);
data->config = *config;
#if SCREEN_COLOR_DEPTH_BITS == 8
// Apply defaults if client didn't assign foreground/background colors
if (gcolor_is_invisible(data->config.colors.background)) {
data->config.colors.background = ACTION_MENU_DEFAULT_BACKGROUND_COLOR;
}
if (gcolor_is_invisible((data->config.colors.foreground))) {
data->config.colors.foreground = gcolor_legible_over(data->config.colors.background);
}
#else
data->config.colors.background = GColorLightGray;
data->config.colors.foreground = GColorBlack;
#endif
Window *window = &data->action_menu.window;
window_init(window, WINDOW_NAME("Action Menu"));
window_set_user_data(window, data);
window_set_fullscreen(window, true);
window_set_background_color(window, GColorBlack);
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_action_window_load,
.unload = prv_action_window_unload,
});
prv_action_window_push(window_stack, &data->action_menu, true /* animated */);
return &data->action_menu;
}
ActionMenu *app_action_menu_open(ActionMenuConfig *config) {
return action_menu_open(app_state_get_window_stack(), config);
}

View File

@@ -0,0 +1,137 @@
/*
* 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/ui/window.h"
#include "applib/ui/window_stack.h"
#include "services/normal/timeline/item.h"
//! @file action_menu_window.h
//! @addtogroup UI
//! @{
//! @addtogroup ActionMenu
//!
//! \brief Configurable menu that displays a hierarchy of selectable choices to the user
//!
//! @{
typedef enum {
ActionMenuAlignTop = 0,
ActionMenuAlignCenter
} ActionMenuAlign;
struct ActionMenuItem;
//! An ActionMenuItem is an entry in the ActionMenu
//! You can think of it as a node in the action tree.
//! ActionMenuItems are either actions (i.e. leaf nodes) or levels.
typedef struct ActionMenuItem ActionMenuItem;
struct ActionMenuLevel;
typedef struct ActionMenuLevel ActionMenuLevel;
struct ActionMenu;
typedef struct ActionMenu ActionMenu;
//! Callback executed after the ActionMenu has closed, so memory may be freed.
//! @param root_level the root level passed to the ActionMenu
//! @param performed_action the ActionMenuItem for the action that was performed,
//! NULL if the ActionMenu is closing without an action being selected by the user
//! @param context the context passed to the ActionMenu
typedef void (*ActionMenuDidCloseCb)(ActionMenu *menu,
const ActionMenuItem *performed_action,
void *context);
//! Callback executed immediately before the ActionMenu closes.
//! @param root_level the root ActionMenuLevel passed to the ActionMenu
//! @param performed_action the ActionMenuItem for the action that was performed,
//! NULL if the ActionMenu is closing without an action being selected by the user
//! @param context the context passed to the ActionMenu
typedef void (*ActionMenuWillCloseCb)(ActionMenu *menu,
const ActionMenuItem *performed_action,
void *context);
//! Configuration struct for the ActionMenu
typedef struct {
const ActionMenuLevel *root_level; //!< the root level of the ActionMenu
void *context; //!< a context pointer which will be accessbile when actions are performed
struct {
GColor background; //!< the color of the left column of the ActionMenu
GColor foreground; //!< the color of the individual "crumbs" that indicate menu depth
} colors;
ActionMenuDidCloseCb will_close; //!< Called immediately before the ActionMenu closes
ActionMenuDidCloseCb did_close; //!< a callback used to cleanup memory after the menu has closed
ActionMenuAlign align;
} ActionMenuConfig;
//! Get the context pointer this ActionMenu was created with
//! @param action_menu A pointer to an ActionMenu
//! @return the context pointer initially provided in the \ref ActionMenuConfig.
//! NULL if none exists.
void *action_menu_get_context(ActionMenu *action_menu);
//! Get the root level of an ActionMenu
//! @param action_menu the ActionMenu you want to know about
//! @return a pointer to the root ActionMenuLevel for the given ActionMenu, NULL if invalid
ActionMenuLevel *action_menu_get_root_level(ActionMenu *action_menu);
//! @internal
//! Open a new ActionMenu.
//! The ActionMenu acts much like a window. It fills the whole screen and handles clicks.
//! @param window_stack The \ref WindowStack to push the ActionMenu to
//! @param config the configuration info for this new ActionMenu
//! @return the new ActionMenu
ActionMenu *action_menu_open(WindowStack *window_stack, ActionMenuConfig *config);
//! Open a new ActionMenu.
//! The ActionMenu acts much like a window. It fills the whole screen and handles clicks.
//! @param config the configuration info for this new ActionMenu
//! @return the new ActionMenu
ActionMenu *app_action_menu_open(ActionMenuConfig *config);
//! Freeze the ActionMenu. The ActionMenu will no longer respond to user input.
//! @note this API should be used when waiting for asynchronous operation.
//! @param action_menu the ActionMenu
void action_menu_freeze(ActionMenu *action_menu);
//! Unfreeze the ActionMenu previously frozen with \ref action_menu_freeze
//! @param action_menu the ActionMenu to unfreeze
void action_menu_unfreeze(ActionMenu *action_menu);
//! Check if an ActionMenu is frozen.
bool action_menu_is_frozen(ActionMenu *action_menu);
//! Set the result window for an ActionMenu. The result window will be
//! shown when the ActionMenu closes
//! @param action_menu the ActionMenu
//! @param result_window the window to insert, pass NULL to remove the current result window
//! @note repeated call will result in only the last call to be applied, i.e. only
//! one result window is ever set
void action_menu_set_result_window(ActionMenu *action_menu, Window *result_window);
//! @internal
void action_menu_set_align(ActionMenuConfig *config, ActionMenuAlign align);
//! Close the ActionMenu, whether it is frozen or not.
//! @note this API can be used on a frozen ActionMenu once the data required to
//! build the result window has been received and the result window has been set
//! @param action_menu the ActionMenu to close
//! @param animated whether or not show a close animation
void action_menu_close(ActionMenu *action_menu, bool animated);
//! @} // end addtogroup ActionMenu
//! @} // end addtogroup UI

View File

@@ -0,0 +1,81 @@
/*
* 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 "action_menu_hierarchy.h"
#include "action_menu_layer.h"
#include "applib/ui/crumbs_layer.h"
struct ActionMenu {
Window window;
};
typedef struct AnimationContext {
Window *window;
const ActionMenuLevel *next_level;
} AnimationContext;
typedef struct {
const ActionMenuLevel *cur_level;
int num_dots;
GEdgeInsets menu_insets;
} ActionMenuViewModel;
typedef struct {
ActionMenu action_menu;
ActionMenuConfig config;
ActionMenuLayer action_menu_layer;
CrumbsLayer crumbs_layer;
ActionMenuViewModel view_model;
Animation *level_change_anim;
const ActionMenuItem *performed_item;
Window *result_window;
bool frozen;
} ActionMenuData;
// ActionMenuItem is a union of two types:
// * In the leaf case we have:
// perform_action is a valid pointer and thus is_leaf is non 0 (== true)
// action_data is a valid pointer, next_level is not used
// * In the level case we have:
// is_leaf is 0 (== false), perform_action is not a valid pointer
// next_level points to a valid ActionMenuLevel, action_data is not used
struct ActionMenuItem {
const char *label;
union {
ActionMenuPerformActionCb perform_action;
uintptr_t is_leaf;
};
union {
void *action_data;
ActionMenuLevel *next_level;
};
};
struct ActionMenuLevel {
ActionMenuLevel *parent_level;
uint16_t max_items;
uint16_t num_items;
unsigned default_selected_item;
// The separator (dotted line) will appear just above this index (an index of 0 will be ignored)
// [PG] It should be used to help differentiate item specific actions vs global actions.
// Double check with design before using this for another purpose.
unsigned separator_index;
ActionMenuLevelDisplayMode display_mode;
ActionMenuItem items[];
};

View File

@@ -0,0 +1,202 @@
/*
* 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 "action_toggle.h"
#include "applib/app_launch_button.h"
#include "applib/app_launch_reason.h"
#include "applib/applib_malloc.auto.h"
#include "applib/ui/dialogs/actionable_dialog.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/vibes.h"
#include "applib/ui/window_manager.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "system/passert.h"
typedef struct ActionToggleDialogConfig {
const char *window_name;
const char *message;
ResourceId icon;
GColor text_color;
GColor background_color;
unsigned int timeout_ms;
} ActionToggleDialogConfig;
typedef struct ActionToggleContext {
ActionToggleConfig config;
bool enabled;
bool defer_destroy;
} ActionToggleContext;
static void prv_action_toggle_dialog_unload(void *context);
static bool prv_should_prompt(const ActionToggleConfig *config);
static ActionToggleState prv_get_toggled_state_index(ActionToggleContext *ctx) {
return ctx->enabled ? ActionToggleState_Disabled : ActionToggleState_Enabled;
}
static void prv_setup_state_config(ActionToggleContext *ctx, ActionToggleDialogConfig *config,
ActionToggleDialogType dialog_type) {
if (!config->window_name) {
config->window_name = ctx->config.impl->window_name;
}
if (!config->icon) {
config->icon = ctx->config.impl->icons[dialog_type];
}
if (!config->timeout_ms) {
// Set the default prompt or result dialog timeout
config->timeout_ms = prv_should_prompt(&ctx->config) ? 4500 : 1800;
}
if (!config->text_color.argb) {
config->text_color = GColorBlack;
}
if (!config->background_color.argb) {
config->background_color = !ctx->enabled ? GColorMediumAquamarine : GColorMelon;
}
}
static void prv_setup_dialog(Dialog *dialog, const ActionToggleDialogConfig *config,
void *context) {
const char *msg = i18n_get(config->message, dialog);
dialog_set_text(dialog, msg);
i18n_free(msg, dialog);
dialog_set_icon(dialog, config->icon);
dialog_set_text_color(dialog, config->text_color);
dialog_set_background_color(dialog, config->background_color);
dialog_set_timeout(dialog, config->timeout_ms);
dialog_set_callbacks(dialog, &(DialogCallbacks) {
.unload = prv_action_toggle_dialog_unload,
}, context);
}
static void prv_vibe(const bool enabled) {
if (enabled) {
vibes_short_pulse();
} else {
vibes_double_pulse();
}
}
static WindowStack *prv_get_window_stack(void) {
return window_manager_get_window_stack(ModalPriorityNotification);
}
static void prv_push_result_dialog(ActionToggleContext *ctx) {
ActionToggleDialogConfig config = {
.message = ctx->config.impl->result_messages[prv_get_toggled_state_index(ctx)],
};
prv_setup_state_config(ctx, &config, ActionToggleDialogType_Result);
SimpleDialog *simple_dialog = simple_dialog_create(config.window_name);
prv_setup_dialog(simple_dialog_get_dialog(simple_dialog), &config, (void *)ctx);
simple_dialog_set_icon_animated(simple_dialog, !ctx->config.impl->result_icon_static);
simple_dialog_push(simple_dialog, prv_get_window_stack());
}
static bool prv_call_get_state_callback(ActionToggleContext *ctx) {
if (ctx->config.impl->callbacks.get_state) {
ctx->enabled = ctx->config.impl->callbacks.get_state(ctx->config.context);
}
return ctx->enabled;
}
static void prv_call_set_state_callback(ActionToggleContext *ctx) {
if (!ctx->config.impl->callbacks.set_state) {
return;
}
const bool next_state = !ctx->enabled;
ctx->config.impl->callbacks.set_state(next_state, ctx->config.context);
ctx->enabled = next_state;
if (ctx->config.set_exit_reason) {
app_exit_reason_set(APP_EXIT_ACTION_PERFORMED_SUCCESSFULLY);
}
prv_vibe(next_state);
}
static void prv_handle_prompt_confirm(ClickRecognizerRef recognizer, void *context) {
ActionableDialog *actionable_dialog = context;
ActionToggleContext *ctx = actionable_dialog->dialog.callback_context;
prv_push_result_dialog(ctx);
// Don't destroy the context since it is being reused for the result dialog
ctx->defer_destroy = true;
actionable_dialog_pop(actionable_dialog);
prv_call_set_state_callback(ctx);
}
static void prv_action_toggle_dialog_unload(void *context) {
ActionToggleContext *ctx = context;
if (!ctx) {
return;
}
if (ctx->defer_destroy) {
ctx->defer_destroy = false;
return;
}
applib_free(ctx);
}
static void prv_prompt_click_config_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_SELECT, prv_handle_prompt_confirm);
}
static void prv_push_prompt_dialog(ActionToggleContext *ctx) {
ActionToggleDialogConfig config = {
.message = ctx->config.impl->prompt_messages[prv_get_toggled_state_index(ctx)],
};
prv_setup_state_config(ctx, &config, ActionToggleDialogType_Prompt);
ActionableDialog *actionable_dialog = actionable_dialog_create(config.window_name);
actionable_dialog_set_action_bar_type(actionable_dialog, DialogActionBarConfirm, NULL);
actionable_dialog_set_click_config_provider(actionable_dialog, prv_prompt_click_config_provider);
prv_setup_dialog(actionable_dialog_get_dialog(actionable_dialog), &config, (void *)ctx);
actionable_dialog_push(actionable_dialog, prv_get_window_stack());
}
static bool prv_should_prompt(const ActionToggleConfig *config) {
switch (config->prompt) {
case ActionTogglePrompt_Auto:
#if PLATFORM_SPALDING
return ((pebble_task_get_current() == PebbleTask_App) &&
(app_launch_reason() == APP_LAUNCH_QUICK_LAUNCH) &&
(app_launch_button() == BUTTON_ID_BACK));
#else
return false;
#endif
case ActionTogglePrompt_NoPrompt:
return false;
case ActionTogglePrompt_Prompt:
return true;
}
return false;
}
void action_toggle_push(const ActionToggleConfig *config) {
ActionToggleContext *context = applib_zalloc(sizeof(ActionToggleContext));
PBL_ASSERTN(context);
*context = (ActionToggleContext) {
.config = *config,
};
prv_call_get_state_callback(context);
if (prv_should_prompt(config)) {
prv_push_prompt_dialog(context);
} else {
prv_push_result_dialog(context);
prv_call_set_state_callback(context);
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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"
#include "resource/resource_ids.auto.h"
typedef bool (*ActionToggleGetStateCallback)(void *context);
typedef void (*ActionToggleSetStateCallback)(bool enabled, void *context);
typedef enum ActionToggleState {
ActionToggleState_Disabled = 0,
ActionToggleState_Enabled,
ActionToggleStateCount,
} ActionToggleState;
typedef enum ActionToggleDialogType {
ActionToggleDialogType_Prompt = 0,
ActionToggleDialogType_Result,
ActionToggleDialogTypeCount,
} ActionToggleDialogType;
typedef enum ActionTogglePrompt {
ActionTogglePrompt_Auto = 0,
ActionTogglePrompt_NoPrompt,
ActionTogglePrompt_Prompt,
} ActionTogglePrompt;
typedef struct ActionToggleCallbacks {
ActionToggleGetStateCallback get_state;
ActionToggleSetStateCallback set_state;
} ActionToggleCallbacks;
typedef struct ActionToggleImpl {
ActionToggleCallbacks callbacks;
const char *window_name;
union {
struct {
const char *prompt_disable_message;
const char *prompt_enable_message;
};
const char *prompt_messages[ActionToggleStateCount];
};
union {
struct {
const char *result_disable_message;
const char *result_enable_message;
};
const char *result_messages[ActionToggleStateCount];
};
union {
struct {
ResourceId prompt_icon;
ResourceId result_icon;
};
ResourceId icons[ActionToggleDialogTypeCount];
};
bool result_icon_static;
} ActionToggleImpl;
typedef struct ActionToggleConfig {
const ActionToggleImpl *impl;
void *context;
ActionTogglePrompt prompt;
bool set_exit_reason;
} ActionToggleConfig;
//! Pushes either a prompt or result dialog depending on the prompt config option. If a prompt
//! dialog is requested, the result dialog will be pushed if the user confirms the prompt dialog
//! and the new toggled state would be set. Otherwise, a result dialog is unconditionally pushed
//! and the new toggled state is set.
//! @param config The action toggle configuration.
void action_toggle_push(const ActionToggleConfig *config);

1932
src/fw/applib/ui/animation.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,532 @@
/*
* 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 <stdint.h>
#include <stdbool.h>
#include "animation_interpolate.h"
#include "drivers/rtc.h"
#include "util/list.h"
//! @file animation.h
//! @addtogroup UI
//! @{
//! @addtogroup Animation
//! \brief Abstract framework to create arbitrary animations
//!
//! The Animation framework provides your Pebble app with an base layer to create arbitrary
//! animations. The simplest way to work with animations is to use the layer frame
//! \ref PropertyAnimation, which enables you to move a Layer around on the screen.
//! Using animation_set_implementation(), you can implement a custom animation.
//!
//! Refer to the \htmlinclude UiFramework.html (chapter "Animation") for a conceptual overview
//! of the animation framework and on how to write custom animations.
//! @{
///////////////////
// Base Animation
//
struct Animation;
typedef struct Animation Animation;
struct AnimationImplementation;
struct AnimationHandlers;
//! @internal
//! Immutable animations are animations whose properties cannot be changed, such as animations that
//! have been already scheduled. They can be typecasted to Animations for use, but keep in mind not
//! all animation methods will have an effect.
struct ImmutableAnimation;
typedef struct ImmutableAnimation ImmutableAnimation;
//! The normalized distance at the start of the animation.
#define ANIMATION_NORMALIZED_MIN 0
//! The normalized distance at the end of the animation.
#define ANIMATION_NORMALIZED_MAX 65535
//! Constant to indicate infinite play count.
//! Can be passed to \ref animation_set_play_count() to repeat indefinitely.
//! @note This can be returned by \ref animation_get_play_count().
#define ANIMATION_PLAY_COUNT_INFINITE UINT32_MAX
//! Constant to indicate "infinite" duration.
//! This can be used with \ref animation_set_duration() to indicate that the animation
//! should run indefinitely. This is useful when implementing for example a frame-by-frame
//! simulation that does not have a clear ending (e.g. a game).
//! @note Note that `distance_normalized` parameter that is passed
//! into the `.update` implementation is meaningless in when an infinite duration is used.
//! @note This can be returned by animation_get_duration (if the play count is infinite)
#define ANIMATION_DURATION_INFINITE UINT32_MAX
//! @internal
//! The default animation duration in milliseconds
#define ANIMATION_DEFAULT_DURATION_MS 250
//! @internal
//! aimed to duration of a single frame
//! 1000ms / 30 Hz
#define ANIMATION_TARGET_FRAME_INTERVAL_MS 33
//! The type used to represent how far an animation has progressed. This is passed to the
//! animation's update handler
typedef int32_t AnimationProgress;
//! Values that are used to indicate the different animation curves,
//! which determine the speed at which the animated value(s) change(s).
typedef enum {
//! Linear curve: the velocity is constant.
AnimationCurveLinear = 0,
//! Bicubic ease-in: accelerate from zero velocity
AnimationCurveEaseIn = 1,
//! Bicubic ease-in: decelerate to zero velocity
AnimationCurveEaseOut = 2,
//! Bicubic ease-in-out: accelerate from zero velocity, decelerate to zero velocity
AnimationCurveEaseInOut = 3,
AnimationCurveDefault = AnimationCurveEaseInOut,
//! Custom (user-provided) animation curve
AnimationCurveCustomFunction = 4,
//! User-provided interpolation function
AnimationCurveCustomInterpolationFunction = 5,
// Two more Reserved for forward-compatibility use.
AnimationCurve_Reserved1 = 6,
AnimationCurve_Reserved2 = 7,
} AnimationCurve;
//! Creates a new Animation on the heap and initalizes it with the default values.
//!
//! * Duration: 250ms,
//! * Curve: \ref AnimationCurveEaseInOut (ease-in-out),
//! * Delay: 0ms,
//! * Handlers: `{NULL, NULL}` (none),
//! * Context: `NULL` (none),
//! * Implementation: `NULL` (no implementation),
//! * Scheduled: no
//! @return A pointer to the animation. `NULL` if the animation could not
//! be created
Animation * animation_create(void);
//! Destroys an Animation previously created by animation_create.
//! @return true if successful, false on failure
bool animation_destroy(Animation *animation);
// Clone an existing animation. Especially useful when it will be used in 2 or more other
// sequence or spawn animations.
Animation *animation_clone(Animation *from);
//! Create a new sequence animation from a list of 2 or more other animations. The returned
//! animation owns the animations that were provided as arguments and no further write operations
//! on those handles are allowed. The variable length argument list must be terminated with a NULL
//! ptr
//! @note the maximum number of animations that can be supplied to this method is 20
//! @param animation_a the first required component animation
//! @param animation_b the second required component animation
//! @param animation_c either the third component, or NULL if only adding 2 components
//! @return The newly created sequence animation
Animation *animation_sequence_create(Animation *animation_a, Animation *animation_b,
Animation *animation_c, ...);
//! An alternate form of animation_sequence_create() that accepts an array of other animations.
//! @note the maximum number of elements allowed in animation_array is 256
//! @param animation_array an array of component animations to include
//! @param array_len the number of elements in the animation_array
//! @return The newly created sequence animation
Animation *animation_sequence_create_from_array(Animation **animation_array, uint32_t array_len);
//! @internal
//! An alternate form of animation_sequence_create() that accepts an array of other animations.
//! It also takes an animation that will be converted into the animation sequence.
//! @note the maximum number of elements allowed in animation_array is 256
//! @param parent a freshly created animation to convert into a sequence
//! @param animation_array an array of component animations to include
//! @param array_len the number of elements in the animation_array
//! @return The initialized sequence animation
Animation *animation_sequence_init_from_array(Animation *parent, Animation **animation_array,
uint32_t array_len);
//! Create a new spawn animation from a list of 2 or more other animations. The returned
//! animation owns the animations that were provided as arguments and no further write operations
//! on those handles are allowed. The variable length argument list must be terminated with a NULL
//! ptr
//! @note the maximum number of animations that can be supplied to this method is 20
//! @param animation_a the first required component animation
//! @param animation_b the second required component animation
//! @param animation_c either the third component, or NULL if only adding 2 components
//! @return The newly created spawn animation or NULL on failure
Animation *animation_spawn_create(Animation *animation_a, Animation *animation_b,
Animation *animation_c, ...);
//! An alternate form of animation_spawn_create() that accepts an array of other animations.
//! @note the maximum number of elements allowed in animation_array is 256
//! @param animation_array an array of component animations to include
//! @param array_len the number of elements in the animation_array
//! @return The newly created spawn animation or NULL on failure
Animation *animation_spawn_create_from_array(Animation **animation_array, uint32_t array_len);
//! @internal
//! Sets an animation as immutable. An immutable animation cannot have its properties changed.
//! Useful for animations that are meant to be passed publicly, but have special handlers that
//! must not be overwritten.
//! @return true if successful, false on failure
bool animation_set_immutable(Animation *animation);
//! @internal
bool animation_is_immutable(Animation *animation);
//! @internal
//! Set the auto-destroy flag for this animation. If set on, then the animation will be
//! automatically destroyed if/when the animation finishes after being scheduled or if
//! animation_unschedule() or animation_unschedule_all() are called.
//! @param animation the animation for which to set the auto_destroy setting
//! @param auto_destroy the new setting
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @return true if successful, false on failure
bool animation_set_auto_destroy(Animation *animation, bool auto_destroy);
//! Seek to a specific location in the animation. Only forward seeking is allowed. Returns true
//! if successful, false if the passed in seek location is invalid.
//! @param animation the animation for which to set the elapsed.
//! @param elapsed_ms the new elapsed time in milliseconds
//! @return true if successful, false if the requested elapsed is invalid.
bool animation_set_elapsed(Animation *animation, uint32_t elapsed_ms);
//! Get the current location in the animation.
//! @note The animation must be scheduled to get the elapsed time. If it is not schedule,
//! this method will return false.
//! @param animation The animation for which to fetch the elapsed.
//! @param[out] elapsed_ms pointer to variable that will contain the elapsed time in milliseconds
//! @return true if successful, false on failure
bool animation_get_elapsed(Animation *animation, int32_t *elapsed_ms);
//! @internal
//! Get the current progress of the animation.
//! @note The animation must be scheduled to get the progress time. If it is not scheduled,
//! this method will return false.
//! @param animation The animation for which to fetch the progress.
//! @param[out] progress_out Pointer to variable that will contain the progress.
//! @return true if successful, false on failure
bool animation_get_progress(Animation *animation, AnimationProgress *progress_out);
//! Set an animation to run in reverse (or forward)
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation the animation to operate on
//! @param reverse set to true to run in reverse, false to run forward
//! @return true if successful, false on failure
bool animation_set_reverse(Animation *animation, bool reverse);
//! Get the reverse setting of an animation
//! @param animation The animation for which to get the setting
//! @return the reverse setting
bool animation_get_reverse(Animation *animation);
//! Set an animation to play N times. The default is 1.
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation the animation to set the play count of
//! @param play_count number of times to play this animation. Set to ANIMATION_PLAY_COUNT_INFINITE
//! to make an animation repeat indefinitely.
//! @return true if successful, false on failure
bool animation_set_play_count(Animation *animation, uint32_t play_count);
//! Get the play count of an animation
//! @param animation The animation for which to get the setting
//! @return the play count
uint32_t animation_get_play_count(Animation *animation);
//! Sets the time in milliseconds that an animation takes from start to finish.
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation The animation for which to set the duration.
//! @param duration_ms The duration in milliseconds of the animation. This excludes
//! any optional delay as set using \ref animation_set_delay().
//! @return true if successful, false on failure
bool animation_set_duration(Animation *animation, uint32_t duration_ms);
//! Get the static duration of an animation from start to end (ignoring how much has already
//! played, if any).
//! @param animation The animation for which to get the duration
//! @param include_delay if true, include the delay time
//! @param include_play_count if true, incorporate the play_count
//! @return the duration, in milliseconds. This includes any optional delay a set using
//! \ref animation_set_delay.
uint32_t animation_get_duration(Animation *animation, bool include_delay, bool include_play_count);
//! Sets an optional delay for the animation.
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation The animation for which to set the delay.
//! @param delay_ms The delay in milliseconds that the animation system should
//! wait from the moment the animation is scheduled to starting the animation.
//! @return true if successful, false on failure
bool animation_set_delay(Animation *animation, uint32_t delay_ms);
//! Get the delay of an animation in milliseconds
//! @param animation The animation for which to get the setting
//! @return the delay in milliseconds
uint32_t animation_get_delay(Animation *animation);
//! Sets the animation curve for the animation.
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation The animation for which to set the curve.
//! @param curve The type of curve.
//! @see AnimationCurve
//! @return true if successful, false on failure
bool animation_set_curve(Animation *animation, AnimationCurve curve);
//! Gets the animation curve for the animation.
//! @param animation The animation for which to get the curve.
//! @return The type of curve.
AnimationCurve animation_get_curve(Animation *animation);
//! The function pointer type of a custom animation curve.
//! @param linear_distance The linear normalized animation distance to be curved.
//! @see animation_set_custom_curve
typedef AnimationProgress (*AnimationCurveFunction)(AnimationProgress linear_distance);
//! Sets a custom animation curve function.
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation The animation for which to set the curve.
//! @param curve_function The custom animation curve function.
//! @see AnimationCurveFunction
//! @return true if successful, false on failure
bool animation_set_custom_curve(Animation *animation, AnimationCurveFunction curve_function);
//! Gets the custom animation curve function for the animation.
//! @param animation The animation for which to get the curve.
//! @return The custom animation curve function for the given animation. NULL if not set.
AnimationCurveFunction animation_get_custom_curve(Animation *animation);
//! @internal
//! Sets the custom interpolation function for the animation to override the underlying behavior
//! of \ref interpolate_int64 and related functions. This can be used to implement spatial easing.
//! Animation curve and interpolation function are mutually exclusive.
//! @param animation The animation for which to set the interpolation function.
//! @param interpolate_function The custom interpolation function to use.
bool animation_set_custom_interpolation(Animation *animation_h,
InterpolateInt64Function interpolate_function);
//! @internal
//! Get the custom interpolation function for the animation.
//! @param animation The animation for which to get the interpolation function.
//! @return The custom interpolation function for the given animation. NULL if not used.
InterpolateInt64Function animation_get_custom_interpolation(Animation *animation);
//! The function pointer type of the handler that will be called when an animation is started,
//! just before updating the first frame of the animation.
//! @param animation The animation that was started.
//! @param context The pointer to custom, application specific data, as set using
//! \ref animation_set_handlers()
//! @note This is called after any optional delay as set by \ref animation_set_delay() has expired.
//! @see animation_set_handlers
typedef void (*AnimationStartedHandler)(Animation *animation, void *context);
//! The function pointer type of the handler that will be called when the animation is stopped.
//! @param animation The animation that was stopped.
//! @param finished True if the animation was stopped because it was finished normally,
//! or False if the animation was stopped prematurely, because it was unscheduled before finishing.
//! @param context The pointer to custom, application specific data, as set using
//! \ref animation_set_handlers()
//! @see animation_set_handlers
//! \note
//! This animation (i.e.: the `animation` parameter) may be destroyed here.
//! It is not recommended to unschedule or destroy a **different** Animation within this
//! Animation's `stopped` handler.
typedef void (*AnimationStoppedHandler)(Animation *animation, bool finished, void *context);
//! The handlers that will get called when an animation starts and stops.
//! See documentation with the function pointer types for more information.
//! @see animation_set_handlers
typedef struct AnimationHandlers {
//! The handler that will be called when an animation is started.
AnimationStartedHandler started;
//! The handler that will be called when an animation is stopped.
AnimationStoppedHandler stopped;
} AnimationHandlers;
//! Sets the callbacks for the animation.
//! Often an application needs to run code at the start or at the end of an animation.
//! Using this function is possible to register callback functions with an animation,
//! that will get called at the start and end of the animation.
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation The animation for which to set up the callbacks.
//! @param callbacks The callbacks.
//! @param context A pointer to application specific data, that will be passed as an argument by
//! the animation subsystem when a callback is called.
//! @return true if successful, false on failure
bool animation_set_handlers(Animation *animation, AnimationHandlers callbacks, void *context);
//! Gets the callbacks for the animation.
//! @param animation The animation for which to set up the callbacks.
//! @return the callbacks in use by this animation
AnimationHandlers animation_get_handlers(Animation *animation_h);
//! Gets the application-specific callback context of the animation.
//! This `void` pointer is passed as an argument when the animation system calls AnimationHandlers
//! callbacks. The context pointer can be set to point to any application specific data using
//! \ref animation_set_handlers().
//! @param animation The animation.
//! @see animation_set_handlers
void *animation_get_context(Animation *animation);
//! Schedules the animation. Call this once after configuring an animation to get it to
//! start running.
//!
//! If the animation's implementation has a `.setup` callback it will get called before
//! this function returns.
//!
//! @note If the animation was already scheduled,
//! it will first unschedule it and then re-schedule it again.
//! Note that in that case, the animation's `.stopped` handler, the implementation's
//! `.teardown` and `.setup` will get called, due to the unscheduling and scheduling.
//! @param animation The animation to schedule.
//! @see \ref animation_unschedule()
//! @return true if successful, false on failure
bool animation_schedule(Animation *animation);
//! Unschedules the animation, which in effect stops the animation.
//! @param animation The animation to unschedule.
//! @note If the animation was not yet finished, unscheduling it will
//! cause its `.stopped` handler to get called, with the "finished" argument set to false.
//! @note If the animation is not scheduled or NULL, calling this routine is
//! effectively a no-op
//! @see \ref animation_schedule()
//! @return true if successful, false on failure
bool animation_unschedule(Animation *animation);
//! Unschedules all animations of the application.
//! @see animation_unschedule
void animation_unschedule_all(void);
//! @return True if the animation was scheduled, or false if it was not.
//! @note An animation will be scheduled when it is running and not finished yet.
//! An animation that has finished is automatically unscheduled.
//! For convenience, passing in a NULL animation argument will simply return false
//! @param animation The animation for which to get its scheduled state.
//! @see animation_schedule
//! @see animation_unschedule
bool animation_is_scheduled(Animation *animation);
///////////////////////////////////////
// Implementing custom animation types
//! Pointer to function that (optionally) prepares the animation for running.
//! This callback is called when the animation is added to the scheduler.
//! @param animation The animation that needs to be set up.
//! @see animation_schedule
//! @see AnimationTeardownImplementation
typedef void (*AnimationSetupImplementation)(Animation *animation);
//! Pointer to function that updates the animation according to the given normalized progress.
//! This callback will be called repeatedly by the animation scheduler whenever the animation needs
//! to be updated.
//! @param animation The animation that needs to update; gets passed in by the animation framework.
//! @param progress The current normalized progress; gets passed in by the animation
//! framework for each animation frame.
//! The value \ref ANIMATION_NORMALIZED_MIN represents the start and \ref ANIMATION_NORMALIZED_MAX
//! represents the end. Values outside this range (generated by a custom curve function) can be used
//! to implement features like a bounce back effect, where the progress exceeds the desired final
//! value before returning to complete the animation.
//! When using a system provided curve function, each frame during the animation will have a
//! progress value between \ref ANIMATION_NORMALIZED_MIN and \ref ANIMATION_NORMALIZED_MAX based on
//! the animation duration and the \ref AnimationCurve.
//! For example, say an animation was scheduled at t = 1.0s, has a delay of 1.0s, a duration of 2.0s
//! and a curve of AnimationCurveLinear. Then the .update callback will get called on t = 2.0s with
//! distance_normalized = \ref ANIMATION_NORMALIZED_MIN. For each frame thereafter until t = 4.0s,
//! the update callback will get called where distance_normalized is (\ref ANIMATION_NORMALIZED_MIN
//! + (((\ref ANIMATION_NORMALIZED_MAX - \ref ANIMATION_NORMALIZED_MIN) * t) / duration)).
//! Other system animation curve functions will result in a non-linear relation between
//! distance_normalized and time.
//! @internal
//! @see animation_timing.h
typedef void (*AnimationUpdateImplementation)(Animation *animation,
const AnimationProgress progress);
//! Pointer to function that (optionally) cleans up the animation.
//! This callback is called when the animation is removed from the scheduler.
//! In case the `.setup` implementation
//! allocated any memory, this is a good place to release that memory again.
//! @param animation The animation that needs to be teared down.
//! @see animation_unschedule
//! @see AnimationSetupImplementation
typedef void (*AnimationTeardownImplementation)(Animation *animation);
//! The 3 callbacks that implement a custom animation.
//! Only the `.update` callback is mandatory, `.setup` and `.teardown` are optional.
//! See the documentation with the function pointer typedefs for more information.
//!
//! @note The `.setup` callback is called immediately after scheduling the animation,
//! regardless if there is a delay set for that animation using \ref animation_set_delay().
//!
//! The diagram below illustrates the order in which callbacks can be expected to get called
//! over the life cycle of an animation. It also illustrates where the implementation of
//! different animation callbacks are intended to be “living”.
//! ![](animations.png)
//!
//! @see AnimationSetupImplementation
//! @see AnimationUpdateImplementation
//! @see AnimationTeardownImplementation
typedef struct AnimationImplementation {
//! Called by the animation system when an animation is scheduled, to prepare it for running.
//! This callback is optional and can be left `NULL` when not needed.
AnimationSetupImplementation setup;
//! Called by the animation system when the animation needs to calculate the next animation frame.
//! This callback is mandatory and should not be left `NULL`.
AnimationUpdateImplementation update;
//! Called by the animation system when an animation is unscheduled, to clean up after it has run.
//! This callback is optional and can be left `NULL` when not needed.
AnimationTeardownImplementation teardown;
} AnimationImplementation;
//! Gets the implementation of the custom animation.
//! @param animation The animation for which to get the implementation.
//! @see AnimationImplementation
//! @return NULL if animation implementation has not been setup.
const AnimationImplementation* animation_get_implementation(Animation *animation);
//! Sets the implementation of the custom animation.
//! When implementing custom animations, use this function to specify what functions need to be
//! called to for the setup, frame update and teardown of the animation.
//! @note Trying to set an attribute when an animation is immutable will return false (failure). An
//! animation is immutable once it has been added to a sequence or spawn animation or has been
//! scheduled.
//! @param animation The animation for which to set the implementation.
//! @param implementation The structure with function pointers to the implementation of the setup,
//! update and teardown functions.
//! @see AnimationImplementation
//! @return true if successful, false on failure
bool animation_set_implementation(Animation *animation,
const AnimationImplementation *implementation);
//! @} // group Animation
//! @} // group UI

View File

@@ -0,0 +1,179 @@
/*
* 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 "animation.h"
#include "animation_interpolate.h"
#include "animation_private.h"
#include "applib/graphics/gtypes.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/size.h"
int64_t interpolate_int64_linear(int32_t normalized, int64_t from, int64_t to) {
return from + ((normalized * (to - from)) / ANIMATION_NORMALIZED_MAX);
}
int64_t interpolate_int64(int32_t normalized, int64_t from, int64_t to) {
InterpolateInt64Function interpolate =
animation_private_current_interpolate_override() ?: interpolate_int64_linear;
return interpolate(normalized, from, to);
}
int16_t interpolate_int16(int32_t normalized, int16_t from, int16_t to) {
const int64_t interpolated = interpolate_int64(normalized, from, to);
return (int16_t)CLIP(interpolated, INT16_MIN, INT16_MAX);
}
uint32_t interpolate_uint32(int32_t normalized, uint32_t from, uint32_t to) {
const int64_t interpolated = interpolate_int64(normalized, from, to);
return (uint32_t)CLIP(interpolated, 0, UINT32_MAX);
}
Fixed_S32_16 interpolate_fixed32(int32_t normalized, Fixed_S32_16 from, Fixed_S32_16 to) {
const int64_t interpolated = interpolate_int64(normalized, from.raw_value, to.raw_value);
const int32_t raw_value =
(int32_t) CLIP(interpolated, INT32_MIN, INT32_MAX);
return Fixed_S32_16(raw_value);
}
GSize interpolate_gsize(int32_t normalized, GSize from, GSize to) {
return (GSize) {
.w = interpolate_int16(normalized, from.w, to.w),
.h = interpolate_int16(normalized, from.w, to.w),
};
}
GPoint interpolate_gpoint(int32_t normalized, GPoint from, GPoint to) {
return (GPoint) {
.x = interpolate_int16(normalized, from.x, to.x),
.y = interpolate_int16(normalized, from.y, to.y),
};
}
int16_t scale_int16(int16_t value, int16_t from, int16_t to) {
return (int16_t) ((int32_t) value * to / from);
}
int32_t scale_int32(int32_t value, int32_t from, int32_t to) {
return (int32_t) ((int64_t) value * to / from);
}
// -------------------------------------------------------
// these values are directly taken from "easing red line 001.mov"
// _in will be added to first value (easing in, anticipation)
// _out will be added to second value (overshoot, swing-back)
static const int32_t s_delta_moook_in[] = {0, 1, 20};
static const int32_t s_delta_moook_out[] = {INTERPOLATE_MOOOK_BOUNCE_BACK, 2, 1, 0};
// TODO: export these as interpolation functions as well
uint32_t interpolate_moook_in_duration() {
return ARRAY_LENGTH(s_delta_moook_in) * ANIMATION_TARGET_FRAME_INTERVAL_MS;
}
uint32_t interpolate_moook_out_duration() {
return ARRAY_LENGTH(s_delta_moook_out) * ANIMATION_TARGET_FRAME_INTERVAL_MS;
}
uint32_t interpolate_moook_duration() {
return interpolate_moook_in_duration() + interpolate_moook_out_duration();
}
uint32_t interpolate_moook_soft_duration(int32_t num_frames_mid) {
return interpolate_moook_duration() + num_frames_mid * ANIMATION_TARGET_FRAME_INTERVAL_MS;
}
uint32_t interpolate_moook_custom_duration(const MoookConfig *config) {
PBL_ASSERTN(config);
return ((config->num_frames_in + config->num_frames_mid + config->num_frames_out) *
ANIMATION_TARGET_FRAME_INTERVAL_MS);
}
static int64_t prv_interpolate_moook(
int32_t normalized, int64_t from, int64_t to, const int32_t *frames_in, int32_t num_frames_in,
const int32_t *frames_out, int32_t num_frames_out, int32_t num_frames_mid, bool bounce_back) {
const int32_t direction = ((from == to) ? 0 : ((from < to) ? 1 : -1));
if (direction == 0) {
return from;
}
const int32_t direction_out = direction * (bounce_back ? 1 : -1);
const size_t num_frames_total = num_frames_in + num_frames_mid + num_frames_out;
int32_t frame_idx =
((normalized * num_frames_total + (ANIMATION_NORMALIZED_MAX / (2 * num_frames_total))) /
ANIMATION_NORMALIZED_MAX);
frame_idx = CLIP(frame_idx, 0, (int)num_frames_total - 1);
if (normalized == ANIMATION_NORMALIZED_MAX) {
return to;
} else if (frame_idx < 0) {
return from;
} else if (frame_idx < num_frames_in) {
return from + (frames_in ? (direction * frames_in[frame_idx]) : 0);
} else if ((frame_idx < (num_frames_in + num_frames_mid)) && (num_frames_mid > 0)) {
const int64_t shifted_normalized = normalized -
(((int64_t) num_frames_in * ANIMATION_NORMALIZED_MAX) / num_frames_total);
const int32_t mid_normalized = ((int64_t) num_frames_total * shifted_normalized) /
num_frames_mid;
return interpolate_int64_linear(mid_normalized,
from + (direction * frames_in[num_frames_in - 1]),
to + (direction_out * frames_out[0]));
} else {
return to + (frames_out ? (direction_out *
frames_out[frame_idx - (num_frames_in + num_frames_mid)]) : 0);
}
}
int64_t interpolate_moook_in(int32_t normalized, int64_t from, int64_t to, int32_t num_frames_to) {
return prv_interpolate_moook(normalized, from, to, s_delta_moook_in,
ARRAY_LENGTH(s_delta_moook_in), NULL, num_frames_to, 0, true);
}
int64_t interpolate_moook_in_only(int32_t normalized, int64_t from, int64_t to) {
return prv_interpolate_moook(normalized, from, to, s_delta_moook_in,
ARRAY_LENGTH(s_delta_moook_in), NULL, 0, 0, true);
}
int64_t interpolate_moook_out(int32_t normalized, int64_t from, int64_t to,
int32_t num_frames_from, bool bounce_back) {
return prv_interpolate_moook(normalized, from, to, NULL, num_frames_from, s_delta_moook_out,
ARRAY_LENGTH(s_delta_moook_out), 0, bounce_back);
}
int64_t interpolate_moook(int32_t normalized, int64_t from, int64_t to) {
return prv_interpolate_moook(normalized, from, to,
s_delta_moook_in, ARRAY_LENGTH(s_delta_moook_in),
s_delta_moook_out, ARRAY_LENGTH(s_delta_moook_out), 0, true);
}
int64_t interpolate_moook_soft(int32_t normalized, int64_t from, int64_t to,
int32_t num_frames_mid) {
return prv_interpolate_moook(normalized, from, to,
s_delta_moook_in, ARRAY_LENGTH(s_delta_moook_in),
s_delta_moook_out, ARRAY_LENGTH(s_delta_moook_out),
num_frames_mid, true);
}
int64_t interpolate_moook_custom(int32_t normalized, int64_t from, int64_t to,
const MoookConfig *config) {
PBL_ASSERTN(config);
return prv_interpolate_moook(normalized, from, to,
config->frames_in, config->num_frames_in,
config->frames_out, config->num_frames_out,
config->num_frames_mid, !config->no_bounce_back);
}

View File

@@ -0,0 +1,142 @@
/*
* 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"
#include "util/math_fixed.h"
#include <stdint.h>
//! @file interpolate.h
//! Routines for interpolating between values and points. Useful for animations.
typedef struct MoookConfig {
const int32_t *frames_in; //!< In frame lookup table applied as delta * direction to the `from`
size_t num_frames_in; //!< Number of in frames in the frame lookup table
const int32_t *frames_out; //!< Out frame lookup table applied as delta * direction to the `to`
size_t num_frames_out; //!< Number of out frames in the frame lookup table
size_t num_frames_mid; //!< Number of soft mid frames to insert
bool no_bounce_back; //!< Whether the direction should be reversed for out frames.
} MoookConfig;
#define INTERPOLATE_MOOOK_BOUNCE_BACK 4
//! Performs an interpolation between from and to.
//! Progress represents 0..1 as fixed point between 0..ANIMATION_NORMALIZED_MAX,
//! but can have values <0 and >1 as well to support overshooting.
//! Likewise, it can return values outside of the range from..to.
typedef int64_t (*InterpolateInt64Function)(int32_t progress, int64_t from, int64_t to);
//! @internal
//! Truly linear interpolation between two int64_t.
//! Does not consider any overriding of interpolation for spatial easing.
int64_t interpolate_int64_linear(int32_t normalized, int64_t from, int64_t to);
//! Interpolation between two int64_t.
//! In most cases, this is a linear interpolation but the behavior can vary if this function
//! is called from within an animation's update handdler that uses
//! AnimationCurveCustomInterpolationFunction. This allows clients to transparently implement
//! effects such as spatial easing. See \ref animation_set_custom_interpolation().
int64_t interpolate_int64(int32_t normalized, int64_t from, int64_t to);
//! Interpolation between two int16_t.
//! See \ref interpolate_int64() for special cases.
int16_t interpolate_int16(int32_t normalized, int16_t from, int16_t to);
//! Interpolation between two uint32_t.
//! See \ref interpolate_int64() for special cases.
uint32_t interpolate_uint32(int32_t normalized, uint32_t from, uint32_t to);
//! Interpolation between two Fixed_S32_16.
//! See \ref interpolate_int64() for special cases.
Fixed_S32_16 interpolate_fixed32(int32_t normalized, Fixed_S32_16 from, Fixed_S32_16 to);
//! Interpolation between two GSize.
//! See \ref interpolate_int64() for special cases.
GSize interpolate_gsize(int32_t normalized, GSize from, GSize to);
//! Interpolation between two GPoint.
//! See \ref interpolate_int64() for special cases.
GPoint interpolate_gpoint(int32_t normalized, GPoint from, GPoint to);
//! linear scale a int16_t between two int16_t lengths
int16_t scale_int16(int16_t value, int16_t from, int16_t to);
//! linear scale a int32_t between two int32_t lengths
int32_t scale_int32(int32_t value, int32_t from, int32_t to);
uint32_t interpolate_moook_in_duration();
uint32_t interpolate_moook_out_duration();
uint32_t interpolate_moook_duration();
//! @param num_frames_mid Number of additional linearly interpolated middle frames
uint32_t interpolate_moook_soft_duration(int32_t num_frames_mid);
//! Calculates the duration of a given custom Moook curve configuration.
//! @param config Custom Moook curve configuration.
//! @return Duration of the custom Moook curve in milliseconds.
uint32_t interpolate_moook_custom_duration(const MoookConfig *config);
//! Moook ease in curve. Useful for composing larger interpolation curves.
//! @param normalized Time of the point in the ease curve
//! @param from Starting point in space of the animation
//! @param to Ending point in space of the animation
//! @param num_frames_to Remaining number of frames in the animation that do not consist of the
//! Moook ease in curve.
int64_t interpolate_moook_in(int32_t normalized, int64_t from, int64_t to,
int32_t num_frames_to);
//! Only the Moook ease in curve. Used for animations that only consist of the ease in.
//! @param normalized Time of the point in the ease curve
//! @param from Starting point in space of the animation
//! @param to Ending point in space of the animation
int64_t interpolate_moook_in_only(int32_t normalized, int64_t from, int64_t to);
//! Moook ease out curve. Useful for composing larger interpolation curves.
//! @param normalized Time of the point in the ease curve
//! @param from Starting point in space of the animation
//! @param to Ending point in space of the animation
//! @param bounce_back Whether to lead up to the end point from the opposite direction if we were
//! to lead up from the start poing, which a normal Moook curve would do.
int64_t interpolate_moook_out(int32_t normalized, int64_t from, int64_t to,
int32_t num_frames_from, bool bounce_back);
//! Moook curve. This is a ease in and ease out curve with a hard cut between the two easings.
//! When using this curve, the duration must be set to \ref interpolate_moook_duration()
//! @param normalized Time of the point in the ease curve
//! @param from Starting point in space of the animation
//! @param to Ending point in space of the animation
int64_t interpolate_moook(int32_t normalized, int64_t from, int64_t to);
//! Moook curve with additional linearly interpolated frames between the ease in and ease out.
//! When using this curve, the duration must be set to \ref interpolate_moook_soft_duration()
//! with the same number of frames in the parameter.
//! @param normalized Time of the point in the ease curve
//! @param from Starting point in space of the animation
//! @param to Ending point in space of the animation
//! @param num_frames_mid Number of additional linearly interpolated middle frames
int64_t interpolate_moook_soft(int32_t normalized, int64_t from, int64_t to,
int32_t num_frames_mid);
//! Custom Moook curve which supports arbitrary delta frame tables.
//! @param normalized Time of the point in the ease curve.
//! @param from Starting point in space of the animation.
//! @param to Ending point in space of the animation.
//! @param config Custom Moook curve configuration.
//! @return Moook interpolated spacial value.
int64_t interpolate_moook_custom(int32_t normalized, int64_t from, int64_t to,
const MoookConfig *config);

View File

@@ -0,0 +1,168 @@
/*
* 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 "animation.h"
#define ANIMATION_LOG_DEBUG(fmt, args...) \
PBL_LOG_D(LOG_DOMAIN_ANIMATION, LOG_LEVEL_DEBUG, fmt, ## args)
#define ANIMATION_MAX_CHILDREN 256
#define ANIMATION_PLAY_COUNT_INFINITE_STORED ((uint16_t)~0)
#define ANIMATION_MAX_CREATE_VARGS 20
typedef enum {
AnimationTypePrimitive,
AnimationTypeSequence,
AnimationTypeSpawn
} AnimationType;
//! The data structure of an animation.
typedef struct AnimationPrivate {
//! At any one time, an animation is either in the scheduled list (scheduled_head of
//! AnimationState) or the unscheduled list (unscheduled_head of AnimationState)
ListNode list_node;
//! integer handle assigned to this animation. This integer gets typecast to an
//! (Animation *) to be used from the client's perspective.
Animation *handle;
const AnimationImplementation *implementation;
AnimationHandlers handlers;
void *context;
//! Absolute time when the animation got scheduled, in ms since system start.
uint32_t abs_start_time_ms;
uint32_t delay_ms;
uint32_t duration_ms;
uint16_t play_count; // Desired play count
uint16_t times_played; // incremented each time we play it
AnimationCurve curve:3;
bool is_completed:1;
bool auto_destroy:1;
bool being_destroyed:1;
AnimationType type:2;
bool is_property_animation:1; // used for cloning
bool reverse:1;
bool started:1; // set true after we call the started handler
bool calling_end_handlers:1;
bool defer_delete:1;
bool did_setup:1;
bool immutable:1;
union {
AnimationCurveFunction custom_curve_function;
InterpolateInt64Function custom_interpolation_function;
void *custom_function;
};
// If this animation is part of a complex animation, this is the parent
struct AnimationPrivate *parent;
uint8_t child_idx; // for children of complex animations, this is the child's idx
#ifdef UNITTEST
//! Points to the next sibling if this is a child in a complex animation and one exists
struct AnimationPrivate *sibling;
//! Points to the first child if this is a complex animation
struct AnimationPrivate *first_child;
// gets set to true when schedule() is called, false when unschedule() is called for unit tests
bool scheduled;
#endif
} AnimationPrivate;
//! In case the 3rd party app was built for 2.0, we can't use more memory in the app state than
//! the 2.0 legacy animation does. So, we put additional context required for 3.0 into this
//! dynamically allocated block
typedef struct {
//! Each created animation gets a unique integer handle ID
uint32_t next_handle;
//! Reference to the animation that we are calling the .update handler for
//! Will be reset to NULL once the .update handler finishes
AnimationPrivate *current_animation;
//! The delay the animation scheduler uses between finishing a frame and starting a new one.
//! Derived from actual rendering/calculation times, using a PID-like control algorithm.
uint32_t last_delay_ms;
uint32_t last_frame_time_ms; //! Absolute time of the moment the last animation frame started.
//! The next Animation to be iterated, NULL if at end of iteration or not iterating.
//! This allows arbitrarily unscheduling any animation at any time.
ListNode *iter_next;
} AnimationAuxState;
//! The currently running app task and the KernelMain task each have their own instance of
//! AnimationState which is stored as part of the app_state structure. In order to support
//! legacy 2.0 applications, this structure can be no larger than the AnimationLegacy2State
//! structure.
#define ANIMATION_STATE_3_X_SIGNATURE ((uint32_t)(~0))
typedef struct {
//! Signature used to distinguish these globals from the legacy 2.0 globals. The legacy 2.0
//! globals start with a ListNode pointer. We put a value here (ANIMATION_STATE_3_X_SIGNATURE)
//! that is guaranteed to be unique from a pointer.
uint32_t signature;
//! Pointer to dynamically allocated auxiliary information
AnimationAuxState *aux;
//! All unscheduled Animation_t's for this app appear in this list
ListNode *unscheduled_head;
//! All scheduled Animation_t's for this app appear in this list
ListNode *scheduled_head;
} AnimationState;
//! Init animation state. Should be called once when task starts up
void animation_private_state_init(AnimationState *state);
//! Deinit animation state. Should be called once when task exits
void animation_private_state_deinit(AnimationState *state);
//! Init an animation structure, register it with the current task, and assign it a handle
Animation *animation_private_animation_init(AnimationPrivate *animation);
//! Return the animation object pointer for the given handle
AnimationPrivate *animation_private_animation_find(Animation *handle);
//! Timer callback triggered by the animation_service system timer
void animation_private_timer_callback(void *state);
//! Return true if the legacy2 animation manager is instantiated. State can be NULL if not already
//! known.
bool animation_private_using_legacy_2(AnimationState *state);
//! Returns the interpolation function that overrides the built-in linear interpolation,
//! or NULL if one was not set. Used to implement spatial easing.
InterpolateInt64Function animation_private_current_interpolate_override(void);
//! Returns the progress of the provided animation
AnimationProgress animation_private_get_animation_progress(const AnimationPrivate *animation);
//! Does easing and book-keeping when calling animation.implementation.update()
void animation_private_update(AnimationState *state, AnimationPrivate *animation,
AnimationProgress progress_raw);
//! Prevents animations from running through the animation service.
//! Any currently executing animation is not guaranteed to restart at the same frame upon resume.
//! @note this is used by test automation
void animation_private_pause(void);
//! see \ref animation_private_pause
void animation_private_resume(void);

View File

@@ -0,0 +1,161 @@
/*
* 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 "animation_timing.h"
#include "animation_interpolate.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math_fixed.h"
#include "util/size.h"
//! @file animation_timing.c
static const uint16_t s_ease_in_table[] = {
0, 64, 256, 576,
1024, 1600, 2304, 3136,
4096, 5184, 6400, 7744,
9216, 10816, 12544, 14400,
16384, 18496, 20736, 23104,
25600, 28224, 30976, 33856,
36864, 40000, 43264, 46656,
50176, 53824, 57600, 61504,
65535
};
static const uint16_t s_ease_out_table[] = {
0, 4031, 7935, 11711,
15359, 18879, 22271, 25535,
28671, 31679, 34559, 37311,
39935, 42431, 44799, 47039,
49151, 51135, 52991, 54719,
56319, 57791, 59135, 60351,
61439, 62399, 63231, 63935,
64511, 64959, 65279, 65471,
65535
};
static const uint16_t s_ease_in_out_table[] = {
0, 128, 512, 1152,
2048, 3200, 4608, 6272,
8192, 10368, 12800, 15488,
18432, 21632, 25088, 28800,
32770, 36737, 40449, 43905,
47105, 50049, 52737, 55169,
57345, 59265, 60929, 62337,
63488, 64384, 65024, 65408,
65535
};
typedef struct {
const size_t num_entries;
const uint16_t *table;
} EasingTable;
static const EasingTable s_easing_tables[] = {
[AnimationCurveEaseIn] = { ARRAY_LENGTH(s_ease_in_table), s_ease_in_table },
[AnimationCurveEaseOut] = { ARRAY_LENGTH(s_ease_out_table), s_ease_out_table },
[AnimationCurveEaseInOut] = { ARRAY_LENGTH(s_ease_in_out_table), s_ease_in_out_table },
};
int32_t animation_timing_segmented(int32_t time_normalized, int32_t index,
uint32_t num_segments, Fixed_S32_16 duration_fraction) {
PBL_ASSERTN(num_segments > 0 && duration_fraction.raw_value > 0);
if (index < 0) {
return ANIMATION_NORMALIZED_MAX;
}
if ((uint32_t)index >= num_segments) {
return 0;
}
const int32_t duration_per_item = ((int64_t) ANIMATION_NORMALIZED_MAX
* duration_fraction.raw_value) / FIXED_S32_16_ONE.raw_value;
const int32_t delay_per_item = (ANIMATION_NORMALIZED_MAX - duration_per_item) / num_segments;
const int32_t normalized_offset = time_normalized - index * delay_per_item;
if (normalized_offset < 0) {
return 0;
}
const int32_t relative_progress = ((int64_t) normalized_offset
* FIXED_S32_16_ONE.raw_value) / duration_fraction.raw_value;
if (relative_progress > ANIMATION_NORMALIZED_MAX) {
return ANIMATION_NORMALIZED_MAX;
}
return relative_progress;
}
typedef int64_t (*ArrayAccessorInt64)(const void *array, size_t index);
static int64_t prv_uint16_getter(const void *array, size_t idx) {
return ((uint16_t*)array)[idx];
}
static int64_t prv_int32_getter(const void *array, size_t idx) {
return ((int32_t*)array)[idx];
}
AnimationProgress prv_animation_timing_interpolate(AnimationProgress progress, const void *array,
ArrayAccessorInt64 getter, size_t num_entries) {
PBL_ASSERTN(num_entries > 0);
const size_t max_entry = num_entries - 1;
if (progress <= ANIMATION_NORMALIZED_MIN) {
return (AnimationProgress) getter(array, 0);
}
if (progress >= ANIMATION_NORMALIZED_MAX) {
return (AnimationProgress) getter(array, max_entry);
}
// Linear interpolate from the easing table.
const size_t stride = ANIMATION_NORMALIZED_MAX / max_entry;
const size_t index = (progress * max_entry) / ANIMATION_NORMALIZED_MAX;
const int64_t from = getter(array, index);
const int64_t delta = getter(array, index + 1) - from;
return (AnimationProgress) (from + (delta * (progress - index * stride)) / stride);
}
AnimationProgress animation_timing_interpolate(
AnimationProgress time_normalized, const uint16_t *table, size_t num_entries) {
return prv_animation_timing_interpolate(time_normalized, table, prv_uint16_getter, num_entries);
}
AnimationProgress animation_timing_interpolate32(
AnimationProgress time_normalized, const int32_t *table, size_t num_entries) {
return prv_animation_timing_interpolate(time_normalized, table, prv_int32_getter, num_entries);
}
AnimationProgress animation_timing_curve(AnimationProgress time_normalized,
AnimationCurve curve) {
switch (curve) {
case AnimationCurveEaseIn:
case AnimationCurveEaseOut:
case AnimationCurveEaseInOut: {
const EasingTable *easing = &s_easing_tables[curve];
return animation_timing_interpolate(time_normalized, easing->table, easing->num_entries);
}
case AnimationCurveLinear:
default:
return time_normalized;
}
}
AnimationProgress animation_timing_scaled(AnimationProgress time_normalized,
AnimationProgress interval_start,
AnimationProgress interval_end) {
int64_t result = time_normalized - interval_start;
result = (result * ANIMATION_NORMALIZED_MAX) / (interval_end - interval_start);
// no overflow worries here, result and ANIMATION_NORMALIZED_MAX are both <= 2^16.
return (AnimationProgress)result;
}

View File

@@ -0,0 +1,62 @@
/*
* 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/ui/animation.h"
#include "util/math.h"
#include "util/math_fixed.h"
//! @file animation_timing.h
//! Converts normalized time to a segmented-delayed fractional duration. The duration is computed
//! by multiplying with duration_fraction which is less than 1. The delay segment is calculated by
//! taking the non-animating duration given by the complete normalized duration minus the
//! fractional duration. The non-animating duration is then divided by the number of segments
//! specified to obtain the amount of time to delay an animation item for each index. The zeroth
//! index has no delay, and each subsequent item receives a multiple of delay segments to wait.
//! @param time_normalized the normalized time between 0 and ANIMATION_NORMALIZED_MAX inclusive
//! @param index determines how many delay segments to wait until timing starts
//! @param num_segments the number of segments to partition non-animating duration by
//! @param duration_fraction a Fixed_S32_16 fraction between 0 and 1 of the animation duration time
//! @returns the segmented time
AnimationProgress animation_timing_segmented(AnimationProgress time_normalized, int32_t index,
uint32_t num_segments, Fixed_S32_16 duration_fraction);
//! Converts normalized time to a timing based on a curve defined by a table.
//! @param time_normalized the normalized time between 0 and ANIMATION_NORMALIZED_MAX inclusive
//! @param table a curve with entries eased from 0 to ANIMATION_NORMALIZED_MAX
//! @param num_entries number of entries in the table
AnimationProgress animation_timing_interpolate(
AnimationProgress time_normalized, const uint16_t *table, size_t num_entries);
AnimationProgress animation_timing_interpolate32(
AnimationProgress time_normalized, const int32_t *table, size_t num_entries);
//! Converts normalized time to a timing based on a specified curve
//! @param time_normalized the normalized time between 0 and ANIMATION_NORMALIZED_MAX inclusive
//! @param curve a system animation curve to convert to. @see AnimationCurve
//! @returns the curved time
AnimationProgress animation_timing_curve(AnimationProgress time_normalized, AnimationCurve curve);
static inline AnimationProgress animation_timing_clip(AnimationProgress time_normalized) {
return CLIP(time_normalized, 0, ANIMATION_NORMALIZED_MAX);
}
//! Rescales a given time as with respect to a given interval
AnimationProgress animation_timing_scaled(AnimationProgress time_normalized,
AnimationProgress interval_start,
AnimationProgress interval_end);

View File

@@ -0,0 +1,51 @@
/*
* 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 "app_window_click_glue.h"
#include "process_state/app_state/app_state.h"
#include "applib/ui/click_internal.h"
#include "applib/ui/window.h"
#include "applib/ui/window_stack_private.h"
#include "system/passert.h"
#include "util/size.h"
////////////////////////////////////////////////
// App + Click Recognizer + Window : Glue code
//
// [MT] This is a bit ugly, because I decided to to save memory and have all windows in an app share an array of
// click recognizers (which lives in AppContext) instead of each window having its own.
// See the comment near AppContext.click_recognizer.
void app_click_config_setup_with_window(ClickManager *click_manager, struct Window *window) {
void *context = window->click_config_context;
if (!context) {
// Default context is the window.
context = window;
}
click_manager_clear(click_manager);
for (unsigned int button_id = 0; button_id < NUM_BUTTONS; ++button_id) {
// For convenience, assign the context:
click_manager->recognizers[button_id].config.context = context;
}
if (window->click_config_provider) {
window_call_click_config_provider(window, context);
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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 "process_management/app_manager.h"
#include "applib/ui/click_internal.h"
////////////////////////////////////////////////
// App + Click Recognizer + Window = Glue code
//! Calls the provider function of the window with the ClickConfig structs of the "app global" click recognizers.
//! The window is set as context to of each of the ClickConfig's .context fields for convenience.
//! In case window has a click_config_context set, it will use that as context instead of the window itself.
//! @see AppContext.click_recognizer[]
struct Window;
void app_click_config_setup_with_window(ClickManager* click_manager, struct Window *window);

View 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.
*/
#include "app_window_stack.h"
#include "window.h"
#include "window_stack.h"
#include "window_stack_private.h"
#include "console/prompt.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "system/logging.h"
#include "util/list.h"
#include "util/size.h"
#include "FreeRTOS.h"
#include "semphr.h"
void app_window_stack_push(Window *window, bool animated) {
PBL_LOG(LOG_LEVEL_DEBUG, "Pushing window %p onto app window stack %p",
window, app_state_get_window_stack());
window_stack_push(app_state_get_window_stack(), window, animated);
}
void app_window_stack_insert_next(Window *window) {
window_stack_insert_next(app_state_get_window_stack(), window);
}
Window *app_window_stack_pop(bool animated) {
return window_stack_pop(app_state_get_window_stack(), animated);
}
void app_window_stack_pop_all(const bool animated) {
window_stack_pop_all(app_state_get_window_stack(), animated);
}
bool app_window_stack_remove(Window *window, bool animated) {
return window_stack_remove(window, animated);
}
Window *app_window_stack_get_top_window(void) {
return window_stack_get_top_window(app_state_get_window_stack());
}
bool app_window_stack_contains_window(Window *window) {
return window_stack_contains_window(app_state_get_window_stack(), window);
}
uint32_t app_window_stack_count(void) {
return window_stack_count(app_state_get_window_stack());
}
// Commands
////////////////////////////////////
typedef struct WindowStackInfoContext {
SemaphoreHandle_t interlock;
WindowStackDump *dump;
size_t count;
} WindowStackInfoContext;
static void prv_window_stack_info_cb(void *ctx) {
// Note: Because of the nature of modal windows that has us re-using the Window Stack code for
// everything (for simplicity), while a normal call to any of the stack functions would yield
// us the appropriate window stack based on our current task, for the sake of this command, we
// only care about the application's window stack, so we'll work with that directly.
WindowStackInfoContext *info = ctx;
WindowStack *stack = app_state_get_window_stack();
info->count = window_stack_dump(stack, &info->dump);
xSemaphoreGive(info->interlock);
}
void command_window_stack_info(void) {
struct WindowStackInfoContext info = {
.interlock = xSemaphoreCreateBinary(),
};
if (!info.interlock) {
prompt_send_response("Couldn't allocate semaphore for window stack");
return;
}
// FIXME: Dumping the app window stack from another task without a
// lock exposes us to the possibility of catching the window stack in
// an inconsistent state. It's been like this for years without issue
// but we could just be really lucky. Switch to the app task to dump
// the window stack?
launcher_task_add_callback(prv_window_stack_info_cb, &info);
xSemaphoreTake(info.interlock, portMAX_DELAY);
vSemaphoreDelete(info.interlock);
if (info.count > 0 && !info.dump) {
prompt_send_response("Couldn't allocate buffers for window stack data");
goto cleanup;
}
char buffer[128];
prompt_send_response_fmt(
buffer, sizeof(buffer),
"Window Stack, top to bottom: (%zu)", info.count);
for (size_t i = 0; i < info.count; ++i) {
prompt_send_response_fmt(buffer, sizeof(buffer), "window %p <%s>",
info.dump[i].addr, info.dump[i].name);
}
cleanup:
kernel_free(info.dump);
}

View File

@@ -0,0 +1,101 @@
/*
* 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 "window.h"
#include "window_stack.h"
#include "util/list.h"
//! @addtogroup UI
//! @{
//! @addtogroup WindowStack Window Stack
//! \brief The multiple window manager
//!
//! In Pebble OS, the window stack serves as the global manager of what window is presented,
//! ensuring that input events are forwarded to the topmost window.
//! The navigation model of Pebble centers on the concept of a vertical “stack” of windows, similar
//! to mobile app interactions.
//!
//! In working with the Window Stack API, the basic operations include push and pop. When an app wants to
//! display a new window, it pushes a new window onto the stack. This appears like a window sliding in
//! from the right. As an app is closed, the window is popped off the stack and disappears.
//!
//! For more complicated operations, involving multiple windows, you can determine which windows reside
//! on the stack, using window_stack_contains_window() and remove any specific window, using window_stack_remove().
//!
//! Refer to the \htmlinclude UiFramework.html (chapter "Window Stack") for a conceptual overview
//! of the window stack and relevant code examples.
//!
//! Also see the \ref WindowHandlers of a \ref Window for the callbacks that can be added to a window
//! in order to act upon window stack transitions.
//!
//! @{
//! Pushes the given window on the window navigation stack,
//! on top of the current topmost window of the app.
//! @param window The window to push on top
//! @param animated Pass in `true` to animate the push using a sliding animation,
//! or `false` to skip the animation.
void app_window_stack_push(Window *window, bool animated);
//! Inserts the given window below the topmost window on the window
//! navigation stack. If there is no window on the navigation stack, this is
//! the same as calling \ref window_stack_push() , otherwise, when the topmost
//! window is popped, this window will be visible.
//! @param window The window to insert next
void app_window_stack_insert_next(Window *window);
//! Pops the topmost window on the navigation stack
//! @param animated See \ref window_stack_remove()
//! @return The window that is popped, or NULL if there are no windows to pop.
Window* app_window_stack_pop(bool animated);
//! Pops all windows.
//! See \ref window_stack_remove() for a description of the `animated` parameter and notes.
void app_window_stack_pop_all(const bool animated);
//! Removes a given window from the window stack
//! that belongs to the app task.
//! @note If there are no windows for the app left on the stack, the app
//! will be killed by the system, shortly. To avoid this, make sure
//! to push another window shortly after or before removing the last window.
//! @param window The window to remove. If the window is NULL or if it
//! is not on the stack, this function is a no-op.
//! @param animated Pass in `true` to animate the removal of the window using
//! a side-to-side sliding animation to reveal the next window.
//! This is only used in case the window happens to be on top of the window
//! stack (thus visible).
//! @return True if window was successfully removed, false otherwise.
bool app_window_stack_remove(Window *window, bool animated);
//! Gets the topmost window on the stack that belongs to the app.
//! @return The topmost window on the stack that belongs to the app or
//! NULL if no app window could be found.
Window* app_window_stack_get_top_window(void);
//! Checks if the window is on the window stack
//! @param window The window to look for on the window stack
//! @return true if the window is currently on the window stack.
bool app_window_stack_contains_window(Window *window);
//! @internal
//! @return count of the number of windows are on the app window stack
uint32_t app_window_stack_count(void);
//! @} // end addtogroup WindowStack
//! @} // end addtogroup UI

View File

@@ -0,0 +1,124 @@
/*
* 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 "bitmap_layer.h"
#include "applib/graphics/graphics.h"
#include "util/trig.h"
#include "applib/applib_malloc.auto.h"
#include "process_management/process_manager.h"
#include <string.h>
void bitmap_layer_update_proc(BitmapLayer *image, GContext* ctx) {
const GColor bg_color = image->background_color;
if (!gcolor_is_transparent(bg_color)) {
graphics_context_set_fill_color(ctx, bg_color);
graphics_fill_rect(ctx, &image->layer.bounds);
}
graphics_context_set_compositing_mode(ctx, image->compositing_mode);
if (image->bitmap != NULL) {
const GSize size = image->bitmap->bounds.size;
const bool clips = true; // bitmap layer not allowed to draw outside of its frame
GRect rect = (GRect){{0, 0}, size};
grect_align(&rect, &image->layer.bounds, image->alignment, clips);
if (!process_manager_compiled_with_legacy2_sdk()) {
// Dirty workaround for calculation of offset in graphics_draw_bitmap_in_rect
// and preserving state of bitmap alignment in bitmap_layer
// The previous behavior is relied on by some 2.x apps, and therefore we exlude
// the fix for apps compiled with older SDKs. See PBL-19136 for details.
rect.origin.x -= image->layer.bounds.origin.x;
rect.origin.y -= image->layer.bounds.origin.y;
}
graphics_draw_bitmap_in_rect(ctx, image->bitmap, &rect);
}
}
void bitmap_layer_init(BitmapLayer *image, const GRect *frame) {
*image = (BitmapLayer){};
image->layer.frame = *frame;
image->layer.bounds = (GRect){{0, 0}, frame->size};
image->layer.update_proc = (LayerUpdateProc)bitmap_layer_update_proc;
layer_set_clips(&image->layer, true);
image->background_color = GColorClear;
image->compositing_mode = GCompOpAssign;
layer_mark_dirty(&(image->layer));
}
BitmapLayer* bitmap_layer_create(GRect frame) {
BitmapLayer* layer = applib_type_malloc(BitmapLayer);
if (layer) {
bitmap_layer_init(layer, &frame);
}
return layer;
}
void bitmap_layer_deinit(BitmapLayer *bitmap_layer) {
layer_deinit(&bitmap_layer->layer);
}
void bitmap_layer_destroy(BitmapLayer* bitmap_layer) {
if (bitmap_layer == NULL) {
return;
}
bitmap_layer_deinit(bitmap_layer);
applib_free(bitmap_layer);
}
Layer* bitmap_layer_get_layer(const BitmapLayer *bitmap_layer) {
return &((BitmapLayer *)bitmap_layer)->layer;
}
const GBitmap* bitmap_layer_get_bitmap(BitmapLayer* bitmap_layer) {
return bitmap_layer->bitmap;
}
void bitmap_layer_set_bitmap(BitmapLayer *image, const GBitmap *bitmap) {
if (image == NULL) {
return;
}
image->bitmap = bitmap;
layer_mark_dirty(&(image->layer));
}
void bitmap_layer_set_alignment(BitmapLayer *image, GAlign alignment) {
if (alignment == image->alignment) {
return;
}
image->alignment = alignment;
layer_mark_dirty(&(image->layer));
}
void bitmap_layer_set_background_color(BitmapLayer *image, GColor color) {
const GColor image_color = image->background_color;
if (gcolor_equal(color, image_color)) {
return;
}
image->background_color = color;
layer_mark_dirty(&(image->layer));
}
void bitmap_layer_set_background_color_2bit(BitmapLayer *bitmap_layer, GColor2 color) {
bitmap_layer_set_background_color(bitmap_layer, get_native_color(color));
}
void bitmap_layer_set_compositing_mode(BitmapLayer *image, GCompOp mode) {
if (image->compositing_mode == mode) {
return;
}
image->compositing_mode = mode;
layer_mark_dirty(&(image->layer));
}

View File

@@ -0,0 +1,163 @@
/*
* 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.
*/
//! @file ui/bitmap_layer.h
//!
#pragma once
#include "applib/ui/layer.h"
#include "applib/graphics/gtypes.h"
//! @file bitmap_layer.h
//! @addtogroup UI
//! @{
//! @addtogroup Layer Layers
//! @{
//! @addtogroup BitmapLayer
//! \brief Layer that displays a bitmap image.
//!
//! ![](bitmap_layer.png)
//! BitmapLayer is a Layer subtype that draws a GBitmap within its frame. It uses an alignment property
//! to specify how to position the bitmap image within its frame. Optionally, when the
//! background color is not GColorClear, it draws a solid background color behind the
//! bitmap image, filling areas of the frame that are not covered by the bitmap image.
//! Lastly, using the compositing mode property of the BitmapLayer, determines the way the
//! bitmap image is drawn on top of what is underneath it (either the background color, or
//! the layers beneath it).
//!
//! <h3>Inside the Implementation</h3>
//! The implementation of BitmapLayer is fairly straightforward and relies heavily on the
//! functionality as exposed by the core drawing functions (see \ref Drawing).
//! \ref BitmapLayer's drawing callback uses \ref graphics_draw_bitmap_in_rect()
//! to perform the actual drawing of the \ref GBitmap. It uses \ref grect_align() to perform
//! the layout of the image and it uses \ref graphics_fill_rect() to draw the background plane.
//! @{
//! The data structure of a BitmapLayer, containing a Layer data structure, a pointer to
//! the GBitmap, and all necessary state to draw itself (the background color, alignment and
//! the compositing mode).
//! @note a `BitmapLayer *` can safely be casted to a `Layer *` and can thus be used
//! with all other functions that take a `Layer *` as an argument.
//! <br/>For example, the following is legal:
//! \code{.c}
//! BitmapLayer bitmap_layer;
//! ...
//! layer_set_hidden((Layer *)&bitmap_layer, true);
//! \endcode
typedef struct BitmapLayer {
Layer layer;
const GBitmap *bitmap;
GColor8 background_color;
GAlign alignment:4;
GCompOp compositing_mode:3;
} BitmapLayer;
//! Initializes the BitmapLayer
//! All previous contents are erased and the following default values are set:
//! * Bitmap: `NULL` (none)
//! * Background color: \ref GColorClear
//! * Compositing mode: \ref GCompOpAssign
//! * Clips: `true`
//!
//! The bitmap layer is automatically marked dirty after this operation.
//! @param bitmap_layer The BitmapLayer to initialize
//! @param frame The frame with which to initialze the BitmapLayer
void bitmap_layer_init(BitmapLayer *bitmap_layer, const GRect *frame);
//! Creates a new bitmap layer on the heap and initalizes it the default values.
//!
//! * Bitmap: `NULL` (none)
//! * Background color: \ref GColorClear
//! * Compositing mode: \ref GCompOpAssign
//! * Clips: `true`
//! @return A pointer to the BitmapLayer. `NULL` if the BitmapLayer could not
//! be created
BitmapLayer* bitmap_layer_create(GRect frame);
//! De-initializes the BitmapLayer
//! Removes the layer from the parent layer.
//! @param bitmap_layer The BitmapLayer to de-initialize
void bitmap_layer_deinit(BitmapLayer *bitmap_layer);
//! Destroys a window previously created by bitmap_layer_create
void bitmap_layer_destroy(BitmapLayer* bitmap_layer);
//! Gets the "root" Layer of the bitmap layer, which is the parent for the sub-
//! layers used for its implementation.
//! @param bitmap_layer Pointer to the BitmapLayer for which to get the "root" Layer
//! @return The "root" Layer of the bitmap layer.
//! @internal
//! @note The result is always equal to `(Layer *) bitmap_layer`.
Layer* bitmap_layer_get_layer(const BitmapLayer *bitmap_layer);
//! Gets the pointer to the bitmap image that the BitmapLayer is using.
//!
//! @param bitmap_layer The BitmapLayer for which to get the bitmap image
//! @return A pointer to the bitmap image that the BitmapLayer is using
const GBitmap* bitmap_layer_get_bitmap(BitmapLayer *bitmap_layer);
//! Sets the bitmap onto the BitmapLayer. The bitmap is set by reference (no deep
//! copy), thus the caller of this function has to make sure the bitmap is kept
//! in memory.
//!
//! The bitmap layer is automatically marked dirty after this operation.
//! @param bitmap_layer The BitmapLayer for which to set the bitmap image
//! @param bitmap The new \ref GBitmap to set onto the BitmapLayer
void bitmap_layer_set_bitmap(BitmapLayer *bitmap_layer, const GBitmap *bitmap);
//! Sets the alignment of the image to draw with in frame of the BitmapLayer.
//! The aligment parameter specifies which edges of the bitmap should overlap
//! with the frame of the BitmapLayer.
//! If the bitmap is smaller than the frame of the BitmapLayer, the background
//! is filled with the background color.
//!
//! The bitmap layer is automatically marked dirty after this operation.
//! @param bitmap_layer The BitmapLayer for which to set the aligment
//! @param alignment The new alignment for the image inside the BitmapLayer
void bitmap_layer_set_alignment(BitmapLayer *bitmap_layer, GAlign alignment);
//! Sets the background color of bounding box that will be drawn behind the image
//! of the BitmapLayer.
//!
//! The bitmap layer is automatically marked dirty after this operation.
//! @param bitmap_layer The BitmapLayer for which to set the background color
//! @param color The new \ref GColor to set the background to
void bitmap_layer_set_background_color(BitmapLayer *bitmap_layer, GColor color);
void bitmap_layer_set_background_color_2bit(BitmapLayer *bitmap_layer, GColor2 color);
//! Sets the compositing mode of how the bitmap image is composited onto the
//! BitmapLayer's background plane, or how it is composited onto what has been
//! drawn beneath the BitmapLayer.
//!
//! The compositing mode only affects the drawing of the bitmap and not the
//! drawing of the background color.
//!
//! For Aplite, there is no notion of "transparency" in the graphics system. However, the effect of
//! transparency can be created by masking and using compositing modes.
//!
//! For Basalt, when drawing \ref GBitmap images, \ref GCompOpSet will be required to apply any
//! transparency.
//!
//! The bitmap layer is automatically marked dirty after this operation.
//! @param bitmap_layer The BitmapLayer for which to set the compositing mode
//! @param mode The compositing mode to set
//! @see See \ref GCompOp for visual examples of the different compositing modes.
void bitmap_layer_set_compositing_mode(BitmapLayer *bitmap_layer, GCompOp mode);
//! @} // end addtogroup BitmapLayer
//! @} // end addtogroup Layer
//! @} // end addtogroup UI

386
src/fw/applib/ui/click.c Normal file
View File

@@ -0,0 +1,386 @@
/*
* 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 "click.h"
#include "click_internal.h"
#include "window_stack_private.h"
#include "process_state/app_state/app_state.h"
#include "process_management/app_manager.h"
#include "util/size.h"
#include <stddef.h>
#include <string.h>
//! The time that the user has to hold the button before repetition kicks in.
static const uint32_t CLICK_REPETITION_DELAY_MS = 400;
//! Default minimum number of multi-clicks before the multi_click.handler gets fired
static const uint8_t MULTI_CLICK_DEFAULT_MIN = 2;
//! Default timeout after which looking for follow up clicks will be stopped
static const uint32_t MULTI_CLICK_DEFAULT_TIMEOUT_MS = 300;
//! Default delay before long click is fired
static const uint32_t LONG_CLICK_DEFAULT_DELAY_MS = 400;
typedef enum {
ClickHandlerOffsetSingle = offsetof(ClickConfig, click.handler),
ClickHandlerOffsetMulti = offsetof(ClickConfig, multi_click.handler),
ClickHandlerOffsetLong = offsetof(ClickConfig, long_click.handler),
ClickHandlerOffsetLongRelease = offsetof(ClickConfig, long_click.release_handler),
ClickHandlerOffsetRawUp = offsetof(ClickConfig, raw.up_handler),
ClickHandlerOffsetRawDown = offsetof(ClickConfig, raw.down_handler),
} ClickHandlerOffset;
static ClickHandler prv_get_handler(ClickRecognizer *recognizer, ClickHandlerOffset offset) {
return *((ClickHandler*)(((uint8_t*)&recognizer->config) + offset));
}
static void prv_cancel_timer(AppTimer **timer) {
if (*timer) {
app_timer_cancel(*timer);
*timer = NULL;
}
}
static void prv_click_reset(ClickRecognizerRef recognizer_ref) {
ClickRecognizer *recognizer = (ClickRecognizer *)recognizer_ref;
recognizer->number_of_clicks_counted = 0;
recognizer->is_button_down = false;
recognizer->is_repeating = false;
prv_cancel_timer(&recognizer->hold_timer);
prv_cancel_timer(&recognizer->multi_click_timer);
}
static bool prv_dispatch_event(ClickRecognizer *recognizer, ClickHandlerOffset handler_offset,
bool needs_reset) {
if (recognizer) {
ClickHandler handler = prv_get_handler(recognizer, handler_offset);
if (handler) {
void *context;
if ((handler_offset == ClickHandlerOffsetRawUp || handler_offset == ClickHandlerOffsetRawDown) &&
recognizer->config.raw.context != NULL) {
// The context for raw click events is overridable:
context = recognizer->config.raw.context;
} else {
context = recognizer->config.context;
}
handler(recognizer, context);
}
if (needs_reset) {
prv_click_reset(recognizer);
}
return true;
} else {
return false;
}
}
inline static bool prv_is_hold_to_repeat_enabled(ClickRecognizer *recognizer) {
return (recognizer->config.click.repeat_interval_ms >= 30);
}
inline static bool prv_is_multi_click_enabled(ClickRecognizer *recognizer) {
return (recognizer->config.multi_click.handler != NULL);
}
inline static bool prv_is_long_click_enabled(ClickRecognizer *recognizer) {
return (recognizer->config.long_click.handler != NULL || recognizer->config.long_click.release_handler != NULL);
}
static void prv_auto_repeat_single_click(ClickRecognizer *recognizer) {
if (!recognizer->is_button_down) {
// If this button isn't being held down anymore, don't re-register the timer.
return;
}
++(recognizer->number_of_clicks_counted);
// Start the repetition timer:
// Note: We're not using the timer_register_repeating() here, so we have the possibility
// of changing the interval in the handler.
recognizer->hold_timer = app_timer_register(recognizer->config.click.repeat_interval_ms,
(AppTimerCallback)prv_auto_repeat_single_click, recognizer);
recognizer->is_repeating = true;
// Fire right once:
const bool needs_reset = false;
prv_dispatch_event(recognizer, ClickHandlerOffsetSingle, needs_reset);
}
static void prv_repetition_delay_callback(void *data) {
ClickRecognizer *recognizer = (ClickRecognizer *) data;
// User has been holding the button down for more than the repetition delay.
prv_auto_repeat_single_click(recognizer);
}
static uint8_t prv_multi_click_get_min(ClickRecognizer *recognizer) {
if (false == prv_is_multi_click_enabled(recognizer)) {
return 0;
}
if (recognizer->config.multi_click.min == 0) {
return MULTI_CLICK_DEFAULT_MIN;
}
return recognizer->config.multi_click.min;
}
static uint8_t prv_multi_click_get_max(ClickRecognizer *recognizer) {
if (false == prv_is_multi_click_enabled(recognizer)) {
return 0;
}
if (recognizer->config.multi_click.max == 0) {
return prv_multi_click_get_min(recognizer);
}
return recognizer->config.multi_click.max;
}
static uint32_t prv_multi_click_get_timeout(ClickRecognizer *recognizer) {
if (false == prv_is_multi_click_enabled(recognizer)) {
return 0;
}
if (recognizer->config.multi_click.timeout == 0) {
return MULTI_CLICK_DEFAULT_TIMEOUT_MS;
}
return recognizer->config.multi_click.timeout;
}
static uint32_t prv_long_click_get_delay(ClickRecognizer *recognizer) {
if (false == prv_is_long_click_enabled(recognizer)) {
return 0;
}
if (recognizer->config.long_click.delay_ms == 0) {
return LONG_CLICK_DEFAULT_DELAY_MS;
}
return recognizer->config.long_click.delay_ms;
}
inline static bool prv_can_more_clicks_follow(ClickRecognizer *recognizer) {
if (recognizer->number_of_clicks_counted >= prv_multi_click_get_max(recognizer)) {
return false;
}
return true;
}
uint8_t click_number_of_clicks_counted(ClickRecognizerRef recognizer_ref) {
ClickRecognizer *recognizer = (ClickRecognizer*)recognizer_ref;
return recognizer->number_of_clicks_counted;
}
ButtonId click_recognizer_get_button_id(ClickRecognizerRef recognizer_ref) {
ClickRecognizer *recognizer = (ClickRecognizer*)recognizer_ref;
return recognizer->button;
}
bool click_recognizer_is_repeating(ClickRecognizerRef recognizer_ref) {
ClickRecognizer *recognizer = (ClickRecognizer*)recognizer_ref;
return recognizer->is_repeating;
}
bool click_recognizer_is_held_down(ClickRecognizerRef recognizer_ref) {
return (((ClickRecognizer *)recognizer_ref)->is_button_down);
}
ClickConfig *click_recognizer_get_config(ClickRecognizerRef recognizer_ref) {
ClickRecognizer *recognizer = (ClickRecognizer*)recognizer_ref;
return &recognizer->config;
}
static void prv_long_click_callback(void *data) {
ClickRecognizer *recognizer = (ClickRecognizer *) data;
recognizer->hold_timer = NULL;
const bool needs_reset = false;
prv_dispatch_event(recognizer, ClickHandlerOffsetLong, needs_reset);
}
//! Called at the end of a click pattern, either on the button up, or after a multi-click timeout:
static void prv_click_pattern_done(ClickRecognizer *recognizer) {
// In case multi_click is also configured, if there was only one click, regard it as
// a "single click" after the multi-click timeout passed and this callback is called:
if (recognizer->number_of_clicks_counted >= 1 && recognizer->is_repeating == false) {
int clicks_over = recognizer->number_of_clicks_counted;
for(int i = 0; i < clicks_over; i++) {
prv_dispatch_event(recognizer, ClickHandlerOffsetSingle, false);
}
}
prv_click_reset(recognizer);
}
static void prv_multi_click_timeout_callback(void *data) {
ClickRecognizer *recognizer = (ClickRecognizer *) data;
recognizer->multi_click_timer = NULL;
if (recognizer->config.multi_click.last_click_only &&
(recognizer->number_of_clicks_counted >= prv_multi_click_get_min(recognizer)) &&
(recognizer->number_of_clicks_counted <= prv_multi_click_get_max(recognizer))) {
const bool needs_reset = true;
prv_dispatch_event(recognizer, ClickHandlerOffsetMulti, needs_reset);
} else {
prv_click_pattern_done(recognizer);
}
}
void command_put_button_event(const char* button_index, const char* click_type) {
int button = atoi(button_index);
const bool needs_reset = false;
ClickHandlerOffset offset;
if ((button < 0 || button > NUM_BUTTONS)) {
return;
}
switch(*click_type) {
case 's':
offset = ClickHandlerOffsetSingle;
break;
case 'm':
offset = ClickHandlerOffsetMulti;
break;
case 'l':
offset = ClickHandlerOffsetLong;
break;
case 'r':
offset = ClickHandlerOffsetLongRelease;
break;
case 'u':
offset = ClickHandlerOffsetRawUp;
break;
case 'd':
offset = ClickHandlerOffsetRawDown;
break;
default:
return;
}
prv_dispatch_event(&(app_state_get_click_manager()->recognizers[button]), offset, needs_reset);
}
void click_recognizer_handle_button_down(ClickRecognizer *recognizer) {
recognizer->is_button_down = true;
prv_cancel_timer(&recognizer->multi_click_timer);
const bool needs_reset = false;
prv_dispatch_event(recognizer, ClickHandlerOffsetRawDown, needs_reset);
if (prv_is_long_click_enabled(recognizer)) {
const uint32_t long_click_delay = prv_long_click_get_delay(recognizer);
recognizer->hold_timer = app_timer_register(
long_click_delay, prv_long_click_callback, recognizer);
} else {
const bool local_is_hold_to_repeat_enabled = prv_is_hold_to_repeat_enabled(recognizer);
if (local_is_hold_to_repeat_enabled) {
// If there's a repeat interval configured, start the repetition delay timer:
recognizer->hold_timer = app_timer_register(
CLICK_REPETITION_DELAY_MS, prv_repetition_delay_callback, recognizer);
}
if (false == prv_is_multi_click_enabled(recognizer)) {
// No long click nor multi click, fire handler immediately on button down:
++(recognizer->number_of_clicks_counted);
const bool needs_reset = (false == local_is_hold_to_repeat_enabled);
prv_dispatch_event(recognizer, ClickHandlerOffsetSingle, needs_reset);
}
}
}
void click_recognizer_handle_button_up(ClickRecognizer *recognizer) {
const bool needs_reset = false;
prv_dispatch_event(recognizer, ClickHandlerOffsetRawUp, needs_reset);
if (recognizer->is_button_down == false) {
// Ignore this button up event. Most likely, the recognizer has been
// reset while the button was still pressed down.
return;
}
recognizer->is_button_down = false;
const bool local_is_long_click_enabled = prv_is_long_click_enabled(recognizer);
const bool local_is_multi_click_enabled = prv_is_multi_click_enabled(recognizer);
//const bool local_is_hold_to_repeat_enabled = is_hold_to_repeat_enabled(recognizer);
if (false == local_is_long_click_enabled &&
false == local_is_multi_click_enabled) {
// Handler already fired in button down.
prv_click_reset(recognizer);
return;
}
++(recognizer->number_of_clicks_counted);
const bool has_long_click_been_fired = (local_is_long_click_enabled && recognizer->hold_timer == NULL);
if (has_long_click_been_fired) {
const bool needs_reset = true;
prv_dispatch_event(recognizer, ClickHandlerOffsetLongRelease, needs_reset);
return;
}
prv_cancel_timer(&recognizer->hold_timer);
if (local_is_multi_click_enabled && false == recognizer->is_repeating) {
const bool local_can_more_clicks_follow = prv_can_more_clicks_follow(recognizer);
bool should_fire_multi_click_handler = ((recognizer->config.multi_click.last_click_only && local_can_more_clicks_follow) == false);
bool reset_using_event = false;
if (should_fire_multi_click_handler) {
if ((recognizer->number_of_clicks_counted >= prv_multi_click_get_min(recognizer)) &&
(recognizer->number_of_clicks_counted <= prv_multi_click_get_max(recognizer))) {
reset_using_event = (false == local_can_more_clicks_follow);
prv_dispatch_event(recognizer, ClickHandlerOffsetMulti, reset_using_event);
}
}
if (prv_can_more_clicks_follow(recognizer)) {
const uint32_t timeout = prv_multi_click_get_timeout(recognizer);
recognizer->multi_click_timer = app_timer_register(
timeout, prv_multi_click_timeout_callback, recognizer);
return;
} else {
if (reset_using_event) {
return;
}
}
// fall-through if no more clicks can follow,
// and we're not resetting using a click event that has been put.
}
prv_click_pattern_done(recognizer);
}
void click_manager_init(ClickManager* click_manager) {
for (unsigned int button_id = 0;
button_id < ARRAY_LENGTH(click_manager->recognizers); ++button_id) {
ClickRecognizer *recognizer = &click_manager->recognizers[button_id];
recognizer->button = button_id;
prv_click_reset(recognizer);
}
}
void click_manager_clear(ClickManager* click_manager) {
for (unsigned int button_id = 0;
button_id < ARRAY_LENGTH(click_manager->recognizers); ++button_id) {
prv_click_reset(&click_manager->recognizers[button_id]);
click_manager->recognizers[button_id].config = (ClickConfig){};
}
}
void click_manager_reset(ClickManager* click_manager) {
for (unsigned int button_id = 0; button_id < NUM_BUTTONS; button_id++) {
prv_click_reset(&click_manager->recognizers[button_id]);
}
}

235
src/fw/applib/ui/click.h Normal file
View File

@@ -0,0 +1,235 @@
/*
* 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.
*/
//! @file click.h
//!
#pragma once
#include "drivers/button_id.h"
#include <stdint.h>
#include <stdbool.h>
//! @addtogroup UI
//! @{
//! @addtogroup Clicks
//! \brief Handling button click interactions
//!
//! Each Pebble window handles Pebble's buttons while it is displayed. Raw button down and button
//! up events are transformed into click events that can be transferred to your app:
//!
//! * Single-click. Detects a single click, that is, a button down event followed by a button up event.
//! It also offers hold-to-repeat functionality (repeated click).
//! * Multi-click. Detects double-clicking, triple-clicking and other arbitrary click counts.
//! It can fire its event handler on all of the matched clicks, or just the last.
//! * Long-click. Detects long clicks, that is, press-and-hold.
//! * Raw. Simply forwards the raw button events. It is provided as a way to use both the higher level
//! "clicks" processing and the raw button events at the same time.
//!
//! To receive click events when a window is displayed, you must register a \ref ClickConfigProvider for
//! this window with \ref window_set_click_config_provider(). Your \ref ClickConfigProvider will be called every time
//! the window becomes visible with one context argument. By default this context is a pointer to the window but you can
//! change this with \ref window_set_click_config_provider_with_context().
//!
//! In your \ref ClickConfigProvider you call the \ref window_single_click_subscribe(), \ref window_single_repeating_click_subscribe(),
//! \ref window_multi_click_subscribe(), \ref window_long_click_subscribe() and \ref window_raw_click_subscribe() functions to register
//! a handler for each event you wish to receive.
//!
//! For convenience, click handlers are provided with a \ref ClickRecognizerRef and a user-specified context.
//!
//! The \ref ClickRecognizerRef can be used in combination with \ref click_number_of_clicks_counted(), \ref
//! click_recognizer_get_button_id() and \ref click_recognizer_is_repeating() to get more information about the click. This is
//! useful if you want different buttons or event types to share the same handler.
//!
//! The user-specified context is the context of your \ref ClickConfigProvider (see above). By default it points to the window.
//! You can override it for all handlers with \ref window_set_click_config_provider_with_context() or for a specific button with \ref
//! window_set_click_context().
//!
//! <h3>User interaction in watchfaces</h3>
//! Watchfaces cannot use the buttons to interact with the user. Instead, you can use the \ref AccelerometerService.
//!
//! <h3>About the Back button</h3>
//! By default, the Back button will always pop to the previous window on the \ref WindowStack (and leave the app if the current
//! window is the only window). You can override the default back button behavior with \ref window_single_click_subscribe() and
//! \ref window_multi_click_subscribe() but you cannot set a repeating, long or raw click handler on the back button because a long press
//! will always terminate the app and return to the main menu.
//!
//! <h3>Usage example</h3>
//! First associate a click config provider callback with your window:
//! \code{.c}
//! void app_init(void) {
//! ...
//! window_set_click_config_provider(&window, (ClickConfigProvider) config_provider);
//! ...
//! }
//! \endcode
//! Then in the callback, you set your desired configuration for each button:
//! \code{.c}
//! void config_provider(Window *window) {
//! // single click / repeat-on-hold config:
//! window_single_click_subscribe(BUTTON_ID_DOWN, down_single_click_handler);
//! window_single_repeating_click_subscribe(BUTTON_ID_SELECT, 1000, select_single_click_handler);
//!
//! // multi click config:
//! window_multi_click_subscribe(BUTTON_ID_SELECT, 2, 10, 0, true, select_multi_click_handler);
//!
//! // long click config:
//! window_long_click_subscribe(BUTTON_ID_SELECT, 700, select_long_click_handler, select_long_click_release_handler);
//! }
//! \endcode
//! Now you implement the handlers for each click you've subscribed to and set up:
//! \code{.c}
//! void down_single_click_handler(ClickRecognizerRef recognizer, void *context) {
//! ... called on single click ...
//! Window *window = (Window *)context;
//! }
//! void select_single_click_handler(ClickRecognizerRef recognizer, void *context) {
//! ... called on single click, and every 1000ms of being held ...
//! Window *window = (Window *)context;
//! }
//!
//! void select_multi_click_handler(ClickRecognizerRef recognizer, void *context) {
//! ... called for multi-clicks ...
//! Window *window = (Window *)context;
//! const uint16_t count = click_number_of_clicks_counted(recognizer);
//! }
//!
//! void select_long_click_handler(ClickRecognizerRef recognizer, void *context) {
//! ... called on long click start ...
//! Window *window = (Window *)context;
//! }
//!
//! void select_long_click_release_handler(ClickRecognizerRef recognizer, void *context) {
//! ... called when long click is released ...
//! Window *window = (Window *)context;
//! }
//! \endcode
//!
//! <h3>See also</h3>
//! Refer to the \htmlinclude UiFramework.html (chapter "Clicks") for a conceptual
//! overview of clicks and relevant code examples.
//!
//! @{
//! Reference to opaque click recognizer
//! When a \ref ClickHandler callback is called, the recognizer that fired the handler is passed in.
//! @see \ref ClickHandler
//! @see \ref click_number_of_clicks_counted()
//! @see \ref click_recognizer_get_button_id()
//! @see \ref click_recognizer_is_repeating()
typedef void *ClickRecognizerRef;
//! Function signature of the callback that handles a recognized click pattern
//! @param recognizer The click recognizer that detected a "click" pattern
//! @param context Pointer to application specified data (see \ref window_set_click_config_provider_with_context() and
//! \ref window_set_click_context()). This defaults to the window.
//! @see \ref ClickConfigProvider
typedef void (*ClickHandler)(ClickRecognizerRef recognizer, void *context);
//! @internal
//! Data structure that defines the configuration for one click recognizer.
//! An array of these configuration structures is passed into the \ref ClickConfigProvider
//! callback, for the application to configure.
//! @see ClickConfigProvider
typedef struct ClickConfig {
//! Pointer to developer-supplied data that is also passed to ClickHandler callbacks
void *context;
/** Single-click */
struct click {
//! Fired when a single click is detected and every time "repeat_interval_ms" has been reached.
//! @note When there is a multi_click and/or long_click setup, there will be a delay depending before the single click handler
//! will get fired. On the other hand, when there is no multi_click nor long_click setup, the single click handler will
//! fire directly on button down.
ClickHandler handler;
//! When holding button down, milliseconds after which "handler" is fired again. The default 0ms means 'no repeat timer'.
//! 30 ms is the minimum allowable value. Values below will be disregarded.
//! In case long_click.handler is configured, `repeat_interval_ms` will not be used.
uint16_t repeat_interval_ms;
} click; //!< Single-click configuration
/** Multi-click */
struct multi_click {
uint8_t min; //!< Minimum number of clicks before handler is fired. Defaults to 2.
uint8_t max; //!< Maximum number of clicks after which the click counter is reset. The default 0 means use "min" also as "max".
bool last_click_only; //!< Defaults to false. When true, only the for the last multi-click the handler is called.
ClickHandler handler; //!< Fired for multi-clicks, as "filtered" by the `reset_delay`, `last_click_only`, `min` and `max` parameters.
uint16_t timeout; //!< The delay after which a sequence of clicks is considered finished, and the click counter is reset. The default 0 means 300ms.
} multi_click; //!< Multi-click configuration
/** Long-click */
struct long_click {
uint16_t delay_ms; //!< Milliseconds after which "handler" is fired. Defaults to 500ms.
ClickHandler handler; //!< Fired once, directly as soon as "delay" has been reached.
ClickHandler release_handler; //!< In case a long click has been detected, fired when the button is released.
} long_click; //!< Long-click configuration
/** Raw button up & down */
struct raw {
ClickHandler up_handler; //!< Fired on button up events
ClickHandler down_handler; //!< Fired on button down events
void *context; //!< If this context is not NULL, it will override the general context.
} raw; //!< Raw button event pass-through configuration
} ClickConfig;
//! This callback is called every time the window becomes visible (and when you call \ref window_set_click_config_provider() if
//! the window is already visible).
//!
//! Subscribe to click events using
//! \ref window_single_click_subscribe()
//! \ref window_single_repeating_click_subscribe()
//! \ref window_multi_click_subscribe()
//! \ref window_long_click_subscribe()
//! \ref window_raw_click_subscribe()
//! These subscriptions will get used by the click recognizers of each of the 4 buttons.
//! @param context Pointer to application specific data (see \ref window_set_click_config_provider_with_context()).
typedef void (*ClickConfigProvider)(void *context);
//! Gets the click count.
//! You can use this inside a click handler implementation to get the click count for multi_click
//! and (repeated) click events.
//! @param recognizer The click recognizer for which to get the click count
//! @return The number of consecutive clicks, and for auto-repeating the number of repetitions.
uint8_t click_number_of_clicks_counted(ClickRecognizerRef recognizer);
//! @internal
//! Returns a pointer to the click recognizer's ClickConfig
ClickConfig *click_recognizer_get_config(ClickRecognizerRef recognizer);
//! Gets the button identifier.
//! You can use this inside a click handler implementation to get the button id for the click event.
//! @param recognizer The click recognizer for which to get the button id that caused the click event
//! @return the ButtonId of the click recognizer
ButtonId click_recognizer_get_button_id(ClickRecognizerRef recognizer);
//! Is this a repeating click.
//! You can use this inside a click handler implementation to find out whether this is a repeating click or not.
//! @param recognizer The click recognizer for which to find out whether this is a repeating click.
//! @return true if this is a repeating click.
bool click_recognizer_is_repeating(ClickRecognizerRef recognizer);
//! @internal
//! Is this button being held down
//! You can use this inside a click handler implementation to check if it's being held down or not
//! @@param recognizer The click recognizer for which to find out whether this is being held down.
//! @return true if this is being held down.
bool click_recognizer_is_held_down(ClickRecognizerRef recognizer);
//! @} // end addtogroup Clicks
//! @} // end addtogroup UI

View File

@@ -0,0 +1,89 @@
/*
* 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.
*/
//! @file click_internal.h
//!
#pragma once
#include "click.h"
#include "applib/app_timer.h"
/**
A bag of parameters that holds all of the state required to identify
different types of clicks performed on a single button. You can think
of this as the per-button "context" used by the click detection
system. A single set of ClickRecognizers are shared between all
windows within an app, though only the top-most window may use the
recognizers (see the notes in app_window_click_glue.c).
<p/>
Each ClickRecognizer contains a ClickConfig struct that holds the
callbacks (ClickHandlers) to be fired after a click has been
detected/dispatched to the system event loop. ClickConfigs are
typically instantiated by calling a configuration callback (the
window's ClickConfigProvider) that is responsible for copying over a
template to the app's ClickRecognizers.
<p/>
Whenever a the head of the window stack changes, the OS is responsible
for ensuring that all of its registered click recognizers are reset
and reconfigured using the new visible window's
ClickConfigProvider. This happens in the window_stack_private_push &
window_stack_private_pop functions used to place a new window at the
top of the stack.
@see ClickConfig
@see ClickHandler
@see ClickConfigProvider
@see app_window_click_glue.c
@see window_set_click_config_provider_with_context
*/
typedef struct ClickRecognizer {
ButtonId button;
ClickConfig config;
bool is_button_down;
bool is_repeating;
uint8_t number_of_clicks_counted;
AppTimer *hold_timer;
AppTimer *multi_click_timer;
} ClickRecognizer;
typedef struct ClickManager {
ClickRecognizer recognizers[NUM_BUTTONS];
} ClickManager;
//! Tell the particular recognizer that the associated button has been released.
void click_recognizer_handle_button_up(ClickRecognizer *recognizer);
//! Tell the particular recognizer that the associated button has been pressed.
void click_recognizer_handle_button_down(ClickRecognizer *recognizer);
//! Initialize a click manager for use. This only needs to be called once to initialize the structure, and then the
//! same struct can be reconfigured multiple times by using click_manager_clear.
void click_manager_init(ClickManager* click_manager);
//! Clear out any state from the click manager, including configuration. This ClickManager can be reconfigured at any
//! time.
void click_manager_clear(ClickManager* click_manager);
//! Reset the state from the click manager, including timers.
void click_manager_reset(ClickManager* click_manager);

View File

@@ -0,0 +1,381 @@
/*
* 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);
}

View File

@@ -0,0 +1,118 @@
/*
* 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"
#include "applib/ui/layer.h"
#include <stdbool.h>
// Forward declare ScrollLayer to avoid cyclic header include for scroll_layer <-> content_indicator
struct ScrollLayer;
typedef struct ScrollLayer ScrollLayer;
//! @file content_indicator.h
//! @addtogroup UI
//! @{
//! @addtogroup ContentIndicator
//! \brief Convenience class for rendering arrows to indicate additional content
//! @{
//! Value to describe directions for \ref ContentIndicator.
//! @see \ref content_indicator_configure_direction
//! @see \ref content_indicator_set_content_available
typedef enum {
ContentIndicatorDirectionUp = 0, //!< The up direction.
ContentIndicatorDirectionDown, //!< The down direction.
NumContentIndicatorDirections //!< The number of supported directions.
} ContentIndicatorDirection;
//! Struct used to configure directions for \ref ContentIndicator.
//! @see \ref content_indicator_configure_direction
typedef struct {
Layer *layer; //!< The layer where the arrow indicator will be rendered when content is available.
bool times_out; //!< Whether the display of the arrow indicator should timeout.
GAlign alignment; //!< The alignment of the arrow within the provided layer.
struct {
GColor foreground; //!< The color of the arrow.
GColor background; //!< The color of the layer behind the arrow.
} colors;
} ContentIndicatorConfig;
struct ContentIndicator;
typedef struct ContentIndicator ContentIndicator;
//! Creates a ContentIndicator on the heap.
//! @return A pointer to the ContentIndicator. `NULL` if the ContentIndicator could not be created.
ContentIndicator *content_indicator_create(void);
//! Destroys a ContentIndicator previously created using \ref content_indicator_create().
//! @param content_indicator The ContentIndicator to destroy.
void content_indicator_destroy(ContentIndicator *content_indicator);
//! @internal
//! Initializes the given ContentIndicator.
//! @param content_indicator The ContentIndicator to initialize.
void content_indicator_init(ContentIndicator *content_indicator);
//! @internal
//! Deinitializes the given ContentIndicator.
//! @param content_indicator The ContentIndicator to deinitialize.
void content_indicator_deinit(ContentIndicator *content_indicator);
//! @internal
//! Draw an arrow in a rect.
//! @param ctx The graphics context we are drawing in
//! @param frame The rectangle to draw the arrow in
//! @param direction The direction that the arrow points in
//! @param GColor fg_color The fill color of the arrow
//! @param GColor bg_color The fill color of the background
//! @param GAlign alignment The alignment of the arrow within the provided bounds
void content_indicator_draw_arrow(GContext *ctx, const GRect *frame,
ContentIndicatorDirection direction, GColor fg_color,
GColor bg_color, GAlign alignment);
//! Configures a ContentIndicator for the given direction.
//! @param content_indicator The ContentIndicator to configure.
//! @param direction The direction for which to configure the ContentIndicator.
//! @param config The configuration to use to configure the ContentIndicator. If NULL, the data
//! for the specified direction will be reset.
//! @return True if the ContentIndicator was successfully configured for the given direction,
//! false otherwise.
bool content_indicator_configure_direction(ContentIndicator *content_indicator,
ContentIndicatorDirection direction,
const ContentIndicatorConfig *config);
//! Retrieves the availability status of content in the given direction.
//! @param content_indicator The ContentIndicator for which to get the content availability.
//! @param direction The direction for which to get the content availability.
//! @return True if content is available in the given direction, false otherwise.
bool content_indicator_get_content_available(ContentIndicator *content_indicator,
ContentIndicatorDirection direction);
//! Sets the availability status of content in the given direction.
//! @param content_indicator The ContentIndicator for which to set the content availability.
//! @param direction The direction for which to set the content availability.
//! @param available Whether or not content is available.
//! @note If times_out is enabled, calling this function resets any previously scheduled timeout
//! timer for the ContentIndicator.
void content_indicator_set_content_available(ContentIndicator *content_indicator,
ContentIndicatorDirection direction,
bool available);
//! @} // end addtogroup ContentIndicator
//! @} // end addtogroup UI

View File

@@ -0,0 +1,82 @@
/*
* 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/app_timer.h"
#include "applib/ui/scroll_layer.h"
#include "applib/ui/layer.h"
#include "util/buffer.h"
typedef struct {
ContentIndicatorDirection direction:2;
bool content_available:1;
AppTimer *timeout_timer;
ContentIndicatorConfig config;
LayerUpdateProc original_update_proc;
} ContentIndicatorDirectionData;
struct ContentIndicator {
ContentIndicatorDirectionData direction_data[NumContentIndicatorDirections];
//! Needed to find the ContentIndicator belonging to a ScrollLayer
//! @see \ref content_indicator_get_or_create_for_scroll_layer()
ScrollLayer *scroll_layer;
};
//! TODO: There are no videos from design yet for this timeout, so it was arbitrarily chosen.
#define CONTENT_INDICATOR_TIMEOUT_MS 1200
//! The maximum number of ContentIndicator pointers that a ContentIndicatorsBuffer should hold.
//! This affects two separate buffers: one for kernel (i.e. all modals together) and one for the
//! currently running app. If an attempt is made to exceed this size by initializing an additional
//! ContentIndicator, then \ref content_indicator_init() will trigger an assertion. If an attempt
//! is made to exceed this size by creating an additional ContentIndicator, then
//! \ref content_indicator_create() will return `NULL`.
#define CONTENT_INDICATOR_BUFFER_SIZE 4
//! The maximum size (in Bytes) of the buffer of ContentIndicators.
#define CONTENT_INDICATOR_BUFFER_SIZE_BYTES (CONTENT_INDICATOR_BUFFER_SIZE * \
sizeof(ContentIndicator *))
//! This union allows us to statically allocate the storage for a buffer of content indicators.
typedef union {
Buffer buffer;
uint8_t buffer_storage[sizeof(Buffer) + CONTENT_INDICATOR_BUFFER_SIZE_BYTES];
} ContentIndicatorsBuffer;
//! @internal
//! Retrieves the ContentIndicator for the given ScrollLayer.
//! @param scroll_layer The ScrollLayer for which to retrieve the ContentIndicator.
//! @return A pointer to the ContentIndicator.
//! `NULL` if the ContentIndicator could not be found.
ContentIndicator *content_indicator_get_for_scroll_layer(ScrollLayer *scroll_layer);
//! @internal
//! Retrieves the ContentIndicator for the given ScrollLayer, or creates one if none exists.
//! @param scroll_layer The ScrollLayer for which to retrieve the ContentIndicator.
//! @return A pointer to the ContentIndicator.
//! `NULL` if the ContentIndicator could not be found and could not be created.
ContentIndicator *content_indicator_get_or_create_for_scroll_layer(ScrollLayer *scroll_layer);
//! @internal
//! Destroys a ContentIndicator for the given ScrollLayer.
//! @param scroll_layer The ScrollLayer for which to destroy a ContentIndicator.
void content_indicator_destroy_for_scroll_layer(ScrollLayer *scroll_layer);
//! @internal
//! Initializes the given ContentIndicatorsBuffer.
//! @param content_indicators_buffer A pointer to the ContentIndicatorsBuffer to initialize.
void content_indicator_init_buffer(ContentIndicatorsBuffer *content_indicators_buffer);

View File

@@ -0,0 +1,200 @@
/*
* 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 "crumbs_layer.h"
#include "applib/applib_malloc.auto.h"
#include "shell/system_theme.h"
#include "applib/ui/property_animation.h"
#include "process_management/process_manager.h"
#include "system/logging.h"
#include "util/trig.h"
typedef struct CrumbsLayerSizeConfig {
int layer_width;
int crumb_radius;
int crumb_spacing;
int crumb_space_from_top;
} CrumbsLayerSizeConfig;
static const CrumbsLayerSizeConfig s_crumb_configs[NumPreferredContentSizes] = {
//! @note this is the same as Medium until Small is designed
[PreferredContentSizeSmall] = {
.layer_width = 14,
.crumb_radius = 2,
.crumb_spacing = 8,
.crumb_space_from_top = 8,
},
[PreferredContentSizeMedium] = {
.layer_width = 14,
.crumb_radius = 2,
.crumb_spacing = 8,
.crumb_space_from_top = 8,
},
[PreferredContentSizeLarge] = {
.layer_width = 16,
.crumb_radius = 2,
.crumb_spacing = 10,
.crumb_space_from_top = 10,
},
//! @note this is the same as Large until ExtraLarge is designed
[PreferredContentSizeExtraLarge] = {
.layer_width = 16,
.crumb_radius = 2,
.crumb_spacing = 10,
.crumb_space_from_top = 10,
},
};
static const CrumbsLayerSizeConfig *prv_crumb_config(void) {
const PreferredContentSize runtime_platform_default_size =
system_theme_get_default_content_size_for_runtime_platform();
return &s_crumb_configs[runtime_platform_default_size];
}
int crumbs_layer_width(void) {
return prv_crumb_config()->layer_width;
}
static int prv_crumb_x_position(void) {
return prv_crumb_config()->layer_width / 2;
}
static int prv_crumb_radius(void) {
return prv_crumb_config()->crumb_radius;
}
static int prv_crumb_spacing(void) {
return prv_crumb_config()->crumb_spacing;
}
static int prv_crumb_space_from_top(void) {
return prv_crumb_config()->crumb_space_from_top;
}
static int prv_crumb_maximum_count(void) {
// NOTE: Was originally:
// static const int MAX_CRUMBS = 16; // 168 / (4px diameter + 4px in between each)
// However that math literally doesn't add up, it would've been 20 like that.
// I'm going to just return 16 all the time like we used to, but leave a "correct" version
// commented out.
// return (PBL_DISPLAY_HEIGHT - prv_crumb_space_from_top()) / prv_crumb_spacing();
return 16;
}
static void prv_crumbs_layer_update_proc_rect(Layer *layer, GContext *ctx) {
const int crumb_radius = prv_crumb_radius();
const int crumb_spacing = prv_crumb_spacing();
const int xpos = prv_crumb_x_position();
GPoint p = GPoint(xpos, crumb_radius + prv_crumb_space_from_top());
CrumbsLayer *cl = (CrumbsLayer *)layer;
graphics_context_set_fill_color(ctx, cl->bg_color);
graphics_fill_rect(ctx, &layer->bounds);
for (int i = cl->level; i > 0; --i) {
p.x = xpos + (cl->crumbs_x_increment / i);
graphics_context_set_fill_color(ctx, cl->fg_color);
graphics_fill_circle(ctx, p, crumb_radius);
p.y += crumb_spacing;
}
}
static void prv_crumbs_layer_update_proc_round(Layer *layer, GContext *ctx) {
CrumbsLayer *cl = (CrumbsLayer *)layer;
graphics_context_set_fill_color(ctx, cl->bg_color);
// TODO: remove stroke color again, once it's been fixed in fill_radial
graphics_context_set_stroke_color(ctx, cl->bg_color);
// compensate for problems with rounding errors and physical display shape
const uint16_t overdraw = 2;
graphics_fill_radial(ctx, grect_inset(layer->bounds, GEdgeInsets(-overdraw)),
GOvalScaleModeFillCircle, crumbs_layer_width(), 0, TRIG_MAX_ANGLE);
}
void crumbs_layer_set_level(CrumbsLayer *crumbs_layer, int level) {
const int max_crumbs = prv_crumb_maximum_count();
if (level > max_crumbs) {
PBL_LOG(LOG_LEVEL_WARNING, "exceeded max number of crumbs");
level = max_crumbs;
}
crumbs_layer->level = level;
layer_mark_dirty((Layer *)crumbs_layer);
}
void crumbs_layer_init(CrumbsLayer *crumbs_layer, const GRect *frame, GColor bg_color,
GColor fg_color) {
layer_init(&crumbs_layer->layer, frame);
crumbs_layer->level = 0;
crumbs_layer->fg_color = fg_color;
crumbs_layer->bg_color = bg_color;
const LayerUpdateProc update_proc = PBL_IF_RECT_ELSE(prv_crumbs_layer_update_proc_rect,
prv_crumbs_layer_update_proc_round);
layer_set_update_proc(&crumbs_layer->layer, update_proc);
}
CrumbsLayer *crumbs_layer_create(GRect frame, GColor bg_color, GColor fg_color) {
// Note: Not yet exported for 3rd party applications so no padding needed
CrumbsLayer *cl = applib_malloc(sizeof(CrumbsLayer));
if (cl) {
crumbs_layer_init(cl, &frame, fg_color, bg_color);
}
return cl;
}
void crumbs_layer_deinit(CrumbsLayer *crumbs_layer) {
if (crumbs_layer == NULL) {
return;
}
layer_deinit((Layer *)crumbs_layer);
}
void crumbs_layer_destroy(CrumbsLayer *crumbs_layer) {
crumbs_layer_deinit(crumbs_layer);
applib_free(crumbs_layer);
}
int16_t prv_x_getter(void *subject) {
CrumbsLayer *crumbs_layer = subject;
return crumbs_layer->crumbs_x_increment;
}
void prv_x_setter(void *subject, int16_t int16) {
CrumbsLayer *crumbs_layer = subject;
crumbs_layer->crumbs_x_increment = int16;
}
static const PropertyAnimationImplementation s_prop_impl = {
.base = {
.update = (AnimationUpdateImplementation)property_animation_update_int16,
},
.accessors = {
.getter.int16 = prv_x_getter,
.setter.int16 = prv_x_setter,
},
};
Animation *crumbs_layer_get_animation(CrumbsLayer *crumbs_layer) {
uint16_t from = crumbs_layer->level * 2 * prv_crumb_radius();
uint16_t to = 0;
PropertyAnimation *prop_anim = property_animation_create(&s_prop_impl, crumbs_layer, &from, &to);
return property_animation_get_animation(prop_anim);
}

View File

@@ -0,0 +1,45 @@
/*
* 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/ui/animation.h"
#include "applib/ui/layer.h"
#include "applib/graphics/graphics.h"
typedef struct {
Layer layer;
int level;
GColor bg_color;
GColor fg_color;
// used internally
int16_t crumbs_x_increment;
} CrumbsLayer;
void crumbs_layer_init(CrumbsLayer *crumbs_layer, const GRect *frame, GColor fg_color,
GColor bg_color);
CrumbsLayer *crumbs_layer_create(GRect frame, GColor fg_color, GColor bg_color);
void crumbs_layer_set_level(CrumbsLayer *crumbs_layer, int level);
void crumbs_layer_deinit(CrumbsLayer *crumbs_layer);
void crumbs_layer_destroy(CrumbsLayer *crumbs_layer);
Animation *crumbs_layer_get_animation(CrumbsLayer *crumbs_layer);
int crumbs_layer_width(void);

View File

@@ -0,0 +1,109 @@
/*
* 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 "date_time_selection_window_private.h"
#include "services/common/clock.h"
#include "services/common/i18n/i18n.h"
#include "util/date.h"
#include "util/math.h"
#include <stdio.h>
static const int MIN_SELECTABLE_YEAR = 2010;
static const int MAX_SELECTABLE_YEAR = 2037; // Work around Y2038 problem
static int prv_wrap(int x, int max, int delta) {
x = (x + delta) % max;
return x < 0 ? x + max : x;
}
int date_time_selection_step_hour(int hour, int delta) {
return prv_wrap(hour, 24, delta);
}
int date_time_selection_step_minute(int minute, int delta) {
return prv_wrap(minute, 60, delta);
}
int date_time_selection_step_day(int year, int month, int day, int delta) {
bool is_leap_year = date_util_is_leap_year(year);
// This function expects Jan == 0, but date_util_get_max_days_in_month expects Jan == 1
int max_days = date_util_get_max_days_in_month(month + 1, is_leap_year);
// This functions expects the first day of the month is 1, but wrap expects the first day of the
// month is 0 (based off the mday element of the "tm" struct)
return prv_wrap(day - 1, max_days, delta) + 1;
}
int date_time_selection_step_month(int month, int delta) {
return prv_wrap(month, 12, delta);
}
int date_time_selection_truncate_date(int year, int month, int day) {
bool is_leap_year = date_util_is_leap_year(year);
// date_util_get_max_days_in_month expects Jan == 1, but this function expects Jan == 0
int max_days = date_util_get_max_days_in_month(month + 1, is_leap_year);
return MIN(day, max_days);
}
int date_time_selection_step_year(int year, int delta) {
year += delta;
return CLIP(year, MIN_SELECTABLE_YEAR - STDTIME_YEAR_OFFSET,
MAX_SELECTABLE_YEAR - STDTIME_YEAR_OFFSET);
}
char *date_time_selection_get_text(TimeData *data, TimeInputIndex index, char *buf) {
switch (index) {
case TimeInputIndexHour: {
unsigned hour = data->hour;
if (!clock_is_24h_style()) {
hour = hour % 12;
if (hour == 0) {
hour = 12;
}
}
snprintf(buf, 3, "%02u", hour);
return buf;
}
case TimeInputIndexMinute:
snprintf(buf, 3, "%02u", data->minute);
return buf;
case TimeInputIndexAMPM: // We should only get this in 12h style
if (data->hour < 12) {
i18n_get_with_buffer("AM", buf, 3);
} else {
i18n_get_with_buffer("PM", buf, 3);
}
return buf;
default:
return "";
}
}
void date_time_handle_time_change(TimeData *data, TimeInputIndex index, int delta) {
switch (index) {
case TimeInputIndexHour:
data->hour = date_time_selection_step_hour(data->hour, delta);
break;
case TimeInputIndexMinute:
data->minute = date_time_selection_step_minute(data->minute, delta);
break;
case TimeInputIndexAMPM: // We should only get this in 12h style
data->hour = date_time_selection_step_hour(data->hour, 12 * delta);
break;
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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 <stdint.h>
typedef struct {
uint8_t hour;
uint8_t minute;
} TimeData;
typedef enum {
TimeInputIndexHour = 0,
TimeInputIndexMinute,
TimeInputIndexAMPM,
} TimeInputIndex;
typedef enum {
DateInputIndexYear = 0,
DateInputIndexMonth,
DateInputIndexDay,
} DateInputIndex;
int date_time_selection_step_hour(int hour, int delta);
int date_time_selection_step_minute(int minute, int delta);
int date_time_selection_step_day(int year, int month, int day, int delta);
int date_time_selection_step_month(int month, int delta);
int date_time_selection_truncate_date(int year, int month, int day);
int date_time_selection_step_year(int year, int delta);
char *date_time_selection_get_text(TimeData *data, TimeInputIndex index, char *buf);
void date_time_handle_time_change(TimeData *data, TimeInputIndex index, int delta);

View File

@@ -0,0 +1,252 @@
/*
* 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 "actionable_dialog.h"
#include "applib/applib_malloc.auto.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/bitmap_layer.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/dialog_private.h"
#include "applib/ui/kino/kino_reel/scale_segmented.h"
#include "applib/ui/layer.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/window.h"
#include "kernel/ui/kernel_ui.h"
#include "resource/resource_ids.auto.h"
#include "system/passert.h"
#include <limits.h>
#include <string.h>
static void prv_actionable_dialog_load(Window *window) {
ActionableDialog *actionable_dialog = window_get_user_data(window);
Dialog *dialog = &actionable_dialog->dialog;
Layer *window_root_layer = window_get_root_layer(window);
// Ownership of icon is taken over by KinoLayer in dialog_init_icon_layer() call below
KinoReel *icon = dialog_create_icon(dialog);
const GSize icon_size = icon ? kino_reel_get_size(icon) : GSizeZero;
const GRect *bounds = &window_root_layer->bounds;
const uint16_t icon_single_line_text_offset_px = 13;
const uint16_t left_margin_px = PBL_IF_RECT_ELSE(5, 0);
const uint16_t content_and_action_bar_horizontal_spacing = PBL_IF_RECT_ELSE(5, 7);
const uint16_t right_margin_px = ACTION_BAR_WIDTH +
content_and_action_bar_horizontal_spacing;
const uint16_t text_single_line_text_offset_px = icon_single_line_text_offset_px - 1;
const GFont dialog_text_font = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
const int single_line_text_height_px = fonts_get_font_height(dialog_text_font);
const int max_text_line_height_px = 2 * single_line_text_height_px + 8;
const uint16_t status_layer_offset = dialog->show_status_layer ? 6 : 0;
uint16_t text_top_margin_px = icon ? icon_size.h + 22 : 6;
uint16_t icon_top_margin_px = 18;
uint16_t x = 0;
uint16_t y = 0;
uint16_t w = PBL_IF_RECT_ELSE(bounds->size.w - ACTION_BAR_WIDTH, bounds->size.w);
uint16_t h = STATUS_BAR_LAYER_HEIGHT;
if (dialog->show_status_layer) {
dialog_add_status_bar_layer(dialog, &GRect(x, y, w, h));
}
x = left_margin_px;
w = bounds->size.w - left_margin_px - right_margin_px;
GTextAttributes *text_attributes = NULL;
#if PBL_ROUND
// Create a GTextAttributes for the TextLayer. Note that the matching
// graphics_text_attributes_destroy() will not need to be called here, as the ownership
// of text_attributes will be transferred to the TextLayer we assign it to.
text_attributes = graphics_text_attributes_create();
graphics_text_attributes_enable_screen_text_flow(text_attributes, 8);
#endif
// Check if the text takes up more than one line. If the dialog has a single line of text,
// the icon and line of text are positioned lower so as to be more vertically centered.
GContext *ctx = graphics_context_get_current_context();
const GTextAlignment text_alignment = PBL_IF_RECT_ELSE(GTextAlignmentCenter, GTextAlignmentRight);
{
// do all this in a block so we enforce that nobody uses these variables outside of the block
// when dealing with round displays, sizes change depending on location.
const GRect probe_rect = GRect(x, y + text_single_line_text_offset_px,
w, max_text_line_height_px);
const uint16_t text_height = graphics_text_layout_get_max_used_size(ctx,
dialog->buffer,
dialog_text_font,
probe_rect,
GTextOverflowModeWordWrap,
text_alignment,
text_attributes).h;
if (text_height <= single_line_text_height_px) {
text_top_margin_px += text_single_line_text_offset_px;
icon_top_margin_px += icon_single_line_text_offset_px;
} else {
text_top_margin_px += status_layer_offset;
icon_top_margin_px += status_layer_offset;
}
}
y = text_top_margin_px;
h = bounds->size.h - y;
// Set up the text.
TextLayer *text_layer = &dialog->text_layer;
text_layer_init_with_parameters(text_layer, &GRect(x, y, w, h),
dialog->buffer, dialog_text_font,
dialog->text_color, GColorClear, text_alignment,
GTextOverflowModeWordWrap);
if (text_attributes) {
text_layer->should_cache_layout = true;
text_layer->layout_cache = text_attributes;
}
layer_add_child(&window->layer, &text_layer->layer);
// Action bar. If the user hasn't given a custom action bar, we'll create one of the preset
// types.
if (actionable_dialog->action_bar_type != DialogActionBarCustom) {
actionable_dialog->action_bar = action_bar_layer_create();
action_bar_layer_set_click_config_provider(actionable_dialog->action_bar,
actionable_dialog->config_provider);
}
ActionBarLayer *action_bar = actionable_dialog->action_bar;
if (actionable_dialog->action_bar_type == DialogActionBarConfirm) {
#if !defined(RECOVERY_FW)
actionable_dialog->select_icon = gbitmap_create_with_resource(
RESOURCE_ID_ACTION_BAR_ICON_CHECK);
#endif
action_bar_layer_set_context(action_bar, window);
action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, actionable_dialog->select_icon);
} else if (actionable_dialog->action_bar_type == DialogActionBarDecline) {
#if !defined(RECOVERY_FW)
actionable_dialog->select_icon = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_X);
#endif
action_bar_layer_set_context(action_bar, window);
action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, actionable_dialog->select_icon);
} else if (actionable_dialog->action_bar_type == DialogActionBarConfirmDecline) {
#if !defined(RECOVERY_FW)
actionable_dialog->up_icon = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_CHECK);
actionable_dialog->down_icon = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_X);
#endif
action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, actionable_dialog->up_icon);
action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, actionable_dialog->down_icon);
action_bar_layer_set_context(action_bar, window);
}
action_bar_layer_add_to_window(action_bar, window);
// Icon
// On rectangular displays we just center it horizontally b/w the left edge of the display and
// the left edge of the action bar
#if PBL_RECT
x = (grect_get_max_x(bounds) - ACTION_BAR_WIDTH - icon_size.w) / 2;
#else
// On round displays we right align it with respect to the same imaginary vertical line that the
// text is right aligned to
x = grect_get_max_x(bounds) - ACTION_BAR_WIDTH - content_and_action_bar_horizontal_spacing -
icon_size.w;
#endif
y = icon_top_margin_px;
if (dialog_init_icon_layer(dialog, icon, GPoint(x, y), true)) {
layer_add_child(window_root_layer, &dialog->icon_layer.layer);
}
dialog_load(&actionable_dialog->dialog);
}
static void prv_actionable_dialog_appear(Window *window) {
ActionableDialog *actionable_dialog = window_get_user_data(window);
Dialog *dialog = actionable_dialog_get_dialog(actionable_dialog);
dialog_appear(dialog);
}
static void prv_actionable_dialog_unload(Window *window) {
ActionableDialog *actionable_dialog = window_get_user_data(window);
dialog_unload(&actionable_dialog->dialog);
// Destroy action bar if it was a predefined type. If the action bar is custom, it is the user's
// responsibility to free it.
if (actionable_dialog->action_bar_type != DialogActionBarCustom) {
action_bar_layer_destroy(actionable_dialog->action_bar);
if (actionable_dialog->action_bar_type == DialogActionBarConfirmDecline) {
gbitmap_destroy(actionable_dialog->up_icon);
gbitmap_destroy(actionable_dialog->down_icon);
} else { // DialogActionBarConfirm || DialogActionBarDecline
gbitmap_destroy(actionable_dialog->select_icon);
}
}
if (actionable_dialog->dialog.destroy_on_pop) {
applib_free(actionable_dialog);
}
}
Dialog *actionable_dialog_get_dialog(ActionableDialog *actionable_dialog) {
return &actionable_dialog->dialog;
}
void actionable_dialog_push(ActionableDialog *actionable_dialog, WindowStack *window_stack) {
dialog_push(&actionable_dialog->dialog, window_stack);
}
void app_actionable_dialog_push(ActionableDialog *actionable_dialog) {
app_dialog_push(&actionable_dialog->dialog);
}
void actionable_dialog_pop(ActionableDialog *actionable_dialog) {
dialog_pop(&actionable_dialog->dialog);
}
void actionable_dialog_init(ActionableDialog *actionable_dialog, const char *dialog_name) {
PBL_ASSERTN(actionable_dialog);
*actionable_dialog = (ActionableDialog){};
dialog_init(&actionable_dialog->dialog, dialog_name);
Window *window = &actionable_dialog->dialog.window;
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_actionable_dialog_load,
.unload = prv_actionable_dialog_unload,
.appear = prv_actionable_dialog_appear,
});
window_set_user_data(window, actionable_dialog);
}
ActionableDialog *actionable_dialog_create(const char *dialog_name) {
// Note: Not exported so no need for padding.
ActionableDialog *actionable_dialog = applib_malloc(sizeof(ActionableDialog));
if (actionable_dialog) {
actionable_dialog_init(actionable_dialog, dialog_name);
}
return actionable_dialog;
}
void actionable_dialog_set_action_bar_type(ActionableDialog *actionable_dialog,
DialogActionBarType action_bar_type,
ActionBarLayer *action_bar) {
if (action_bar_type == DialogActionBarCustom) {
PBL_ASSERTN(action_bar); // Action bar must not be NULL if it is a custom type.
actionable_dialog->action_bar = action_bar;
} else {
actionable_dialog->action_bar = NULL;
}
actionable_dialog->action_bar_type = action_bar_type;
}
void actionable_dialog_set_click_config_provider(ActionableDialog *actionable_dialog,
ClickConfigProvider click_config_provider) {
actionable_dialog->config_provider = click_config_provider;
}

View File

@@ -0,0 +1,75 @@
/*
* 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 "actionable_dialog_private.h"
#include "applib/graphics/gtypes.h"
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/click.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/window_stack.h"
//! Creates a new ActionableDialog on the heap.
//! @param dialog_name The debug name to give the \ref ActionableDialog
//! @return Pointer to a \ref ActionableDialog
ActionableDialog *actionable_dialog_create(const char *dialog_name);
//! @internal
//! Initializes the passed \ref ActionableDialog
//! @param actionable_dialog Pointer to a \ref ActionableDialog to initialize
//! @param dialog_name The debug name to give the \ref ActionableDialog
void actionable_dialog_init(ActionableDialog *actionable_dialog, const char *dialog_name);
//! Retrieves the internal Dialog object from the ActionableDialog.
//! @param actionable_dialog Pointer to a \ref ActionableDialog from which to grab it's dialog
//! @return The underlying \ref Dialog of the given \ref ActionableDialog
Dialog *actionable_dialog_get_dialog(ActionableDialog *actionable_dialog);
//! Sets the type of action bar to used to one of the pre-defined types or a custom one.
//! @param actionable_dialog Pointer to a \ref ActioanbleDialog whom which to set
//! @param action_bar_type The type of action bar to give the passed dialog
//! @param action_bar Pointer to an \ref ActionBarLayer to assign to the dialog
//! @note: The pointer to an \ref ActionBarLayer is optional and only required when the
//! the \ref DialogActionBarType is \ref DialogActionBarCustom. If the type is not
//! custom, then the given action bar will not be set on the dialog, regardless of if
//! it is `NULL` or not.
void actionable_dialog_set_action_bar_type(ActionableDialog *actionable_dialog,
DialogActionBarType action_bar_type,
ActionBarLayer *action_bar);
//! Sets the ClickConfigProvider of the action bar. If the dialog has a custom action bar then
//! this function has no effect. The action bar is responsible for setting up it's own click
//! config provider
//! @param actionable_dialog Pointer to a \ref ActionableDialog to which to set the provider on
//! @param click_config_provider The \ref ClickConfigProvider to set
void actionable_dialog_set_click_config_provider(ActionableDialog *actionable_dialog,
ClickConfigProvider click_config_provider);
//! @internal
//! Pushes the \ref ActionableDialog onto the given window stack.
//! @param actionable_dialog Pointer to a \ref ActionableDialog to push
//! @param window_stack Pointer to a \ref WindowStack to push the \ref ActionableDialog to
void actionable_dialog_push(ActionableDialog *actionable_dialog, WindowStack *window_stack);
//! Wrapper to call \ref actionable_dialog_push() for an app.
//! @note: Put a better comment here when we export
void app_actionable_dialog_push(ActionableDialog *actionable_dialog);
//! Pops the given \ref ActionableDialog from the window stack it was pushed to.
//! @param actionable_dialog Pointer to a \ref ActionableDialog to pop
void actionable_dialog_pop(ActionableDialog *actionable_dialog);

View File

@@ -0,0 +1,57 @@
/*
* 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.
*/
//! An ActionableDialog is a dialog that has an action bar on the right hand side
//! of the window. The user can specify there own custom \ref ActionBarLayer to
//! override the default behaviour or specify a \ref ClickConfigProvider to tie
//! into the default \ref ActionBarLayer provided by the dialog.
#pragma once
#include "applib/graphics/gtypes.h"
#include "applib/graphics/perimeter.h"
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/click.h"
#include "applib/ui/dialogs/dialog.h"
//! Different types of action bar. Two commonly used types are built in:
//! Confirm and Decline. Alternatively, the user can supply their own
//! custom action bar.
typedef enum DialogActionBarType {
//! SELECT: Confirm icon
DialogActionBarConfirm,
//! SELECT: Decline icon
DialogActionBarDecline,
//! UP: Confirm icon, DOWN: Decline icon
DialogActionBarConfirmDecline,
//! Provide your own action bar
DialogActionBarCustom
} DialogActionBarType;
typedef struct ActionableDialog {
Dialog dialog;
DialogActionBarType action_bar_type;
union {
struct {
GBitmap *select_icon;
};
struct {
GBitmap *up_icon;
GBitmap *down_icon;
};
};
ActionBarLayer *action_bar;
ClickConfigProvider config_provider;
} ActionableDialog;

View File

@@ -0,0 +1,117 @@
/*
* 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 "bt_conn_dialog.h"
#include "applib/applib_malloc.auto.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/dialog_private.h"
#include "applib/ui/window.h"
#include "kernel/events.h"
#include "kernel/pebble_tasks.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "syscall/syscall.h"
#include "system/passert.h"
static void prv_handle_comm_session_event(PebbleEvent *e, void *context) {
BtConnDialog *bt_dialog = context;
if (!e->bluetooth.comm_session_event.is_system) {
return;
}
if (e->bluetooth.comm_session_event.is_open) {
if (bt_dialog->connected_handler) {
bt_dialog->connected_handler(true, bt_dialog->context);
}
// handler to NULL so it won't be called again during the unload
bt_dialog->connected_handler = NULL;
dialog_pop(&bt_dialog->dialog.dialog);
}
}
static void prv_bt_dialog_unload(void *context) {
BtConnDialog *bt_dialog = context;
event_service_client_unsubscribe(&bt_dialog->pebble_app_event_sub);
if (bt_dialog->connected_handler) {
bt_dialog->connected_handler(false, bt_dialog->context);
}
if (bt_dialog->owns_buffer) {
applib_free(bt_dialog->text_buffer);
}
}
void bt_conn_dialog_push(BtConnDialog *bt_dialog, BtConnDialogResultHandler handler,
void *context) {
if (!bt_dialog) {
bt_dialog = bt_conn_dialog_create();
if (!bt_dialog) {
return;
}
}
bt_dialog->connected_handler = handler;
bt_dialog->context = context;
bt_dialog->pebble_app_event_sub = (EventServiceInfo) {
.type = PEBBLE_COMM_SESSION_EVENT,
.handler = prv_handle_comm_session_event,
.context = bt_dialog
};
event_service_client_subscribe(&bt_dialog->pebble_app_event_sub);
WindowStack *window_stack = NULL;
if (pebble_task_get_current() == PebbleTask_App) {
window_stack = app_state_get_window_stack();
} else {
// Bluetooth disconnection events are always displayed at maximum priority.
window_stack = modal_manager_get_window_stack(ModalPriorityCritical);
}
simple_dialog_push(&bt_dialog->dialog, window_stack);
}
BtConnDialog *bt_conn_dialog_create(void) {
BtConnDialog *bt_dialog = applib_malloc(sizeof(BtConnDialog));
bt_conn_dialog_init(bt_dialog, NULL, 0);
return bt_dialog;
}
void bt_conn_dialog_init(BtConnDialog *bt_dialog, char *text_buffer, size_t buffer_size) {
memset(bt_dialog, 0, sizeof(BtConnDialog));
simple_dialog_init(&bt_dialog->dialog, "Bluetooth Disconnected");
Dialog *dialog = &bt_dialog->dialog.dialog;
size_t len = sys_i18n_get_length("Check bluetooth connection");
if (text_buffer) {
PBL_ASSERTN(len < buffer_size);
bt_dialog->text_buffer = text_buffer;
bt_dialog->owns_buffer = false;
} else {
buffer_size = len + 1;
bt_dialog->text_buffer = applib_malloc(buffer_size);
bt_dialog->owns_buffer = true;
}
sys_i18n_get_with_buffer("Check bluetooth connection", bt_dialog->text_buffer, buffer_size);
dialog_set_text(dialog, bt_dialog->text_buffer);
dialog_set_icon(dialog, RESOURCE_ID_WATCH_DISCONNECTED_LARGE);
dialog_show_status_bar_layer(dialog, true);
dialog_set_callbacks(dialog, &(DialogCallbacks) {
.unload = prv_bt_dialog_unload
}, bt_dialog);
}

View File

@@ -0,0 +1,52 @@
/*
* 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/event_service_client.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include <stdint.h>
#include <stdbool.h>
typedef void (*BtConnDialogResultHandler)(bool connected, void *context);
typedef struct {
SimpleDialog dialog;
EventServiceInfo pebble_app_event_sub;
BtConnDialogResultHandler connected_handler;
void *context;
char *text_buffer;
bool owns_buffer;
} BtConnDialog;
//! @internal
//! Wrapper around a \ref SimpleDialog for showing a bluetooth connection event.
//! @param bt_dialog Pointer to the \ref BtConnDialog to push
//! @param handler The \ref BtConnDialogResultHandler to be called when
//! bluetooth is reconnected.
//! @param context The context to pass to the handler
void bt_conn_dialog_push(BtConnDialog *bt_dialog, BtConnDialogResultHandler handler, void *context);
//! @internal
//! Allocates a \ref BtConnDialog on the heap and returns it
//! @return Pointer to a \ref BtConnDialog
BtConnDialog *bt_conn_dialog_create(void);
//! @internal
//! Initializes a \ref BtConnDialog
//! @param bt_dialog Pointer to the \ref BtConnDialog to initialize
void bt_conn_dialog_init(BtConnDialog *bt_dialog, char *text_buffer, size_t buffer_size);

View File

@@ -0,0 +1,112 @@
/*
* 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 "confirmation_dialog.h"
#include "applib/graphics/gtypes.h"
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/dialogs/actionable_dialog.h"
#include "applib/ui/dialogs/dialog_private.h"
#include "applib/ui/window_stack.h"
#include "kernel/pbl_malloc.h"
#include "resource/resource_ids.auto.h"
#include "system/passert.h"
struct ConfirmationDialog {
ActionableDialog action_dialog;
ActionBarLayer action_bar;
GBitmap confirm_icon;
GBitmap decline_icon;
};
ConfirmationDialog *confirmation_dialog_create(const char *dialog_name) {
// Note: This isn't a memory leak as the ConfirmationDialog has the action dialog as its
// first member, so when we call init and pass the ConfirmationDialog as the argument, when
// it frees the associated data, it actually frees the ConfirmationDialog.
ConfirmationDialog *confirmation_dialog = task_zalloc_check(sizeof(ConfirmationDialog));
if (!gbitmap_init_with_resource(&confirmation_dialog->confirm_icon,
RESOURCE_ID_ACTION_BAR_ICON_CHECK)) {
task_free(confirmation_dialog);
return NULL;
}
if (!gbitmap_init_with_resource(&confirmation_dialog->decline_icon,
RESOURCE_ID_ACTION_BAR_ICON_X)) {
gbitmap_deinit(&confirmation_dialog->confirm_icon);
task_free(confirmation_dialog);
return NULL;
}
// In order to create a custom ActionDialog type, we need to create an action bar
ActionBarLayer *action_bar = &confirmation_dialog->action_bar;
action_bar_layer_init(action_bar);
action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, &confirmation_dialog->confirm_icon);
action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, &confirmation_dialog->decline_icon);
action_bar_layer_set_background_color(action_bar, GColorBlack);
action_bar_layer_set_context(action_bar, confirmation_dialog);
// Create the underlying actionable dialog as a custom type
ActionableDialog *action_dialog = &confirmation_dialog->action_dialog;
actionable_dialog_init(action_dialog, dialog_name);
actionable_dialog_set_action_bar_type(action_dialog, DialogActionBarCustom, action_bar);
return confirmation_dialog;
}
Dialog *confirmation_dialog_get_dialog(ConfirmationDialog *confirmation_dialog) {
if (confirmation_dialog == NULL) {
return NULL;
}
return actionable_dialog_get_dialog(&confirmation_dialog->action_dialog);
}
ActionBarLayer *confirmation_dialog_get_action_bar(ConfirmationDialog *confirmation_dialog) {
if (confirmation_dialog == NULL) {
return NULL;
}
return &confirmation_dialog->action_bar;
}
void confirmation_dialog_set_click_config_provider(ConfirmationDialog *confirmation_dialog,
ClickConfigProvider click_config_provider) {
if (confirmation_dialog == NULL) {
return;
}
ActionBarLayer *action_bar = &confirmation_dialog->action_bar;
action_bar_layer_set_click_config_provider(action_bar, click_config_provider);
}
void confirmation_dialog_push(ConfirmationDialog *confirmation_dialog, WindowStack *window_stack) {
actionable_dialog_push(&confirmation_dialog->action_dialog, window_stack);
}
void app_confirmation_dialog_push(ConfirmationDialog *confirmation_dialog) {
app_actionable_dialog_push(&confirmation_dialog->action_dialog);
}
void confirmation_dialog_pop(ConfirmationDialog *confirmation_dialog) {
if (confirmation_dialog == NULL) {
return;
}
action_bar_layer_remove_from_window(&confirmation_dialog->action_bar);
action_bar_layer_deinit(&confirmation_dialog->action_bar);
gbitmap_deinit(&confirmation_dialog->confirm_icon);
gbitmap_deinit(&confirmation_dialog->decline_icon);
actionable_dialog_pop(&confirmation_dialog->action_dialog);
}

View File

@@ -0,0 +1,65 @@
/*
* 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.
*/
//! A ConfirmationDialog is a wrapper around an ActionableDialog implementing
//! the common features provided by a confirmation window. The user specifies
//! callbacks for confirm/decline and can also override the back button behaviour.
#pragma once
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/click.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/window_stack.h"
typedef struct ConfirmationDialog ConfirmationDialog;
//! Creates a ConfirmationDialog on the heap.
//! @param dialog_name The debug name to give the created dialog
//! @return Pointer to the created \ref ConfirmationDialog
ConfirmationDialog *confirmation_dialog_create(const char *dialog_name);
//! Retrieves the internal Dialog object from the ConfirmationDialog.
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog whom's dialog to get
//! @return Pointer to the underlying \ref Dialog
Dialog *confirmation_dialog_get_dialog(ConfirmationDialog *confirmation_dialog);
//! Retrieves the internal ActionBarLayer object from the ConfirmationDialog.
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog whom's
//! \ref ActionBarLayer to get
//! @return \ref ActionBarLayer
ActionBarLayer *confirmation_dialog_get_action_bar(ConfirmationDialog *confirmation_dialog);
//! Sets the click ClickConfigProvider for the ConfirmationDialog.
//! Passes the ConfirmationDialog as the context to the click handlers.
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to which to set
//! @param click_config_provider The \ref ClickConfigProvider to set
void confirmation_dialog_set_click_config_provider(ConfirmationDialog *confirmation_dialog,
ClickConfigProvider click_config_provider);
//! Pushes the ConfirmationDialog onto the given window stack
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to push
//! @param window_stack Pointer to a \ref WindowStack to push the dialog to
void confirmation_dialog_push(ConfirmationDialog *confirmation_dialog, WindowStack *window_stack);
//! Wrapper for an app to call \ref confirmation_dialog_push()
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to push to
//! the app's window stack
//! @note: Put a better comment here before exporting
void app_confirmation_dialog_push(ConfirmationDialog *confirmation_dialog);
//! Pops the ConfirmationDialog from the window stack.
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to pop from its window stack
void confirmation_dialog_pop(ConfirmationDialog *confirmation_dialog);

View File

@@ -0,0 +1,105 @@
/*
* 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 "dialog.h"
#include "applib/ui/window.h"
#include "applib/applib_malloc.auto.h"
#include <string.h>
void dialog_set_fullscreen(Dialog *dialog, bool is_fullscreen) {
window_set_fullscreen(&dialog->window, is_fullscreen);
}
void dialog_show_status_bar_layer(Dialog *dialog, bool show_status_layer) {
dialog->show_status_layer = show_status_layer;
}
void dialog_set_text(Dialog *dialog, const char *text) {
dialog_set_text_buffer(dialog, NULL, false);
uint16_t len = strlen(text);
dialog->is_buffer_owned = true;
dialog->buffer = applib_malloc(len + 1);
strncpy(dialog->buffer, text, len + 1);
text_layer_set_text(&dialog->text_layer, dialog->buffer);
}
void dialog_set_text_buffer(Dialog *dialog, char *buffer, bool take_ownership) {
if (dialog->buffer && dialog->is_buffer_owned) {
applib_free(dialog->buffer);
}
dialog->is_buffer_owned = take_ownership;
dialog->buffer = buffer;
}
void dialog_set_text_color(Dialog *dialog, GColor text_color) {
dialog->text_color = PBL_IF_COLOR_ELSE(text_color, GColorBlack);
text_layer_set_text_color(&dialog->text_layer, dialog->text_color);
}
void dialog_set_background_color(Dialog *dialog, GColor background_color) {
window_set_background_color(&dialog->window, PBL_IF_COLOR_ELSE(background_color, GColorWhite));
}
void dialog_set_icon(Dialog *dialog, uint32_t icon_id) {
if (dialog->icon_id == icon_id) {
// Why bother destroying and then recreating the same icon?
// Restart the animation to preserve behavior.
kino_layer_rewind(&dialog->icon_layer);
kino_layer_play(&dialog->icon_layer);
return;
}
dialog->icon_id = icon_id;
if (window_is_loaded(&dialog->window)) {
kino_layer_set_reel_with_resource(&dialog->icon_layer, icon_id);
}
}
void dialog_set_icon_animate_direction(Dialog *dialog, DialogIconAnimationDirection direction) {
dialog->icon_anim_direction = direction;
}
void dialog_set_vibe(Dialog *dialog, bool vibe_on_show) {
dialog->vibe_on_show = vibe_on_show;
}
void dialog_set_timeout(Dialog *dialog, uint32_t timeout) {
dialog->timeout = timeout;
}
void dialog_set_callbacks(Dialog *dialog, const DialogCallbacks *callbacks,
void *callback_context) {
if (!callbacks) {
dialog->callbacks = (DialogCallbacks) {};
return;
}
dialog->callbacks = *callbacks;
dialog->callback_context = callback_context;
}
void dialog_set_destroy_on_pop(Dialog *dialog, bool destroy_on_pop) {
dialog->destroy_on_pop = destroy_on_pop;
}
void dialog_appear(Dialog *dialog) {
KinoLayer *icon_layer = &dialog->icon_layer;
if (kino_layer_get_reel(icon_layer)) {
kino_layer_play(icon_layer);
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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/app_timer.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/status_bar_layer.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/window.h"
#include <stdbool.h>
#define DIALOG_MAX_MESSAGE_LEN 140
#define DIALOG_IS_ANIMATED true
// TODO PBL-38106: Replace uses of DIALOG_TIMEOUT_DEFAULT with preferred_result_display_duration()
// The number of milliseconds it takes for the dialog to automatically go away if has_timeout is
// set to true.
#define DIALOG_TIMEOUT_DEFAULT (1000)
#define DIALOG_TIMEOUT_INFINITE (0)
struct Dialog;
typedef void (*DialogCallback)(void *context);
typedef enum {
// most dialogs will be pushed. FromRight works best for that (it is default)
DialogIconAnimateNone = 0,
DialogIconAnimationFromRight,
DialogIconAnimationFromLeft,
} DialogIconAnimationDirection;
typedef struct {
DialogCallback load;
DialogCallback unload;
} DialogCallbacks;
//! A newly created Dialog will have the following defaults:
//! * Fullscreen: True,
//! * Show Status Layer: False,
//! * Text Color: GColorBlack,
//! * Background Color: GColorLightGray (GColorWhite for tintin)
//! * Vibe: False
// Dialog object used as the core of other dialog types. The Dialog object shouldn't be used
// directly to create a dialog window. Instead, one of specific types that wraps a Dialog should
// be used, such as the SimpleDialog.
typedef struct Dialog {
Window window;
// Time out. The dialog can be configured to timeout after DIALOG_TIMEOUT_DURATION ms.
uint32_t timeout;
AppTimer *timer;
// Buffer for the main text of the dialog.
char *buffer;
bool is_buffer_owned;
// True if the dialog should vibrate when it opens, false otherwise.
bool vibe_on_show;
bool show_status_layer;
StatusBarLayer status_layer;
// Icon for the dialog.
KinoLayer icon_layer;
uint32_t icon_id;
DialogIconAnimationDirection icon_anim_direction;
// Text layer on which the main text goes.
TextLayer text_layer;
// Color of the dialog text.
GColor text_color;
// Callbacks and context for unloading the dialog. The user is allowed to set these callbacks to
// perform actions (such as freeing resources) when the dialog window has appeared or is unloaded.
// They are also useful if the user is wanted to change the KinoReel for the exit animation.
DialogCallbacks callbacks;
void *callback_context;
bool destroy_on_pop;
} Dialog;
// If set to true, sets the dialog window to fullscreen.
void dialog_set_fullscreen(Dialog *dialog, bool is_fullscreen);
// If set to true, shows a status bar layer at the top of the dialog.
void dialog_show_status_bar_layer(Dialog *dialog, bool show_status_layer);
// Sets the dialog's main text.
// Allocates a buffer on the application heap to store the text. The dialog will retain ownership of
// the buffer and will free it if different text is set or a different buffer is specified with
// dialog_set_text_buffer.
void dialog_set_text(Dialog *dialog, const char *text);
// Sets the dialog's main text using the string in the buffer passed. Any buffer owned by the dialog
// will be freed when the dialog is unloaded or when another buffer or text (dialog_set_text) is
// supplied
void dialog_set_text_buffer(Dialog *dialog, char *buffer, bool take_ownership);
// Sets the color of the dialog's text.
// if SCREEN_COLOR_DEPTH_BITS == 1 then the color will always be set to black
void dialog_set_text_color(Dialog *dialog, GColor text_color);
// Sets the background color of the dialog window.
// if SCREEN_COLOR_DEPTH_BITS == 1 then the color will always be set to white
void dialog_set_background_color(Dialog *dialog, GColor background_color);
// Sets the icon displayed by the dialog.
void dialog_set_icon(Dialog *dialog, uint32_t icon_id);
// Sets the direction from which in the icon animates in.
void dialog_set_icon_animate_direction(Dialog *dialog, DialogIconAnimationDirection direction);
// If set to true, the dialog will emit a short vibe pulse when first opened.
void dialog_set_vibe(Dialog *dialog, bool vibe_on_show);
// Set the timeout of the dialog. Using DIALOG_TIMEOUT_DEFAULT will set the timeout to 1s, using
// DIALOG_TIMEOUT_INFINITE (0) will disable the timeout
void dialog_set_timeout(Dialog *dialog, uint32_t timeout);
// Allows the user to provide a custom callback and optionally a custom context for unloading the
// dialog. This callback will be called from the dialog's own unload function and can be used
// to clean up resources used by the dialog such as icons. If the unload context is NULL, the
// parent dialog object will be passed instead.
void dialog_set_callbacks(Dialog *dialog, const DialogCallbacks *callbacks,
void *callback_context);
// Enable or disable automatically destroying the dialog when it's popped.
void dialog_set_destroy_on_pop(Dialog *dialog, bool destroy_on_pop);

View File

@@ -0,0 +1,176 @@
/*
* 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 "dialog_private.h"
#include "applib/app_timer.h"
#include "applib/applib_malloc.auto.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/kino/kino_reel/transform.h"
#include "applib/ui/kino/kino_reel/scale_segmented.h"
#include "applib/ui/kino/kino_reel_pdci.h"
#include "applib/ui/vibes.h"
#include "applib/ui/window_stack.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "system/passert.h"
static void prv_app_timer_callback(void *context) {
dialog_pop(context);
}
void dialog_init(Dialog *dialog, const char *dialog_name) {
PBL_ASSERTN(dialog);
*dialog = (Dialog){};
window_init(&dialog->window, dialog_name);
window_set_background_color(&dialog->window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
// initial values
dialog->icon_anim_direction = DialogIconAnimationFromRight;
dialog->destroy_on_pop = true;
dialog->text_color = GColorBlack;
}
void dialog_pop(Dialog *dialog) {
window_stack_remove(&dialog->window, DIALOG_IS_ANIMATED);
}
void dialog_push(Dialog *dialog, WindowStack *window_stack) {
window_stack_push(window_stack, &dialog->window, DIALOG_IS_ANIMATED);
}
void app_dialog_push(Dialog *dialog) {
dialog_push(dialog, app_state_get_window_stack());
}
// Loads the core dialog. Should be called from each dialog window's load callback.
void dialog_load(Dialog *dialog) {
if (dialog->vibe_on_show) {
vibes_short_pulse();
}
if (dialog->timeout != DIALOG_TIMEOUT_INFINITE) {
dialog->timer = app_timer_register(dialog->timeout, prv_app_timer_callback, dialog);
}
// Calls the user-given load callback, if it exists. If the user gave a non-null context,
// the function will use that, otherwise it will default to use the default context of the
// containing dialog.
if (dialog->callbacks.load) {
if (dialog->callback_context) {
dialog->callbacks.load(dialog->callback_context);
} else {
dialog->callbacks.load(dialog);
}
}
}
// Unloads the core dialog. Should be called from each dialog window's unload callback.
void dialog_unload(Dialog *dialog) {
app_timer_cancel(dialog->timer);
if (dialog->show_status_layer) {
status_bar_layer_deinit(&dialog->status_layer);
}
dialog_set_icon(dialog, INVALID_RESOURCE);
text_layer_deinit(&dialog->text_layer);
kino_layer_deinit(&dialog->icon_layer);
if (dialog->buffer && dialog->is_buffer_owned) {
applib_free(dialog->buffer);
}
// Calls the user-given unload callback, if it exists. If the user gave a non-null context,
// the function will use that, otherwise it will default to use the default context of the
// containing dialog.
if (dialog->callbacks.unload) {
if (dialog->callback_context) {
dialog->callbacks.unload(dialog->callback_context);
} else {
dialog->callbacks.unload(dialog);
}
}
}
KinoReel *dialog_create_icon(Dialog *dialog) {
return kino_reel_create_with_resource_system(SYSTEM_APP, dialog->icon_id);
}
bool dialog_init_icon_layer(Dialog *dialog, KinoReel *image,
GPoint icon_origin, bool animated) {
if (!image) {
return false;
}
const GRect icon_rect = (GRect) {
.origin = icon_origin,
.size = kino_reel_get_size(image)
};
KinoLayer *icon_layer = &dialog->icon_layer;
kino_layer_init(icon_layer, &icon_rect);
layer_set_clips(&icon_layer->layer, false);
GRect from = icon_rect;
// Animate from off screen. We need to be at least -80, since that is our largest icon size.
const int16_t DISP_OFFSET = 80;
if (dialog->icon_anim_direction == DialogIconAnimationFromLeft) {
from.origin.x = -DISP_OFFSET;
} else if (dialog->icon_anim_direction == DialogIconAnimationFromRight) {
from.origin.x = DISP_OFFSET;
}
const int16_t ICON_TARGET_PT_X = icon_rect.size.w;
const int16_t ICON_TARGET_PT_Y = (icon_rect.size.h / 2);
KinoReel *reel = NULL;
if (animated) {
reel = kino_reel_scale_segmented_create(image, true, icon_rect);
kino_reel_transform_set_from_frame(reel, from);
kino_reel_transform_set_transform_duration(reel, 300);
kino_reel_scale_segmented_set_deflate_effect(reel, 10);
kino_reel_scale_segmented_set_delay_by_distance(
reel, GPoint(ICON_TARGET_PT_X, ICON_TARGET_PT_Y));
}
if (!reel) {
// Fall back to using the image reel as is which could be an animation without the scaling
reel = image;
}
kino_layer_set_reel(icon_layer, reel, true);
kino_layer_play(icon_layer);
uint32_t icon_duration = kino_reel_get_duration(image);
if (dialog->timeout != DIALOG_TIMEOUT_INFINITE && // Don't shorten infinite dialogs
icon_duration != PLAY_DURATION_INFINITE && // Don't extend dialogs with infinite animations
icon_duration > dialog->timeout) {
// The finite image animation is longer, increase the finite dialog timeout
dialog_set_timeout(dialog, icon_duration);
}
return true;
}
void dialog_add_status_bar_layer(Dialog *dialog, const GRect *status_layer_frame) {
StatusBarLayer *status_layer = &dialog->status_layer;
status_bar_layer_init(status_layer);
layer_set_frame(&status_layer->layer, status_layer_frame);
status_bar_layer_set_colors(status_layer, GColorClear, dialog->text_color);
layer_add_child(&dialog->window.layer, &status_layer->layer);
}

View File

@@ -0,0 +1,72 @@
/*
* 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/ui/dialogs/dialog.h"
#include "applib/ui/window_stack.h"
//! Initializes the dialog.
//! @param dialog Pointer to a \ref Dialog to initialize
//! @param dialog_name The debug name to give the dialog
void dialog_init(Dialog *dialog, const char *dialog_name);
//! Pushes the dialog onto the window stack.
//! @param dialog Pointer to a \ref Dialog to push
//! @param window_stack Pointer to a \ref WindowStack to push the \ref Dialog to
void dialog_push(Dialog *dialog, WindowStack *window_stack);
//! Wrapper to call \ref dialog_push() for an application
//! @note: Put a better comment here when we export
void app_dialog_push(Dialog *dialog);
//! Pops the dialog off the window stack.
//! @param dialog Pointer to a \ref Dialog to push
void dialog_pop(Dialog *dialog);
//! This function is called by each type of dialog's load functions to execute common dialog code.
//! @param dialog Pointer to a \ref Dialog to load
void dialog_load(Dialog *dialog);
//! Displays the icon by playing the kino layer
//! @param dialog Pointer to the \ref Dialog to appear
void dialog_appear(Dialog *dialog);
//! This function is called by each type of dialog's unload function. The dialog_context is the
//! the dialog object being unloaded.
//! @param dialog Pointer to a \ref Dialog to unload
void dialog_unload(Dialog *dialog);
//! Draw the status layer on the dialog.
//! @param dialog Pointer to a \ref Dialog to draw the status layer on
//! @param status_layer_frame The frame of the status layer
void dialog_add_status_bar_layer(Dialog *dialog, const GRect *status_layer_frame);
//! Create the icon for the dialog.
//! @param dialog Pointer to a \ref Dialog from which to grab the icon id
//! @return the \ref KinoReel for the dialog's icon
KinoReel *dialog_create_icon(Dialog *dialog);
//! Initialize the dialog's icon layer with the provided image and frame origin.
//! @param dialog Pointer to \ref Dialog from which to initialize it's \ref KinoLayer
//! @param image Pointer to a \ref KinoReel to put on the dialog's \ref KinoLayer
//! @param icon_origin The starting point for the icon, if it is being animated,
//! this is the point it will animate to.
//! @param animated `True` if animated, otherwise `False`
//! @return `True` if successfully initialized the dialog's \ref KinoLayer, otherwise
//! `False`
bool dialog_init_icon_layer(Dialog *dialog, KinoReel *image,
GPoint icon_origin, bool animated);

View File

@@ -0,0 +1,443 @@
/*
* 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 "expandable_dialog.h"
#include "applib/applib_malloc.auto.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/text.h"
#include "applib/ui/bitmap_layer.h"
#include "applib/ui/dialogs/dialog_private.h"
#include "applib/ui/layer.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/window.h"
#include "applib/ui/window_private.h"
#include "kernel/ui/kernel_ui.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "system/passert.h"
#include <limits.h>
#include <stdint.h>
#include <string.h>
static void prv_show_action_bar_icon(ExpandableDialog *expandable_dialog, ButtonId button_id) {
ActionBarLayer *action_bar = &expandable_dialog->action_bar;
const GBitmap *icon = (button_id ==
BUTTON_ID_UP) ? expandable_dialog->up_icon : expandable_dialog->down_icon;
action_bar_layer_set_icon_animated(action_bar, button_id, icon,
expandable_dialog->show_action_icon_animated);
ActionBarLayerIconPressAnimation animation = (button_id == BUTTON_ID_UP)
? ActionBarLayerIconPressAnimationMoveUp
: ActionBarLayerIconPressAnimationMoveDown;
action_bar_layer_set_icon_press_animation(action_bar, button_id, animation);
}
// Manually scrolls the scroll layer up or down. The manual scrolling is required so that the
// click handlers of the scroll layer and the action bar play nicely together.
static void prv_manual_scroll(ScrollLayer *scroll_layer, int8_t dir) {
scroll_layer_scroll(scroll_layer, dir, true);
}
static void prv_offset_changed_handler(ScrollLayer *scroll_layer, void *context) {
ExpandableDialog *expandable_dialog = context;
ActionBarLayer *action_bar = &expandable_dialog->action_bar;
GPoint offset = scroll_layer_get_content_offset(scroll_layer);
if (!expandable_dialog->show_action_bar) {
// Prematurely return if we are not showing the action bar.
return;
}
if (offset.y < 0) {
// We have scrolled down, so we want to display the up arrow.
prv_show_action_bar_icon(expandable_dialog, BUTTON_ID_UP);
} else if (offset.y == 0) {
// Hide the up arrow as we've reached the top.
action_bar_layer_clear_icon(action_bar, BUTTON_ID_UP);
}
Layer *layer = scroll_layer_get_layer(scroll_layer);
const GRect *bounds = &layer->bounds;
GSize content_size = scroll_layer_get_content_size(scroll_layer);
if (offset.y + content_size.h > bounds->size.h) {
// We have scrolled up, so we want to display the down arrow.
prv_show_action_bar_icon(expandable_dialog, BUTTON_ID_DOWN);
} else if (offset.y + content_size.h <= bounds->size.h) {
// Hide the down arrow as we've reached the bottom.
action_bar_layer_clear_icon(action_bar, BUTTON_ID_DOWN);
}
}
static void prv_up_click_handler(ClickRecognizerRef recognizer, void *context) {
ExpandableDialog *expandable_dialog = context;
prv_manual_scroll(&expandable_dialog->scroll_layer, 1);
}
static void prv_down_click_handler(ClickRecognizerRef recognizer, void *context) {
ExpandableDialog *expandable_dialog = context;
prv_manual_scroll(&expandable_dialog->scroll_layer, -1);
}
static void prv_config_provider(void *context) {
ExpandableDialog *expandable_dialog = context;
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, prv_up_click_handler);
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100, prv_down_click_handler);
if (expandable_dialog->select_click_handler) {
window_single_click_subscribe(BUTTON_ID_SELECT, expandable_dialog->select_click_handler);
}
}
static void prv_expandable_dialog_load(Window *window) {
ExpandableDialog *expandable_dialog = window_get_user_data(window);
Dialog *dialog = &expandable_dialog->dialog;
GRect frame = window->layer.bounds;
static const uint16_t ICON_TOP_MARGIN_PX = 16;
static const uint16_t BOTTOM_MARGIN_PX = 6;
static const uint16_t CONTENT_DOWN_ARROW_HEIGHT = PBL_IF_RECT_ELSE(16, 10);
// Small margin is shown when we have an action bar to fit more text on the line.
const uint16_t SM_LEFT_MARGIN_PX = 4;
// Normal margin shown when there is no action bar in the expandable dialog.
const uint16_t NM_LEFT_MARGIN_PX = 10;
bool show_action_bar = expandable_dialog->show_action_bar;
uint16_t left_margin_px = show_action_bar ? SM_LEFT_MARGIN_PX : NM_LEFT_MARGIN_PX;
uint16_t right_margin_px = left_margin_px;
bool has_header = *expandable_dialog->header ? true : false;
const GFont header_font = expandable_dialog->header_font;
int32_t header_content_height = has_header ? DISP_ROWS : 0;
uint16_t status_layer_offset = dialog->show_status_layer * STATUS_BAR_LAYER_HEIGHT;
uint16_t action_bar_offset = show_action_bar * ACTION_BAR_WIDTH;
uint16_t x = 0;
uint16_t y = 0;
uint16_t w = PBL_IF_RECT_ELSE(frame.size.w - action_bar_offset, frame.size.w);
uint16_t h = STATUS_BAR_LAYER_HEIGHT;
if (dialog->show_status_layer) {
dialog_add_status_bar_layer(dialog, &GRect(x, y, w, h));
}
GContext *ctx = graphics_context_get_current_context();
// Ownership of icon is taken over by KinoLayer in dialog_init_icon_layer() call below
KinoReel *icon = dialog_create_icon(dialog);
const GSize icon_size = icon ? kino_reel_get_size(icon) : GSizeZero;
uint16_t icon_offset = (icon ? ICON_TOP_MARGIN_PX - status_layer_offset : 0);
x = 0;
y = status_layer_offset;
w = frame.size.w;
h = frame.size.h - y;
ScrollLayer *scroll_layer = &expandable_dialog->scroll_layer;
scroll_layer_init(scroll_layer, &GRect(x, y, w, h));
layer_add_child(&window->layer, &scroll_layer->layer);
#if PBL_ROUND
uint16_t page_height = scroll_layer->layer.bounds.size.h;
#endif
// Set up the header if this dialog is set to have one.
GTextAlignment alignment = PBL_IF_RECT_ELSE(GTextAlignmentLeft,
(show_action_bar ?
GTextAlignmentRight : GTextAlignmentCenter));
uint16_t right_aligned_box_reduction = PBL_IF_RECT_ELSE(0, show_action_bar ? 10 : 0);
if (has_header) {
const uint16_t HEADER_OFFSET = 6;
#if PBL_RECT
x = left_margin_px;
w = frame.size.w - right_margin_px - left_margin_px - action_bar_offset
- right_aligned_box_reduction;
#else
x = 0;
w = frame.size.w - right_margin_px - action_bar_offset - right_aligned_box_reduction;
#endif
y = icon ? icon_offset + icon_size.h : -HEADER_OFFSET;
TextLayer *header_layer = &expandable_dialog->header_layer;
text_layer_init_with_parameters(header_layer, &GRect(x, y, w, header_content_height),
expandable_dialog->header, header_font, dialog->text_color,
GColorClear, alignment, GTextOverflowModeWordWrap);
// layer must be added immediately to scroll layer for perimeter and paging
scroll_layer_add_child(scroll_layer, &header_layer->layer);
#if PBL_ROUND
text_layer_enable_screen_text_flow_and_paging(header_layer, 8);
#endif
// We account for a header that may be larger than the available space
// by adjusting our height variable that is passed to the body layer
GSize header_size = text_layer_get_content_size(ctx, header_layer);
header_size.h += 4; // See PBL-1741
header_size.w = w;
header_content_height = header_size.h;
text_layer_set_size(header_layer, header_size);
}
// Set up the text.
const uint16_t TEXT_OFFSET = 6;
x = left_margin_px;
y = (icon ? icon_offset + icon_size.h : -TEXT_OFFSET) + header_content_height;
w = frame.size.w - right_margin_px - left_margin_px - action_bar_offset
- right_aligned_box_reduction;
h = INT16_MAX; // height is clamped to content size
GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
TextLayer *text_layer = &dialog->text_layer;
text_layer_init_with_parameters(text_layer, &GRect(x, y, w, h), dialog->buffer, font,
dialog->text_color, GColorClear,
alignment, GTextOverflowModeWordWrap);
// layer must be added immediately to scroll layer for perimeter and paging
scroll_layer_add_child(scroll_layer, &text_layer->layer);
#if PBL_ROUND
text_layer_set_line_spacing_delta(text_layer, -1);
text_layer_enable_screen_text_flow_and_paging(text_layer, 8);
#endif
int32_t text_content_height = text_layer_get_content_size(ctx, text_layer).h;
text_content_height += 4; // See PBL-1741
text_layer_set_size(text_layer, GSize(w, text_content_height));
uint16_t scroll_height = icon_offset + icon_size.h +
header_content_height + text_content_height + (icon ? BOTTOM_MARGIN_PX : 0);
scroll_layer_set_content_size(scroll_layer, GSize(frame.size.w, scroll_height));
scroll_layer_set_shadow_hidden(scroll_layer, true);
scroll_layer_set_callbacks(scroll_layer, (ScrollLayerCallbacks) {
.content_offset_changed_handler = prv_offset_changed_handler
});
scroll_layer_set_context(scroll_layer, expandable_dialog);
#if PBL_ROUND
scroll_layer_set_paging(scroll_layer, true);
#endif
if (show_action_bar) {
// Icons for up and down on the action bar.
#ifndef RECOVERY_FW
expandable_dialog->up_icon = gbitmap_create_with_resource_system(SYSTEM_APP,
RESOURCE_ID_ACTION_BAR_ICON_UP);
expandable_dialog->down_icon = gbitmap_create_with_resource_system(SYSTEM_APP,
RESOURCE_ID_ACTION_BAR_ICON_DOWN);
PBL_ASSERTN(expandable_dialog->down_icon && expandable_dialog->up_icon);
#endif
// Set up the Action bar.
ActionBarLayer *action_bar = &expandable_dialog->action_bar;
action_bar_layer_init(action_bar);
if (expandable_dialog->action_bar_background_color.a != 0) {
action_bar_layer_set_background_color(action_bar,
expandable_dialog->action_bar_background_color);
}
if (expandable_dialog->select_icon) {
action_bar_layer_set_icon_animated(action_bar, BUTTON_ID_SELECT,
expandable_dialog->select_icon, expandable_dialog->show_action_icon_animated);
}
action_bar_layer_set_context(action_bar, expandable_dialog);
action_bar_layer_set_click_config_provider(action_bar, prv_config_provider);
action_bar_layer_add_to_window(action_bar, window);
} else {
window_set_click_config_provider_with_context(window, prv_config_provider, expandable_dialog);
}
x = PBL_IF_RECT_ELSE(left_margin_px, (show_action_bar) ?
(frame.size.w - right_margin_px - left_margin_px -
action_bar_offset - right_aligned_box_reduction - icon_size.h) :
(90 - icon_size.h / 2));
y = icon_offset + PBL_IF_RECT_ELSE(0, 5);
if (dialog_init_icon_layer(dialog, icon, GPoint(x, y), false /* not animated */)) {
scroll_layer_add_child(scroll_layer, &dialog->icon_layer.layer);
}
// Check if we should show the down arrow by checking if we have enough content to warrant
// the scroll layer scrolling.
if (scroll_height > frame.size.h) {
if (show_action_bar) {
prv_show_action_bar_icon(expandable_dialog, BUTTON_ID_DOWN);
} else {
// If there isn't an action bar and there is more content than fits the screen
// setup the status layer and content_down_arrow_layer
ContentIndicator *indicator = scroll_layer_get_content_indicator(scroll_layer);
content_indicator_configure_direction(
indicator, ContentIndicatorDirectionUp,
&(ContentIndicatorConfig) {
.layer = &expandable_dialog->dialog.status_layer.layer,
.times_out = true,
.colors.foreground = dialog->text_color,
.colors.background = dialog->window.background_color,
});
layer_init(&expandable_dialog->content_down_arrow_layer, &GRect(
0, frame.size.h - CONTENT_DOWN_ARROW_HEIGHT,
PBL_IF_RECT_ELSE(frame.size.w - action_bar_offset, frame.size.w),
CONTENT_DOWN_ARROW_HEIGHT));
layer_add_child(&window->layer, &expandable_dialog->content_down_arrow_layer);
content_indicator_configure_direction(
indicator, ContentIndicatorDirectionDown,
&(ContentIndicatorConfig) {
.layer = &expandable_dialog->content_down_arrow_layer,
.times_out = false,
.alignment = PBL_IF_RECT_ELSE(GAlignCenter, GAlignTop),
.colors.foreground = dialog->text_color,
.colors.background = dialog->window.background_color,
});
}
}
dialog_load(dialog);
}
static void prv_expandable_dialog_appear(Window *window) {
ExpandableDialog *expandable_dialog = window_get_user_data(window);
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
scroll_layer_update_content_indicator(&expandable_dialog->scroll_layer);
dialog_appear(dialog);
}
static void prv_expandable_dialog_unload(Window *window) {
ExpandableDialog *expandable_dialog = window_get_user_data(window);
dialog_unload(&expandable_dialog->dialog);
if (expandable_dialog->show_action_bar) {
action_bar_layer_deinit(&expandable_dialog->action_bar);
}
gbitmap_destroy(expandable_dialog->up_icon);
gbitmap_destroy(expandable_dialog->down_icon);
if (*expandable_dialog->header) {
text_layer_deinit(&expandable_dialog->header_layer);
}
scroll_layer_deinit(&expandable_dialog->scroll_layer);
if (expandable_dialog->dialog.destroy_on_pop) {
applib_free(expandable_dialog);
}
}
Dialog *expandable_dialog_get_dialog(ExpandableDialog *expandable_dialog) {
return &expandable_dialog->dialog;
}
void expandable_dialog_init(ExpandableDialog *expandable_dialog, const char *dialog_name) {
PBL_ASSERTN(expandable_dialog);
*expandable_dialog = (ExpandableDialog) {
.header_font = fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD),
};
dialog_init(&expandable_dialog->dialog, dialog_name);
Window *window = &expandable_dialog->dialog.window;
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_expandable_dialog_load,
.unload = prv_expandable_dialog_unload,
.appear = prv_expandable_dialog_appear,
});
expandable_dialog->show_action_bar = true;
window_set_user_data(window, expandable_dialog);
}
ExpandableDialog *expandable_dialog_create(const char *dialog_name) {
// Note: Note exported so no padding necessary
ExpandableDialog *expandable_dialog = applib_type_malloc(ExpandableDialog);
if (expandable_dialog) {
expandable_dialog_init(expandable_dialog, dialog_name);
}
return expandable_dialog;
}
void expandable_dialog_close_cb(ClickRecognizerRef recognizer, void *e_dialog) {
expandable_dialog_pop(e_dialog);
}
ExpandableDialog *expandable_dialog_create_with_params(const char *dialog_name, ResourceId icon,
const char *text, GColor text_color,
GColor background_color,
DialogCallbacks *callbacks,
ResourceId select_icon,
ClickHandler select_click_handler) {
ExpandableDialog *expandable_dialog = expandable_dialog_create(dialog_name);
if (expandable_dialog) {
expandable_dialog_set_select_action(expandable_dialog, select_icon, select_click_handler);
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
dialog_set_icon(dialog, icon);
dialog_set_text(dialog, text);
dialog_set_background_color(dialog, background_color);
dialog_set_text_color(dialog, gcolor_legible_over(background_color));
dialog_set_callbacks(dialog, callbacks, dialog);
}
return expandable_dialog;
}
void expandable_dialog_show_action_bar(ExpandableDialog *expandable_dialog,
bool show_action_bar) {
expandable_dialog->show_action_bar = show_action_bar;
}
void expandable_dialog_set_action_icon_animated(ExpandableDialog *expandable_dialog,
bool animated) {
expandable_dialog->show_action_icon_animated = animated;
}
void expandable_dialog_set_action_bar_background_color(ExpandableDialog *expandable_dialog,
GColor background_color) {
expandable_dialog->action_bar_background_color = background_color;
}
void expandable_dialog_set_header(ExpandableDialog *expandable_dialog, const char *header) {
if (!header) {
expandable_dialog->header[0] = 0;
return;
}
strncpy(expandable_dialog->header, header, DIALOG_MAX_HEADER_LEN);
expandable_dialog->header[DIALOG_MAX_HEADER_LEN] = '\0';
}
void expandable_dialog_set_header_font(ExpandableDialog *expandable_dialog, GFont header_font) {
expandable_dialog->header_font = header_font;
}
void expandable_dialog_set_select_action(ExpandableDialog *expandable_dialog,
uint32_t resource_id,
ClickHandler select_click_handler) {
if (expandable_dialog->select_icon) {
gbitmap_destroy(expandable_dialog->select_icon);
expandable_dialog->select_icon = NULL;
}
if (resource_id != RESOURCE_ID_INVALID) {
expandable_dialog->select_icon = gbitmap_create_with_resource_system(SYSTEM_APP, resource_id);
}
expandable_dialog->select_click_handler = select_click_handler;
}
void expandable_dialog_push(ExpandableDialog *expandable_dialog, WindowStack *window_stack) {
dialog_push(&expandable_dialog->dialog, window_stack);
}
void app_expandable_dialog_push(ExpandableDialog *expandable_dialog) {
app_dialog_push(&expandable_dialog->dialog);
}
void expandable_dialog_pop(ExpandableDialog *expandable_dialog) {
dialog_pop(&expandable_dialog->dialog);
}

View File

@@ -0,0 +1,148 @@
/*
* 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"
#include "applib/ui/click.h"
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/scroll_layer.h"
#include "applib/ui/window_stack.h"
#include "resource/resource_ids.auto.h"
#include <stdint.h>
#define DIALOG_MAX_HEADER_LEN 30
// An ExpandableDialog is dialog that contains a large amount of text that can be scrolled. It also
// contains an action bar which indicates which directions can currently be scrolled and optionally
// a SELECT button action.
typedef struct ExpandableDialog {
Dialog dialog;
bool show_action_bar;
bool show_action_icon_animated;
GColor action_bar_background_color;
ActionBarLayer action_bar;
ClickHandler select_click_handler;
GBitmap *up_icon;
GBitmap *select_icon;
GBitmap *down_icon;
GFont header_font;
char header[DIALOG_MAX_HEADER_LEN + 1];
TextLayer header_layer;
ScrollLayer scroll_layer;
Layer content_down_arrow_layer;
} ExpandableDialog;
//! Creates a new ExpandableDialog on the heap.
//! @param dialog_name The name to give the dialog
//! @return Pointer to an \ref ExpandableDialog
ExpandableDialog *expandable_dialog_create(const char *dialog_name);
//! Creates a new ExpandableDialog on the heap with additional parameters
//! @param dialog_name The name to give the dialog
//! @param icon The icon which appears at the top of the dialog
//! @param text The text to display in the dialog
//! @param text_color The color of the dialog's text
//! @param background_color The background color of the dialog
//! @param callbacks The \ref DialogCallbacks to assign to the dialog
//! @param select_icon The icon to assign to the select button on the action menu
//! @param select_click_handler The \ref ClickHandler to assign to the select button
//! @return Pointer to an \ref ExpandableDialog
ExpandableDialog *expandable_dialog_create_with_params(const char *dialog_name, ResourceId icon,
const char *text, GColor text_color,
GColor background_color,
DialogCallbacks *callbacks,
ResourceId select_icon,
ClickHandler select_click_handler);
//! Simple callback which closes the dialog when called
void expandable_dialog_close_cb(ClickRecognizerRef recognizer, void *e_dialog);
//! Intializes an ExpandableDialog
//! @param expandable_dialog Pointer to an \ref ExpandableDialog
//! param dialog_name The name to give the \ref ExpandableDialog
void expandable_dialog_init(ExpandableDialog *expandable_dialog, const char *dialog_name);
//! Retrieves the internal Dialog object of the Expandable Dialog.
//! @param expandable_dialog The \ref ExpandableDialog to retrieve from.
//! @return \ref Dialog
Dialog *expandable_dialog_get_dialog(ExpandableDialog *expandable_dialog);
//! Sets whether or not the expandable dialog should should show its action bar.
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to set on
//! @param show_action_bar Boolean indicating whether to show the action bar
void expandable_dialog_show_action_bar(ExpandableDialog *expandable_dialog,
bool show_action_bar);
//! Sets whether to animate the action bar items.
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to set on
//! @param animated Boolean indicating whether or not to animate the icons
//! @note Unless \ref expandable_dialog_show_action_bar is called with true, this function
//! will not have any noticeable change on the \ref ExpandableDialog
void expandable_dialog_set_action_icon_animated(ExpandableDialog *expandable_dialog,
bool animated);
//! Sets the action bar background color
//! @param expandable_dialog Pointer to the \ref ExpandableDialog for which to set
//! @param background_color The background color of the dialog's action bar
void expandable_dialog_set_action_bar_background_color(ExpandableDialog *expandable_dialog,
GColor background_color);
//! Sets the text of the optional header text. The header has a maximum length of
//! \ref DIALOG_MAX_HEADER_LEN and the text passed in will be clipped if it exceeds that
//! length.
//! @param expandable_dialog Pointer to the \ref ExpandableDialog on which to set the header
//! @param header Text to set as the header.
//! @note If set to NULL, the header will not appear.
void expandable_dialog_set_header(ExpandableDialog *expandable_dialog, const char *header);
//! Sets the header font
//! @param expandable_dialog Pointer to the \ref ExpandableDialog on which to set the header
//! @param header_font The font to use for the header text
void expandable_dialog_set_header_font(ExpandableDialog *expandable_dialog, GFont header_font);
//! Sets the icon and ClickHandler of the SELECT button on the action bar.
//! @param expandable_dialog Pointer to the \ref ExpandableDialog for which to set
//! @param resource_id The resource id of the resource to be used to create the select bitmap
//! @param select_click_handler Handler to call when the select handler is clicked in the
//! Expandable Dialog's action bar layer.
//! @note Passing \ref RESOURCE_ID_INVALID as the resource_id to the function will allow you
//! to set an action with no icon appearing in the \ref ActionBarLayer
void expandable_dialog_set_select_action(ExpandableDialog *expandable_dialog,
uint32_t resource_id,
ClickHandler select_click_handler);
//! Pushes the dialog onto the window stack.
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to push.
//! @param window_stack Pointer to the \ref WindowStack to push the dialog to
void expandable_dialog_push(ExpandableDialog *expandable_dialog, WindowStack *window_stack);
//! Pushes the dialog onto the app's window stack
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to push.
//! @note: Put a better comment here before exporting
void app_expandable_dialog_push(ExpandableDialog *expandable_dialog);
//! Wrapper for popping the underlying dialog off of the window stack. Useful for when the
//! user overrides the default behaviour of the select action to allow them to pop the dialog.
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to pop.
void expandable_dialog_pop(ExpandableDialog *expandable_dialog);

View File

@@ -0,0 +1,227 @@
/*
* 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 "simple_dialog.h"
#include "applib/applib_malloc.auto.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/bitmap_layer.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/dialog_private.h"
#include "applib/ui/layer.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/window.h"
#include "kernel/ui/kernel_ui.h"
#include "system/passert.h"
#include <limits.h>
#include <string.h>
#if (RECOVERY_FW || UNITTEST)
#define SIMPLE_DIALOG_ANIMATED false
#else
#define SIMPLE_DIALOG_ANIMATED true
#endif
// Layout Defines
#define TEXT_ALIGNMENT (GTextAlignmentCenter)
#define TEXT_OVERFLOW (GTextOverflowModeWordWrap)
#define TEXT_FONT (fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD))
#define TEXT_LEFT_MARGIN_PX (PBL_IF_RECT_ELSE(6, 0))
#define TEXT_RIGHT_MARGIN_PX (PBL_IF_RECT_ELSE(6, 0))
#define TEXT_FLOW_INSET_PX (PBL_IF_RECT_ELSE(0, 8))
#define TEXT_LINE_HEIGHT_PX (fonts_get_font_height(TEXT_FONT))
#define TEXT_MAX_HEIGHT_PX ((2 * TEXT_LINE_HEIGHT_PX) + 8) // 2 line + some space for descenders
static int prv_get_rendered_text_height(const char *text, const GRect *text_box) {
GContext *ctx = graphics_context_get_current_context();
TextLayoutExtended layout = { 0 };
graphics_text_attributes_enable_screen_text_flow((GTextLayoutCacheRef) &layout,
TEXT_FLOW_INSET_PX);
return graphics_text_layout_get_max_used_size(ctx,
text,
TEXT_FONT,
*text_box,
TEXT_OVERFLOW,
TEXT_ALIGNMENT,
(GTextLayoutCacheRef) &layout).h;
}
static int prv_get_icon_top_margin(bool has_status_bar, int icon_height, int window_height) {
const uint16_t status_layer_offset = has_status_bar ? 6 : 0;
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
const uint16_t icon_top_default_margin_px = 42 + status_layer_offset;
#else
const uint16_t icon_top_default_margin_px = 18 + status_layer_offset;
#endif
const uint16_t frame_height_claimed = icon_height + TEXT_MAX_HEIGHT_PX + status_layer_offset;
const uint16_t icon_top_adjusted_margin_px = MAX(window_height - frame_height_claimed, 0);
// Try and use the default value if possible.
return (icon_top_adjusted_margin_px < icon_top_default_margin_px) ? icon_top_adjusted_margin_px :
icon_top_default_margin_px;
}
static void prv_get_text_box(GSize frame_size, GSize icon_size,
int icon_top_margin_px, GRect *text_box_out) {
const uint16_t icon_text_spacing_px = PBL_IF_ROUND_ELSE(2, 4);
const uint16_t text_x = TEXT_LEFT_MARGIN_PX;
const uint16_t text_y = icon_top_margin_px + MAX(icon_size.h, 6) + icon_text_spacing_px;
const uint16_t text_w = frame_size.w - TEXT_LEFT_MARGIN_PX - TEXT_RIGHT_MARGIN_PX;
// Limit to 2 lines if there is an icon
const uint16_t text_h = icon_size.h ? TEXT_MAX_HEIGHT_PX : frame_size.h - text_y;
*text_box_out = GRect(text_x, text_y, text_w, text_h);
}
static void prv_simple_dialog_load(Window *window) {
SimpleDialog *simple_dialog = window_get_user_data(window);
Dialog *dialog = &simple_dialog->dialog;
// Ownership of icon is taken over by KinoLayer in dialog_init_icon_layer() call below
KinoReel *icon = dialog_create_icon(dialog);
const GSize icon_size = icon ? kino_reel_get_size(icon) : GSizeZero;
GRect frame = window->layer.bounds;
// Status Layer
if (dialog->show_status_layer) {
dialog_add_status_bar_layer(dialog, &GRect(0, 0, frame.size.w, STATUS_BAR_LAYER_HEIGHT));
}
uint16_t icon_top_margin_px = prv_get_icon_top_margin(dialog->show_status_layer,
icon_size.h, frame.size.h);
// Text
GRect text_box;
prv_get_text_box(frame.size, icon_size, icon_top_margin_px, &text_box);
const uint16_t text_height = prv_get_rendered_text_height(dialog->buffer, &text_box);
if (text_height <= TEXT_LINE_HEIGHT_PX) {
const int additional_icon_top_offset_for_single_line_text_px = 13;
// Move the icon down by increasing the margin to vertically center things
icon_top_margin_px += additional_icon_top_offset_for_single_line_text_px;
// Move the text down as well to preserve spacing
// The -1 is there to preserve prior functionality ¯\_(ツ)_/¯
text_box.origin.y += additional_icon_top_offset_for_single_line_text_px - 1;
}
TextLayer *text_layer = &dialog->text_layer;
text_layer_init_with_parameters(text_layer, &text_box, dialog->buffer, TEXT_FONT,
dialog->text_color, GColorClear, TEXT_ALIGNMENT, TEXT_OVERFLOW);
layer_add_child(&window->layer, &text_layer->layer);
#if PBL_ROUND
text_layer_enable_screen_text_flow_and_paging(text_layer, TEXT_FLOW_INSET_PX);
#endif
// Icon
const GPoint icon_origin = GPoint((grect_get_max_x(&frame) - icon_size.w) / 2,
icon_top_margin_px);
if (dialog_init_icon_layer(dialog, icon, icon_origin, !simple_dialog->icon_static)) {
layer_add_child(&dialog->window.layer, &dialog->icon_layer.layer);
}
dialog_load(dialog);
}
static void prv_simple_dialog_appear(Window *window) {
SimpleDialog *simple_dialog = window_get_user_data(window);
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
dialog_appear(dialog);
}
static void prv_simple_dialog_unload(Window *window) {
SimpleDialog *simple_dialog = window_get_user_data(window);
dialog_unload(&simple_dialog->dialog);
if (simple_dialog->dialog.destroy_on_pop) {
applib_free(simple_dialog);
}
}
static void prv_click_handler(ClickRecognizerRef recognizer, void *context) {
SimpleDialog *simple_dialog = context;
if (!simple_dialog->buttons_disabled) {
dialog_pop(&simple_dialog->dialog);
}
}
static void prv_config_provider(void *context) {
// Simple dialogs are dimissed when any button is pushed.
window_single_click_subscribe(BUTTON_ID_SELECT, prv_click_handler);
window_single_click_subscribe(BUTTON_ID_UP, prv_click_handler);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_click_handler);
}
Dialog *simple_dialog_get_dialog(SimpleDialog *simple_dialog) {
return &simple_dialog->dialog;
}
void simple_dialog_push(SimpleDialog *simple_dialog, WindowStack *window_stack) {
dialog_push(&simple_dialog->dialog, window_stack);
}
void app_simple_dialog_push(SimpleDialog *simple_dialog) {
app_dialog_push(&simple_dialog->dialog);
}
void simple_dialog_init(SimpleDialog *simple_dialog, const char *dialog_name) {
PBL_ASSERTN(simple_dialog);
*simple_dialog = (SimpleDialog) {
.icon_static = !SIMPLE_DIALOG_ANIMATED,
};
dialog_init(&simple_dialog->dialog, dialog_name);
Window *window = &simple_dialog->dialog.window;
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_simple_dialog_load,
.unload = prv_simple_dialog_unload,
.appear = prv_simple_dialog_appear,
});
window_set_click_config_provider_with_context(window, prv_config_provider, simple_dialog);
window_set_user_data(window, simple_dialog);
}
SimpleDialog *simple_dialog_create(const char *dialog_name) {
SimpleDialog *simple_dialog = applib_malloc(sizeof(SimpleDialog));
if (simple_dialog) {
simple_dialog_init(simple_dialog, dialog_name);
}
return simple_dialog;
}
void simple_dialog_set_buttons_enabled(SimpleDialog *simple_dialog, bool enabled) {
simple_dialog->buttons_disabled = !enabled;
}
void simple_dialog_set_icon_animated(SimpleDialog *simple_dialog, bool animated) {
// This cannot be set after the window has been loaded
PBL_ASSERTN(!window_is_loaded(&simple_dialog->dialog.window));
simple_dialog->icon_static = !animated;
}
bool simple_dialog_does_text_fit(const char *text, GSize window_size,
GSize icon_size, bool has_status_bar) {
const uint16_t icon_top_margin_px = prv_get_icon_top_margin(has_status_bar, icon_size.h,
window_size.h);
GRect text_box;
prv_get_text_box(window_size, icon_size, icon_top_margin_px, &text_box);
return prv_get_rendered_text_height(text, &text_box) <= TEXT_MAX_HEIGHT_PX;
}

View 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.
*/
#pragma once
#include "applib/graphics/perimeter.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/window_stack.h"
//! Simple dialogs just contain a large icon and some text.
//! @internal
typedef struct SimpleDialog {
Dialog dialog;
bool buttons_disabled;
bool icon_static;
} SimpleDialog;
//! Creates a new SimpleDialog on the heap.
//! @param dialog_name The debug name to give the dialog
//! @return Pointer to a \ref SimpleDialog
SimpleDialog *simple_dialog_create(const char *dialog_name);
//! @internal
//! @param simple_dialog Pointer to a \ref SimpleDialog to initialize
//! @param dialog_name The debug name to give the dialog
void simple_dialog_init(SimpleDialog *simple_dialog, const char *dialog_name);
//! Retrieves the internal Dialog object from the SimpleDialog.
//! @param simple_dialog Pointer to a \ref SimpleDialog whom's dialog to retrieve
//! @return pointer to the underlying dialog of the \ref SimpleDialog
Dialog *simple_dialog_get_dialog(SimpleDialog *simple_dialog);
//! Push the \ref SimpleDialog onto the given window stack.
//! @param simple_dialog Pointer to a \ref SimpleDialog to push onto the window stack
//! @param window_stack Pointer to a \ref WindowStack to push the dialog onto
void simple_dialog_push(SimpleDialog *simple_dialog, WindowStack *window_stack);
//! Wrapper to call \ref simple_dialog_push() for an app
//! @param simple_dialog Pointer to a \ref SimpleDialog to push onto the app's window stack
//! @note: Put a better comment here before exporting
void app_simple_dialog_push(SimpleDialog *simple_dialog);
//! Disables buttons for a \ref SimpleDialog. Usually used in conjunction with
//! \ref dialog_set_timeout()
//! @param simple_dialog Pointer to a \ref SimpleDialog
//! @param enabled Boolean expressing whether buttons should be enabled for the dialog
void simple_dialog_set_buttons_enabled(SimpleDialog *simple_dialog, bool enabled);
//! Sets whether the dialog icon is animated
//! @param simple_dialog Pointer to a \ref SimpleDialog
//! @param animated Whether the icon should animate or not
void simple_dialog_set_icon_animated(SimpleDialog *simple_dialog, bool animated);
bool simple_dialog_does_text_fit(const char *text, GSize window_size,
GSize icon_size, bool has_status_bar);

View File

@@ -0,0 +1,113 @@
/*
* 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 "inverter_layer.h"
#include "applib/graphics/graphics.h"
#include "applib/applib_malloc.auto.h"
#include "system/passert.h"
#include <string.h>
inline static void prv_inverter_layer_update_proc_color(GContext *ctx) {
// ctx->draw_state.drawing_box is the correct rect when this function gets
// called through layer_render_tree(),
GRect rect = ctx->draw_state.drawing_box;
// invert bytes in rect
grect_clip(&rect, &ctx->dest_bitmap.bounds); // clip to display bounds
for (int16_t y = rect.origin.y; y < rect.origin.y + rect.size.h; y++) {
int16_t row_offset = y * ctx->dest_bitmap.row_size_bytes;
for (int16_t x = rect.origin.x; x < rect.origin.x + rect.size.w; x++) {
uint8_t *pixel_addr = &(((uint8_t*)ctx->dest_bitmap.addr)[row_offset + x]);
// Only invert the RGB and not the alpha
*pixel_addr = (~(*pixel_addr) & 0b00111111) | (*pixel_addr & 0b11000000);
}
}
graphics_context_mark_dirty_rect(ctx, ctx->draw_state.drawing_box);
}
inline static void prv_inverter_layer_update_proc_bw(GContext *ctx) {
// For 1Bit, just revert to the 2.x code.
GBitmap sub_bitmap;
GBitmap* context_bitmap = graphics_context_get_bitmap(ctx);
// ctx->draw_state.drawing_box is the correct rect when this function gets called through
// layer_render_tree(), although it might be nicer to have a function to map a rect to another
// coordinate system...
gbitmap_init_as_sub_bitmap(&sub_bitmap, context_bitmap, ctx->draw_state.drawing_box);
// The sub-bitmap might have different bounds than this layer:
// when the requested bounds lie outside of the original bitmap it will be clipped.
// The following work-around will make sure the sub-bitmap gets painted at
// exactly the same spot as it came from:
GRect rect = sub_bitmap.bounds;
rect.origin.x -= ctx->draw_state.drawing_box.origin.x;
rect.origin.y -= ctx->draw_state.drawing_box.origin.y;
graphics_context_set_compositing_mode(ctx, GCompOpAssignInverted);
graphics_draw_bitmap_in_rect(ctx, &sub_bitmap, &rect);
}
void inverter_layer_update_proc(InverterLayer *inverter, GContext* ctx) {
#if SCREEN_COLOR_DEPTH_BITS == 1
prv_inverter_layer_update_proc_bw(ctx);
#else
prv_inverter_layer_update_proc_color(ctx);
#endif
(void)inverter;
}
void inverter_layer_init(InverterLayer *inverter, const GRect *frame) {
if (inverter == NULL) {
return;
}
*inverter = (InverterLayer){};
inverter->layer.frame = *frame;
inverter->layer.bounds = (GRect){{0, 0}, frame->size};
inverter->layer.update_proc = (LayerUpdateProc)inverter_layer_update_proc;
layer_set_clips(&inverter->layer, true);
layer_mark_dirty(&(inverter->layer));
}
InverterLayer* inverter_layer_create(GRect frame) {
InverterLayer* layer = applib_type_malloc(InverterLayer);
if (layer) {
inverter_layer_init(layer, &frame);
}
return layer;
}
void inverter_layer_deinit(InverterLayer *inverter_layer) {
if (inverter_layer == NULL) {
return;
}
layer_deinit(&inverter_layer->layer);
}
void inverter_layer_destroy(InverterLayer *inverter_layer) {
if (inverter_layer == NULL) {
return;
}
inverter_layer_deinit(inverter_layer);
applib_free(inverter_layer);
}
Layer* inverter_layer_get_layer(InverterLayer *inverter_layer) {
if (inverter_layer == NULL) {
return NULL;
}
return &inverter_layer->layer;
}

View File

@@ -0,0 +1,82 @@
/*
* 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 "layer.h"
//! @file inverter_layer.h
//! @addtogroup UI
//! @{
//! @addtogroup Layer Layers
//! @{
//! @addtogroup InverterLayer
//! \brief Layer that inverts anything "below it".
//!
//! ![](inverter_layer.png)
//! This layer takes what has been drawn into the graphics context by layers
//! that are "behind" it in the layer hierarchy.
//! Then, the inverter layer uses its geometric information (bounds, frame) as
//! the area to invert in the graphics context. Inverting will cause black
//! pixels to become white and vice versa.
//!
//! The InverterLayer is useful, for example, to highlight the selected item
//! in a menu. In fact, the \ref MenuLayer itself uses InverterLayer to
//! accomplish its selection highlighting.
//! @{
//! Data structure of an InverterLayer
//! @note an `InverterLayer *` can safely be casted to a `Layer *` and can
//! thus be used with all other functions that take a `Layer *` as an argument.
//! <br/>For example, the following is legal:
//! \code{.c}
//! InverterLayer inverter_layer;
//! ...
//! layer_set_frame((Layer *)&inverter_layer, GRect(10, 10, 50, 50));
//! \endcode
typedef struct InverterLayer {
Layer layer;
} InverterLayer;
//! Initializes the InverterLayer and resets it to the defaults:
//! * Clips: `true`
//! * Hidden: `false`
//! @param inverter The inverter layer
//! @param frame The frame at which to initialize the layer
void inverter_layer_init(InverterLayer *inverter, const GRect *frame);
//! Creates a new InverterLayer on the heap and initializes it with the default values.
//! * Clips: `true`
//! * Hidden: `false`
//! @return A pointer to the InverterLayer. `NULL` if the InverterLayer could not
//! be created
InverterLayer* inverter_layer_create(GRect frame);
void inverter_layer_deinit(InverterLayer *inverter_layer);
//! Destroys an InverterLayer previously created by inverter_layer_create
void inverter_layer_destroy(InverterLayer* inverter_layer);
//! Gets the "root" Layer of the inverter layer, which is the parent for the sub-
//! layers used for its implementation.
//! @param inverter_layer Pointer to the InverterLayer for which to get the "root" Layer
//! @return The "root" Layer of the inverter layer.
//! @internal
//! @note The result is always equal to `(Layer *) inverter_layer`.
Layer* inverter_layer_get_layer(InverterLayer *inverter_layer);
//! @} // end addtogroup InverterLayer
//! @} // end addtogroup Layer
//! @} // end addtogroup UI

View File

@@ -0,0 +1,187 @@
/*
* 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 "kino_layer.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/graphics.h"
static void prv_update_proc(Layer *layer, GContext *ctx) {
KinoLayer *kino_layer = (KinoLayer *)layer;
// Fill background
if (kino_layer->background_color.a != 0) {
graphics_context_set_fill_color(ctx, kino_layer->background_color);
graphics_fill_rect(ctx, &kino_layer->layer.bounds);
}
// Draw Reel
KinoReel *reel = kino_player_get_reel(&kino_layer->player);
if (reel == NULL) {
return;
}
const GRect reel_bounds = kino_layer_get_reel_bounds(kino_layer);
kino_player_draw(&kino_layer->player, ctx, reel_bounds.origin);
}
//////////////////////
// Player Callbacks
//////////////////////
static void prv_player_frame_did_change(KinoPlayer *player, void *context) {
KinoLayer *kino_layer = context;
layer_mark_dirty((Layer *)kino_layer);
}
static void prv_player_did_stop(KinoPlayer *player, bool finished, void *context) {
KinoLayer *kino_layer = context;
if (kino_layer->callbacks.did_stop) {
kino_layer->callbacks.did_stop(kino_layer, finished, kino_layer->context);
}
}
///////////////////////////////////////////
// Kino Layer API
///////////////////////////////////////////
void kino_layer_init(KinoLayer *kino_layer, const GRect *frame) {
*kino_layer = (KinoLayer){};
// init layer
layer_init(&kino_layer->layer, frame);
layer_set_update_proc(&kino_layer->layer, prv_update_proc);
// init kino layer
kino_layer->background_color = GColorClear;
// init kino player
kino_player_set_callbacks(&kino_layer->player, (KinoPlayerCallbacks){
.frame_did_change = prv_player_frame_did_change,
.did_stop = prv_player_did_stop,
}, kino_layer);
}
void kino_layer_deinit(KinoLayer *kino_layer) {
kino_player_deinit(&kino_layer->player);
layer_deinit(&kino_layer->layer);
}
KinoLayer *kino_layer_create(GRect frame) {
KinoLayer *layer = applib_type_malloc(KinoLayer);
if (layer) {
kino_layer_init(layer, &frame);
}
return layer;
}
void kino_layer_destroy(KinoLayer *kino_layer) {
if (kino_layer == NULL) {
return;
}
kino_layer_deinit(kino_layer);
applib_free(kino_layer);
}
Layer *kino_layer_get_layer(KinoLayer *kino_layer) {
if (kino_layer) {
return &kino_layer->layer;
} else {
return NULL;
}
}
void kino_layer_set_reel(KinoLayer *kino_layer, KinoReel *reel, bool take_ownership) {
kino_player_set_reel(&kino_layer->player, reel, take_ownership);
}
void kino_layer_set_reel_with_resource(KinoLayer *kino_layer, uint32_t resource_id) {
kino_player_set_reel_with_resource(&kino_layer->player, resource_id);
}
void kino_layer_set_reel_with_resource_system(KinoLayer *kino_layer, ResAppNum app_num,
uint32_t resource_id) {
kino_player_set_reel_with_resource_system(&kino_layer->player, app_num, resource_id);
}
KinoReel *kino_layer_get_reel(KinoLayer *kino_layer) {
return kino_player_get_reel(&kino_layer->player);
}
KinoPlayer *kino_layer_get_player(KinoLayer *kino_layer) {
return &kino_layer->player;
}
void kino_layer_set_alignment(KinoLayer *kino_layer, GAlign alignment) {
kino_layer->alignment = alignment;
layer_mark_dirty(&kino_layer->layer);
}
void kino_layer_set_background_color(KinoLayer *kino_layer, GColor color) {
kino_layer->background_color = color;
layer_mark_dirty(&kino_layer->layer);
}
void kino_layer_play(KinoLayer *kino_layer) {
kino_player_play(&kino_layer->player);
}
void kino_layer_play_section(KinoLayer *kino_layer, uint32_t from_position, uint32_t to_position) {
kino_player_play_section(&kino_layer->player, from_position, to_position);
}
ImmutableAnimation *kino_layer_create_play_animation(KinoLayer *kino_layer) {
return kino_player_create_play_animation(&kino_layer->player);
}
ImmutableAnimation *kino_layer_create_play_section_animation(
KinoLayer *kino_layer, uint32_t from_position, uint32_t to_position) {
return kino_player_create_play_section_animation(&kino_layer->player, from_position,
to_position);
}
void kino_layer_pause(KinoLayer *kino_layer) {
kino_player_pause(&kino_layer->player);
}
void kino_layer_rewind(KinoLayer *kino_layer) {
kino_player_rewind(&kino_layer->player);
}
GColor kino_layer_get_background_color(KinoLayer *kino_layer) {
return kino_layer->background_color;
}
GAlign kino_layer_get_alignment(KinoLayer *kino_layer) {
return kino_layer->alignment;
}
GRect kino_layer_get_reel_bounds(KinoLayer *kino_layer) {
KinoPlayer *player = kino_layer_get_player(kino_layer);
KinoReel *reel = player ? kino_player_get_reel(player) : NULL;
if (!reel) {
return GRectZero;
}
const GSize size = kino_reel_get_size(reel);
GRect rect = (GRect){{0, 0}, size};
grect_align(&rect, &kino_layer->layer.bounds, kino_layer->alignment, false /*clips*/);
return rect;
}
void kino_layer_set_callbacks(KinoLayer *kino_layer, KinoLayerCallbacks callbacks, void *context) {
kino_layer->callbacks = callbacks;
kino_layer->context = context;
}

View File

@@ -0,0 +1,86 @@
/*
* 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 "kino_reel.h"
#include "kino_player.h"
#include "applib/ui/layer.h"
struct KinoLayer;
typedef struct KinoLayer KinoLayer;
typedef void (*KinoLayerDidStopCb)(KinoLayer *kino_layer, bool finished, void *context);
typedef struct {
KinoLayerDidStopCb did_stop;
} KinoLayerCallbacks;
struct KinoLayer {
Layer layer;
KinoPlayer player;
GColor background_color;
GAlign alignment;
KinoLayerCallbacks callbacks;
void *context;
};
void kino_layer_init(KinoLayer *kino_layer, const GRect *frame);
void kino_layer_deinit(KinoLayer *kino_layer);
KinoLayer *kino_layer_create(GRect frame);
void kino_layer_destroy(KinoLayer *kino_layer);
Layer *kino_layer_get_layer(KinoLayer *kino_layer);
void kino_layer_set_reel(KinoLayer *kino_layer, KinoReel *reel, bool take_ownership);
//! @internal
void kino_layer_set_reel_with_resource(KinoLayer *kino_layer, uint32_t resource_id);
void kino_layer_set_reel_with_resource_system(KinoLayer *kino_layer, ResAppNum app_num,
uint32_t resource_id);
KinoReel *kino_layer_get_reel(KinoLayer *kino_layer);
KinoPlayer *kino_layer_get_player(KinoLayer *kino_layer);
GColor kino_layer_get_background_color(KinoLayer *kino_layer);
GAlign kino_layer_get_alignment(KinoLayer *kino_layer);
void kino_layer_set_alignment(KinoLayer *kino_layer, GAlign alignment);
void kino_layer_set_background_color(KinoLayer *kino_layer, GColor color);
void kino_layer_play(KinoLayer *kino_layer);
void kino_layer_play_section(KinoLayer *kino_layer, uint32_t from_position, uint32_t to_position);
ImmutableAnimation *kino_layer_create_play_animation(KinoLayer *kino_layer);
ImmutableAnimation *kino_layer_create_play_section_animation(
KinoLayer *kino_layer, uint32_t from_position, uint32_t to_position);
void kino_layer_pause(KinoLayer *kino_layer);
void kino_layer_rewind(KinoLayer *kino_layer);
GRect kino_layer_get_reel_bounds(KinoLayer *kino_layer);
void kino_layer_set_callbacks(KinoLayer *kino_layer, KinoLayerCallbacks callbacks, void *context);

View File

@@ -0,0 +1,218 @@
/*
* 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 "kino_player.h"
#include <limits.h>
#include "applib/ui/animation_interpolate.h"
#include "system/logging.h"
#include "util/math.h"
//////////////////////////////////
// Callbacks
//////////////////////////////////
static void prv_announce_frame_did_change(KinoPlayer *player, bool frame_changed) {
if (player->callbacks.frame_did_change && frame_changed) {
player->callbacks.frame_did_change(player, player->context);
}
}
static void prv_announce_did_stop(KinoPlayer *player, bool finished) {
if (player->callbacks.did_stop) {
player->callbacks.did_stop(player, finished, player->context);
}
}
///////////////////////////////
// Play Animation
///////////////////////////////
T_STATIC void prv_play_animation_update(Animation *animation, const AnimationProgress normalized) {
KinoPlayer *player = animation_get_context(animation);
int32_t animation_elapsed_ms = 0;
uint32_t elapsed_ms = 0;
uint32_t kino_reel_duration = kino_reel_get_duration(player->reel);
bool is_reel_infinite = (kino_reel_duration == PLAY_DURATION_INFINITE);
bool is_animation_reversed = animation_get_reverse(animation);
bool is_animation_infinite =
(animation_get_duration(animation, false, false) == PLAY_DURATION_INFINITE);
if (!is_animation_infinite && !is_reel_infinite) {
// If neither animation nor reel is infinite
elapsed_ms = interpolate_uint32(normalized, player->from_elapsed_ms, player->to_elapsed_ms);
} else if ((is_animation_infinite || is_reel_infinite) && !is_animation_reversed) {
// If either animation or reel is infinite and animation is not reversed
animation_get_elapsed(animation, &animation_elapsed_ms);
elapsed_ms = (int32_t)player->from_elapsed_ms + animation_elapsed_ms;
} else if (is_animation_infinite && !is_reel_infinite && is_animation_reversed) {
// If animation is infinite, reel is not infinite and animation is reversed
animation_get_elapsed(animation, &animation_elapsed_ms);
elapsed_ms = MAX(0, (int32_t)player->to_elapsed_ms - animation_elapsed_ms);
} else {
elapsed_ms = player->to_elapsed_ms;
}
bool frame_changed = kino_reel_set_elapsed(player->reel, elapsed_ms);
prv_announce_frame_did_change(player, frame_changed);
}
static void prv_play_anim_stopped(Animation *anim, bool finished, void *context) {
KinoPlayer *player = context;
player->animation = NULL;
prv_announce_did_stop(player, finished);
}
static const AnimationImplementation s_play_animation_impl = {
.update = prv_play_animation_update,
};
static const AnimationHandlers s_play_anim_handlers = {
.stopped = prv_play_anim_stopped,
};
//////////////////////////////////
// API
//////////////////////////////////
void kino_player_set_callbacks(KinoPlayer *player, KinoPlayerCallbacks callbacks, void *context) {
player->callbacks = callbacks;
player->context = context;
}
void kino_player_set_reel(KinoPlayer *player, KinoReel *reel, bool take_ownership) {
if (!player) {
return;
}
// stop any ongoing animation
kino_player_pause(player);
// delete the old reel if owned
if (player->reel && player->owns_reel && player->reel != reel) {
kino_reel_destroy(player->reel);
}
player->reel = reel;
player->owns_reel = take_ownership;
prv_announce_frame_did_change(player, true /*frame_changed*/);
}
void kino_player_set_reel_with_resource(KinoPlayer *player, uint32_t resource_id) {
kino_player_set_reel(player, NULL, false);
KinoReel *new_reel = kino_reel_create_with_resource(resource_id);
kino_player_set_reel(player, new_reel, true);
}
void kino_player_set_reel_with_resource_system(KinoPlayer *player, ResAppNum app_num,
uint32_t resource_id) {
kino_player_set_reel(player, NULL, false);
KinoReel *new_reel = kino_reel_create_with_resource_system(app_num, resource_id);
kino_player_set_reel(player, new_reel, true);
}
KinoReel *kino_player_get_reel(KinoPlayer *player) {
return player->reel;
}
static void prv_create_play_animation(KinoPlayer *player, uint32_t from_value, uint32_t to_value) {
// stop any ongoing animation
kino_player_pause(player);
player->from_elapsed_ms = from_value;
player->to_elapsed_ms = to_value;
Animation *animation = animation_create();
if (!animation) {
return;
}
animation_set_implementation(animation, &s_play_animation_impl);
animation_set_curve(animation, AnimationCurveLinear);
animation_set_duration(animation, to_value - from_value);
animation_set_handlers(animation, s_play_anim_handlers, (void *)player);
animation_set_immutable(animation);
player->animation = animation;
}
void kino_player_play(KinoPlayer *player) {
Animation *animation = (Animation *)kino_player_create_play_animation(player);
if (animation) {
animation_schedule(animation);
}
}
void kino_player_play_section(KinoPlayer *player, uint32_t from_elapsed_ms,
uint32_t to_elapsed_ms) {
if (player && player->reel) {
kino_reel_set_elapsed(player->reel, from_elapsed_ms);
prv_create_play_animation(player, from_elapsed_ms, to_elapsed_ms);
animation_schedule(player->animation);
}
}
ImmutableAnimation *kino_player_create_play_animation(KinoPlayer *player) {
if (player && player->reel) {
const uint32_t from_value = kino_reel_get_elapsed(player->reel);
const uint32_t to_value = kino_reel_get_duration(player->reel);
prv_create_play_animation(player, from_value, to_value);
return (ImmutableAnimation *)player->animation;
}
return NULL;
}
ImmutableAnimation *kino_player_create_play_section_animation(
KinoPlayer *player, uint32_t from_elapsed_ms, uint32_t to_elapsed_ms) {
if (player && player->reel) {
prv_create_play_animation(player, from_elapsed_ms, to_elapsed_ms);
return (ImmutableAnimation *)player->animation;
}
return NULL;
}
void kino_player_pause(KinoPlayer *player) {
if (player && player->reel) {
animation_unschedule(player->animation);
player->animation = NULL;
}
}
void kino_player_rewind(KinoPlayer *player) {
if (player && player->reel) {
// first pause the player, in case it is running
kino_player_pause(player);
// reset the elapsed time to the start
bool frame_changed = kino_reel_set_elapsed(player->reel, 0);
prv_announce_frame_did_change(player, frame_changed);
}
}
void kino_player_draw(KinoPlayer *player, GContext *ctx, GPoint offset) {
if (player && player->reel) {
kino_reel_draw(player->reel, ctx, offset);
}
}
void kino_player_deinit(KinoPlayer *player) {
player->callbacks = (KinoPlayerCallbacks) { 0 };
kino_player_set_reel(player, NULL, false);
}

View File

@@ -0,0 +1,79 @@
/*
* 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 "kino_reel.h"
#include "applib/ui/animation.h"
struct KinoPlayer;
typedef struct KinoPlayer KinoPlayer;
typedef void (*KinoPlayerFrameDidChangeCb)(KinoPlayer *player, void *context);
typedef void (*KinoPlayerDidStopCb)(KinoPlayer *player, bool finished, void *context);
typedef struct {
KinoPlayerFrameDidChangeCb frame_did_change;
KinoPlayerDidStopCb did_stop;
} KinoPlayerCallbacks;
struct KinoPlayer {
KinoReel *reel;
bool owns_reel;
Animation *animation;
KinoPlayerCallbacks callbacks;
uint32_t from_elapsed_ms;
uint32_t to_elapsed_ms;
void *context;
};
void kino_player_set_callbacks(KinoPlayer *player, KinoPlayerCallbacks callbacks, void *context);
void kino_player_set_reel(KinoPlayer *player, KinoReel *reel, bool take_ownership);
//! @internal
void kino_player_set_reel_with_resource(KinoPlayer *player, uint32_t resource_id);
void kino_player_set_reel_with_resource_system(KinoPlayer *player, ResAppNum app_num,
uint32_t resource_id);
KinoReel *kino_player_get_reel(KinoPlayer *player);
void kino_player_play(KinoPlayer *player);
void kino_player_play_section(KinoPlayer *player, uint32_t from_elapsed_ms, uint32_t to_elapsed_ms);
//! @internal
//! Creates a play animation that can be composed with complex animations. This animation will call
//! the KinoPlayer callbacks when it animates just as directly playing the KinoPlayer would.
//! Creating another play animation or directly playing, pausing or rewinding the player will
//! immediately unschedule the returned animation, even if it has not been scheduled yet.
//! /note The returned animation is an immutable animation and thus does not have the full range of
//! animation setters available for use. It is as though it has already been scheduled.
//! @param player KinoPlayer to create a play animation of
//! @return a pointer to a ImmutableAnimation object that plays the KinoPlayer when scheduled
ImmutableAnimation *kino_player_create_play_animation(KinoPlayer *player);
ImmutableAnimation *kino_player_create_play_section_animation(
KinoPlayer *player, uint32_t from_elapsed_ms, uint32_t to_elapsed_ms);
void kino_player_pause(KinoPlayer *player);
void kino_player_rewind(KinoPlayer *player);
void kino_player_deinit(KinoPlayer *player);
void kino_player_draw(KinoPlayer *player, GContext *ctx, GPoint offset);

View File

@@ -0,0 +1,179 @@
/*
* 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 "kino_reel.h"
#include "kino_reel_custom.h"
#include "kino_reel_pdci.h"
#include "kino_reel_pdcs.h"
#include "kino_reel_gbitmap.h"
#include "kino_reel_gbitmap_sequence.h"
#include "applib/graphics/gdraw_command.h"
#include "applib/graphics/gdraw_command_private.h"
#include "applib/graphics/gbitmap_png.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "util/net.h"
KinoReel *kino_reel_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return kino_reel_create_with_resource_system(app_num, resource_id);
}
KinoReel *kino_reel_create_with_resource_system(ResAppNum app_num, uint32_t resource_id) {
if (resource_id == RESOURCE_ID_INVALID) {
return NULL;
}
// The first 4 bytes for media data files contains the type signature (except legacy PBI)
uint32_t data_signature;
if (sys_resource_load_range(app_num, resource_id, 0, (uint8_t*)&data_signature,
sizeof(data_signature)) != sizeof(data_signature)) {
return NULL;
}
switch (ntohl(data_signature)) {
case PDCS_SIGNATURE:
return kino_reel_pdcs_create_with_resource_system(app_num, resource_id);
case PDCI_SIGNATURE:
return kino_reel_pdci_create_with_resource_system(app_num, resource_id);
case PNG_SIGNATURE:
{
bool is_apng = false;
// Check if the PNG is an APNG by seeking for the actl chunk
png_seek_chunk_in_resource_system(app_num, resource_id, PNG_HEADER_SIZE, true, &is_apng);
if (is_apng) {
return kino_reel_gbitmap_sequence_create_with_resource_system(app_num, resource_id);
} else {
return kino_reel_gbitmap_create_with_resource_system(app_num, resource_id);
}
}
default:
// We don't have any good way to validate that something
// is indeed a gbitmap. We use it as our fallback.
return kino_reel_gbitmap_create_with_resource_system(app_num, resource_id);
}
return NULL;
}
void kino_reel_destroy(KinoReel *reel) {
if (reel) {
reel->impl->destructor(reel);
}
}
void kino_reel_draw_processed(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
if (reel) {
reel->impl->draw_processed(reel, ctx, offset, processor);
}
}
void kino_reel_draw(KinoReel *reel, GContext *ctx, GPoint offset) {
kino_reel_draw_processed(reel, ctx, offset, NULL);
}
GSize kino_reel_get_size(KinoReel *reel) {
if (reel && reel->impl->get_size) {
return reel->impl->get_size(reel);
}
return GSize(0, 0);
}
size_t kino_reel_get_data_size(const KinoReel *reel) {
if (reel && reel->impl->get_data_size) {
return reel->impl->get_data_size(reel);
}
return 0;
}
bool kino_reel_set_elapsed(KinoReel *reel, uint32_t elapsed) {
if (reel && reel->impl->set_elapsed) {
return reel->impl->set_elapsed(reel, elapsed);
}
return false;
}
uint32_t kino_reel_get_elapsed(KinoReel *reel) {
if (reel && reel->impl->get_elapsed) {
return reel->impl->get_elapsed(reel);
}
return 0;
}
uint32_t kino_reel_get_duration(KinoReel *reel) {
if (reel && reel->impl->get_duration) {
return reel->impl->get_duration(reel);
}
return 0;
}
GDrawCommandImage *kino_reel_get_gdraw_command_image(KinoReel *reel) {
if (reel && reel->impl->get_gdraw_command_image) {
return reel->impl->get_gdraw_command_image(reel);
}
return NULL;
}
GDrawCommandList *kino_reel_get_gdraw_command_list(KinoReel *reel) {
if (reel && reel->impl->get_gdraw_command_list) {
return reel->impl->get_gdraw_command_list(reel);
}
return NULL;
}
GDrawCommandSequence *kino_reel_get_gdraw_command_sequence(KinoReel *reel) {
if (reel && reel->impl->get_gdraw_command_sequence) {
return reel->impl->get_gdraw_command_sequence(reel);
}
return NULL;
}
GBitmap *kino_reel_get_gbitmap(KinoReel *reel) {
if (reel && reel->impl->get_gbitmap) {
return reel->impl->get_gbitmap(reel);
}
return NULL;
}
GBitmapSequence* kino_reel_get_gbitmap_sequence(KinoReel *reel) {
if (reel && reel->impl->get_gbitmap_sequence) {
return reel->impl->get_gbitmap_sequence(reel);
}
return NULL;
}
KinoReelType kino_reel_get_type(KinoReel *reel) {
if (reel) {
return reel->impl->reel_type;
}
return KinoReelTypeInvalid;
}

View 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 <stdlib.h>
#include "applib/graphics/gbitmap_sequence.h"
#include "applib/graphics/gdraw_command_image.h"
#include "applib/graphics/gdraw_command_sequence.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
struct KinoReel;
typedef struct KinoReel KinoReel;
struct KinoReelImpl;
typedef struct KinoReelImpl KinoReelImpl;
struct KinoReelProcessor;
typedef struct KinoReelProcessor KinoReelProcessor;
typedef void (*KinoReelDestructor)(KinoReel *reel);
typedef uint32_t (*KinoReelElapsedGetter)(KinoReel *reel);
typedef bool (*KinoReelElapsedSetter)(KinoReel *reel, uint32_t elapsed_ms);
typedef uint32_t (*KinoReelDurationGetter)(KinoReel *reel);
#pragma push_macro("GSize")
#undef GSize
typedef GSize (*KinoReelSizeGetter)(KinoReel *reel);
#pragma pop_macro("GSize")
typedef size_t (*KinoReelDataSizeGetter)(const KinoReel *reel);
typedef void (*KinoReelDrawProcessedFunc)(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor);
typedef GDrawCommandImage* (*KinoReelGDrawCommandImageGetter)(KinoReel *reel);
typedef GDrawCommandList* (*KinoReelGDrawCommandListGetter)(KinoReel *reel);
typedef GDrawCommandSequence* (*KinoReelGDrawCommandSequenceGetter)(KinoReel *reel);
typedef GBitmap* (*KinoReelGBitmapGetter)(KinoReel *reel);
typedef GBitmapSequence* (*KinoReelGBitmapSequenceGetter)(KinoReel *reel);
struct KinoReelProcessor {
GBitmapProcessor * const bitmap_processor;
GDrawCommandProcessor * const draw_command_processor;
};
typedef enum {
KinoReelTypeInvalid = 0,
KinoReelTypeGBitmap,
KinoReelTypeGBitmapSequence,
KinoReelTypePDCI,
KinoReelTypePDCS,
KinoReelTypeCustom,
} KinoReelType;
struct KinoReelImpl {
KinoReelType reel_type;
KinoReelDestructor destructor;
KinoReelElapsedSetter set_elapsed;
KinoReelElapsedGetter get_elapsed;
KinoReelDurationGetter get_duration;
KinoReelSizeGetter get_size;
KinoReelDataSizeGetter get_data_size;
KinoReelDrawProcessedFunc draw_processed;
// Kino Reel data retrieval, allows access to underlying assets
KinoReelGDrawCommandImageGetter get_gdraw_command_image;
KinoReelGDrawCommandListGetter get_gdraw_command_list;
KinoReelGDrawCommandSequenceGetter get_gdraw_command_sequence;
KinoReelGBitmapGetter get_gbitmap;
KinoReelGBitmapSequenceGetter get_gbitmap_sequence;
};
struct KinoReel {
const KinoReelImpl *impl;
};
KinoReel *kino_reel_create_with_resource(uint32_t resource_id);
KinoReel *kino_reel_create_with_resource_system(ResAppNum app_num, uint32_t resource_id);
void kino_reel_destroy(KinoReel *reel);
void kino_reel_draw_processed(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor);
void kino_reel_draw(KinoReel *reel, GContext *ctx, GPoint offset);
bool kino_reel_set_elapsed(KinoReel *reel, uint32_t elapsed_ms);
uint32_t kino_reel_get_elapsed(KinoReel *reel);
uint32_t kino_reel_get_duration(KinoReel *reel);
GSize kino_reel_get_size(KinoReel *reel);
size_t kino_reel_get_data_size(const KinoReel *reel);
GDrawCommandImage *kino_reel_get_gdraw_command_image(KinoReel *reel);
GDrawCommandList *kino_reel_get_gdraw_command_list(KinoReel *reel);
GDrawCommandSequence *kino_reel_get_gdraw_command_sequence(KinoReel *reel);
GBitmap *kino_reel_get_gbitmap(KinoReel *reel);
GBitmapSequence *kino_reel_get_gbitmap_sequence(KinoReel *reel);
KinoReelType kino_reel_get_type(KinoReel *reel);

View File

@@ -0,0 +1,76 @@
/*
* 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 "morph_square.h"
#include "transform.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/gdraw_command_transforms.h"
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/kino/kino_reel_custom.h"
#include "applib/ui/animation_timing.h"
typedef struct {
KinoReel *reel;
} MorphSquareData;
static void prv_destructor(void *context) {
MorphSquareData *data = context;
applib_free(data);
}
static void prv_apply_transform(GDrawCommandList *list, const GSize size, const GRect *from,
const GRect *to, AnimationProgress normalized, void *context) {
MorphSquareData *data = context;
AnimationProgress curved;
if (!kino_reel_transform_get_to_reel(data->reel)) {
curved = animation_timing_curve(normalized, AnimationCurveEaseInOut);
} else if (normalized < ANIMATION_NORMALIZED_MAX / 2) {
curved = animation_timing_curve(2 * normalized, AnimationCurveEaseInOut);
} else {
curved = animation_timing_curve(2 * (ANIMATION_NORMALIZED_MAX - normalized),
AnimationCurveEaseInOut);
}
gdraw_command_list_attract_to_square(list, size, curved);
}
static const TransformImpl MORPH_SQUARE_TRANSFORM_IMPL = {
.destructor = prv_destructor,
.apply = prv_apply_transform,
};
KinoReel *kino_reel_morph_square_create(KinoReel *from_reel, bool take_ownership) {
MorphSquareData *data = applib_malloc(sizeof(MorphSquareData));
if (!data) {
return NULL;
}
GRect frame = { GPointZero, kino_reel_get_size(from_reel) };
KinoReel *reel = kino_reel_transform_create(&MORPH_SQUARE_TRANSFORM_IMPL, data);
if (reel) {
data->reel = reel;
kino_reel_transform_set_from_reel(reel, from_reel, take_ownership);
kino_reel_transform_set_layer_frame(reel, frame);
kino_reel_transform_set_from_frame(reel, frame);
kino_reel_transform_set_to_frame(reel, frame);
} else {
prv_destructor(data);
}
return reel;
}

View 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/ui/kino/kino_reel.h"
//! A KinoReel that can transform an image to a square or an image to another
//! with a square as an intermediate.
//! @param from_reel KinoReel to begin with
//! @param take_ownership true if this KinoReel will free `image` when destroyed.
//! @return a morph to square KinoReel
//! @see gpoint_attract_to_square
KinoReel *kino_reel_morph_square_create(KinoReel *from_reel, bool take_ownership);

View File

@@ -0,0 +1,295 @@
/*
* 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 "transform.h"
#include "scale_segmented.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/gdraw_command_transforms.h"
#include "applib/ui/animation.h"
#include "applib/ui/animation_interpolate.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/kino/kino_reel.h"
#include "system/logging.h"
typedef struct {
GPoint bounce;
GPointIndexLookup *index_lookup;
InterpolateInt64Function interpolate;
Fixed_S32_16 point_duration;
Fixed_S32_16 effect_duration;
int16_t expand;
struct {
AnimationCurveFunction curve;
Fixed_S16_3 from;
Fixed_S16_3 to;
GStrokeWidthOp from_op;
GStrokeWidthOp to_op;
} stroke_width;
struct {
GPointIndexLookupCreator creator;
void *userdata;
bool owns_userdata;
} lookup;
} ScaleSegmentedData;
typedef struct {
GPoint target;
} DistanceLookupData;
static GPointIndexLookup *prv_create_lookup_by_distance(GDelayCreatorContext *ctx, void *userdata) {
ctx->owns_lookup = true;
DistanceLookupData *data = userdata;
return gdraw_command_list_create_index_lookup_by_distance(ctx->list, data->target);
};
static void prv_destructor(void *context) {
ScaleSegmentedData *data = context;
if (data->lookup.owns_userdata) {
applib_free(data->lookup.userdata);
}
applib_free(context);
}
static void prv_apply_transform(GDrawCommandList *list, GSize size, const GRect *from,
const GRect *to, AnimationProgress normalized, void *context) {
if (!list || !context) {
return;
}
ScaleSegmentedData *data = context;
GDelayCreatorContext delay_ctx = {
.list = list,
.size = size,
};
if (!data->lookup.creator) {
return;
}
GPointIndexLookup *index_lookup = data->lookup.creator(&delay_ctx, data->lookup.userdata);
GRect intermediate;
AnimationProgress second_normalized;
const bool two_stage = (data->expand || data->bounce.x || data->bounce.y);
if (two_stage) {
intermediate = grect_scalar_expand(*to, data->expand);
gpoint_add_eq(&intermediate.origin, data->bounce);
const AnimationProgress first_normalized = animation_timing_segmented(
normalized, 0, 2, data->effect_duration);
gdraw_command_list_scale_segmented_to(
list, size, *from, intermediate, first_normalized, data->interpolate, index_lookup,
data->point_duration, false);
size = intermediate.size;
second_normalized = animation_timing_segmented(normalized, 1, 2, data->effect_duration);
} else {
intermediate = *from;
second_normalized = normalized;
}
gdraw_command_list_scale_segmented_to(
list, size, intermediate, *to, second_normalized, data->interpolate, index_lookup,
data->point_duration, two_stage);
const AnimationProgress stroke_width_progress = data->stroke_width.curve ?
data->stroke_width.curve(normalized) :
animation_timing_curve(normalized, AnimationCurveEaseInOut);
gdraw_command_list_scale_stroke_width(
list, data->stroke_width.from, data->stroke_width.to,
data->stroke_width.from_op, data->stroke_width.to_op, stroke_width_progress);
if (delay_ctx.owns_lookup) {
applib_free(index_lookup);
}
}
static GPoint prv_calc_bounce_offset(GRect from, GRect to, int16_t bounce) {
GPoint bounce_offset = GPointZero;
const int16_t delta_x = to.origin.x - from.origin.x;
const int16_t delta_y = to.origin.y - from.origin.y;
if (!delta_x && !delta_y) {
return bounce_offset;
}
const int16_t magnitude = integer_sqrt(delta_x * delta_x + delta_y * delta_y);
if (!magnitude) {
return bounce_offset;
}
bounce_offset.x = bounce * delta_x / magnitude;
bounce_offset.y = bounce * delta_y / magnitude;
return bounce_offset;
}
static const TransformImpl SCALE_SEGMENTED_TRANSFORM_IMPL = {
.destructor = prv_destructor,
.apply = prv_apply_transform,
};
KinoReel *kino_reel_scale_segmented_create(KinoReel *from_reel, bool take_ownership,
GRect screen_frame) {
ScaleSegmentedData *data = applib_malloc(sizeof(ScaleSegmentedData));
if (!data) {
return NULL;
}
*data = (ScaleSegmentedData) {
.point_duration = SCALE_SEGMENTED_DEFAULT_POINT_DURATION,
.effect_duration = SCALE_SEGMENTED_DEFAULT_EFFECT_DURATION,
.stroke_width = {
.from = FIXED_S16_3_ONE,
.to = FIXED_S16_3_ONE,
.from_op = GStrokeWidthOpMultiply,
.to_op = GStrokeWidthOpMultiply,
},
};
KinoReel *reel = kino_reel_transform_create(&SCALE_SEGMENTED_TRANSFORM_IMPL, data);
if (reel) {
kino_reel_transform_set_from_reel(reel, from_reel, take_ownership);
kino_reel_transform_set_layer_frame(reel, screen_frame);
kino_reel_transform_set_from_frame(reel, screen_frame);
kino_reel_transform_set_to_frame(reel, screen_frame);
kino_reel_transform_set_global(reel, true);
} else {
prv_destructor(data);
}
return reel;
}
void kino_reel_scale_segmented_set_delay_lookup_creator(
KinoReel *reel, GPointIndexLookupCreator creator, void *userdata, bool take_ownership) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (!data) {
return;
}
if (data->lookup.owns_userdata) {
applib_free(data->lookup.userdata);
}
data->lookup.creator = creator;
data->lookup.userdata = userdata;
data->lookup.owns_userdata = take_ownership;
}
bool kino_reel_scale_segmented_set_delay_by_distance(KinoReel *reel, GPoint target) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (!data) {
return false;
}
KinoReel *from_reel = kino_reel_transform_get_from_reel(reel);
GDrawCommandList *list = kino_reel_get_gdraw_command_list(from_reel);
if (!list) {
return false;
}
DistanceLookupData *lookup_data = applib_malloc(sizeof(DistanceLookupData));
if (!lookup_data) {
return false;
}
*lookup_data = (DistanceLookupData) { .target = target };
const bool take_ownership = true;
kino_reel_scale_segmented_set_delay_lookup_creator(reel, prv_create_lookup_by_distance,
lookup_data, take_ownership);
return true;
}
void kino_reel_scale_segmented_set_point_duration(KinoReel *reel, Fixed_S32_16 point_duration) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
data->point_duration = point_duration;
}
}
void kino_reel_scale_segmented_set_effect_duration(KinoReel *reel, Fixed_S32_16 effect_duration) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
data->effect_duration = effect_duration;
}
}
void kino_reel_scale_segmented_set_interpolate(KinoReel *reel,
InterpolateInt64Function interpolate) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
data->interpolate = interpolate;
}
}
void kino_reel_scale_segmented_set_deflate_effect(KinoReel *reel, int16_t expand) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
data->expand = expand;
}
}
void kino_reel_scale_segmented_set_bounce_effect(KinoReel *reel, int16_t bounce) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
GRect from = kino_reel_transform_get_from_frame(reel);
GRect to = kino_reel_transform_get_to_frame(reel);
data->bounce = bounce ? prv_calc_bounce_offset(from, to, bounce) : GPointZero;
}
}
void kino_reel_scale_segmented_set_from_stroke_width(KinoReel *reel, Fixed_S16_3 from,
GStrokeWidthOp from_op) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
data->stroke_width.from = from;
data->stroke_width.from_op = from_op;
}
}
void kino_reel_scale_segmented_set_to_stroke_width(KinoReel *reel, Fixed_S16_3 to,
GStrokeWidthOp to_op) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
data->stroke_width.to = to;
data->stroke_width.to_op = to_op;
}
}
void kino_reel_scale_segmented_set_stroke_width_curve(KinoReel *reel,
AnimationCurveFunction curve) {
ScaleSegmentedData *data = kino_reel_transform_get_context(reel);
if (data) {
data->stroke_width.curve = curve;
}
}
static AnimationProgress prv_ease_in_out_last_half(AnimationProgress progress) {
return animation_timing_curve(animation_timing_clip(2 * (progress - ANIMATION_NORMALIZED_MAX / 2))
, AnimationCurveEaseInOut);
}
void kino_reel_scale_segmented_set_end_as_dot(KinoReel *reel, int16_t radius) {
GRect frame = kino_reel_transform_get_to_frame(reel);
kino_reel_transform_set_to_frame(
reel, (GRect) { grect_center_point(&frame), SCALE_SEGMENTED_DOT_SIZE });
const Fixed_S16_3 to = Fixed_S16_3((2 * radius) << FIXED_S16_3_PRECISION);
kino_reel_scale_segmented_set_to_stroke_width(reel, to, GStrokeWidthOpSet);
kino_reel_scale_segmented_set_stroke_width_curve(reel, prv_ease_in_out_last_half);
}

View File

@@ -0,0 +1,114 @@
/*
* 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 "transform.h"
#include "applib/graphics/gdraw_command_transforms.h"
#include "applib/ui/kino/kino_reel.h"
// These are KinoReels that use the per-point segmented delayed scaling animation.
// @see gdraw_command_list_scale_segmented_to.
#define SCALE_SEGMENTED_DEFAULT_POINT_DURATION \
Fixed_S32_16(2 * FIXED_S32_16_ONE.raw_value / 3)
#define SCALE_SEGMENTED_DEFAULT_EFFECT_DURATION \
Fixed_S32_16(2 * FIXED_S32_16_ONE.raw_value / 3)
#define SCALE_SEGMENTED_DOT_SIZE_PX 0
#define SCALE_SEGMENTED_DOT_SIZE GSize(SCALE_SEGMENTED_DOT_SIZE_PX, SCALE_SEGMENTED_DOT_SIZE_PX)
//! A GDelayCreatorContext gives the information needed to build a delay index lookup for a given
//! GDrawCommandList.
typedef struct {
GDrawCommandList * const list;
const GSize size;
//! Whether the transform should free the lookup after use.
//! Specifying false allows the creator to reuse buffers or references existing lookups.
bool owns_lookup;
} GDelayCreatorContext;
//! GPointIndexLookup creator which is passed a GDelayCreatorContext.
//! @param ctx GDelayCreatorContext with the information needed to build the delay index lookup
//! @param userdata User supplied data for delay index lookup specific data.
typedef GPointIndexLookup *(*GPointIndexLookupCreator)(GDelayCreatorContext *ctx, void *userdata);
//! A KinoReel that can perform a one-stage or two-stage scale and translate with or
//! without a deflation and bounce back effect defined by a custom \ref GPointIndexLookup.
//! The effects can be simultaneous or independent and are achieved by overlapping two
//! invocations of \ref gdraw_command_list_scale_segmented_to.
//! If looking for a stretching effect, consider using the other factory functions available.
//! @param from_reel KinoReel to display and animate.
//! @param take_ownership true if this KinoReel will free `image` when destroyed.
//! @param screen_frame Position and size of the parent KinoLayer in global coordinates.
//! @return a scale segmented KinoReel
//! @see gdraw_command_list_scale_segmented_to, kino_reel_dci_transform_create, GPointIndexLookup
KinoReel *kino_reel_scale_segmented_create(KinoReel *from_reel, bool take_ownership,
GRect screen_frame);
//! Sets a GPointIndexLookup
//! @param index_lookup GPointIndexLookup with the assigned delay multiplier for each point
void kino_reel_scale_segmented_set_delay_lookup_creator(
KinoReel *reel, GPointIndexLookupCreator creator, void *context, bool take_ownership);
//! Sets a GPointIndexLookup based on the distance to a target
//! @param target Position to pull and stretch the image from in image coordinates.
bool kino_reel_scale_segmented_set_delay_by_distance(KinoReel *reel, GPoint target);
//! Set the point duration
//! @param point_duration Fraction of the total animation time a point should animate.
void kino_reel_scale_segmented_set_point_duration(KinoReel *reel, Fixed_S32_16 point_duration);
//! Set the effect duration. Ignored if expand and bounce are disabled
//! @param effect_duration Fraction of the total animation time an effect should animate.
void kino_reel_scale_segmented_set_effect_duration(KinoReel *reel, Fixed_S32_16 point_duration);
//! Set the magnitude of the deflate effect
//! @param expand Expansion length of `to` in pixels that would result in a deflation animation.
//! Set as 0 to disable.
void kino_reel_scale_segmented_set_deflate_effect(KinoReel *reel, int16_t expand);
//! Set the magnitude of the bounce back effect. Requires all frames to be set before use.
//! @param bounce Overshoot length of `to` in pixels that would result in a bounce back animation.
//! Set as 0 to disable.
void kino_reel_scale_segmented_set_bounce_effect(KinoReel *reel, int16_t bounce);
//! Set the animation interpolation
void kino_reel_scale_segmented_set_interpolate(KinoReel *reel,
InterpolateInt64Function interpolate);
//! Set from stroke width
//! @param from Fixed_S16_3 From stroke width operator value
//! @param from_op GStrokeWidthOp operation to start with
//! @see GStrokeWidthOp
void kino_reel_scale_segmented_set_from_stroke_width(KinoReel *reel, Fixed_S16_3 from,
GStrokeWidthOp from_op);
//! Set to stroke width
//! @param to Fixed_S16_3 To stroke width operator value
//! @param to_op GStrokeWidthOp operation to end with
//! @see GStrokeWidthOp
void kino_reel_scale_segmented_set_to_stroke_width(KinoReel *reel, Fixed_S16_3 to,
GStrokeWidthOp to_op);
//! Set the stroke width curve
void kino_reel_scale_segmented_set_stroke_width_curve(KinoReel *reel,
AnimationCurveFunction curve);
//! Set the animation to end as a dot. Requires the to frame to be set before use.
void kino_reel_scale_segmented_set_end_as_dot(KinoReel *reel, int16_t radius);

View File

@@ -0,0 +1,423 @@
/*
* 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 "transform.h"
#include "applib/graphics/gdraw_command_private.h"
#include "applib/graphics/gdraw_command_transforms.h"
#include "applib/ui/animation.h"
#include "applib/ui/animation_interpolate.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/kino/kino_reel_pdci.h"
#include "applib/ui/kino/kino_reel_custom.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
#include "util/math.h"
#include "util/net.h"
#include "util/struct.h"
typedef struct {
GRect layer_frame;
GRect from;
GRect to;
const TransformImpl *impl;
void *context;
int32_t normalized;
uint32_t elapsed;
uint32_t duration;
KinoReel *from_reel;
KinoReel *to_reel;
GDrawCommandList *list_copy;
GSize list_copy_size;
bool owns_from_reel;
bool owns_to_reel;
bool global;
} KinoReelTransformData;
static bool prv_is_currently_from(KinoReelTransformData *data) {
return (!data->to_reel || (data->normalized < ANIMATION_NORMALIZED_MAX / 2));
}
static KinoReel *prv_get_current_reel(KinoReelTransformData *data) {
return prv_is_currently_from(data) ? data->from_reel : data->to_reel;
}
static GSize prv_get_current_size(KinoReelTransformData *data) {
return prv_is_currently_from(data) ? data->from.size : data->to.size;
}
static GPoint prv_get_interpolated_origin(KinoReelTransformData *data) {
const GSize size = prv_get_current_size(data);
const GPoint offset = interpolate_gpoint(data->normalized, grect_center_point(&data->from),
grect_center_point(&data->to));
return gpoint_sub(offset, GPoint(size.w / 2, size.h / 2));
}
static bool prv_image_size_eq_rect_size(KinoReelTransformData *data, GRect rect) {
GSize size = kino_reel_get_size(prv_get_current_reel(data));
return gsize_equal(&size, &rect.size);
}
static void prv_free_list_copy(KinoReelTransformData *data) {
applib_free(data->list_copy);
data->list_copy = NULL;
}
static GDrawCommandList *prv_get_or_create_list_copy(KinoReelTransformData *data,
GDrawCommandList *list) {
if (data->list_copy && (gdraw_command_list_get_data_size(data->list_copy) >=
gdraw_command_list_get_data_size(list))) {
return data->list_copy;
}
prv_free_list_copy(data);
data->list_copy = gdraw_command_list_clone(list);
return data->list_copy;
}
static void prv_destructor(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data->impl->destructor) {
data->impl->destructor(data->context);
}
if (data->owns_from_reel) {
kino_reel_destroy(data->from_reel);
}
if (data->owns_to_reel) {
kino_reel_destroy(data->to_reel);
}
gdraw_command_list_destroy(data->list_copy);
applib_free(data);
}
static uint32_t prv_elapsed_getter(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
return scale_int32(data->normalized, ANIMATION_NORMALIZED_MAX, data->duration);
}
static uint32_t prv_get_duration(KinoReelTransformData *data) {
uint32_t duration = data->duration;
if (data->from_reel) {
const uint32_t from_duration = kino_reel_get_duration(data->from_reel);
// If we don't have a 'to_reel' then we are looping back to the 'from_reel' so it's okay for
// the 'from_reel' to have an infinite duration
//
// If we have a 'to_reel' then ignore infinite duration requests because we will never get to
// it and burn a lot of power along the way!
if ((data->to_reel == NULL) || (from_duration != PLAY_DURATION_INFINITE)) {
duration = MAX(duration, from_duration);
}
}
if (data->to_reel) {
// We want to make sure the transform duration is at least as long as the to_reel duration
// so that the resource transition runs to completion if it's animated
const uint32_t to_duration = kino_reel_get_duration(data->to_reel);
duration = MAX(duration, to_duration);
}
return duration;
}
static bool prv_elapsed_setter(KinoReel *reel, uint32_t elapsed) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data->elapsed == elapsed) {
return false;
}
bool changed = false;
data->elapsed = elapsed;
if (data->from_reel && kino_reel_set_elapsed(data->from_reel, elapsed)) {
changed = true;
}
if (data->to_reel && kino_reel_set_elapsed(data->to_reel, elapsed)) {
changed = true;
}
const int32_t normalized = animation_timing_clip(
scale_int32(elapsed, data->duration, ANIMATION_NORMALIZED_MAX));
if (data->normalized == normalized) {
return changed;
}
data->normalized = normalized;
// No position setter is shorthand for always triggering a transform on any position setting
bool transform_changed = true;
if (data->impl->position_setter &&
!data->impl->position_setter(normalized, data->context)) {
transform_changed = false;
}
return (changed || transform_changed);
}
static uint32_t prv_duration_getter(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
return prv_get_duration(data);
}
static GSize prv_size_getter(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
return interpolate_gsize(data->normalized, data->from.size, data->to.size);
}
static void prv_transform_list(KinoReelTransformData *data) {
KinoReel *reel = prv_get_current_reel(data);
GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
GDrawCommandList *list = prv_get_or_create_list_copy(data, source_list);
if (!list) {
return;
}
if (!gdraw_command_list_copy(list, gdraw_command_list_get_data_size(source_list),
source_list)) {
return;
}
if (data->impl->apply) {
const GSize size = kino_reel_get_size(reel);
data->impl->apply(list, size, &data->from, &data->to, data->normalized, data->context);
}
}
static void prv_draw_command_list_processed(GContext *ctx, GDrawCommandList *list, GPoint offset,
KinoReelProcessor *processor) {
GPoint draw_box_origin = ctx->draw_state.drawing_box.origin;
graphics_context_move_draw_box(ctx, offset);
gdraw_command_list_draw_processed(ctx, list, NULL_SAFE_FIELD_ACCESS(processor,
draw_command_processor,
NULL));
ctx->draw_state.drawing_box.origin = draw_box_origin;
}
static void prv_draw_reel_or_command_list_processed(
GContext *ctx, KinoReel *reel, GDrawCommandList *list, GPoint offset,
KinoReelProcessor *processor) {
if (list) {
prv_draw_command_list_processed(ctx, list, offset, processor);
} else {
kino_reel_draw_processed(reel, ctx, offset, processor);
}
}
static void prv_draw_processed_in_local(KinoReelTransformData *data, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReel *reel = prv_get_current_reel(data);
GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
GDrawCommandList *list = prv_get_or_create_list_copy(data, source_list);
if (data->global) {
offset = gpoint_sub(GPointZero, data->layer_frame.origin);
}
prv_draw_reel_or_command_list_processed(ctx, reel, list, offset, processor);
}
static void prv_draw_processed_in_global(KinoReelTransformData *data, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReel *reel = prv_get_current_reel(data);
GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
GDrawCommandList *list = prv_get_or_create_list_copy(data, source_list);
offset = gpoint_to_local_coordinates(GPointZero, ctx);
if (!list) {
// There is no list with global coordinates embedded. Instead, interpolate the offset.
gpoint_add_eq(&offset, prv_get_interpolated_origin(data));
}
prv_draw_reel_or_command_list_processed(ctx, reel, list, offset, processor);
}
static void prv_draw_processed_at_rect(KinoReelTransformData *data, GContext *ctx, GPoint offset,
GRect rect, KinoReelProcessor *processor) {
if (!prv_image_size_eq_rect_size(data, rect)) {
prv_transform_list(data);
prv_draw_processed_in_local(data, ctx, offset, processor);
return;
}
prv_free_list_copy(data);
if (data->global) {
offset = gpoint_sub(GPointZero, data->layer_frame.origin);
}
offset = gpoint_add(offset, rect.origin);
KinoReel *reel = prv_get_current_reel(data);
GDrawCommandList *source_list = kino_reel_get_gdraw_command_list(reel);
prv_draw_reel_or_command_list_processed(ctx, reel, source_list, offset, processor);
}
static void prv_draw_processed_func(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data->normalized == 0) {
prv_draw_processed_at_rect(data, ctx, offset, data->from, processor);
return;
}
if (data->normalized == ANIMATION_NORMALIZED_MAX) {
prv_draw_processed_at_rect(data, ctx, offset, data->to, processor);
return;
}
prv_transform_list(data);
if (data->global) {
prv_draw_processed_in_global(data, ctx, offset, processor);
} else {
prv_draw_processed_in_local(data, ctx, offset, processor);
}
}
static GDrawCommandList *prv_get_gdraw_command_list(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
KinoReel *reel = prv_get_current_reel(data);
return kino_reel_get_gdraw_command_list(reel);
}
return NULL;
}
static const KinoReelImpl s_kino_reel_impl_transform = {
.destructor = prv_destructor,
.get_elapsed = prv_elapsed_getter,
.set_elapsed = prv_elapsed_setter,
.get_duration = prv_duration_getter,
.get_size = prv_size_getter,
.draw_processed = prv_draw_processed_func,
.get_gdraw_command_list = prv_get_gdraw_command_list,
};
KinoReel *kino_reel_transform_create(const TransformImpl *impl, void *context) {
KinoReelTransformData *data = applib_malloc(sizeof(KinoReelTransformData));
if (!data) {
return NULL;
}
*data = (KinoReelTransformData) {
.impl = impl,
.context = context,
.duration = ANIMATION_DEFAULT_DURATION_MS,
};
KinoReel *reel = kino_reel_custom_create(&s_kino_reel_impl_transform, data);
if (!reel) {
applib_free(data);
}
return reel;
}
void *kino_reel_transform_get_context(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
return data->context;
}
return NULL;
}
void kino_reel_transform_set_from_reel(KinoReel *reel, KinoReel *from_reel,
bool take_ownership) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (!data) {
return;
}
if (data->owns_from_reel) {
kino_reel_destroy(data->from_reel);
}
data->from_reel = from_reel;
data->owns_from_reel = take_ownership;
prv_free_list_copy(data);
}
KinoReel *kino_reel_transform_get_from_reel(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
return data->from_reel;
}
return NULL;
}
void kino_reel_transform_set_to_reel(KinoReel *reel, KinoReel *to_reel,
bool take_ownership) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (!data) {
return;
}
if (data->owns_to_reel) {
kino_reel_destroy(data->to_reel);
}
data->to_reel = to_reel;
data->owns_to_reel = take_ownership;
prv_free_list_copy(data);
}
KinoReel *kino_reel_transform_get_to_reel(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
return data->to_reel;
}
return NULL;
}
void kino_reel_transform_set_layer_frame(KinoReel *reel, GRect layer_frame) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
data->layer_frame = layer_frame;
}
}
void kino_reel_transform_set_from_frame(KinoReel *reel, GRect from) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
data->from = from;
}
}
void kino_reel_transform_set_to_frame(KinoReel *reel, GRect to) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
data->to = to;
}
}
GRect kino_reel_transform_get_from_frame(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
return data->from;
}
return GRectZero;
}
GRect kino_reel_transform_get_to_frame(KinoReel *reel) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
return data->to;
}
return GRectZero;
}
void kino_reel_transform_set_global(KinoReel *reel, bool global) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
data->global = global;
}
}
void kino_reel_transform_set_transform_duration(KinoReel *reel, uint32_t duration) {
KinoReelTransformData *data = kino_reel_custom_get_data(reel);
if (data) {
data->duration = duration;
}
}

View 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/ui/animation.h"
#include "applib/ui/kino/kino_reel.h"
// Transform Kino Reel
// This Kino Reel is meant for plugging in transform logic. Memory management and kino reel
// compatibility is automatically handled. It is a building block for exporting GDraw Command
// Transforms as Kino Reels.
//! Transform context destructor for optionally freeing or performing any cleanup.
//! @param context User supplied context for destruction.
typedef void (*TransformDestructor)(void *context);
//! Transform position set handler.
//! @param normalized Position in animation process (0..ANIMATION_NORMALIZED_MAX)
//! @param context User supplied context for destruction.
//! @return whether the normalized set results in an animation change
typedef bool (*TransformPositionSetter)(int32_t normalized, void *context);
//! Transform applier. The image supplied is always in its source form.
//! @param list GDrawCommandList copy of the image to be modified in-place.
//! @param size GSize of the list bounds.
//! @param from GRect pointer to the from frame of the starting position and size.
//! @param to GRect pointer to the to frame of the ending position and size.
//! @param normalized Animation position of the current transform.
//! @param context User supplied context for transform specific data.
typedef void (*TransformApply)(GDrawCommandList *list, const GSize size, const GRect *from,
const GRect *to, AnimationProgress normalized, void *context);
//! Transform Implementation Callbacks.
typedef struct {
//! Callback that is called when the kino reel is destroyed.
TransformDestructor destructor;
//! Callback that is called when the kino reel position is set.
//! If no position setter is set, it is assumed that any change in position results in a change
//! in the transform.
TransformPositionSetter position_setter;
//! Callback that is called when the kino reel is asked to draw.
//! This callback is only called once for the start or end position unless the kino reel's
//! position is changed again after reaching the start or end.
TransformApply apply;
} TransformImpl;
//! Creates Transform Kino Reel with a custom transform implementation.
//! It is acceptable to continue to use this KinoReel after or before the animation when there
//! is no animation taking place.
//! \note This keeps in memory a copy of the image and creates an additional copy during
//! animation or at rest. At rest at a stage with a rect with a size equal to the image bounds size,
//! only a single copy is kept in memory. This is true even if arriving at the beginning stage
//! through rewinding.
//! @param impl TransformImpl pointer to a Transform implementation.
//! @param context User supplied context for transform specific data.
KinoReel *kino_reel_transform_create(const TransformImpl *impl, void *context);
//! Get the user supplied context
//! @param reel Transform Kino Reel to get a context from
void *kino_reel_transform_get_context(KinoReel *reel);
void kino_reel_transform_set_from_reel(KinoReel *reel, KinoReel *from_reel,
bool take_ownership);
//! Get the from reel.
//! @param reel Transform Kino Reel to get the from reel from
KinoReel *kino_reel_transform_get_from_reel(KinoReel *reel);
void kino_reel_transform_set_to_reel(KinoReel *reel, KinoReel *to_reel,
bool take_ownership);
//! Get the to reel.
//! @param reel Transform Kino Reel to get the to reel from
KinoReel *kino_reel_transform_get_to_reel(KinoReel *reel);
//! Set the layer frame. Unused if the transform was not set to be global.
//! @param reel Transform Kino Reel to set the layer frame
//! @param layer_frame Position and size of the parent KinoLayer.
void kino_reel_transform_set_layer_frame(KinoReel *reel, GRect layer_frame);
//! Set the from animation.
//! @param reel Transform Kino Reel to set the from frame
//! @param from Position and size to start from.
void kino_reel_transform_set_from_frame(KinoReel *reel, GRect from);
//! Set the to animation.
//! @param reel Transform Kino Reel to set the to frame
//! @param to Position and size to end at.
void kino_reel_transform_set_to_frame(KinoReel *reel, GRect to);
//! Get the from frame.
GRect kino_reel_transform_get_from_frame(KinoReel *reel);
//! Get the to frame.
GRect kino_reel_transform_get_to_frame(KinoReel *reel);
//! Set whether the transform takes global frames and draws globally positioned.
//! @param reel Transform Kino Reel to set whether operate in global
//! @param global Whether to draw the animation in global coordinates. If true, all frames must
//! be specified in absolute coordinates.
void kino_reel_transform_set_global(KinoReel *reel, bool global);
//! Set the duration of the transform.
//! @param reel Transform Kino Reel to set the duration of
//! @param duration Transform animation time in milliseconds.
void kino_reel_transform_set_transform_duration(KinoReel *reel, uint32_t duration);

View File

@@ -0,0 +1,88 @@
/*
* 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 "transform.h"
#include "scale_segmented.h"
#include "unfold.h"
#include "applib/applib_malloc.auto.h"
#include "util/trig.h"
#include "applib/graphics/gdraw_command_private.h"
#include "system/logging.h"
#include "syscall/syscall.h"
static AnimationProgress prv_ease_in_out_first_quarter(AnimationProgress progress) {
return animation_timing_curve(animation_timing_clip(4 * progress), AnimationCurveEaseInOut);
}
typedef struct {
int32_t angle;
int num_delay_groups;
Fixed_S32_16 group_delay;
} AngleLookupContext;
static GPointIndexLookup *prv_create_lookup_by_angle(GDelayCreatorContext *ctx, void *userdata) {
AngleLookupContext *data = userdata;
GPoint origin = { ctx->size.w / 2, ctx->size.h / 2 };
GPointIndexLookup *lookup = gdraw_command_list_create_index_lookup_by_angle(ctx->list, origin,
data->angle);
gpoint_index_lookup_set_groups(lookup, data->num_delay_groups, data->group_delay);
ctx->owns_lookup = true;
return lookup;
}
KinoReel *kino_reel_unfold_create(KinoReel *from_reel, bool take_ownership, GRect screen_frame,
int32_t angle, int num_delay_groups, Fixed_S32_16 group_delay) {
GDrawCommandList *list = kino_reel_get_gdraw_command_list(from_reel);
if (!list) {
return from_reel;
}
AngleLookupContext *ctx = applib_malloc(sizeof(AngleLookupContext));
if (!ctx) {
return NULL;
}
*ctx = (AngleLookupContext) {
.angle = angle,
.num_delay_groups = num_delay_groups,
.group_delay = group_delay,
};
if (!ctx->angle) {
ctx->angle = rand() % TRIG_MAX_ANGLE;
}
KinoReel *reel = kino_reel_scale_segmented_create(from_reel, take_ownership, screen_frame);
if (reel) {
const bool take_ownership = true;
kino_reel_scale_segmented_set_delay_lookup_creator(reel, prv_create_lookup_by_angle, ctx,
take_ownership);
kino_reel_scale_segmented_set_point_duration(reel, UNFOLD_DEFAULT_POINT_DURATION);
kino_reel_scale_segmented_set_effect_duration(reel, UNFOLD_DEFAULT_EFFECT_DURATION);
}
return reel;
}
void kino_reel_unfold_set_start_as_dot(KinoReel *reel, int16_t radius) {
GRect frame = kino_reel_transform_get_from_frame(reel);
kino_reel_transform_set_from_frame(
reel, (GRect) { grect_center_point(&frame), UNFOLD_DOT_SIZE });
const Fixed_S16_3 from = Fixed_S16_3((2 * radius) << FIXED_S16_3_PRECISION);
kino_reel_scale_segmented_set_from_stroke_width(reel, from, GStrokeWidthOpSet);
kino_reel_scale_segmented_set_stroke_width_curve(reel, prv_ease_in_out_first_quarter);
}

View File

@@ -0,0 +1,54 @@
/*
* 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 "scale_segmented.h"
#include "applib/ui/kino/kino_reel.h"
#define UNFOLD_DEFAULT_POINT_DURATION \
Fixed_S32_16(FIXED_S32_16_ONE.raw_value / 6)
#define UNFOLD_DEFAULT_EFFECT_DURATION \
Fixed_S32_16(3 * FIXED_S32_16_ONE.raw_value / 4)
#define UNFOLD_DEFAULT_NUM_DELAY_GROUPS 3
#define UNFOLD_DEFAULT_GROUP_DELAY Fixed_S32_16(FIXED_S32_16_ONE.raw_value * 3 / 2)
#define UNFOLD_DOT_SIZE_PX SCALE_SEGMENTED_DOT_SIZE_PX
#define UNFOLD_DOT_SIZE SCALE_SEGMENTED_DOT_SIZE
//! A KinoReel that can perform a one-stage or two-stage unfold with or
//! without a deflation and bounce back effect. The effects can be simultaneous or independent and
//! are achieved by overlapping two invocations of \ref gdraw_command_image_scale_segmented_to.
//! The unfold effect is achieved by using a GPointIndexLookup created by
//! \ref gdraw_command_image_create_index_lookup_by_angle.
//! @param from_reel KinoReel to display and animate.
//! @param take_ownership true if this KinoReel will free `image` when destroyed.
//! @param screen_frame Position and size of the parent KinoLayer in absolute drawing coordinates.
//! @param angle Angle to start the unfold effect from, where TRIG_MAX_ANGLE represents 2 pi.
//! If 0, a random angle will be used.
//! @param num_delay_groups Number of distinct point groups that will move together in time.
//! @param group_delay Amount of additional delay to add between each group proportional to the
//! animation duration of one group
//! @return a scale segmented KinoReel with an angle-based GPointIndexLookup
//! @see gdraw_command_image_scale_segmented_to, kino_reel_dci_transform_create
KinoReel *kino_reel_unfold_create(KinoReel *from_reel, bool take_ownership, GRect screen_frame,
int32_t angle, int num_delay_groups, Fixed_S32_16 group_delay);
//! Set the animation to start as a dot. Requires the from frame to be set before use.
void kino_reel_unfold_set_start_as_dot(KinoReel *reel, int16_t radius);

View File

@@ -0,0 +1,166 @@
/*
* 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 "kino_reel_custom.h"
#include "applib/applib_malloc.auto.h"
const uint32_t CUSTOM_REEL_CANARY = 0xbaebaef8;
typedef struct {
KinoReel base;
uint32_t canary;
const KinoReelImpl *impl;
void *data;
} KinoReelImplCustom;
static void prv_destructor(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->destructor) {
custom_reel->impl->destructor(reel);
}
applib_free(custom_reel);
}
static uint32_t prv_elapsed_getter(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_elapsed) {
return custom_reel->impl->get_elapsed(reel);
}
return 0;
}
static bool prv_elapsed_setter(KinoReel *reel, uint32_t elapsed_ms) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->set_elapsed) {
return custom_reel->impl->set_elapsed(reel, elapsed_ms);
}
return false;
}
static uint32_t prv_duration_getter(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_duration) {
return custom_reel->impl->get_duration(reel);
}
return 0;
}
static GSize prv_size_getter(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_size) {
return custom_reel->impl->get_size(reel);
}
return GSize(0, 0);
}
static void prv_draw_processed_func(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->draw_processed) {
custom_reel->impl->draw_processed(reel, ctx, offset, processor);
}
}
GDrawCommandImage *prv_get_gdraw_command_image(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_gdraw_command_image) {
return custom_reel->impl->get_gdraw_command_image(reel);
}
return NULL;
}
GDrawCommandList *prv_get_gdraw_command_list(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_gdraw_command_list) {
return custom_reel->impl->get_gdraw_command_list(reel);
}
return NULL;
}
GDrawCommandSequence *prv_get_gdraw_command_sequence(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_gdraw_command_sequence) {
return custom_reel->impl->get_gdraw_command_sequence(reel);
}
return NULL;
}
GBitmap *prv_get_gbitmap(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_gbitmap) {
return custom_reel->impl->get_gbitmap(reel);
}
return NULL;
}
GBitmapSequence* prv_get_gbitmap_sequence(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
if (custom_reel->impl->get_gbitmap_sequence) {
return custom_reel->impl->get_gbitmap_sequence(reel);
}
return NULL;
}
static const KinoReelImpl KINO_REEL_IMPL_CUSTOM = {
.reel_type = KinoReelTypeCustom,
.destructor = prv_destructor,
.get_elapsed = prv_elapsed_getter,
.set_elapsed = prv_elapsed_setter,
.get_duration = prv_duration_getter,
.get_size = prv_size_getter,
.draw_processed = prv_draw_processed_func,
.get_gdraw_command_image = prv_get_gdraw_command_image,
.get_gdraw_command_list = prv_get_gdraw_command_list,
.get_gdraw_command_sequence = prv_get_gdraw_command_sequence,
.get_gbitmap = prv_get_gbitmap,
.get_gbitmap_sequence = prv_get_gbitmap_sequence,
};
KinoReel *kino_reel_custom_create(const KinoReelImpl *custom_impl, void *data) {
KinoReelImplCustom *reel = applib_zalloc(sizeof(KinoReelImplCustom));
if (reel) {
reel->base.impl = &KINO_REEL_IMPL_CUSTOM;
reel->canary = CUSTOM_REEL_CANARY;
reel->impl = custom_impl;
reel->data = data;
}
return (KinoReel *)reel;
}
static bool prv_kino_reel_custom_is_custom(KinoReel *reel) {
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
return (custom_reel->canary == CUSTOM_REEL_CANARY);
}
void *kino_reel_custom_get_data(KinoReel *reel) {
if (!prv_kino_reel_custom_is_custom(reel)) {
return NULL;
}
KinoReelImplCustom *custom_reel = (KinoReelImplCustom *)reel;
return custom_reel->data;
}

View File

@@ -0,0 +1,23 @@
/*
* 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 "kino_reel.h"
KinoReel *kino_reel_custom_create(const KinoReelImpl *custom_impl, void *data);
void *kino_reel_custom_get_data(KinoReel *reel);

View File

@@ -0,0 +1,131 @@
/*
* 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 "kino_reel_gbitmap.h"
#include "kino_reel_gbitmap_private.h"
#include "applib/graphics/gtypes.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
#include "util/struct.h"
static void prv_destructor(KinoReel *reel) {
KinoReelImplGBitmap *bitmap_reel = (KinoReelImplGBitmap *)reel;
if (bitmap_reel->owns_bitmap) {
gbitmap_destroy(bitmap_reel->bitmap);
}
applib_free(bitmap_reel);
}
static void prv_draw_processed_func(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReelImplGBitmap *bitmap_reel = (KinoReelImplGBitmap *)reel;
GRect bounds = gbitmap_get_bounds(bitmap_reel->bitmap);
bounds.origin = gpoint_add(bounds.origin, offset);
// Save compositing mode
GCompOp prev_compositing_mode = ctx->draw_state.compositing_mode;
GCompOp op = (bitmap_reel->bitmap->info.format == GBitmapFormat1Bit)
? GCompOpAssign
: GCompOpSet;
graphics_context_set_compositing_mode(ctx, op);
graphics_draw_bitmap_in_rect_processed(ctx, bitmap_reel->bitmap, &bounds,
NULL_SAFE_FIELD_ACCESS(processor, bitmap_processor, NULL));
// Restore previous compositing mode
graphics_context_set_compositing_mode(ctx, prev_compositing_mode);
}
static GSize prv_get_size(KinoReel *reel) {
KinoReelImplGBitmap *bitmap_reel = (KinoReelImplGBitmap *)reel;
GRect bounds = gbitmap_get_bounds(bitmap_reel->bitmap);
return bounds.size;
}
static size_t prv_get_data_size(const KinoReel *reel) {
KinoReelImplGBitmap *bitmap_reel = (KinoReelImplGBitmap *)reel;
GBitmap *bitmap = bitmap_reel->bitmap;
size_t palette_size = 0;
switch (bitmap->info.format) {
case GBitmapFormat1BitPalette:
palette_size = 2;
break;
case GBitmapFormat2BitPalette:
palette_size = 4;
break;
case GBitmapFormat4BitPalette:
palette_size = 16;
break;
case GBitmapFormat1Bit:
case GBitmapFormat8Bit:
case GBitmapFormat8BitCircular:
break;
}
return (bitmap->row_size_bytes * bitmap->bounds.size.h) + palette_size;
}
static GBitmap *prv_get_gbitmap(KinoReel *reel) {
if (reel) {
return ((KinoReelImplGBitmap*)reel)->bitmap;
}
return NULL;
}
static const KinoReelImpl KINO_REEL_IMPL_GBITMAP = {
.reel_type = KinoReelTypeGBitmap,
.destructor = prv_destructor,
.get_size = prv_get_size,
.get_data_size = prv_get_data_size,
.draw_processed = prv_draw_processed_func,
.get_gbitmap = prv_get_gbitmap,
};
void kino_reel_gbitmap_init(KinoReelImplGBitmap *bitmap_reel, GBitmap *bitmap) {
if (bitmap_reel) {
*bitmap_reel = (KinoReelImplGBitmap) {
.bitmap = bitmap,
.base.impl = &KINO_REEL_IMPL_GBITMAP
};
}
}
KinoReel *kino_reel_gbitmap_create(GBitmap *bitmap, bool take_ownership) {
KinoReelImplGBitmap *reel = applib_zalloc(sizeof(KinoReelImplGBitmap));
if (reel) {
kino_reel_gbitmap_init(reel, bitmap);
reel->owns_bitmap = take_ownership;
}
return (KinoReel *)reel;
}
KinoReel *kino_reel_gbitmap_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return kino_reel_gbitmap_create_with_resource_system(app_num, resource_id);
}
KinoReel *kino_reel_gbitmap_create_with_resource_system(ResAppNum app_num, uint32_t resource_id) {
GBitmap *bitmap = gbitmap_create_with_resource_system(app_num, resource_id);
if (bitmap == NULL) {
return NULL;
}
return kino_reel_gbitmap_create(bitmap, true);
}

View File

@@ -0,0 +1,25 @@
/*
* 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 "kino_reel.h"
KinoReel *kino_reel_gbitmap_create(GBitmap *bitmap, bool take_ownership);
KinoReel *kino_reel_gbitmap_create_with_resource(uint32_t resource_id);
KinoReel *kino_reel_gbitmap_create_with_resource_system(ResAppNum app_num, uint32_t resource_id);

View File

@@ -0,0 +1,25 @@
/*
* 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
typedef struct {
KinoReel base;
GBitmap *bitmap;
bool owns_bitmap;
} KinoReelImplGBitmap;
void kino_reel_gbitmap_init(KinoReelImplGBitmap *bitmap_reel, GBitmap *bitmap);

View File

@@ -0,0 +1,149 @@
/*
* 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 "kino_reel_gbitmap_sequence.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/gbitmap_sequence.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "util/struct.h"
#include <limits.h>
typedef struct {
KinoReel base;
GBitmapSequence *sequence;
bool owns_sequence;
GBitmap *render_bitmap;
uint32_t elapsed_ms;
} KinoReelImplGBitmapSequence;
static void prv_destructor(KinoReel *reel) {
KinoReelImplGBitmapSequence *sequence_reel = (KinoReelImplGBitmapSequence *)reel;
if (sequence_reel->owns_sequence) {
gbitmap_sequence_destroy(sequence_reel->sequence);
}
gbitmap_destroy(sequence_reel->render_bitmap);
applib_free(sequence_reel);
}
static uint32_t prv_elapsed_getter(KinoReel *reel) {
KinoReelImplGBitmapSequence *sequence_reel = (KinoReelImplGBitmapSequence *)reel;
return sequence_reel->elapsed_ms;
}
static bool prv_elapsed_setter(KinoReel *reel, uint32_t elapsed_ms) {
KinoReelImplGBitmapSequence *sequence_reel = (KinoReelImplGBitmapSequence *)reel;
sequence_reel->elapsed_ms = elapsed_ms;
if (elapsed_ms == 0) {
gbitmap_sequence_restart(sequence_reel->sequence);
}
return gbitmap_sequence_update_bitmap_by_elapsed(sequence_reel->sequence,
sequence_reel->render_bitmap,
sequence_reel->elapsed_ms);
}
static uint32_t prv_duration_getter(KinoReel *reel) {
KinoReelImplGBitmapSequence *sequence_reel = (KinoReelImplGBitmapSequence *)reel;
uint32_t duration = gbitmap_sequence_get_total_duration(sequence_reel->sequence);
if (duration == 0) {
duration = PLAY_DURATION_INFINITE;
}
return duration;
}
static GSize prv_size_getter(KinoReel *reel) {
KinoReelImplGBitmapSequence *sequence_reel = (KinoReelImplGBitmapSequence *)reel;
return gbitmap_sequence_get_bitmap_size(sequence_reel->sequence);
}
static void prv_draw_processed_func(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReelImplGBitmapSequence *sequence_reel = (KinoReelImplGBitmapSequence *)reel;
GRect bounds = gbitmap_get_bounds(sequence_reel->render_bitmap);
bounds.origin = gpoint_add(bounds.origin, offset);
// Save compositing mode
GCompOp prev_compositing_mode = ctx->draw_state.compositing_mode;
graphics_context_set_compositing_mode(ctx, GCompOpSet); // Enable compositing
graphics_draw_bitmap_in_rect_processed(ctx, sequence_reel->render_bitmap, &bounds,
NULL_SAFE_FIELD_ACCESS(processor, bitmap_processor, NULL));
// Restore previous compositing mode
graphics_context_set_compositing_mode(ctx, prev_compositing_mode);
}
static GBitmap *prv_get_gbitmap(KinoReel *reel) {
if (reel) {
return ((KinoReelImplGBitmapSequence*)reel)->render_bitmap;
}
return NULL;
}
static GBitmapSequence *prv_get_gbitmap_sequence(KinoReel *reel) {
if (reel) {
return ((KinoReelImplGBitmapSequence*)reel)->sequence;
}
return NULL;
}
static const KinoReelImpl KINO_REEL_IMPL_GBITMAPSEQUENCE = {
.reel_type = KinoReelTypeGBitmapSequence,
.destructor = prv_destructor,
.get_elapsed = prv_elapsed_getter,
.set_elapsed = prv_elapsed_setter,
.get_duration = prv_duration_getter,
.get_size = prv_size_getter,
.draw_processed = prv_draw_processed_func,
.get_gbitmap = prv_get_gbitmap,
.get_gbitmap_sequence = prv_get_gbitmap_sequence,
};
KinoReel *kino_reel_gbitmap_sequence_create(GBitmapSequence *sequence, bool take_ownership) {
KinoReelImplGBitmapSequence *reel = applib_zalloc(sizeof(KinoReelImplGBitmapSequence));
if (reel) {
reel->sequence = sequence;
reel->owns_sequence = take_ownership;
reel->elapsed_ms = 0;
reel->base.impl = &KINO_REEL_IMPL_GBITMAPSEQUENCE;
// init render bitmap
reel->render_bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(sequence),
GBitmapFormat8Bit);
// Render initial frame upon load
prv_elapsed_setter((KinoReel *)reel, 0);
}
return (KinoReel *)reel;
}
KinoReel *kino_reel_gbitmap_sequence_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return kino_reel_gbitmap_sequence_create_with_resource_system(app_num, resource_id);
}
KinoReel *kino_reel_gbitmap_sequence_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id) {
GBitmapSequence *sequence = gbitmap_sequence_create_with_resource_system(app_num, resource_id);
if (sequence == NULL) {
return NULL;
}
return kino_reel_gbitmap_sequence_create(sequence, true);
}

View File

@@ -0,0 +1,26 @@
/*
* 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 "kino_reel.h"
KinoReel *kino_reel_gbitmap_sequence_create(GBitmapSequence *sequence, bool take_ownership);
KinoReel *kino_reel_gbitmap_sequence_create_with_resource(uint32_t resource_id);
KinoReel *kino_reel_gbitmap_sequence_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id);

View File

@@ -0,0 +1,102 @@
/*
* 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 "kino_reel_pdci.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
#include "util/struct.h"
typedef struct {
KinoReel base;
GDrawCommandImage *image;
bool owns_image;
} KinoReelImplPDCI;
static void prv_destructor(KinoReel *reel) {
KinoReelImplPDCI *dci_reel = (KinoReelImplPDCI *)reel;
if (dci_reel->owns_image) {
gdraw_command_image_destroy(dci_reel->image);
}
applib_free(dci_reel);
}
static void prv_draw_processed_func(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReelImplPDCI *dci_reel = (KinoReelImplPDCI *)reel;
gdraw_command_image_draw_processed(
ctx, dci_reel->image, offset, NULL_SAFE_FIELD_ACCESS(processor, draw_command_processor, NULL));
}
static GSize prv_get_size(KinoReel *reel) {
KinoReelImplPDCI *dci_reel = (KinoReelImplPDCI *)reel;
return gdraw_command_image_get_bounds_size(dci_reel->image);
}
static size_t prv_get_data_size(const KinoReel *reel) {
KinoReelImplPDCI *dci_reel = (KinoReelImplPDCI *)reel;
return gdraw_command_image_get_data_size(dci_reel->image);
}
static GDrawCommandImage *prv_get_gdraw_command_image(KinoReel *reel) {
if (reel) {
return ((KinoReelImplPDCI*)reel)->image;
}
return NULL;
}
static GDrawCommandList *prv_get_gdraw_command_list(KinoReel *reel) {
if (reel) {
return gdraw_command_image_get_command_list(((KinoReelImplPDCI*)reel)->image);
}
return NULL;
}
static const KinoReelImpl KINO_REEL_IMPL_PDCI = {
.reel_type = KinoReelTypePDCI,
.destructor = prv_destructor,
.get_size = prv_get_size,
.get_data_size = prv_get_data_size,
.draw_processed = prv_draw_processed_func,
.get_gdraw_command_image = prv_get_gdraw_command_image,
.get_gdraw_command_list = prv_get_gdraw_command_list,
};
KinoReel *kino_reel_pdci_create(GDrawCommandImage *image, bool take_ownership) {
KinoReelImplPDCI *reel = applib_zalloc(sizeof(KinoReelImplPDCI));
if (reel) {
reel->image = image;
reel->owns_image = take_ownership;
reel->base.impl = &KINO_REEL_IMPL_PDCI;
}
return (KinoReel *)reel;
}
KinoReel *kino_reel_pdci_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return kino_reel_pdci_create_with_resource_system(app_num, resource_id);
}
KinoReel *kino_reel_pdci_create_with_resource_system(ResAppNum app_num, uint32_t resource_id) {
GDrawCommandImage *image = gdraw_command_image_create_with_resource_system(app_num, resource_id);
if (image == NULL) {
return NULL;
}
return kino_reel_pdci_create(image, true);
}

View File

@@ -0,0 +1,25 @@
/*
* 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 "kino_reel.h"
KinoReel *kino_reel_pdci_create(GDrawCommandImage *image, bool take_ownership);
KinoReel *kino_reel_pdci_create_with_resource(uint32_t resource_id);
KinoReel *kino_reel_pdci_create_with_resource_system(ResAppNum app_num, uint32_t resource_id);

View File

@@ -0,0 +1,146 @@
/*
* 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 "kino_reel_pdcs.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/gdraw_command_frame.h"
#include "applib/graphics/gdraw_command_private.h"
#include "applib/graphics/gdraw_command_sequence.h"
#include "resource/resource_ids.auto.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "util/net.h"
#include "util/struct.h"
typedef struct {
KinoReel base;
GDrawCommandSequence *sequence;
bool owns_sequence;
GDrawCommandFrame *current_frame;
uint32_t elapsed_ms;
} KinoReelImplPDCS;
static void prv_destructor(KinoReel *reel) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
if (dcs_reel->owns_sequence) {
gdraw_command_sequence_destroy(dcs_reel->sequence);
}
applib_free(dcs_reel);
}
static uint32_t prv_elapsed_getter(KinoReel *reel) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
return dcs_reel->elapsed_ms;
}
static bool prv_elapsed_setter(KinoReel *reel, uint32_t elapsed_ms) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
dcs_reel->elapsed_ms = elapsed_ms;
GDrawCommandFrame *frame = gdraw_command_sequence_get_frame_by_elapsed(dcs_reel->sequence,
dcs_reel->elapsed_ms);
bool frame_changed = false;
if (frame != dcs_reel->current_frame) {
dcs_reel->current_frame = frame;
frame_changed = true;
}
return frame_changed;
}
static uint32_t prv_duration_getter(KinoReel *reel) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
return gdraw_command_sequence_get_total_duration(dcs_reel->sequence);
}
static GSize prv_size_getter(KinoReel *reel) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
return gdraw_command_sequence_get_bounds_size(dcs_reel->sequence);
}
static size_t prv_data_size_getter(const KinoReel *reel) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
return gdraw_command_sequence_get_data_size(dcs_reel->sequence);
}
static void prv_draw_processed_func(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
if (!dcs_reel->current_frame) {
return;
}
gdraw_command_frame_draw_processed(ctx, dcs_reel->sequence, dcs_reel->current_frame, offset,
NULL_SAFE_FIELD_ACCESS(processor, draw_command_processor,
NULL));
}
static GDrawCommandSequence *prv_get_gdraw_command_sequence(KinoReel *reel) {
if (reel) {
return ((KinoReelImplPDCS*)reel)->sequence;
}
return NULL;
}
static GDrawCommandList *prv_get_gdraw_command_list(KinoReel *reel) {
KinoReelImplPDCS *dcs_reel = (KinoReelImplPDCS *)reel;
if (dcs_reel) {
return gdraw_command_frame_get_command_list(
gdraw_command_sequence_get_frame_by_elapsed(dcs_reel->sequence, dcs_reel->elapsed_ms));
}
return NULL;
}
static const KinoReelImpl KINO_REEL_IMPL_PDCS = {
.reel_type = KinoReelTypePDCS,
.destructor = prv_destructor,
.get_elapsed = prv_elapsed_getter,
.set_elapsed = prv_elapsed_setter,
.get_duration = prv_duration_getter,
.get_size = prv_size_getter,
.get_data_size = prv_data_size_getter,
.draw_processed = prv_draw_processed_func,
.get_gdraw_command_sequence = prv_get_gdraw_command_sequence,
.get_gdraw_command_list = prv_get_gdraw_command_list,
};
KinoReel *kino_reel_pdcs_create(GDrawCommandSequence *sequence, bool take_ownership) {
KinoReelImplPDCS *reel = applib_zalloc(sizeof(KinoReelImplPDCS));
if (reel) {
reel->sequence = sequence;
reel->owns_sequence = take_ownership;
reel->elapsed_ms = 0;
reel->base.impl = &KINO_REEL_IMPL_PDCS;
reel->current_frame = gdraw_command_sequence_get_frame_by_index(sequence, 0);
}
return (KinoReel *)reel;
}
KinoReel *kino_reel_pdcs_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return kino_reel_pdcs_create_with_resource_system(app_num, resource_id);
}
KinoReel *kino_reel_pdcs_create_with_resource_system(ResAppNum app_num, uint32_t resource_id) {
GDrawCommandSequence *sequence = gdraw_command_sequence_create_with_resource_system(app_num,
resource_id);
if (sequence == NULL) {
return NULL;
}
return kino_reel_pdcs_create(sequence, true);
}

View File

@@ -0,0 +1,25 @@
/*
* 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 "kino_reel.h"
KinoReel *kino_reel_pdcs_create(GDrawCommandSequence *sequence, bool take_ownership);
KinoReel *kino_reel_pdcs_create_with_resource(uint32_t resource_id);
KinoReel *kino_reel_pdcs_create_with_resource_system(ResAppNum app_num, uint32_t resource_id);

634
src/fw/applib/ui/layer.c Normal file
View File

@@ -0,0 +1,634 @@
/*
* 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 "layer.h"
#include "layer_private.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/recognizer/recognizer.h"
#include "applib/ui/recognizer/recognizer_list.h"
#include "applib/ui/window_private.h"
#include "applib/unobstructed_area_service_private.h"
#include "kernel/kernel_applib_state.h"
#include "kernel/pebble_tasks.h"
#include "process_management/process_manager.h"
#include "process_state/app_state/app_state.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include <string.h>
void layer_init(Layer *layer, const GRect *frame) {
*layer = (Layer){};
layer->frame = *frame;
layer->bounds = (GRect){{0, 0}, frame->size};
layer->clips = true;
}
Layer* layer_create(GRect frame) {
Layer* layer = applib_type_malloc(Layer);
if (layer) {
layer_init(layer, &frame);
}
return layer;
}
Layer* layer_create_with_data(GRect frame, size_t data_size) {
Layer* layer = applib_malloc(applib_type_size(Layer) + data_size);
if (layer) {
layer_init(layer, &frame);
layer->has_data = true;
DataLayer *data_layer = (DataLayer*) layer;
memset(data_layer->data, 0, data_size);
}
return layer;
}
static bool prv_destroy_recognizer(Recognizer *recognizer, void *context) {
Layer *layer = context;
layer_detach_recognizer(layer, recognizer);
recognizer_destroy(recognizer);
return true;
}
void layer_deinit(Layer *layer) {
if (!layer) {
return;
}
layer_remove_from_parent(layer);
#if CAPABILITY_HAS_TOUCHSCREEN
// Destroy all attached recognizers
recognizer_list_iterate(&layer->recognizer_list, prv_destroy_recognizer, layer);
#endif
}
void layer_destroy(Layer* layer) {
if (layer == NULL) {
return;
}
layer_deinit(layer);
applib_free(layer);
}
void layer_mark_dirty(Layer *layer) {
if (layer->property_changed_proc) {
layer->property_changed_proc(layer);
}
if (layer->window) {
window_schedule_render(layer->window);
}
}
static bool layer_process_tree_level(Layer *node, void *ctx, LayerIteratorFunc iterator_func);
static bool layer_property_changed_tree_node(Layer *node, void *ctx) {
if (node) {
if (node->property_changed_proc) {
node->property_changed_proc(node);
}
}
return true;
}
static bool layer_process_tree_level(Layer *node, void *ctx, LayerIteratorFunc iterator_func) {
while (node) {
if (!iterator_func(node, ctx)) {
return false;
};
if (!layer_process_tree_level(node->first_child, ctx, iterator_func)) {
return false;
};
node = node->next_sibling;
}
return true;
}
void layer_process_tree(Layer *node, void *ctx, LayerIteratorFunc iterator_func) {
layer_process_tree_level(node, ctx, iterator_func);
}
inline static Layer __attribute__((always_inline)) *prv_layer_tree_traverse_next(Layer *stack[],
int const stack_size, uint8_t *current_depth,
const bool descend) {
const Layer *top_of_stack = stack[*current_depth];
// goto first child
if (descend && top_of_stack->first_child) {
if (*current_depth < stack_size-1) {
return stack[++(*current_depth)] = top_of_stack->first_child;
} else {
PBL_LOG(LOG_LEVEL_WARNING, "layer stack exceeded (%d). Will skip rendering.", stack_size);
}
}
// no children, try next sibling
if (top_of_stack->next_sibling) {
return stack[*current_depth] = top_of_stack->next_sibling;
}
// there are no more siblings
// continue with siblings of parents/grandparents
while (*current_depth > 0) {
(*current_depth)--;
const Layer *sibling = stack[*current_depth]->next_sibling;
if (sibling) {
return stack[*current_depth] = (Layer*)sibling;
}
}
// no more siblings on root level of stack
return NULL;
}
Layer *__layer_tree_traverse_next__test_accessor(Layer *stack[],
int const max_depth, uint8_t *current_depth, const bool descend) {
return prv_layer_tree_traverse_next(stack, max_depth, current_depth, descend);
}
void layer_render_tree(Layer *node, GContext *ctx) {
// NOTE: make sure to restore ctx->draw_state before leaving this function
const GDrawState root_draw_state = ctx->draw_state;
uint8_t current_depth = 0;
// We render our layout tree using a stack as opposed to using recursion to optimize for task
// stack usage. We can't allocate this stack on the stack anymore without blowing our stack
// up when doing a few common operations. We don't want to allocate this on the app heap as we
// didn't before and that would cause less RAM to be available to apps after a firmware upgrade.
Layer **stack;
if (pebble_task_get_current() == PebbleTask_App) {
stack = app_state_get_layer_tree_stack();
} else {
stack = kernel_applib_get_layer_tree_stack();
}
stack[0] = node;
while (node) {
bool descend = false;
if (node->hidden) {
goto node_hidden_do_not_descend;
}
// prepare draw_state for the current layer
// it will not be stored and restored but recalculated from the root
// for every layer
for (unsigned int level = 0; level <= current_depth; level++) {
const Layer *levels_layer = stack[level];
if (levels_layer->clips) {
const GRect levels_layer_frame_in_ctx_space = {
.origin = {
// drawing_box is expected to be setup as the bounds of the parent:
.x = ctx->draw_state.drawing_box.origin.x + levels_layer->frame.origin.x,
.y = ctx->draw_state.drawing_box.origin.y + levels_layer->frame.origin.y,
},
.size = levels_layer->frame.size,
};
grect_clip(&ctx->draw_state.clip_box, &levels_layer_frame_in_ctx_space);
}
// translate the drawing_box to the bounds of the layer:
ctx->draw_state.drawing_box.origin.x +=
levels_layer->frame.origin.x + levels_layer->bounds.origin.x;
ctx->draw_state.drawing_box.origin.y +=
levels_layer->frame.origin.y + levels_layer->bounds.origin.y;
ctx->draw_state.drawing_box.size = levels_layer->bounds.size;
}
if (!grect_is_empty(&ctx->draw_state.clip_box)) {
// call the current node's render procedure
if (node->update_proc) {
node->update_proc(node, ctx);
}
// if client has forgotten to release frame buffer
if (ctx->lock) {
graphics_release_frame_buffer(ctx, &ctx->dest_bitmap);
APP_LOG(APP_LOG_LEVEL_WARNING,
"Frame buffer was not released. "
"Make sure to call graphics_release_frame_buffer before leaving update_proc.");
}
descend = true;
}
node_hidden_do_not_descend:
node = prv_layer_tree_traverse_next(stack, LAYER_TREE_STACK_SIZE, &current_depth, descend);
ctx->draw_state = root_draw_state;
}
}
void layer_property_changed_tree(Layer *node) {
layer_process_tree(node, NULL, layer_property_changed_tree_node);
}
void layer_set_update_proc(Layer *layer, LayerUpdateProc update_proc) {
PBL_ASSERTN(layer != NULL);
layer->update_proc = update_proc;
}
void layer_set_frame(Layer *layer, const GRect *frame) {
if (grect_equal(frame, &layer->frame)) {
return;
}
const bool bounds_in_sync = gpoint_equal(&layer->bounds.origin, &GPointZero) &&
gsize_equal(&layer->bounds.size, &layer->frame.size);
layer->frame = *frame;
if (bounds_in_sync && !process_manager_compiled_with_legacy2_sdk()) {
layer->bounds = (GRect){.size = layer->frame.size};
} else {
// Legacy 2.x behavior needed for ScrollLayer
// Grow the bounds if it doesn't cover the area that the frame is showing.
// This is not a necessity, but supposedly a handy thing.
const int16_t visible_width = layer->bounds.size.w + layer->bounds.origin.x;
const int16_t visible_height = layer->bounds.size.h + layer->bounds.origin.y;
if (frame->size.w > visible_width ||
frame->size.h > visible_height) {
layer->bounds.size.w += MAX(frame->size.w - visible_width, 0);
layer->bounds.size.h += MAX(frame->size.h - visible_height, 0);
}
}
layer_mark_dirty(layer);
}
void layer_set_frame_by_value(Layer *layer, GRect frame) {
layer_set_frame(layer, &frame);
}
void layer_get_frame(const Layer *layer, GRect *frame) {
*frame = layer->frame;
}
GRect layer_get_frame_by_value(const Layer *layer) {
GRect frame;
layer_get_frame(layer, &frame);
return frame;
}
void layer_set_bounds(Layer *layer, const GRect *bounds) {
if (grect_equal(bounds, &layer->bounds)) {
return;
}
layer->bounds = *bounds;
layer_mark_dirty(layer);
}
void layer_set_bounds_by_value(Layer *layer, GRect bounds) {
layer_set_bounds(layer, &bounds);
}
void layer_get_bounds(const Layer *layer, GRect *bounds) {
*bounds = layer->bounds;
}
GRect layer_get_bounds_by_value(const Layer *layer) {
GRect bounds;
layer_get_bounds(layer, &bounds);
return bounds;
}
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
PBL_ASSERT_TASK(PebbleTask_App);
if (!layer || !bounds_out) {
return;
}
GRect area;
unobstructed_area_service_get_area(app_state_get_unobstructed_area_state(), &area);
// Convert the area from screen coordinates to layer coordinates
gpoint_sub_eq(&area.origin, layer_convert_point_to_screen(layer->parent, GPointZero));
layer_get_bounds(layer, bounds_out);
grect_clip(bounds_out, &area);
}
GRect layer_get_unobstructed_bounds_by_value(const Layer *layer) {
GRect bounds;
layer_get_unobstructed_bounds(layer, &bounds);
return bounds;
}
//! Sets the window on the layer and on all of its children
static void layer_set_window(Layer *layer, Window *window) {
layer->window = window;
Layer *child = layer->first_child;
while (child) {
layer_set_window(child, window);
child = child->next_sibling;
}
}
struct Window *layer_get_window(const Layer *layer) {
if (layer) {
return layer->window;
}
return NULL;
}
void layer_remove_from_parent(Layer *child) {
if (!child || child->parent == NULL) {
return;
}
if (child->parent->window) {
window_schedule_render(child->parent->window);
}
Layer *node = child->parent->first_child;
if (node == child) {
child->parent->first_child = node->next_sibling;
} else {
while (node->next_sibling != child) {
node = node->next_sibling;
}
node->next_sibling = child->next_sibling;
}
child->parent = NULL;
layer_set_window(child, NULL);
child->next_sibling = NULL;
}
void layer_remove_child_layers(Layer *parent) {
Layer *child = parent->first_child;
while (child) {
// Get the reference to the next now; layer_remove_from_parent will unlink them.
Layer *next_sibling = child->next_sibling;
layer_remove_from_parent(child);
child = next_sibling;
};
}
void layer_add_child(Layer *parent, Layer *child) {
PBL_ASSERTN(parent != NULL);
PBL_ASSERTN(child != NULL);
if (child->parent) {
layer_remove_from_parent(child);
}
PBL_ASSERTN(child->next_sibling == NULL);
child->parent = parent;
layer_set_window(child, parent->window);
if (child->window) {
window_schedule_render(child->window);
}
Layer *sibling = parent->first_child;
if (sibling == NULL) {
parent->first_child = child;
return;
}
for (;;) {
// Prevent setting the child to point to itself, causing infinite loop the next time this is
// called
if (sibling == child) {
PBL_LOG(LOG_LEVEL_DEBUG, "Layer has already been added to this parent!");
return;
}
if (!sibling->next_sibling) {
break;
}
sibling = sibling->next_sibling;
}
sibling->next_sibling = child;
}
// Below means higher up in the hierarchy so it gets drawn earlier,
// and as a result the one below gets occluded by what's draw on top of it.
void layer_insert_below_sibling(Layer *layer_to_insert, Layer *below_layer) {
if (below_layer->parent == NULL) {
return;
}
if (layer_to_insert->parent) {
layer_remove_from_parent(layer_to_insert);
}
PBL_ASSERTN(layer_to_insert->next_sibling == NULL);
layer_to_insert->parent = below_layer->parent;
layer_set_window(layer_to_insert, below_layer->window);
if (layer_to_insert->window) {
window_schedule_render(layer_to_insert->window);
}
Layer *prev_sibling = below_layer->parent->first_child;
if (below_layer == prev_sibling) {
below_layer->parent->first_child = layer_to_insert;
} else {
while (prev_sibling->next_sibling != below_layer) {
prev_sibling = prev_sibling->next_sibling;
}
prev_sibling->next_sibling = layer_to_insert;
}
layer_to_insert->next_sibling = below_layer;
}
// Above means lower down in the hierarchy so it gets drawn later,
// and as a result the drawn on top of what's below it.
void layer_insert_above_sibling(Layer *layer_to_insert, Layer *above_layer) {
if (above_layer->parent == NULL) {
return;
}
if (layer_to_insert->parent) {
layer_remove_from_parent(layer_to_insert);
}
PBL_ASSERTN(layer_to_insert->next_sibling == NULL);
layer_to_insert->parent = above_layer->parent;
layer_set_window(layer_to_insert, above_layer->window);
if (layer_to_insert->window) {
window_schedule_render(layer_to_insert->window);
}
Layer *old_next_sibling = above_layer->next_sibling;
above_layer->next_sibling = layer_to_insert;
layer_to_insert->next_sibling = old_next_sibling;
}
void layer_set_hidden(Layer *layer, bool hidden) {
if (hidden == layer->hidden) {
return;
}
layer->hidden = hidden;
if (layer->parent) {
layer_mark_dirty(layer->parent);
}
}
bool layer_get_hidden(const Layer *layer) {
return layer->hidden;
}
void layer_set_clips(Layer *layer, bool clips) {
if (clips == layer->clips) {
return;
}
layer->clips = clips;
layer_mark_dirty(layer);
}
bool layer_get_clips(const Layer *layer) {
return layer->clips;
}
void* layer_get_data(const Layer *layer) {
if (!layer->has_data) {
PBL_LOG(LOG_LEVEL_ERROR, "Layer was not allocated with a data region.");
return NULL;
}
return ((DataLayer *)layer)->data;
}
// TODO: PBL-25368 cover the following "convert coordinates to screen space" functions with tests
GPoint layer_convert_point_to_screen(const Layer *layer, GPoint point) {
while (layer) {
// don't consider window's root layer's frame/bounds
// and no, we don't need to check for l->window != NULL as &l->window->layer is just an offset
// (an offset of 0 to be precise)
if (&layer->window->layer == layer) {
break;
}
// follow how the drawing_box is computed to obtain the global frame
// see \ref layer_render_tree
point.x += layer->frame.origin.x + layer->bounds.origin.x;
point.y += layer->frame.origin.y + layer->bounds.origin.y;
layer = layer->parent;
}
return point;
}
GRect layer_convert_rect_to_screen(const Layer *layer, GRect rect) {
return (GRect){
.origin = layer_convert_point_to_screen(layer, rect.origin),
.size = rect.size,
};
}
void layer_get_global_frame(const Layer *layer, GRect *global_frame_out) {
*global_frame_out = (GRect) {
.origin = layer_convert_point_to_screen(layer, GPointZero),
.size = layer->frame.size,
};
}
bool layer_contains_point(const Layer *layer, const GPoint *point) {
if (!layer || !point) {
return false;
}
#if CAPABILITY_HAS_TOUCHSCREEN
if (layer->contains_point_override) {
return layer->contains_point_override(layer, point);
}
#endif
return grect_contains_point(&layer->frame, point);
}
void layer_set_contains_point_override(Layer *layer, LayerContainsPointOverride override) {
if (!layer) {
return;
}
#if CAPABILITY_HAS_TOUCHSCREEN
layer->contains_point_override = override;
#endif
}
typedef struct LayerContainsPointIterCtx {
const Layer *layer;
GPoint pos;
} LayerTouchIteratorCtx;
// Recursively search the layer tree for a layer that fulfills the following criteria:
// - contains the specified point
// - is the last sibling added to the parent layer, if more than one sibling contains the point
// - does not have any children that also contain the point
// This function returns true to indicate that the search should continue, and false to indicate
// that a layer has been found and that the search should stop
static bool prv_find_layer_containing_point(const Layer *node, LayerTouchIteratorCtx *iter_ctx) {
while (node) {
if (layer_contains_point(node, &iter_ctx->pos)) {
iter_ctx->layer = node;
if (!node->first_child && !node->next_sibling) {
return false;
}
iter_ctx->pos = gpoint_sub(iter_ctx->pos, node->bounds.origin);
if (!prv_find_layer_containing_point(node->first_child, iter_ctx)) {
return false;
};
iter_ctx->pos = gpoint_add(iter_ctx->pos, node->bounds.origin);
}
node = node->next_sibling;
}
return true;
}
MOCKABLE Layer *layer_find_layer_containing_point(const Layer *node, const GPoint *point) {
if (!node || !point) {
return NULL;
}
LayerTouchIteratorCtx iter_ctx = {
.pos = *point,
};
gpoint_sub(iter_ctx.pos, node->frame.origin);
prv_find_layer_containing_point(node, &iter_ctx);
return (Layer *)iter_ctx.layer;
}
void layer_attach_recognizer(Layer *layer, Recognizer *recognizer) {
#if CAPABILITY_HAS_TOUCHSCREEN
if (!layer || !recognizer) {
return;
}
recognizer_manager_register_recognizer(window_get_recognizer_manager(layer_get_window(layer)),
recognizer);
recognizer_add_to_list(recognizer, &layer->recognizer_list);
#endif
}
void layer_detach_recognizer(Layer *layer, Recognizer *recognizer) {
#if CAPABILITY_HAS_TOUCHSCREEN
if (!layer || !recognizer) {
return;
}
recognizer_remove_from_list(recognizer, &layer->recognizer_list);
recognizer_manager_deregister_recognizer(window_get_recognizer_manager(layer_get_window(layer)),
recognizer);
#endif
}
RecognizerList *layer_get_recognizer_list(const Layer *layer) {
#if CAPABILITY_HAS_TOUCHSCREEN
if (!layer) {
return NULL;
}
return (RecognizerList *)&layer->recognizer_list;
#else
return NULL;
#endif
}
bool layer_is_descendant(const Layer *layer, const Layer *potential_ancestor) {
if (!layer || !potential_ancestor) {
return false;
}
Layer *parent = layer->parent;
while (parent) {
if (parent == potential_ancestor) {
return true;
}
parent = parent->parent;
}
return false;
}

431
src/fw/applib/ui/layer.h Normal file
View File

@@ -0,0 +1,431 @@
/*
* 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"
#include "applib/ui/recognizer/recognizer.h"
#include "applib/ui/recognizer/recognizer_list.h"
#include <stdbool.h>
struct Layer;
struct Animation;
//! How deep our layer tree is allowed to be.
#define LAYER_TREE_STACK_SIZE 16
//! @file layer.h
//! @addtogroup UI
//! @{
//! @addtogroup Layer Layers
//! \brief User interface layers for displaying graphic components
//!
//! Layers are objects that can be displayed on a Pebble watchapp window, enabling users to see
//! visual objects, like text or images. Each layer stores the information about its state
//! necessary to draw or redraw the object that it represents and uses graphics routines along with
//! this state to draw itself when asked. Layers can be used to display various graphics.
//!
//! Layers are the basic building blocks for your application UI. Layers can be nested inside each other.
//! Every window has a root layer which is always the topmost layer.
//! You provide a function that is called to draw the content of the layer when needed; or
//! you can use standard layers that are provided by the system, such as text layer, image layer,
//! menu layer, action bar layer, and so on.
//!
//! The Pebble layer hierarchy is the list of things that need to be drawn to the screen.
//! Multiple layers can be arranged into a hierarchy. This enables ordering (front to back),
//! layout and hierarchy. Through relative positioning, visual objects that are grouped together by
//! adding them into the same layer can be moved all at once. This means that the child layers
//! will move accordingly. If a parent layer has clipping enabled, all the children will be clipped
//! to the frame of the parent.
//!
//! Pebble OS provides convenience layers with built-in logic for displaying different graphic
//! components, like text and bitmap layers.
//!
//! Refer to the \htmlinclude UiFramework.html (chapter "Layers") for a conceptual overview
//! of Layers and relevant code examples.
//!
//! The Modules listed here contain what can be thought of conceptually as subclasses of Layer. The
//! listed types can be safely type-casted to `Layer` (or `Layer *` in case of a pointer).
//! The `layer_...` functions can then be used with the data structures of these subclasses.
//! <br/>For example, the following is legal:
//! \code{.c}
//! TextLayer *text_layer;
//! ...
//! layer_set_hidden((Layer *)text_layer, true);
//! \endcode
//! @{
//! Function signature for a Layer's render callback (the name of the type
//! is derived from the words 'update procedure').
//! The system will call the `.update_proc` callback whenever the Layer needs
//! to be rendered.
//! @param layer The layer that needs to be rendered
//! @param ctx The destination graphics context to draw into
//! @see \ref Graphics
//! @see \ref layer_set_update_proc()
typedef void (*LayerUpdateProc)(struct Layer *layer, GContext* ctx);
typedef void (*PropertyChangedProc)(struct Layer *layer);
//! Layer contains point override function. This can replace the default implementation of
//! \ref layer_contains_point using the \ref layer_set_contains_point_override call. The override
//! function should return true if the point should be deemed within the layer and false if not.
//! The point is relative to the frame origin of the layer
//! @param layer affected layer
//! @param point point relative to the frame origin of the layer
//! @return true if point should be considered to be contained within the layer
typedef bool (*LayerContainsPointOverride)(const struct Layer *layer, const GPoint *point);
//! Data structure of a Layer.
//! It contains the following:
//! * geometry (frame, bounds)
//! * clipping, hidden flags
//! * a reference to its window
//! * a reference to its render callback
//! * references that constitute the layer hierarchy
typedef struct Layer {
/* Geometry */
//! @internal
//! Internal box bounds
GRect bounds;
//! @internal
//! Box bounds relative to parent layer coordinates
GRect frame;
union {
uint8_t flags;
struct {
bool clips:1;
bool hidden:1;
bool has_data:1;
bool is_highlighted:1; //!< Indicates the highlight status of a \ref MenuLayer cell
};
};
/* Layer tree */
struct Layer *next_sibling;
struct Layer *parent;
struct Layer *first_child;
struct Window *window;
//! Drawing callback
//! can be NULL if layer doesn't draw anything
LayerUpdateProc update_proc;
//! Property changed callback
PropertyChangedProc property_changed_proc;
#if CAPABILITY_HAS_TOUCHSCREEN
//! List of attached recognizers
RecognizerList recognizer_list;
//! Override callback to determine whether a layer contains a point
LayerContainsPointOverride contains_point_override;
#endif
} Layer;
typedef struct DataLayer {
Layer layer;
uint8_t data[];
} DataLayer;
//! Initializes the given layer and sets its frame and bounds.
//! Default values:
//! * `bounds` : origin (0, 0) and a size equal to the frame that is passed in.
//! * `clips` : `true`
//! * `hidden` : `false`
//! * `update_proc` : `NULL` (draws nothing)
//! @param layer The layer to initialize
//! @param frame The frame at which the layer should be initialized.
//! @param data_size The size (in bytes) of memory to initialize after the layer struct.
//! @see \ref layer_set_frame()
//! @see \ref layer_set_bounds()
void layer_init(Layer *layer, const GRect *frame);
//! Creates a layer on the heap and sets its frame and bounds.
//! Default values:
//! * `bounds` : origin (0, 0) and a size equal to the frame that is passed in.
//! * `clips` : `true`
//! * `hidden` : `false`
//! * `update_proc` : `NULL` (draws nothing)
//! @param frame The frame at which the layer should be initialized.
//! @see \ref layer_set_frame()
//! @see \ref layer_set_bounds()
//! @return A pointer to the layer. `NULL` if the layer could not
//! be created
Layer* layer_create(GRect frame);
//! Creates a layer on the heap with extra space for callback data, and set its frame andbounds.
//! Default values:
//! * `bounds` : origin (0, 0) and a size equal to the frame that is passed in.
//! * `clips` : `true`
//! * `hidden` : `false`
//! * `update_proc` : `NULL` (draws nothing)
//! @param frame The frame at which the layer should be initialized.
//! @param data_size The size (in bytes) of memory to allocate for callback data.
//! @see \ref layer_create()
//! @see \ref layer_set_frame()
//! @see \ref layer_set_bounds()
//! @return A pointer to the layer. `NULL` if the layer could not be created
Layer* layer_create_with_data(GRect frame, size_t data_size);
void layer_deinit(Layer *layer);
//! Destroys a layer previously created by layer_create
void layer_destroy(Layer* layer);
//! @internal
//! Renders a tree of layers to a graphics context
void layer_render_tree(Layer *root, GContext *ctx);
//! @internal
//! Process the PropertyChangedProc callback for a tree of layers
void layer_property_changed_tree(Layer *root);
//! Marks the complete layer as "dirty", awaiting to be asked by the system to redraw itself.
//! Typically, this function is called whenever state has changed that affects what the layer
//! is displaying.
//! * The layer's `.update_proc` will not be called before this function returns,
//! but will be called asynchronously, shortly.
//! * Internally, a call to this function will schedule a re-render of the window that the
//! layer belongs to. In effect, all layers in that window's layer hierarchy will be asked to redraw.
//! * If an earlier re-render request is still pending, this function is a no-op.
//! @param layer The layer to mark dirty
void layer_mark_dirty(Layer *layer);
//! Sets the layer's render function.
//! The system will call the `update_proc` automatically when the layer needs to redraw itself, see
//! also \ref layer_mark_dirty().
//! @param layer Pointer to the layer structure.
//! @param update_proc Pointer to the function that will be called when the layer needs to be rendered.
//! Typically, one performs a series of drawing commands in the implementation of the `update_proc`,
//! see \ref Drawing, \ref PathDrawing and \ref TextDrawing.
void layer_set_update_proc(Layer *layer, LayerUpdateProc update_proc);
//! Sets the frame of the layer, which is it's bounding box relative to the coordinate
//! system of its parent layer.
//! The size of the layer's bounds will be extended automatically, so that the bounds
//! cover the new frame.
//! @param layer The layer for which to set the frame
//! @param frame The new frame
//! @see \ref layer_set_bounds()
void layer_set_frame_by_value(Layer *layer, GRect frame);
void layer_set_frame(Layer *layer, const GRect *frame);
//! Gets the frame of the layer, which is it's bounding box relative to the coordinate
//! system of its parent layer.
//! If the frame has changed, \ref layer_mark_dirty() will be called automatically.
//! @param layer The layer for which to get the frame
//! @return The frame of the layer
//! @see layer_set_frame
GRect layer_get_frame_by_value(const Layer *layer);
void layer_get_frame(const Layer *layer, GRect *frame);
//! Sets the bounds of the layer, which is it's bounding box relative to its frame.
//! If the bounds has changed, \ref layer_mark_dirty() will be called automatically.
//! @param layer The layer for which to set the bounds
//! @param bounds The new bounds
//! @see \ref layer_set_frame()
void layer_set_bounds_by_value(Layer *layer, GRect bounds);
void layer_set_bounds(Layer *layer, const GRect *bounds);
//! Gets the bounds of the layer
//! @param layer The layer for which to get the bounds
//! @return The bounds of the layer
//! @see layer_set_bounds
GRect layer_get_bounds_by_value(const Layer *layer);
void layer_get_bounds(const Layer *layer, GRect *bounds);
//! Gets the window that the layer is currently attached to.
//! @param layer The layer for which to get the window
//! @return The window that this layer is currently attached to, or `NULL` if it has
//! not been added to a window's layer hierarchy.
//! @see \ref window_get_root_layer()
//! @see \ref layer_add_child()
struct Window *layer_get_window(const Layer *layer);
//! Removes the layer from its current parent layer
//! If removed successfully, the child's parent layer will be marked dirty
//! automatically.
//! @param child The layer to remove
void layer_remove_from_parent(Layer *child);
//! Removes child layers from given layer
//! If removed successfully, the child's parent layer will be marked dirty
//! automatically.
//! @param parent The layer from which to remove all child layers
void layer_remove_child_layers(Layer *parent);
//! Adds the child layer to a given parent layer, making it appear
//! in front of its parent and in front of any existing child layers
//! of the parent.
//! If the child layer was already part of a layer hierarchy, it will
//! be removed from its old parent first.
//! If added successfully, the parent (and children) will be marked dirty
//! automatically.
//! @param parent The layer to which to add the child layer
//! @param child The layer to add to the parent layer
void layer_add_child(Layer *parent, Layer *child);
//! Inserts the layer as a sibling behind another layer. If the layer to insert was
//! already part of a layer hierarchy, it will be removed from its old parent first.
//! The below_layer has to be a child of a parent layer,
//! otherwise this function will be a noop.
//! If inserted successfully, the parent (and children) will be marked dirty
//! automatically.
//! @param layer_to_insert The layer to insert into the hierarchy
//! @param below_sibling_layer The layer that will be used as the sibling layer
//! above which the insertion will take place
void layer_insert_below_sibling(Layer *layer_to_insert, Layer *below_sibling_layer);
//! Inserts the layer as a sibling in front of another layer.
//! The above_layer has to be a child of a parent layer,
//! otherwise this function will be a noop.
//! If inserted successfully, the parent (and children) will be marked dirty
//! automatically.
//! @param layer_to_insert The layer to insert into the hierarchy
//! @param above_sibling_layer The layer that will be used as the sibling layer
//! below which the insertion will take place
void layer_insert_above_sibling(Layer *layer_to_insert, Layer *above_sibling_layer);
//! Sets the visibility of the layer.
//! If the visibility has changed, \ref layer_mark_dirty() will be called automatically
//! on the parent layer.
//! @param layer The layer for which to set the visibility
//! @param hidden Supply `true` to make the layer hidden, or `false` to make it
//! non-hidden.
void layer_set_hidden(Layer *layer, bool hidden);
//! Gets the visibility of the layer.
//! @param layer The layer for which to get the visibility
//! @return True if the layer is hidden, false if it is not hidden.
bool layer_get_hidden(const Layer *layer);
//! Sets whether clipping is enabled for the layer. If enabled, whatever the layer _and
//! its children_ will draw using their `.update_proc` callbacks, will be clipped by the
//! this layer's frame.
//! If the clipping has changed, \ref layer_mark_dirty() will be called automatically.
//! @param layer The layer for which to set the clipping property
//! @param clips Supply `true` to make the layer clip to its frame, or `false`
//! to make it non-clipping.
void layer_set_clips(Layer *layer, bool clips);
//! Gets whether clipping is enabled for the layer. If enabled, whatever the layer _and
//! its children_ will draw using their `.update_proc` callbacks, will be clipped by the
//! this layer's frame.
//! @param layer The layer for which to get the clipping property
//! @return True if clipping is enabled for the layer, false if clipping is not enabled for
//! the layer.
bool layer_get_clips(const Layer *layer);
//! Gets the data from a layer that has been created with an extra data region.
//! @param layer The layer to get the data region from.
//! @return A void pointer to the data region.
void* layer_get_data(const Layer *layer);
//! Converts a point from the layer's local coordinate system to screen coordinates.
//! @note If the layer isn't part of the view hierarchy the result is undefined.
//! @param layer The view whose coordinate system will be used to convert the value to the screen.
//! @param point A point specified in the local coordinate system (bounds) of the layer.
//! @return The point converted to the coordinate system of the screen.
GPoint layer_convert_point_to_screen(const Layer *layer, GPoint point);
//! Converts a rectangle from the layer's local coordinate system to screen coordinates.
//! @note If the layer isn't part of the view hierarchy the result is undefined.
//! @param layer The view whose coordinate system will be used to convert the value to the screen.
//! @param rect A rectangle specified in the local coordinate system (bounds) of the layer.
//! @return The rectangle converted to the coordinate system of the screen.
GRect layer_convert_rect_to_screen(const Layer *layer, GRect rect);
//! @internal
//! Get the layer's frame in global coordinates
//! @param layer The layer whose global frame you seek
//! @param[out] global_frame_out GRect pointer to write the global frame to
void layer_get_global_frame(const Layer *layer, GRect *global_frame_out);
//! Get the largest unobstructed bounds rectangle of a layer.
//! @param layer The layer for which to get the unobstructed bounds.
//! @return The unobstructed bounds of the layer.
//! @see UnobstructedArea
GRect layer_get_unobstructed_bounds_by_value(const Layer *layer);
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out);
//! Return whether a point is contained within the bounds of a layer. Can be overridden by
//! \ref layer_set_contains_point_override. Default behavior is to check that the point is within
//! layer's bounds.
//! @param layer layer to be tested
//! @param point point relative to the frame origin of the layer
//! @return true if the point is contained within the bounds of the layer
bool layer_contains_point(const Layer *layer, const GPoint *point);
//! Override the function layer_contains_point with a custom function
void layer_set_contains_point_override(Layer *layer, LayerContainsPointOverride override);
//! @internal
//! Traverse the tree starting at \ref node and find the layer in the tree which:
//! - contains the specified point,
//! - has no children which also contain the point
//! - is the last layer added to it's parent if any of its siblings match the above criteria, too.
//! When traversing, a branch will only be entered if that node layer contains the point (so each
//! layer down to the first with no children or more recently added siblings must contain the
//! point).
//! \note \ref layer_contains_point is used to perform the test as to whether the point is contained
//! within the layer, which can be overridden with custom implementations
//! @param node layer to start the traversal at
//! @param point point that must be contained within any found layer
//! @return the layer found, otherwise NULL, if no layers contain the point
Layer *layer_find_layer_containing_point(const Layer *node, const GPoint *point);
//! @note Do not export until touch is supported in the SDK!
//! Attach a recognizer to a layer
//! @param layer \ref Layer to which to attach \ref Recognizer
//! @param recognizer \ref Recognizer to attach
void layer_attach_recognizer(Layer *layer, Recognizer *recognizer);
//! @note Do not export until touch is supported in the SDK!
//! Detach a recognizer from a layer
//! @param layer \ref Layer from which to remove \ref Recognizer
//! @param recognizer \ref Recognizer to detach
void layer_detach_recognizer(Layer *layer, Recognizer *recognizer);
//! @note Do not export until touch is supported in the SDK!
//! Get the recognizers attached to a layer
//! @param layer \ref Layer from which to get recognizers
//! @return recognizer list
RecognizerList *layer_get_recognizer_list(const Layer *layer);
//! Return whether or \a layer is a descendant of \a potential_ancestor
//! @param layer check this layer to see if any of it's ancestors are \a potential_ancestor
//! @param potential_ancestor check to see if this layer is an ancestor of \a layer
//! match
//! @return true if \a layer is a descendant of \a potential_ancestor
bool layer_is_descendant(const Layer *layer, const Layer *potential_ancestor);
//! @internal
//! Common Scrolling directions
typedef enum {
ScrollDirectionDown = -1,
ScrollDirectionNone = 0,
ScrollDirectionUp = 1
} ScrollDirection;
//! @} // end addtogroup Layer
//! @} // end addtogroup UI

View File

@@ -0,0 +1,24 @@
/*
* 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 "layer.h"
Layer *__layer_tree_traverse_next__test_accessor(Layer *stack[],
int const max_depth, uint8_t *current_depth, const bool descend);
typedef bool (*LayerIteratorFunc)(Layer *layer, void *ctx);
void layer_process_tree(Layer *node, void *ctx, LayerIteratorFunc iterator_func);

View File

@@ -0,0 +1,176 @@
/*
* 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 "layer.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/text.h"
#include <stdint.h>
#include <stddef.h>
//! @file menu_layer.h
//! @addtogroup UI
//! @{
//! @addtogroup Layer Layers
//! @{
//! @addtogroup MenuLayer
//! @internal
//! TODO: PBL-21467 Implement MenuCellLayer
//! MenuCellLayer is a virtual layer until it is actually implemented
typedef enum MenuCellLayerIconAlign {
MenuCellLayerIconAlign_Left = GAlignLeft,
MenuCellLayerIconAlign_Right = GAlignRight,
MenuCellLayerIconAlign_TopLeft = GAlignTopLeft,
#if PBL_ROUND
MenuCellLayerIconAlign_Top = GAlignTop,
#endif
} MenuCellLayerIconAlign;
typedef struct MenuCellLayerConfig {
const char *title;
const char *subtitle;
const char *value;
GFont title_font;
GFont value_font;
GFont subtitle_font;
GTextOverflowMode overflow_mode;
GBitmap *icon;
MenuCellLayerIconAlign icon_align;
const GBoxModel *icon_box_model;
bool icon_form_fit;
int horizontal_inset;
} MenuCellLayerConfig;
/////////////////////////////
// Cell Drawing functions
void menu_cell_layer_draw(GContext *ctx, const Layer *cell_layer,
const MenuCellLayerConfig *config);
//! Section drawing function to draw a basic section cell with the title, subtitle, and icon of the
//! section. Call this function inside the `.draw_row` callback implementation, see \ref
//! MenuLayerCallbacks. Note that if the size of `cell_layer` is too small to fit all of the cell
//! items specified, not all of them may be drawn.
//! @param ctx The destination graphics context
//! @param cell_layer The layer of the cell to draw
//! @param title If non-null, draws a title in larger text (24 points, bold
//! Raster Gothic system font).
//! @param subtitle If non-null, draws a subtitle in smaller text (18 points,
//! Raster Gothic system font). If `NULL`, the title will be centered vertically
//! inside the menu cell.
//! @param icon If non-null, draws an icon to the left of the text. If `NULL`,
//! the icon will be omitted and the leftover space is used for the title and
//! subtitle.
void menu_cell_basic_draw(GContext* ctx, const Layer *cell_layer, const char *title,
const char *subtitle, GBitmap *icon);
//! Cell drawing function similar to \ref menu_cell_basic_draw with the icon drawn on the right
//! Section drawing function to draw a basic section cell with the title, subtitle, and icon of
//! the section.
//! Call this function inside the `.draw_row` callback implementation, see \ref MenuLayerCallbacks
//! @param ctx The destination graphics context
//! @param cell_layer The layer of the cell to draw
//! @param title If non-null, draws a title in larger text (24 points, bold
//! Raster Gothic system font).
//! @param subtitle If non-null, draws a subtitle in smaller text (18 points,
//! Raster Gothic system font). If `NULL`, the title will be centered vertically
//! inside the menu cell.
//! @param icon If non-null, draws an icon to the right of the text. If `NULL`,
//! the icon will be omitted and the leftover space is used for the title and
//! subtitle.
void menu_cell_basic_draw_icon_right(GContext* ctx, const Layer *cell_layer, const char *title,
const char *subtitle, GBitmap *icon);
//! Cell drawing function to draw a basic menu cell layout with title, subtitle
//! Cell drawing function to draw a menu cell layout with only one big title.
//! Call this function inside the `.draw_row` callback implementation, see
//! \ref MenuLayerCallbacks.
//! @param ctx The destination graphics context
//! @param cell_layer The layer of the cell to draw
//! @param title If non-null, draws a title in larger text (28 points, bold
//! Raster Gothic system font).
void menu_cell_title_draw(GContext* ctx, const Layer *cell_layer, const char *title);
//! @internal
//! Cell drawing function similar to \ref menu_cell_basic_draw_with_value and
//! \ref menu_cell_basic_draw_icon_right, except with specifiable fonts.
void menu_cell_basic_draw_custom(GContext* ctx, const Layer *cell_layer, GFont const title_font,
const char *title, GFont const value_font, const char *value,
GFont const subtitle_font, const char *subtitle, GBitmap *icon,
bool icon_on_right, GTextOverflowMode overflow_mode);
//! Section header drawing function to draw a basic section header cell layout
//! with the title of the section.
//! Call this function inside the `.draw_header` callback implementation, see
//! \ref MenuLayerCallbacks.
//! @param ctx The destination graphics context
//! @param cell_layer The layer of the cell to draw
//! @param title If non-null, draws the title in small text (14 points, bold
//! Raster Gothic system font).
void menu_cell_basic_header_draw(GContext* ctx, const Layer *cell_layer, const char *title);
//! Returns whether or not the given cell layer is highlighted.
//! Using this for determining highlight behaviour is preferable to using
//! \ref menu_layer_get_selected_index. Row drawing callbacks may be invoked multiple
//! times with a different highlight status on the same cell in order to handle partially
//! highlighted cells during animation.
//! @param cell_layer The \ref Layer for the cell to check highlight status.
//! @return true if the given cell layer is highlighted in the menu.
bool menu_cell_layer_is_highlighted(const Layer *cell_layer);
//! Default cell height in pixels.
int16_t menu_cell_basic_cell_height(void);
//! Constant value representing \ref MenuLayer short cell height when this item is
//! the selected item on a round display.
#define MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT ((const int16_t) 68)
//! Constant value representing \ref MenuLayer short cell height when this item is
//! not the selected item on a round display.
#define MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT ((const int16_t) 24)
//! Constant value representing \ref MenuLayer tall cell height when this item is
//! the selected item on a round display.
#define MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT ((const int16_t) 84)
//! Constant value representing \ref MenuLayer tall cell height when this item is
//! not the selected item on a round display.
#define MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT ((const int16_t) 32)
//! "Small" cell height in pixels.
int16_t menu_cell_small_cell_height(void);
//! Default section header height in pixels
#define MENU_CELL_BASIC_HEADER_HEIGHT ((const int16_t) 16)
//! Default menu separator height in pixels
#define MENU_CELL_BASIC_SEPARATOR_HEIGHT ((const int16_t) 0)
//! Default cell horizontal inset in pixels.
int16_t menu_cell_basic_horizontal_inset(void);
#define MENU_CELL_ROUND_FOCUSED_HORIZONTAL_INSET ((const int16_t) 16)
#define MENU_CELL_ROUND_UNFOCUSED_HORIZONTAL_INSET ((const int16_t) 34)
//! @} // end addtogroup MenuLayer
//! @} // end addtogroup Layer
//! @} // end addtogroup UI

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,632 @@
/*
* 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 "inverter_layer.h"
#include "menu_cell_layer.h"
#include "scroll_layer.h"
#include "applib/app_timer.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/text.h"
#include <stdint.h>
#include <stddef.h>
//! @file menu_layer.h
//! @addtogroup UI
//! @{
//! @addtogroup Layer Layers
//! @{
//! @addtogroup MenuLayer
//! \brief Layer that displays a standard list menu. Data is provided using
//! callbacks.
//!
//! ![](menu_layer.png)
//! <h3>Key Points</h3>
//! * The familiar list-style menu widget, as used throughout the Pebble user
//! interface.
//! * Built on top of \ref ScrollLayer, inheriting all its goodness like
//! animated scrolling, automatic "more content" shadow indicators, etc.
//! * All data needed to render the menu is requested on-demand via callbacks,
//! to avoid the need to keep a lot of data in memory.
//! * Support for "sections". A section is a group of items, visually separated
//! by a header with the name at the top of the section.
//! * Variable heights: each menu item cell and each section header can have
//! its own height. The heights are provided by callbacks.
//! * Deviation from the Layer system for cell drawing: Each menu item does
//! _not_ have its own Layer (to minimize memory usage). Instead, a
//! drawing callback is set onto the \ref MenuLayer that is responsible
//! for drawing each menu item. The \ref MenuLayer will call this callback for each
//! menu item that is visible and needs to be rendered.
//! * Cell and header drawing can be customized by implementing a custom drawing
//! callback.
//! * A few "canned" menu cell drawing functions are provided for convenience,
//! which support the default menu cell layout with a title, optional subtitle
//! and icon.
//!
//! For short, static list menus, consider using \ref SimpleMenuLayer.
//! @{
//! @internal
//! Constant to indicate that a menu item index is not found
#define MENU_INDEX_NOT_FOUND ((const uint16_t) ~0)
//////////////////////
// Menu Layer
struct MenuLayer;
//! Data structure to represent an menu item's position in a menu, by specifying
//! the section index and the row index within that section.
typedef struct MenuIndex {
//! The index of the section
uint16_t section;
//! The index of the row within the section with index `.section`
uint16_t row;
} MenuIndex;
//! Macro to create a MenuIndex
#define MenuIndex(section, row) ((MenuIndex){ (section), (row) })
//! Comparator function to determine the order of two MenuIndex values.
//! @param a Pointer to the menu index of the first item
//! @param b Pointer to the menu index of the second item
//! @return 0 if A and B are equal, 1 if A has a higher section & row
//! combination than B or else -1
int16_t menu_index_compare(const MenuIndex *a, const MenuIndex *b);
//! @internal
//! Data structure with geometric information of a cell at specific menu index.
//! This is used internally for caching.
typedef struct MenuCellSpan {
int16_t y;
int16_t h;
int16_t sep;
MenuIndex index;
} MenuCellSpan;
//! Function signature for the callback to get the number of sections in a menu.
//! @param menu_layer The \ref MenuLayer for which the data is requested
//! @param callback_context The callback context
//! @return The number of sections in the menu
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef uint16_t (*MenuLayerGetNumberOfSectionsCallback)(struct MenuLayer *menu_layer,
void *callback_context);
//! Function signature for the callback to get the number of rows in a
//! given section in a menu.
//! @param menu_layer The \ref MenuLayer for which the data is requested
//! @param section_index The index of the section of the menu for which the
//! number of items it contains is requested
//! @param callback_context The callback context
//! @return The number of rows in the given section in the menu
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef uint16_t (*MenuLayerGetNumberOfRowsInSectionsCallback)(struct MenuLayer *menu_layer,
uint16_t section_index,
void *callback_context);
//! Function signature for the callback to get the height of the menu cell
//! at a given index.
//! @param menu_layer The \ref MenuLayer for which the data is requested
//! @param cell_index The MenuIndex for which the cell height is requested
//! @param callback_context The callback context
//! @return The height of the cell at the given MenuIndex
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef int16_t (*MenuLayerGetCellHeightCallback)(struct MenuLayer *menu_layer,
MenuIndex *cell_index,
void *callback_context);
//! Function signature for the callback to get the height of the section header
//! at a given section index.
//! @param menu_layer The \ref MenuLayer for which the data is requested
//! @param section_index The index of the section for which the header height is
//! requested
//! @param callback_context The callback context
//! @return The height of the section header at the given section index
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef int16_t (*MenuLayerGetHeaderHeightCallback)(struct MenuLayer *menu_layer,
uint16_t section_index,
void *callback_context);
//! Function signature for the callback to get the height of the separator
//! at a given index.
//! @param menu_layer The \ref MenuLayer for which the data is requested
//! @param cell_index The MenuIndex for which the cell height is requested
//! @param callback_context The callback context
//! @return The height of the separator at the given MenuIndex
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef int16_t (*MenuLayerGetSeparatorHeightCallback)(struct MenuLayer *menu_layer,
MenuIndex *cell_index,
void *callback_context);
//! Function signature for the callback to render the menu cell at a given
//! MenuIndex.
//! @param ctx The destination graphics context to draw into
//! @param cell_layer The cell's layer, containing the geometry of the cell
//! @param cell_index The MenuIndex of the cell that needs to be drawn
//! @param callback_context The callback context
//! @note The `cell_layer` argument is provided to make it easy to re-use an
//! `.update_proc` implementation in this callback. Only the bounds and frame
//! of the `cell_layer` are actually valid and other properties should be
//! ignored.
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef void (*MenuLayerDrawRowCallback)(GContext* ctx,
const Layer *cell_layer,
MenuIndex *cell_index,
void *callback_context);
//! Function signature for the callback to render the section header at a given
//! section index.
//! @param ctx The destination graphics context to draw into
//! @param cell_layer The header cell's layer, containing the geometry of the
//! header cell
//! @param section_index The section index of the section header that needs to
//! be drawn
//! @param callback_context The callback context
//! @note The `cell_layer` argument is provided to make it easy to re-use an
//! `.update_proc` implementation in this callback. Only the bounds and frame
//! of the `cell_layer` are actually valid and other properties should be
//! ignored.
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef void (*MenuLayerDrawHeaderCallback)(GContext* ctx,
const Layer *cell_layer,
uint16_t section_index,
void *callback_context);
//! Function signature for the callback to render the separator at a given
//! MenuIndex.
//! @param ctx The destination graphics context to draw into
//! @param cell_layer The cell's layer, containing the geometry of the cell
//! @param cell_index The MenuIndex of the separator that needs to be drawn
//! @param callback_context The callback context
//! @note The `cell_layer` argument is provided to make it easy to re-use an
//! `.update_proc` implementation in this callback. Only the bounds and frame
//! of the `cell_layer` are actually valid and other properties should be
//! ignored.
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef void (*MenuLayerDrawSeparatorCallback)(GContext* ctx,
const Layer *cell_layer,
MenuIndex *cell_index,
void *callback_context);
//! Function signature for the callback to handle the event that a user hits
//! the SELECT button.
//! @param menu_layer The \ref MenuLayer for which the selection event occured
//! @param cell_index The MenuIndex of the cell that is selected
//! @param callback_context The callback context
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef void (*MenuLayerSelectCallback)(struct MenuLayer *menu_layer,
MenuIndex *cell_index,
void *callback_context);
//! Function signature for the callback to handle a change in the current
//! selected item in the menu.
//! @param menu_layer The \ref MenuLayer for which the selection event occured
//! @param new_index The MenuIndex of the new item that is selected now
//! @param old_index The MenuIndex of the old item that was selected before
//! @param callback_context The callback context
//! @see \ref menu_layer_set_callbacks()
//! @see \ref MenuLayerCallbacks
typedef void (*MenuLayerSelectionChangedCallback)(struct MenuLayer *menu_layer,
MenuIndex new_index,
MenuIndex old_index,
void *callback_context);
//! Function signature for the callback which allows or changes selection behavior of the menu.
//! In order to change the cell that should be selected, modify the passed in new_index.
//! Preventing the selection from changing, new_index can be assigned the value of old_index.
//! @param menu_layer The \ref MenuLayer for which the selection event that occured
//! @param new_index Pointer to the index that the MenuLayer is going to change selection to.
//! @param old_index The index that is being unselected.
//! @param callback_context The callback context
//! @note \ref menu_layer_set_selected_index will not trigger this callback when
//! the selection changes, but \ref menu_layer_set_selected_next will.
typedef void (*MenuLayerSelectionWillChangeCallback)(struct MenuLayer *menu_layer,
MenuIndex *new_index,
MenuIndex old_index,
void *callback_context);
//! Function signature for the callback which draws the menu's background.
//! The background is underneath the cells of the menu, and is visible in the
//! padding below the bottom cell, or if a cell's background color is set to \ref GColorClear.
//! @param ctx The destination graphics context to draw into.
//! @param bg_layer The background's layer, containing the geometry of the background.
//! @param highlight Whether this should be rendered as highlighted or not. Highlight style
//! should match the highlight style of cells, since this color can be used for animating selection.
typedef void (*MenuLayerDrawBackgroundCallback)(GContext* ctx,
const Layer *bg_layer,
bool highlight,
void *callback_context);
//! Data structure containing all the callbacks of a \ref MenuLayer.
typedef struct MenuLayerCallbacks {
//! Callback that gets called to get the number of sections in the menu.
//! This can get called at various moments throughout the life of a menu.
//! @note When `NULL`, the number of sections defaults to 1.
MenuLayerGetNumberOfSectionsCallback get_num_sections;
//! Callback that gets called to get the number of rows in a section. This
//! can get called at various moments throughout the life of a menu.
//! @note Must be set to a valid callback; `NULL` causes undefined behavior.
MenuLayerGetNumberOfRowsInSectionsCallback get_num_rows;
//! Callback that gets called to get the height of a cell.
//! This can get called at various moments throughout the life of a menu.
//! @note When `NULL`, the default height of \ref MENU_CELL_BASIC_CELL_HEIGHT pixels is used.
//! Developers may wish to use \ref MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT
//! and \ref MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT on a round display
//! to respect the system aesthetic.
MenuLayerGetCellHeightCallback get_cell_height;
//! Callback that gets called to get the height of a section header.
//! This can get called at various moments throughout the life of a menu.
//! @note When `NULL`, the default height of 0 pixels is used. This disables
//! section headers.
MenuLayerGetHeaderHeightCallback get_header_height;
//! Callback that gets called to render a menu item.
//! This gets called for each menu item, every time it needs to be
//! re-rendered.
//! @note Must be set to a valid callback; `NULL` causes undefined behavior.
MenuLayerDrawRowCallback draw_row;
//! Callback that gets called to render a section header.
//! This gets called for each section header, every time it needs to be
//! re-rendered.
//! @note Must be set to a valid callback, unless `.get_header_height` is
//! `NULL`. Causes undefined behavior otherwise.
MenuLayerDrawHeaderCallback draw_header;
//! Callback that gets called when the user triggers a click with the SELECT
//! button.
//! @note When `NULL`, click events for the SELECT button are ignored.
MenuLayerSelectCallback select_click;
//! Callback that gets called when the user triggers a long click with the
//! SELECT button.
//! @note When `NULL`, long click events for the SELECT button are ignored.
MenuLayerSelectCallback select_long_click;
//! Callback that gets called whenever the selection changes.
//! @note When `NULL`, selection change events are ignored.
MenuLayerSelectionChangedCallback selection_changed;
//! Callback that gets called to get the height of a separator
//! This can get called at various moments throughout the life of a menu.
//! @note When `NULL`, the default height of 0 is used.
MenuLayerGetSeparatorHeightCallback get_separator_height;
//! Callback that gets called to render a separator.
//! This gets called for each separator, every time it needs to be
//! re-rendered.
//! @note Must be set to a valid callback, unless `.get_separator_height` is
//! `NULL`. Causes undefined behavior otherwise.
MenuLayerDrawSeparatorCallback draw_separator;
//! Callback that gets called before the selected cell changes.
//! This gets called before the selected item in the MenuLayer is changed,
//! and will allow for the selected cell to be overridden.
//! This allows for skipping cells in the menu, locking selection onto a given item,
MenuLayerSelectionWillChangeCallback selection_will_change;
//! Callback that gets called before any cells are drawn.
//! This supports two states, either highlighted or not highlighted.
//! If highlighted is specified, it is expected to be colored in the same
//! style as the menu's cells are.
//! If this callback is not specified, it will default to the colors set with
//! \ref menu_layer_set_normal_colors and \ref menu_layer_set_highlight_colors.
MenuLayerDrawBackgroundCallback draw_background;
} MenuLayerCallbacks;
enum {
MenuLayerColorBackground = 0,
MenuLayerColorForeground,
MenuLayerColor_Count,
};
#ifndef __clang__
_Static_assert(MenuLayerColor_Count == 2, "Bad enum MenuLayerColor");
#endif
//! Data structure of a MenuLayer.
//! @note a `MenuLayer *` can safely be casted to a `Layer *` and
//! `ScrollLayer *` and can thus be used with all other functions that take a
//! `Layer *` or `ScrollLayer *`, respectively, as an argument.
//! <br/>For example, the following is legal:
//! \code{.c}
//! MenuLayer menu_layer;
//! ...
//! layer_set_hidden((Layer *)&menu_layer, true);
//! \endcode
//! @note However there are a few caveats:
//! * Do not try to change to bounds or frame of a \ref MenuLayer, after
//! initializing it.
typedef struct MenuLayer {
ScrollLayer scroll_layer;
InverterLayer inverter;
//! @internal
struct {
//! @internal
//! Cell index + geometry cache of a cell that was in frame during the last redraw
MenuCellSpan cursor;
} cache;
//! @internal
//! Selected cell index + geometery cache of the selected cell
MenuCellSpan selection;
MenuLayerCallbacks callbacks;
void *callback_context;
//! Default colors to be used for \ref MenuLayer.
//! Use MenuLayerColorNormal and MenuLayerColorHightlight for indexing.
GColor normal_colors[MenuLayerColor_Count];
GColor highlight_colors[MenuLayerColor_Count];
//! Animation used for selection. Note this is only used in 3.x+ apps, legacy2 apps don't
//! use this.
struct {
Animation *animation;
GRect target; //! The target frame of the animation
//! cell_layer's bounds.origin will be modified by this to allow for
//! content scrolling without scrolling the actual cells
int16_t cell_content_origin_offset_y;
//! used to express "bouncing" of the highlight
int16_t selection_extend_top;
//! same as selection_extend_top but for the bottom
int16_t selection_extend_bottom;
//! some animations (e.g. center focused) will use this field to postpone the update of
//! menulayer.selection (especially for the .index)
MenuCellSpan new_selection;
} animation;
//! @internal
//! If true, there will be padding after the bottom item in the menu
//! Defaults to 'true'
bool pad_bottom:1;
//! If true, the MenuLayer will generally scroll the content so that the selected row is
//! on the center of the screen.
bool center_focused:1;
//! If true, the MenuLayer will not perform the selection cell clipping animation. This is
//! independent of the scrolling animation.
bool selection_animation_disabled:1;
//! Add some padding to keep track of the \ref MenuLayer size budget.
//! As long as the size stays within this budget, 2.x apps can safely use the 3.x MenuLayer type.
//! When padding is removed, the assertion below should also be removed.
uint8_t padding[44];
} MenuLayer;
//! Padding used below the last item in pixels
#define MENU_LAYER_BOTTOM_PADDING 20
//! Initializes a \ref MenuLayer with given frame
//! All previous contents are erased and the following default values are set:
//! * Clips: `true`
//! * Hidden: `false`
//! * Content size: `frame.size`
//! * Content offset: \ref GPointZero
//! * Callbacks: None (`NULL` for each one)
//! * Callback context: `NULL`
//! * After the relevant callbacks are called to populate the menu, the item at MenuIndex(0, 0)
//! will be selected initially.
//! The layer is marked dirty automatically.
//! @param menu_layer The \ref MenuLayer to initialize
//! @param frame The frame with which to initialze the \ref MenuLayer
void menu_layer_init(MenuLayer *menu_layer, const GRect *frame);
//! Creates a new \ref MenuLayer on the heap and initalizes it with the default values.
//!
//! * Clips: `true`
//! * Hidden: `false`
//! * Content size: `frame.size`
//! * Content offset: \ref GPointZero
//! * Callbacks: None (`NULL` for each one)
//! * Callback context: `NULL`
//! * After the relevant callbacks are called to populate the menu, the item at MenuIndex(0, 0)
//! will be selected initially.
//! @return A pointer to the \ref MenuLayer. `NULL` if the \ref MenuLayer could not
//! be created
MenuLayer* menu_layer_create(GRect frame);
void menu_layer_deinit(MenuLayer* menu_layer);
//! Destroys a \ref MenuLayer previously created by menu_layer_create.
void menu_layer_destroy(MenuLayer* menu_layer);
//! Gets the "root" Layer of the \ref MenuLayer, which is the parent for the sub-
//! layers used for its implementation.
//! @param menu_layer Pointer to the MenuLayer for which to get the "root" Layer
//! @return The "root" Layer of the \ref MenuLayer.
//! @internal
//! @note The result is always equal to `(Layer *) menu_layer`.
Layer* menu_layer_get_layer(const MenuLayer *menu_layer);
//! Gets the ScrollLayer of the \ref MenuLayer, which is the layer responsible for
//! the scrolling of the \ref MenuLayer.
//! @param menu_layer Pointer to the \ref MenuLayer for which to get the ScrollLayer
//! @return The ScrollLayer of the \ref MenuLayer.
//! @internal
//! @note The result is always equal to `(ScrollLayer *) menu_layer`.
ScrollLayer* menu_layer_get_scroll_layer(const MenuLayer *menu_layer);
//! @internal
//! This function replaces \ref menu_layer_set_callbacks_by_value in order to change the callbacks
//! parameter to be passed by a pointer instead of being passed by value. Callers consume much less
//! code space when passing a pointer compared to passing structs by value.
//! @see menu_layer_set_callbacks_by_value
void menu_layer_set_callbacks(MenuLayer *menu_layer,
void *callback_context,
const MenuLayerCallbacks *callbacks);
//! Sets the callbacks for the MenuLayer.
//! @param menu_layer Pointer to the \ref MenuLayer for which to set the callbacks
//! and callback context.
//! @param callback_context The new callback context. This is passed into each
//! of the callbacks and can be set to point to application provided data.
//! @param callbacks The new callbacks for the \ref MenuLayer. The storage for this
//! data structure must be long lived. Therefore, it cannot be stack-allocated.
//! @see MenuLayerCallbacks
void menu_layer_set_callbacks_by_value(MenuLayer *menu_layer, void *callback_context,
MenuLayerCallbacks callbacks);
//! Convenience function to set the \ref ClickConfigProvider callback on the
//! given window to the \ref MenuLayer internal click config provider. This internal
//! click configuration provider, will set up the default UP & DOWN
//! scrolling / menu item selection behavior.
//! This function calls \ref scroll_layer_set_click_config_onto_window to
//! accomplish this.
//!
//! Click and long click events for the SELECT button can be handled by
//! installing the appropriate callbacks using \ref menu_layer_set_callbacks().
//! This is a deviation from the usual click configuration provider pattern.
//! @param menu_layer The \ref MenuLayer that needs to receive click events.
//! @param window The window for which to set the click configuration.
//! @see \ref Clicks
//! @see \ref window_set_click_config_provider_with_context()
//! @see \ref scroll_layer_set_click_config_onto_window()
void menu_layer_set_click_config_onto_window(MenuLayer *menu_layer,
struct Window *window);
//! This enables or disables padding at the bottom of the \ref MenuLayer.
//! Padding at the bottom of the layer keeps the bottom item from being at the very bottom of the
//! screen.
//! Padding is turned on by default for all MenuLayers.
//! The color of the padded area will be the background color set using
//! \ref menu_layer_set_normal_colors(). To color the padding a different color, use
//! \ref MenuLayerDrawBackgroundCallback.
//! @param menu_layer The menu layer for which to enable or disable the padding.
//! @param enable True = enable padding, False = disable padding.
void menu_layer_pad_bottom_enable(MenuLayer *menu_layer, bool enable);
//! Values to specify how a (selected) row should be aligned relative to the
//! visible area of the \ref MenuLayer.
typedef enum {
//! Don't align or update the scroll offset of the \ref MenuLayer.
MenuRowAlignNone,
//! Scroll the contents of the \ref MenuLayer in such way that the selected row
//! is centered relative to the visible area.
MenuRowAlignCenter,
//! Scroll the contents of the \ref MenuLayer in such way that the selected row
//! is at the top of the visible area.
MenuRowAlignTop,
//! Scroll the contents of the \ref MenuLayer in such way that the selected row
//! is at the bottom of the visible area.
MenuRowAlignBottom,
} MenuRowAlign;
//! Selects the next or previous item, relative to the current selection.
//! @param menu_layer The \ref MenuLayer for which to select the next item
//! @param up Supply `false` to select the next item in the list (downwards),
//! or `true` to select the previous item in the list (upwards).
//! @param scroll_align The alignment of the new selection
//! @param animated Supply `true` to animate changing the selection, or `false`
//! to change the selection instantly.
//! @note If there is no next/previous item, this function is a no-op.
void menu_layer_set_selected_next(MenuLayer *menu_layer,
bool up,
MenuRowAlign scroll_align,
bool animated);
//! Selects the item with given \ref MenuIndex.
//! @param menu_layer The \ref MenuLayer for which to change the selection
//! @param index The index of the item to select
//! @param scroll_align The alignment of the new selection
//! @param animated Supply `true` to animate changing the selection, or `false`
//! to change the selection instantly.
//! @note If the section and/or row index exceeds the avaible number of sections
//! or resp. rows, the exceeding index/indices will be capped, effectively
//! selecting the last section and/or row, resp.
void menu_layer_set_selected_index(MenuLayer *menu_layer,
MenuIndex index, MenuRowAlign scroll_align,
bool animated);
//! Gets the MenuIndex of the currently selected menu item.
//! @param menu_layer The \ref MenuLayer for which to get the current selected index.
//! @see menu_cell_layer_is_highlighted
//! @note This function should not be used to determine whether a cell should be
//! highlighted or not. See \ref menu_cell_layer_is_highlighted for more
//! information.
MenuIndex menu_layer_get_selected_index(const MenuLayer *menu_layer);
//! Returns whether or not the specified cell index is currently selected.
//! @param menu_layer The \ref MenuLayer to use when determining if the index is selected.
//! @param index The \ref MenuIndex of the cell to check for selection.
//! @note This function should not be used to determine whether a cell is highlighted or not.
//! See \ref menu_cell_layer_is_highlighted for more information.
bool menu_layer_is_index_selected(const MenuLayer *menu_layer, MenuIndex *index);
//! Reloads the data of the menu. This causes the menu to re-request the menu
//! item data, by calling the relevant callbacks.
//! The current selection and scroll position will not be changed. See the
//! note with \ref menu_layer_set_selected_index() for the behavior if the
//! old selection is no longer valid.
//! @param menu_layer The \ref MenuLayer for which to reload the data.
void menu_layer_reload_data(MenuLayer *menu_layer);
//! Set the default colors to be used for cells when it is in a normal state (not highlighted).
//! The GContext's text and fill colors will be set appropriately prior to calling the `.draw_row`
//! callback.
//! If this function is not explicitly called on a \ref MenuLayer, it will default to white
//! background with black foreground.
//! @param menu_layer The \ref MenuLayer for which to set the colors.
//! @param background The color to be used for the background of the cell.
//! @param foreground The color to be used for the foreground and text of the cell.
//! @see \ref menu_layer_set_highlight_colors
void menu_layer_set_normal_colors(MenuLayer *menu_layer, GColor background, GColor foreground);
//! Set the default colors to be used for cells when it is in a highlighted state.
//! The GContext's text and fill colors will be set appropriately prior to calling the `.draw_row`
//! callback.
//! If this function is not explicitly called on a \ref MenuLayer, it will default to black
//! background with white foreground.
//! @param menu_layer The \ref MenuLayer for which to set the colors.
//! @param background The color to be used for the background of the cell.
//! @param foreground The color to be used for the foreground and text of the cell.
//! @see \ref menu_layer_set_normal_colors
void menu_layer_set_highlight_colors(MenuLayer *menu_layer, GColor background, GColor foreground);
//! True, if the \ref MenuLayer generally scrolls such that the selected row is in the center.
//! @see \ref menu_layer_set_center_focused
bool menu_layer_get_center_focused(MenuLayer *menu_layer);
//! Controls if the \ref MenuLayer generally scrolls such that the selected row is in the center.
//! For platforms with a round display (PBL_ROUND) the default is true,
//! otherwise false is the default
//! @param menu_layer The menu layer for which to enable or disable the behavior.
//! @param center_focused true = enable the mode, false = disable it.
//! @see \ref menu_layer_get_center_focused
void menu_layer_set_center_focused(MenuLayer *menu_layer, bool center_focused);
//! @} // end addtogroup MenuLayer
//! @} // end addtogroup Layer
//! @} // end addtogroup UI

View File

@@ -0,0 +1,44 @@
/*
* 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 "menu_layer.h"
struct MenuIterator;
typedef void (*MenuIteratorCallback)(struct MenuIterator *it);
typedef struct MenuIterator {
MenuLayer * menu_layer;
MenuCellSpan cursor;
int16_t cell_bottom_y;
MenuIteratorCallback row_callback_before_geometry;
MenuIteratorCallback row_callback_after_geometry;
MenuIteratorCallback section_callback;
bool should_continue; // callback can set this to false if the row-loop should be exited.
} MenuIterator;
typedef struct MenuRenderIterator {
MenuIterator it;
GContext* ctx;
int16_t content_top_y;
int16_t content_bottom_y;
bool cache_set:1;
bool cursor_in_frame:1;
MenuCellSpan new_cache;
Layer cell_layer;
} MenuRenderIterator;

View File

@@ -0,0 +1,618 @@
/*
* 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 "menu_layer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/kino/kino_reel_gbitmap_private.h"
#include "process_management/process_manager.h"
#include "shell/system_theme.h"
#include "system/passert.h"
#include "util/math.h"
/////////////////////////////////
// System Provided Cell Types
//
// NOTES: Below are the implementations of system provided cell drawing functions.
//
//////////////////////
// Basic menu cell
typedef struct MenuCellDimensions {
int16_t basic_cell_height;
int16_t small_cell_height;
int16_t horizontal_inset;
int16_t title_subtitle_left_margin;
} MenuCellDimensions;
static const MenuCellDimensions s_menu_cell_dimensions[NumPreferredContentSizes] = {
//! @note these are the same as Medium until Small is designed
[PreferredContentSizeSmall] = {
.basic_cell_height = 44,
.small_cell_height = 34,
.horizontal_inset = 5,
.title_subtitle_left_margin = 30,
},
[PreferredContentSizeMedium] = {
.basic_cell_height = 44,
.small_cell_height = 34,
.horizontal_inset = 5,
.title_subtitle_left_margin = 30,
},
[PreferredContentSizeLarge] = {
.basic_cell_height = 61,
.small_cell_height = 42,
.horizontal_inset = 10,
.title_subtitle_left_margin = 34,
},
//! @note these are the same as Large until ExtraLarge is designed
[PreferredContentSizeExtraLarge] = {
.basic_cell_height = 61,
.small_cell_height = 42,
.horizontal_inset = 10,
.title_subtitle_left_margin = 34,
},
};
static const MenuCellDimensions *prv_get_dimensions_for_runtime_platform_default_size(void) {
const PreferredContentSize runtime_platform_default_size =
system_theme_get_default_content_size_for_runtime_platform();
return &s_menu_cell_dimensions[runtime_platform_default_size];
}
int16_t menu_cell_basic_cell_height(void) {
return prv_get_dimensions_for_runtime_platform_default_size()->basic_cell_height;
}
int16_t menu_cell_small_cell_height(void) {
return prv_get_dimensions_for_runtime_platform_default_size()->small_cell_height;
}
int16_t menu_cell_basic_horizontal_inset(void) {
return prv_get_dimensions_for_runtime_platform_default_size()->horizontal_inset;
}
static int16_t prv_title_subtitle_left_margin(void) {
return prv_get_dimensions_for_runtime_platform_default_size()->title_subtitle_left_margin;
}
static ALWAYS_INLINE GFont prv_get_cell_title_font(const MenuCellLayerConfig *config) {
return config->title_font ?: system_theme_get_font_for_default_size(TextStyleFont_MenuCellTitle);
}
static ALWAYS_INLINE GFont prv_get_cell_subtitle_font(const MenuCellLayerConfig *config) {
return config->subtitle_font ?:
system_theme_get_font_for_default_size(TextStyleFont_MenuCellSubtitle);
}
static ALWAYS_INLINE GFont prv_get_cell_value_font(const MenuCellLayerConfig *config) {
return config->value_font ?: prv_get_cell_title_font(config);
}
static ALWAYS_INLINE void prv_draw_icon(GContext *ctx, GBitmap *icon, const GRect *icon_frame,
bool is_legacy2) {
if (!is_legacy2) {
bool tint_icon = (icon && (gbitmap_get_format(icon) == GBitmapFormat1Bit));
if (tint_icon) {
graphics_context_set_compositing_mode(ctx, GCompOpTint);
} else if (ctx->draw_state.compositing_mode == GCompOpAssign) {
graphics_context_set_compositing_mode(ctx, GCompOpSet);
}
}
graphics_draw_bitmap_in_rect(ctx, icon, icon_frame);
}
static void prv_menu_cell_basic_draw_custom_rect(
GContext *ctx, const Layer *cell_layer, const MenuCellLayerConfig *config) {
const GRect *bounds = &cell_layer->bounds;
const bool is_legacy2 = process_manager_compiled_with_legacy2_sdk();
const GFont title_font = prv_get_cell_title_font(config);
const int16_t title_height = fonts_get_font_height(title_font);
const GFont subtitle_font = prv_get_cell_subtitle_font(config);
const int16_t subtitle_height = config->subtitle ? fonts_get_font_height(subtitle_font) : 0;
const int16_t full_height = title_height + subtitle_height + 10;
const int horizontal_margin = menu_cell_basic_horizontal_inset();
const int vertical_margin = (bounds->size.h - full_height) / 2;
int left_margin = 0;
GRect box;
const GAlign icon_align = (GAlign)config->icon_align;
if (config->icon) {
box = (GRect) {
.size = config->icon->bounds.size,
.origin = bounds->origin,
};
if (is_legacy2) {
static const GSize s_icon_size = { 33, 44 };
if (icon_align == GAlignRight) {
box.origin.x += bounds->size.w - (horizontal_margin + config->icon->bounds.size.w);
} else { // icon on left
box.origin.x += ((config->icon->bounds.size.w & 1) + // Nudge odd-width icons one right
((s_icon_size.w - config->icon->bounds.size.w) / 2));
}
box.origin.y += (s_icon_size.h - config->icon->bounds.size.h) / 2;
} else {
const GRect container_rect =
grect_inset(*bounds, GEdgeInsets(vertical_margin, horizontal_margin));
grect_align(&box, &container_rect, icon_align, true /* clip */);
if ((icon_align == GAlignTopLeft) || (icon_align == GAlignTop) ||
(icon_align == GAlignTopRight)) {
// Offset by the cap offset to match round's icon-title delta
box.origin.y += fonts_get_font_cap_offset(title_font);
}
}
if (config->icon_box_model) {
box.origin = gpoint_add(box.origin, config->icon_box_model->offset);
}
left_margin = box.origin.x;
prv_draw_icon(ctx, config->icon, &box, is_legacy2);
}
box = *bounds;
if (icon_align == GAlignRight) {
left_margin = horizontal_margin;
box.size.w -= config->icon->bounds.size.w;
} else {
left_margin = !config->icon ? horizontal_margin :
config->icon_form_fit ? (left_margin + config->icon->bounds.size.w +
(config->icon_box_model ? config->icon_box_model->margin.w : 0))
: prv_title_subtitle_left_margin() + horizontal_margin;
}
box.origin.x += left_margin;
box.size.w -= left_margin;
GRect value_box = box;
if (config->overflow_mode != GTextOverflowModeWordWrap) {
box.origin.y += vertical_margin;
box.size.h = title_height + 4;
value_box.origin.y = box.origin.y;
} else {
// Value box is centered when drawing with GTextOverflowModeWordWrap.
value_box.origin.y = MIN(box.size.h - full_height, title_height) / 2;
}
if (is_legacy2) {
// Update the text color to Black for legacy apps - this is to maintain existing behavior for
// 2.x compiled apps - no need to restore original since original 2.x did not do any restore
ctx->draw_state.text_color = GColorBlack;
}
if (config->value && (icon_align != GAlignRight)) {
value_box.size.w -= horizontal_margin;
const GFont value_font = prv_get_cell_value_font(config);
const GSize text_size = graphics_text_layout_get_max_used_size(
ctx, config->value, value_font, value_box, config->overflow_mode,
GTextAlignmentRight, NULL);
box.size.w -= (text_size.w + horizontal_margin * 2);
graphics_draw_text(ctx, config->value, value_font, value_box,
config->overflow_mode, GTextAlignmentRight, NULL);
}
if (config->title) {
graphics_draw_text(ctx, config->title, title_font, box,
config->overflow_mode, GTextAlignmentLeft, NULL);
}
if (config->subtitle) {
box.origin.y += title_height;
box.size.h = subtitle_height + 4;
graphics_draw_text(ctx, config->subtitle, subtitle_font, box,
config->overflow_mode, GTextAlignmentLeft, NULL);
}
}
// This function duplicates `grect_inset()` but helps us save some stack space by using pointer
// arguments and always inlining the function
static ALWAYS_INLINE void prv_grect_inset(GRect *rect, GEdgeInsets *insets) {
grect_standardize(rect);
const int16_t new_width = rect->size.w - insets->left - insets->right;
const int16_t new_height = rect->size.h - insets->top - insets->bottom;
if (new_width < 0 || new_height < 0) {
*rect = GRectZero;
} else {
*rect = GRect(rect->origin.x + insets->left, rect->origin.y + insets->top,
new_width, new_height);
}
}
static ALWAYS_INLINE bool prv_should_render_subtitle_round(const MenuCellLayerConfig *config,
bool is_selected) {
// If the cell isn't selected and there's no value text, then no subtitle text should be shown
return ((is_selected || config->value) && (config->subtitle != NULL));
}
static ALWAYS_INLINE GRect prv_menu_cell_basic_draw_custom_one_column_round(
GContext *ctx, const GRect *cell_layer_bounds, const MenuCellLayerConfig *config,
GTextAlignment text_alignment, GAlign container_alignment, bool is_selected) {
if (!cell_layer_bounds) {
return GRectZero;
}
const GFont title_font = prv_get_cell_title_font(config);
const uint8_t title_font_height = fonts_get_font_height(title_font);
GSize cell_layer_bounds_size = cell_layer_bounds->size;
// Bail out if we can't even fit a single line of the title
if (title_font_height > cell_layer_bounds_size.h) {
return GRectZero;
}
// Initialize our subtitle text height and icon frame size to 0 since we don't know yet if we will
// render them
int16_t subtitle_text_frame_height = 0;
GSize icon_frame_size = GSizeZero;
const GFont subtitle_font = prv_get_cell_subtitle_font(config);
const bool render_subtitle = prv_should_render_subtitle_round(config, is_selected);
if (render_subtitle) {
subtitle_text_frame_height = fonts_get_font_height(subtitle_font);
}
const int subtitle_text_cap_offset =
(config->subtitle) ? fonts_get_font_cap_offset(subtitle_font) : 0;
const GAlign icon_align = (GAlign)config->icon_align;
const bool render_icon = ((config->icon != NULL) && (icon_align != GAlignRight));
const GSize icon_bitmap_size = config->icon ? config->icon->bounds.size : GSizeZero;
if (render_icon) {
icon_frame_size = icon_bitmap_size;
if (config->icon_box_model) {
icon_frame_size = gsize_add(icon_frame_size, config->icon_box_model->margin);
}
}
const bool can_use_two_lines_for_title = !(render_subtitle || render_icon);
const bool can_use_many_lines_for_title = (config->overflow_mode == GTextOverflowModeWordWrap);
const int16_t intitial_title_text_lines = can_use_two_lines_for_title ? 2 : 1;
int16_t title_text_frame_height = can_use_many_lines_for_title
? graphics_text_layout_get_text_height(ctx, config->title, title_font,
cell_layer_bounds_size.w, config->overflow_mode,
text_alignment)
: title_font_height * intitial_title_text_lines;
const int title_text_cap_offset = (config->title) ? fonts_get_font_cap_offset(title_font) : 0;
int16_t container_height = title_text_frame_height + subtitle_text_frame_height;
if (icon_align == GAlignTop) {
// The icon is rendered above the others, add to container height
container_height += icon_frame_size.h;
} else if (icon_frame_size.h > cell_layer_bounds_size.h) {
// The icon is rendered beside but does not fit, cut it out
icon_frame_size = GSizeZero;
}
if (container_height > cell_layer_bounds_size.h) {
// If we couldn't fit one line of title, subtitle, and icon, try cutting out the icon
if (icon_align == GAlignTop) {
container_height -= icon_frame_size.h;
}
if (container_height > cell_layer_bounds_size.h) {
// If we couldn't fit one title line and the subtitle, try cutting out the subtitle instead
container_height = title_text_frame_height + icon_frame_size.h;
if (container_height > cell_layer_bounds_size.h) {
// If we couldn't fit just the title and icon, try just two lines for the title
container_height = title_font_height * 2;
if (container_height > cell_layer_bounds_size.h) {
// If we couldn't fit two title lines, just use one title line
title_text_frame_height = title_font_height;
} else {
title_text_frame_height = container_height;
}
subtitle_text_frame_height = 0;
if (icon_align == GAlignTop) {
icon_frame_size = GSizeZero;
}
} else {
subtitle_text_frame_height = 0;
}
} else {
icon_frame_size = GSizeZero;
}
}
// We'll reuse this rect to conserve stack space; here it is used as the title text frame
GRect rect = (GRect) {
.origin = cell_layer_bounds->origin,
.size = GSize(cell_layer_bounds_size.w, title_text_frame_height),
};
// Update the title text frame's height using the max used size of the title text because we
// might only have one line of text to render even though we have space for two lines
title_text_frame_height = graphics_text_layout_get_max_used_size(
ctx, config->title, title_font, rect, config->overflow_mode, text_alignment, NULL).h;
// Calculate the final container height and create a rectangle for it
container_height = title_text_frame_height + subtitle_text_frame_height;
const bool icon_on_left = ((icon_align == GAlignLeft) || (icon_align == GAlignTopLeft));
if (icon_align == GAlignTop) {
// The icon is on its own line at the top, extend accordingly
container_height += icon_frame_size.h;
} else if (render_icon && (icon_align == GAlignLeft)) {
// Let the icon extend the container height if it's taller than the title/subtitle combo
container_height = MAX(icon_frame_size.h, container_height);
}
GRect container_rect = (GRect) { .size = GSize(cell_layer_bounds_size.w, container_height) };
// Align the container rect in the cell
grect_align(&container_rect, cell_layer_bounds, container_alignment, true /* clip */);
// Here we reuse rect as the icon frame
rect.size = icon_frame_size;
// Align the icon frame (which might have zero height) at the top center of the container
grect_align(&rect, &container_rect, icon_align, true /* clip */);
// Save the title origin y before re-purposing container_rect
int title_text_frame_origin_y = container_rect.origin.y;
// Draw the icon (if one was provided and we have room for it)
if (render_icon && !gsize_equal(&rect.size, &GSizeZero)) {
// Only draw the icon if it fits within the cell
if (gsize_equal(&rect.size, &icon_frame_size)) {
// round has never worked with legacy2 apps
static const bool is_legacy2 = false;
// Reuse container_rect as icon frame
container_rect = (GRect) {
.origin = rect.origin,
.size = icon_bitmap_size,
};
if (config->icon_box_model) {
container_rect.origin = gpoint_add(container_rect.origin, config->icon_box_model->offset);
}
prv_draw_icon(ctx, config->icon, &container_rect, is_legacy2);
}
}
int cell_layer_bounds_origin_x = cell_layer_bounds->origin.x;
// Move the title and subtitle closer together to match designs
const int16_t icon_on_left_title_subtitle_vertical_spacing_offset = -3;
if (icon_align == GAlignTop) {
// Set the title text's frame origin at the bottom of the icon's frame
title_text_frame_origin_y = grect_get_max_y(&rect);
} else if (icon_on_left) {
// Move content to the right for the icon on the left
cell_layer_bounds_origin_x = grect_get_max_x(&rect);
cell_layer_bounds_size.w -= cell_layer_bounds_origin_x - cell_layer_bounds->origin.x;
if (icon_align == GAlignLeft) {
// Vertically center the title and subtitle within the container
title_text_frame_origin_y =
cell_layer_bounds->origin.y +
((cell_layer_bounds->size.h - title_text_frame_height -
subtitle_text_frame_height - 1) / 2);
if (subtitle_text_frame_height) {
title_text_frame_origin_y -= icon_on_left_title_subtitle_vertical_spacing_offset;
}
}
}
// Draw the subtitle (if one was provided and we have room), taking into account the cap offset
if (render_subtitle && (subtitle_text_frame_height != 0)) {
int16_t subtitle_text_frame_origin_y =
(int16_t)(title_text_frame_origin_y + title_text_frame_height - subtitle_text_cap_offset);
if (icon_align == GAlignLeft) {
subtitle_text_frame_origin_y += icon_on_left_title_subtitle_vertical_spacing_offset;
}
// Reuse rect as the subtitle text frame
rect = (GRect) {
.origin = GPoint(cell_layer_bounds_origin_x, subtitle_text_frame_origin_y),
.size = GSize(cell_layer_bounds_size.w, subtitle_text_frame_height)
};
graphics_draw_text(ctx, config->subtitle, subtitle_font, rect, config->overflow_mode,
text_alignment, NULL);
}
// Draw the title, which we're guaranteed to have room for because otherwise we would have bailed
// out at the beginning of this function
// Reuse rect as the title text frame
rect = (GRect) {
.origin = GPoint(cell_layer_bounds_origin_x, title_text_frame_origin_y),
.size = GSize(cell_layer_bounds_size.w, title_text_frame_height)
};
// Accumulate the cap offsets we need to position the title properly
int cap_offsets_to_apply = title_text_cap_offset;
if ((icon_align == GAlignLeft) && subtitle_text_frame_height) {
cap_offsets_to_apply += subtitle_text_cap_offset;
}
rect.origin.y -= cap_offsets_to_apply;
graphics_draw_text(ctx, config->title, title_font, rect, config->overflow_mode,
text_alignment, NULL);
// Add back the cap offset so functions that use the returned title text frame can position
// themselves using the actual frame
rect.origin.y += cap_offsets_to_apply;
return rect;
}
static ALWAYS_INLINE void prv_menu_cell_basic_draw_custom_two_columns_round(
GContext *ctx, const GRect *cell_layer_bounds, const MenuCellLayerConfig *config,
bool is_selected) {
if (!cell_layer_bounds) {
return;
}
const GSize icon_size = config->icon ? config->icon->bounds.size : GSizeZero;
// Calculate the size used by the value or icon on the right
// NOTE: If a value and icon is provided we only draw the value so we can re-use this function for
// drawing "icon on right" and "value"
GSize right_element_size;
const GFont value_font = prv_get_cell_value_font(config);
if (config->value) {
right_element_size = graphics_text_layout_get_max_used_size(
ctx, config->value, value_font, *cell_layer_bounds, config->overflow_mode,
GTextAlignmentRight, NULL);
} else {
right_element_size = icon_size;
}
// We reuse this rect to save stack space; here it is the rect of the left column content
GRect rect = *cell_layer_bounds;
prv_grect_inset(&rect, &GEdgeInsets(0, right_element_size.w, 0, 0));
// We overwrite rect to store the rect of the title text frame drawn in the one-column function
rect = prv_menu_cell_basic_draw_custom_one_column_round(ctx, &rect, config, GTextAlignmentLeft,
GAlignLeft, is_selected);
// Don't draw the right element if we couldn't draw the title in the left column
if (grect_equal(&rect, &GRectZero)) {
return;
}
// Now we store the right element frame in rect
rect = (GRect) {
.origin = GPoint(grect_get_max_x(&rect), rect.origin.y),
.size = right_element_size
};
if (config->value) {
rect.origin.y -= fonts_get_font_cap_offset(value_font);
graphics_draw_text(ctx, config->value, value_font, rect, config->overflow_mode,
GTextAlignmentRight, NULL);
} else {
// Only draw the icon if it fits within the cell after aligning it center right
grect_clip(&rect, cell_layer_bounds);
grect_align(&rect, cell_layer_bounds, GAlignRight, true);
if (gsize_equal(&rect.size, &icon_size)) {
// round has never worked with legacy2 apps
static const bool is_legacy2 = false;
if (config->icon_box_model) {
rect.origin = gpoint_add(rect.origin, config->icon_box_model->offset);
}
prv_draw_icon(ctx, config->icon, &rect, is_legacy2);
}
}
}
static ALWAYS_INLINE void prv_menu_cell_basic_draw_custom_round(
GContext* ctx, const Layer *cell_layer, const MenuCellLayerConfig *config) {
// TODO PBL-23041: When round MenuLayer animations are enabled, we need a "is_selected" function
const bool cell_is_selected = menu_cell_layer_is_highlighted(cell_layer);
const bool draw_two_columns =
(config->value || (config->icon && ((GAlign)config->icon_align == GAlignRight)));
// Determine appropriate insets to match designs
GRect cell_layer_bounds = cell_layer->bounds;
const int16_t horizontal_inset = (cell_is_selected && !draw_two_columns)
? MENU_CELL_ROUND_FOCUSED_HORIZONTAL_INSET
: MENU_CELL_ROUND_UNFOCUSED_HORIZONTAL_INSET;
prv_grect_inset(&cell_layer_bounds,
&GEdgeInsets(0, horizontal_inset + config->horizontal_inset));
if (draw_two_columns) {
prv_menu_cell_basic_draw_custom_two_columns_round(ctx, &cell_layer_bounds, config,
cell_is_selected);
} else {
prv_menu_cell_basic_draw_custom_one_column_round(ctx, &cell_layer_bounds, config,
GTextAlignmentCenter, GAlignCenter,
cell_is_selected);
}
}
static ALWAYS_INLINE void prv_draw_cell(GContext *ctx, const Layer *cell_layer,
const MenuCellLayerConfig *config) {
PBL_IF_RECT_ELSE(prv_menu_cell_basic_draw_custom_rect,
prv_menu_cell_basic_draw_custom_round)(ctx, cell_layer, config);
}
//! TODO: PBL-21467 Replace with MenuCellLayer
void menu_cell_layer_draw(GContext *ctx, const Layer *cell_layer,
const MenuCellLayerConfig *config) {
prv_draw_cell(ctx, cell_layer, config);
}
static ALWAYS_INLINE void prv_draw_basic(
GContext* ctx, const Layer *cell_layer, GFont const title_font, const char *title,
GFont const value_font, const char *value, GFont const subtitle_font, const char *subtitle,
GBitmap *icon, bool icon_on_right, GTextOverflowMode overflow_mode) {
MenuCellLayerConfig config = {
.title_font = title_font,
.subtitle_font = subtitle_font,
.value_font = value_font,
.title = title,
.subtitle = subtitle,
.value = value,
.icon = icon,
.icon_align = icon_on_right ? MenuCellLayerIconAlign_Right :
PBL_IF_RECT_ELSE(MenuCellLayerIconAlign_Left,
MenuCellLayerIconAlign_Top),
.overflow_mode = overflow_mode,
};
prv_draw_cell(ctx, cell_layer, &config);
}
void menu_cell_basic_draw_custom(GContext* ctx, const Layer *cell_layer, GFont const title_font,
const char *title, GFont const value_font, const char *value,
GFont const subtitle_font, const char *subtitle, GBitmap *icon,
bool icon_on_right, GTextOverflowMode overflow_mode) {
prv_draw_basic(ctx, cell_layer, title_font, title, value_font, value, subtitle_font, subtitle,
icon, icon_on_right, overflow_mode);
}
void menu_cell_basic_draw_icon_right(GContext* ctx, const Layer *cell_layer,
const char *title, const char *subtitle, GBitmap *icon) {
prv_draw_basic(ctx, cell_layer, NULL, title, NULL, NULL, NULL, subtitle, icon, true,
GTextOverflowModeFill);
}
void menu_cell_basic_draw(GContext* ctx, const Layer *cell_layer, const char *title,
const char *subtitle, GBitmap *icon) {
prv_draw_basic(ctx, cell_layer, NULL, title, NULL, NULL, NULL, subtitle, icon, false,
GTextOverflowModeFill);
}
//////////////////////
// Title menu cell
void menu_cell_title_draw(GContext* ctx, const Layer *cell_layer, const char *title) {
// Title:
if (title) {
const bool is_legacy2 = process_manager_compiled_with_legacy2_sdk();
if (is_legacy2) {
// Update the text color to Black for legacy apps - this is to maintain existing behavior for
// 2.x compiled apps - no need to restore original since original 2.x did not do any restore
graphics_context_set_text_color(ctx, GColorBlack);
}
GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_28);
GRect box = cell_layer->bounds;
box.origin.x = 3;
box.origin.y -= 4;
box.size.w -= 3;
graphics_draw_text(ctx, title, font, box, GTextOverflowModeFill, GTextAlignmentLeft, NULL);
}
}
//////////////////////
// Basic header cell
void menu_cell_basic_header_draw(GContext* ctx, const Layer *cell_layer, const char *title) {
// Title:
if (title) {
const bool is_legacy2 = process_manager_compiled_with_legacy2_sdk();
if (is_legacy2) {
// Update the text color to Black for legacy apps - this is to maintain existing behavior for
// 2.x compiled apps - no need to restore original since original 2.x did not do any restore
graphics_context_set_text_color(ctx, GColorBlack);
}
GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD);
GRect box = cell_layer->bounds;
// Pixel nudging...
box.origin.x += 2;
box.origin.y -= 1;
graphics_draw_text(ctx, title, font, box, GTextOverflowModeFill, GTextAlignmentLeft, NULL);
}
}

View File

@@ -0,0 +1,244 @@
/*
* 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 "number_window.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/graphics.h"
#include "applib/applib_malloc.auto.h"
#include "kernel/ui/kernel_ui.h"
#include "kernel/ui/system_icons.h"
#include "util/size.h"
#include <stdio.h>
#include <limits.h>
#if RECOVERY_FW || MANUFACTURING_FW
#define NUMBER_FONT_KEY FONT_KEY_GOTHIC_24_BOLD
#else
#define NUMBER_FONT_KEY FONT_KEY_BITHAM_34_MEDIUM_NUMBERS
#endif
// updates the textual output value of the numberwindow to match the actual value
static void update_output_value(NumberWindow *nf) {
layer_mark_dirty(&nf->window.layer);
}
// implemented from: http://stackoverflow.com/questions/707370/clean-efficient-algorithm-for-wrapping-integers-in-c
// answered by: Eddie Parker, <http://stackoverflow.com/users/56349/eddie-parker>
static int wrap(int num, int const lower_bound, int const upper_bound) {
int range_size = upper_bound - lower_bound + 1;
if (num < lower_bound)
num += range_size * ((lower_bound - num) / range_size + 1);
return lower_bound + (num - lower_bound) % range_size;
}
static void up_click_handler(ClickRecognizerRef recognizer, NumberWindow *nf) {
bool is_increased = false;
int32_t new_val = nf->value + nf->step_size;
if (new_val <= nf->max_val && new_val > nf->value) {
nf->value = new_val;
is_increased = true;
}
if (is_increased) {
if (nf->callbacks.incremented != NULL) {
nf->callbacks.incremented(nf, nf->callback_context);
}
update_output_value(nf);
}
}
static void down_click_handler(ClickRecognizerRef recognizer, NumberWindow *nf) {
bool is_decreased = false;
int32_t new_val = nf->value - nf->step_size;
if (new_val >= nf->min_val && new_val < nf->value) {
nf->value = new_val;
is_decreased = true;
}
if (is_decreased) {
if (nf->callbacks.decremented != NULL) {
nf->callbacks.decremented(nf, nf->callback_context);
}
update_output_value(nf);
}
}
static void select_click_handler(ClickRecognizerRef recognizer, NumberWindow *nf) {
if (nf->callbacks.selected != NULL) {
nf->callbacks.selected(nf, nf->callback_context);
}
}
static void click_config_provider(NumberWindow *nf) {
window_single_repeating_click_subscribe(BUTTON_ID_UP, 50, (ClickHandler) up_click_handler);
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 50, (ClickHandler) down_click_handler);
// Work-around: by using a multi-click setup for the select button,
// the handler will get fired with a very short delay, so the inverted segment of
// the action bar is visible for a short period of time as to give visual
// feedback of the button press.
window_multi_click_subscribe(BUTTON_ID_SELECT, 1, 2, 25, true, (ClickHandler)select_click_handler);
}
static GRect prv_get_text_frame(Layer *window_layer) {
const int16_t x_margin = 5;
const int16_t label_y_offset = PBL_IF_ROUND_ELSE(40, 16);
const GEdgeInsets insets = PBL_IF_ROUND_ELSE(GEdgeInsets(ACTION_BAR_WIDTH + x_margin),
GEdgeInsets(0, ACTION_BAR_WIDTH + x_margin, 0,
x_margin));
GRect frame = grect_inset(window_layer->bounds, insets);
frame.origin.y = label_y_offset;
return frame;
}
//! Drawing function for our Window's base Layer. Draws the background, the label, and the value,
//! which is everything on screen with the exception of the child ActionBarLayer
void prv_update_proc(Layer *layer, GContext* ctx) {
graphics_context_set_fill_color(ctx, GColorWhite);
graphics_fill_rect(ctx, &layer->bounds);
// This is safe becase Layer is the first member in Window and Window is the first member in
// NumberWindow.
_Static_assert(offsetof(Window, layer) == 0, "");
_Static_assert(offsetof(NumberWindow, window) == 0, "");
NumberWindow *nw = (NumberWindow*) layer;
graphics_context_set_text_color(ctx, GColorBlack);
GRect frame = prv_get_text_frame(layer);
frame.size.h = 54;
TextLayoutExtended cached_label_layout = {};
graphics_draw_text(ctx, nw->label, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
frame, GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter,
(TextLayout*) &cached_label_layout);
char value_output_buffer[12];
snprintf(value_output_buffer, ARRAY_LENGTH(value_output_buffer), "%"PRId32, nw->value);
frame.origin.y += cached_label_layout.max_used_size.h;
#if PBL_RECT
const int16_t output_offset_from_label = 15;
frame.origin.y += output_offset_from_label;
#endif
frame.size.h = 48;
graphics_draw_text(ctx, value_output_buffer,
fonts_get_system_font(NUMBER_FONT_KEY), frame,
GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL);
}
void number_window_set_label(NumberWindow *nw, const char *label) {
nw->label = label;
layer_mark_dirty(&nw->window.layer);
}
void number_window_set_max(NumberWindow *nf, int32_t max) {
nf->max_val = max;
if (nf->value > max) {
nf->value = max;
update_output_value(nf);
}
if (nf->min_val > max) {
nf->min_val = max;
}
}
void number_window_set_min(NumberWindow *nf, int32_t min) {
nf->min_val = min;
if (nf->value < min) {
nf->value = min;
update_output_value(nf);
}
if (nf->max_val < min) {
nf->max_val = min;
}
}
void number_window_set_value(NumberWindow *nf, int32_t value) {
nf->value = value;
if (nf->value > nf->max_val) {
nf->value = nf->max_val;
}
if (nf->value < nf->min_val) {
nf->value = nf->min_val;
}
update_output_value(nf);
}
void number_window_set_step_size(NumberWindow *nf, int32_t step) {
nf->step_size = step;
}
int32_t number_window_get_value(const NumberWindow *nf) {
return nf->value;
}
static void number_window_load(NumberWindow *nw) {
ActionBarLayer *action_bar = &nw->action_bar;
action_bar_layer_set_context(action_bar, nw);
action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, &s_bar_icon_up_bitmap);
action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, &s_bar_icon_down_bitmap);
action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, &s_bar_icon_check_bitmap);
action_bar_layer_add_to_window(action_bar, &nw->window);
action_bar_layer_set_click_config_provider(action_bar, (ClickConfigProvider) click_config_provider);
}
void number_window_init(NumberWindow *nw, const char *label, NumberWindowCallbacks callbacks, void *callback_context) {
*nw = (NumberWindow) {
.label = label,
.value = 0,
.max_val = INT_MAX,
.min_val = INT_MIN,
.step_size = 1,
.callbacks = callbacks,
.callback_context = callback_context
};
window_init(&nw->window, WINDOW_NAME(label));
window_set_window_handlers(&nw->window, &(WindowHandlers) {
.load = (WindowHandler) number_window_load,
});
layer_set_update_proc(&nw->window.layer, prv_update_proc);
ActionBarLayer *action_bar = &nw->action_bar;
action_bar_layer_init(action_bar);
}
NumberWindow* number_window_create(const char *label, NumberWindowCallbacks callbacks, void *callback_context) {
NumberWindow* window = applib_type_malloc(NumberWindow);
if (window) {
number_window_init(window, label, callbacks, callback_context);
}
return window;
}
static void number_window_deinit(NumberWindow *number_window) {
action_bar_layer_deinit(&number_window->action_bar);
window_deinit(&number_window->window);
}
void number_window_destroy(NumberWindow *number_window) {
if (number_window == NULL) {
return;
}
number_window_deinit(number_window);
applib_free(number_window);
}
Window *number_window_get_window(NumberWindow *numberwindow) {
return (&numberwindow->window);
}

View File

@@ -0,0 +1,150 @@
/*
* 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 "layer.h"
#include "text_layer.h"
#include "action_bar_layer.h"
#include "window.h"
//! @file number_window.h
//! @addtogroup UI
//! @{
//! @addtogroup Window
//! @{
//! @addtogroup NumberWindow
//! \brief A ready-made Window prompting the user to pick a number
//!
//! ![](number_window.png)
//! @{
//! A ready-made Window prompting the user to pick a number
struct NumberWindow;
//! Function signature for NumberWindow callbacks.
typedef void (*NumberWindowCallback)(struct NumberWindow *number_window, void *context);
//! Data structure containing all the callbacks for a NumberWindow.
typedef struct {
//! Callback that gets called as the value is incremented.
//! Optional, leave `NULL` if unused.
NumberWindowCallback incremented;
//! Callback that gets called as the value is decremented.
//! Optional, leave `NULL` if unused.
NumberWindowCallback decremented;
//! Callback that gets called as the value is confirmed, in other words the
//! SELECT button is clicked.
//! Optional, leave `NULL` if unused.
NumberWindowCallback selected;
} NumberWindowCallbacks;
//! Data structure of a NumberWindow.
//! @note a `NumberWindow *` can safely be casted to a `Window *` and can thus
//! be used with all other functions that take a `Window *` as an argument.
//! <br/>For example, the following is legal:
//! \code{.c}
//! NumberWindow number_window;
//! ...
//! window_stack_push((Window *)&number_window, true);
//! \endcode
typedef struct NumberWindow {
//! Make sure this is the first member of this struct, we use that to cast from Layer* all the
//! way up to NumberWindow* in prv_update_proc.
Window window;
ActionBarLayer action_bar;
const char *label;
int32_t value;
int32_t max_val;
int32_t min_val;
int32_t step_size;
NumberWindowCallbacks callbacks;
void *callback_context;
} NumberWindow;
//! Initializes the NumberWindow.
//! @param numberwindow Pointer to the NumberWindow to initialize
//! @param label The title or prompt to display in the NumberWindow. Must be long-lived and cannot be stack-allocated.
//! @param callbacks The callbacks
//! @param callback_context Pointer to application specific data that is passed
//! into the callbacks.
//! @note The number window is not pushed to the window stack. Use \ref window_stack_push() to do this.
//! See code fragment here: NumberWindow
void number_window_init(NumberWindow *numberwindow, const char *label, NumberWindowCallbacks callbacks, void *callback_context);
//! Creates a new NumberWindow on the heap and initalizes it with the default values.
//!
//! @param label The title or prompt to display in the NumberWindow. Must be long-lived and cannot be stack-allocated.
//! @param callbacks The callbacks
//! @param callback_context Pointer to application specific data that is passed
//! @note The number window is not pushed to the window stack. Use \ref window_stack_push() to do this.
//! @return A pointer to the NumberWindow. `NULL` if the NumberWindow could not
//! be created
NumberWindow* number_window_create(const char *label, NumberWindowCallbacks callbacks, void *callback_context);
//! Destroys a NumberWindow previously created by number_window_create.
void number_window_destroy(NumberWindow* number_window);
//! Sets the text of the title or prompt label.
//! @param numberwindow Pointer to the NumberWindow for which to set the label
//! text
//! @param label The new label text. Must be long-lived and cannot be
//! stack-allocated.
void number_window_set_label(NumberWindow *numberwindow, const char *label);
//! Sets the maximum value this field can hold
//! @param numberwindow Pointer to the NumberWindow for which to set the maximum
//! value
//! @param max The maximum value
void number_window_set_max(NumberWindow *numberwindow, int32_t max);
//! Sets the minimum value this field can hold
//! @param numberwindow Pointer to the NumberWindow for which to set the minimum
//! value
//! @param min The minimum value
void number_window_set_min(NumberWindow *numberwindow, int32_t min);
//! Sets the current value of the field
//! @param numberwindow Pointer to the NumberWindow for which to set the current
//! value
//! @param value The new current value
void number_window_set_value(NumberWindow *numberwindow, int32_t value);
//! Sets the amount by which to increment/decrement by on a button click
//! @param numberwindow Pointer to the NumberWindow for which to set the step
//! increment
//! @param step The new step increment
void number_window_set_step_size(NumberWindow *numberwindow, int32_t step);
//! Gets the current value
//! @param numberwindow Pointer to the NumberWindow for which to get the current
//! value
//! @return The current value
int32_t number_window_get_value(const NumberWindow *numberwindow);
//! Gets the "root" Window of the number window
//! @param numberwindow Pointer to the NumberWindow for which to get the "root" Window
//! @return The "root" Window of the number window.
Window *number_window_get_window(NumberWindow *numberwindow);
//! @} // end addtogroup NumberWindow
//! @} // end addtogroup Window
//! @} // end addtogroup UI

View File

@@ -0,0 +1,337 @@
/*
* 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 "option_menu_window.h"
#include "applib/applib_malloc.auto.h"
#include "resource/resource_ids.auto.h"
#include "shell/system_theme.h"
#include "system/passert.h"
typedef struct OptionMenuStyle {
#if PBL_RECT
uint16_t cell_heights[OptionMenuContentTypeCount];
#endif
int16_t top_inset;
int16_t right_icon_spacing;
int16_t text_inset_single;
int16_t text_inset_multi;
int16_t right_text_inset_with_icon;
} OptionMenuStyle;
static const OptionMenuStyle s_style_medium = {
#if PBL_RECT
.cell_heights[OptionMenuContentType_DoubleLine] = 56,
#endif
.right_icon_spacing = PBL_IF_RECT_ELSE(7, 35),
};
static const OptionMenuStyle s_style_large = {
#if PBL_RECT
.cell_heights[OptionMenuContentType_SingleLine] = 46,
#endif
.top_inset = 1,
.right_icon_spacing = PBL_IF_RECT_ELSE(10, 35),
.text_inset_single = -1,
.text_inset_multi = -3,
.right_text_inset_with_icon = 4,
};
static const OptionMenuStyle * const s_styles[NumPreferredContentSizes] = {
[PreferredContentSizeSmall] = &s_style_medium,
[PreferredContentSizeMedium] = &s_style_medium,
[PreferredContentSizeLarge] = &s_style_large,
[PreferredContentSizeExtraLarge] = &s_style_large,
};
static const OptionMenuStyle *prv_get_style(void) {
return s_styles[PreferredContentSizeDefault];
}
static uint16_t prv_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index,
void *context) {
OptionMenu *option_menu = context;
if (option_menu->callbacks.get_num_rows) {
return option_menu->callbacks.get_num_rows(option_menu, option_menu->context);
}
return 0;
}
uint16_t option_menu_default_cell_height(OptionMenuContentType content_type, bool selected) {
const OptionMenuStyle * const UNUSED style = prv_get_style();
const int16_t cell_height =
PBL_IF_ROUND_ELSE(selected ? MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT :
MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT,
style->cell_heights[content_type]);
return cell_height ?: menu_cell_basic_cell_height();
}
static int16_t prv_get_cell_height_callback(MenuLayer *menu_layer, MenuIndex *cell_index,
void *context) {
const bool is_selected = menu_layer_is_index_selected(menu_layer, cell_index);
OptionMenu *option_menu = context;
if (option_menu->callbacks.get_cell_height) {
return option_menu->callbacks.get_cell_height(option_menu, cell_index->row, is_selected,
option_menu->context);
} else {
return option_menu_default_cell_height(option_menu->content_type, is_selected);
}
}
static int32_t prv_draw_selection_icon(const OptionMenu *option_menu, GContext *ctx,
const GRect *cell_layer_bounds, bool is_chosen) {
const int32_t left_icon_spacing = PBL_IF_RECT_ELSE(0, 14);
const GSize not_chosen_icon_bounds = gbitmap_get_bounds(&option_menu->not_chosen_image).size;
const GSize chosen_icon_bounds = gbitmap_get_bounds(&option_menu->chosen_image).size;
PBL_ASSERTN(gsize_equal(&not_chosen_icon_bounds, &chosen_icon_bounds));
GRect icon_frame = { .size = chosen_icon_bounds };
grect_align(&icon_frame, cell_layer_bounds, GAlignRight, false);
const OptionMenuStyle * const style = prv_get_style();
icon_frame.origin.x -= style->right_icon_spacing;
const GBitmap *const icon =
is_chosen ? &option_menu->chosen_image : &option_menu->not_chosen_image;
graphics_context_set_compositing_mode(ctx, GCompOpTint);
graphics_draw_bitmap_in_rect(ctx, icon, &icon_frame);
return icon_frame.size.w + left_icon_spacing + style->right_icon_spacing;
}
static void prv_draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index,
void *context) {
OptionMenu *option_menu = context;
const MenuIndex selected = menu_layer_get_selected_index(&option_menu->menu_layer);
const bool is_selected = (menu_index_compare(&selected, cell_index) == 0);
const GRect *cell_layer_bounds = &cell_layer->bounds;
GRect remaining_rect = *cell_layer_bounds;
if (option_menu->icons_enabled) {
const bool is_chosen = (cell_index->row == option_menu->choice);
const int32_t left_inset_x = PBL_IF_RECT_ELSE(0, 14);
const int32_t right_inset_x = prv_draw_selection_icon(option_menu, ctx, &remaining_rect,
is_chosen);
remaining_rect = grect_inset(remaining_rect, GEdgeInsets(0, right_inset_x, 0, left_inset_x));
}
#if PBL_ROUND
if (!is_selected && option_menu->icons_enabled) {
const int32_t left_text_inset_to_prevent_clipping = 8;
remaining_rect = grect_inset(remaining_rect,
GEdgeInsets(0, 0, 0, left_text_inset_to_prevent_clipping));
}
#else
const OptionMenuStyle * const style = prv_get_style();
const int32_t left_text_inset = menu_cell_basic_horizontal_inset();
const int32_t right_text_inset = option_menu->icons_enabled ? style->right_text_inset_with_icon :
left_text_inset;
remaining_rect = grect_inset(remaining_rect, GEdgeInsets(style->top_inset, right_text_inset, 0,
left_text_inset));
#endif
if (option_menu->callbacks.draw_row) {
option_menu->callbacks.draw_row(option_menu, ctx, cell_layer, &remaining_rect, cell_index->row,
is_selected, option_menu->context);
}
}
static void prv_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *context) {
OptionMenu *option_menu = context;
option_menu->choice = cell_index->row;
layer_mark_dirty((Layer *)&option_menu->menu_layer);
if (option_menu->callbacks.select) {
option_menu->callbacks.select(option_menu, option_menu->choice, option_menu->context);
}
}
static void prv_window_load(Window *window) {
OptionMenu *option_menu = window_get_user_data(window);
menu_layer_set_callbacks(&option_menu->menu_layer, option_menu, &(MenuLayerCallbacks) {
.get_cell_height = prv_get_cell_height_callback,
.get_num_rows = prv_get_num_rows_callback,
.draw_row = prv_draw_row_callback,
.select_click = prv_select_callback
});
menu_layer_set_click_config_onto_window(&option_menu->menu_layer, window);
if (option_menu->choice != OPTION_MENU_CHOICE_NONE) {
menu_layer_set_selected_index(&option_menu->menu_layer, MenuIndex(0, option_menu->choice),
MenuRowAlignCenter, false);
}
layer_add_child(window_get_root_layer(window), menu_layer_get_layer(&option_menu->menu_layer));
}
static void prv_window_unload(Window *window) {
OptionMenu *option_menu = window_get_user_data(window);
if (option_menu->callbacks.unload) {
option_menu->callbacks.unload(option_menu, option_menu->context);
}
}
void option_menu_set_status_colors(OptionMenu *option_menu, GColor background, GColor foreground) {
option_menu->status_colors.background = background;
option_menu->status_colors.foreground = foreground;
status_bar_layer_set_colors(&option_menu->status_layer,
option_menu->status_colors.background,
option_menu->status_colors.foreground);
}
void option_menu_set_normal_colors(OptionMenu *option_menu, GColor background, GColor foreground) {
option_menu->normal_colors.background = background;
option_menu->normal_colors.foreground = foreground;
menu_layer_set_normal_colors(&option_menu->menu_layer,
option_menu->normal_colors.background,
option_menu->normal_colors.foreground);
}
void option_menu_set_highlight_colors(OptionMenu *option_menu, GColor background,
GColor foreground) {
option_menu->highlight_colors.background = background;
option_menu->highlight_colors.foreground = foreground;
menu_layer_set_highlight_colors(&option_menu->menu_layer,
option_menu->highlight_colors.background,
option_menu->highlight_colors.foreground);
}
void option_menu_set_callbacks(OptionMenu *option_menu, const OptionMenuCallbacks *callbacks,
void *context) {
option_menu->callbacks = *callbacks;
option_menu->context = context;
}
void option_menu_set_title(OptionMenu *option_menu, const char *title) {
option_menu->title = title;
status_bar_layer_set_title(&option_menu->status_layer, title, false, false);
}
void option_menu_set_choice(OptionMenu *option_menu, int choice) {
option_menu->choice = choice;
layer_mark_dirty((Layer *)&option_menu->menu_layer);
}
void option_menu_set_content_type(OptionMenu *option_menu, OptionMenuContentType content_type) {
option_menu->content_type = content_type;
}
void option_menu_reload_data(OptionMenu *option_menu) {
menu_layer_reload_data(&option_menu->menu_layer);
}
void option_menu_set_icons_enabled(OptionMenu *option_menu, bool icons_enabled) {
option_menu->icons_enabled = icons_enabled;
}
void option_menu_configure(OptionMenu *option_menu,
const OptionMenuConfig *config) {
option_menu_set_title(option_menu, config->title);
option_menu_set_choice(option_menu, config->choice);
option_menu_set_content_type(option_menu, config->content_type);
option_menu_set_status_colors(option_menu, config->status_colors.background,
config->status_colors.foreground);
option_menu_set_highlight_colors(option_menu, config->highlight_colors.background,
config->highlight_colors.foreground);
option_menu_set_icons_enabled(option_menu, config->icons_enabled);
}
void option_menu_init(OptionMenu *option_menu) {
*option_menu = (OptionMenu) {
.choice = OPTION_MENU_CHOICE_NONE,
.title_font = system_theme_get_font_for_default_size(TextStyleFont_MenuCellTitle),
};
// radio button icons are enabled by default
option_menu_set_icons_enabled(option_menu, true);
GBitmap *chosen_image = &option_menu->chosen_image;
gbitmap_init_with_resource(chosen_image, RESOURCE_ID_CHECKED_RADIO_BUTTON);
GBitmap *not_chosen_image = &option_menu->not_chosen_image;
gbitmap_init_with_resource(not_chosen_image, RESOURCE_ID_UNCHECKED_RADIO_BUTTON);
window_init(&option_menu->window, WINDOW_NAME("OptionMenu"));
window_set_user_data(&option_menu->window, option_menu);
window_set_window_handlers(&option_menu->window, &(WindowHandlers) {
.load = prv_window_load,
.unload = prv_window_unload,
});
StatusBarLayer *status_layer = &option_menu->status_layer;
status_bar_layer_init(status_layer);
status_bar_layer_set_separator_mode(status_layer, OPTION_MENU_STATUS_SEPARATOR_MODE);
layer_add_child(&option_menu->window.layer, &status_layer->layer);
MenuLayer *menu_layer = &option_menu->menu_layer;
GRect bounds = grect_inset(option_menu->window.layer.bounds, (GEdgeInsets) {
.top = STATUS_BAR_LAYER_HEIGHT,
.bottom = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT),
});
menu_layer_init(menu_layer, &bounds);
}
void option_menu_deinit(OptionMenu *option_menu) {
menu_layer_deinit(&option_menu->menu_layer);
status_bar_layer_deinit(&option_menu->status_layer);
window_deinit(&option_menu->window);
gbitmap_deinit(&option_menu->chosen_image);
gbitmap_deinit(&option_menu->not_chosen_image);
}
OptionMenu *option_menu_create(void) {
OptionMenu *option_menu = applib_type_malloc(OptionMenu);
if (!option_menu) {
return NULL;
}
option_menu_init(option_menu);
return option_menu;
}
void option_menu_destroy(OptionMenu *option_menu) {
option_menu_deinit(option_menu);
applib_free(option_menu);
}
void option_menu_system_draw_row(OptionMenu *option_menu, GContext *ctx, const Layer *cell_layer,
const GRect *cell_frame, const char *title, bool selected,
void *context) {
const GTextOverflowMode overflow_mode = GTextOverflowModeTrailingEllipsis;
// On rectangular, always align to the left. On round, align to the right if we have an icon and
// otherwise to the center. Icons on the right with text in the center looks very bad and wastes
// text space.
const GTextAlignment text_alignment =
PBL_IF_RECT_ELSE(GTextAlignmentLeft,
option_menu->icons_enabled ? GTextAlignmentRight : GTextAlignmentCenter);
GFont const title_font = option_menu->title_font;
const GSize text_size = graphics_text_layout_get_max_used_size(ctx, title, title_font,
*cell_frame, overflow_mode,
text_alignment, NULL);
GRect text_frame = *cell_frame;
const int min_text_height = fonts_get_font_height(title_font);
text_frame.size = text_size;
const GAlign text_frame_alignment =
PBL_IF_RECT_ELSE(GAlignLeft, option_menu->icons_enabled ? GAlignRight : GAlignCenter);
grect_align(&text_frame, cell_frame, text_frame_alignment, true /* clips */);
const OptionMenuStyle * const style = prv_get_style();
const int16_t text_inset = (text_size.h > min_text_height) ? style->text_inset_multi :
style->text_inset_single;
text_frame = grect_inset(text_frame, GEdgeInsets(0, text_inset));
text_frame.origin.y -= fonts_get_font_cap_offset(title_font);
if (title) {
graphics_draw_text(ctx, title, title_font, text_frame, overflow_mode, text_alignment, NULL);
}
}

View File

@@ -0,0 +1,126 @@
/*
* 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/ui/ui.h"
#define OPTION_MENU_CHOICE_NONE (-1)
#define OPTION_MENU_STATUS_SEPARATOR_MODE PBL_IF_RECT_ELSE(StatusBarLayerSeparatorModeDotted, \
StatusBarLayerSeparatorModeNone)
typedef struct OptionMenu OptionMenu;
typedef void (*OptionMenuSelectCallback)(OptionMenu *option_menu, int selection, void *context);
typedef uint16_t (*OptionMenuGetNumRowsCallback)(OptionMenu *option_menu, void *context);
typedef void (*OptionMenuDrawRowCallback)(OptionMenu *option_menu, GContext *ctx,
const Layer *cell_layer, const GRect *text_frame,
uint32_t row, bool selected, void *context);
typedef void (*OptionMenuUnloadCallback)(OptionMenu *option_menu, void *context);
typedef uint16_t (*OptionMenuGetCellHeightCallback)(OptionMenu *option_menu, uint16_t row,
bool selected, void *context);
typedef struct OptionMenuCallbacks {
OptionMenuSelectCallback select;
OptionMenuGetNumRowsCallback get_num_rows;
OptionMenuDrawRowCallback draw_row;
OptionMenuUnloadCallback unload;
OptionMenuGetCellHeightCallback get_cell_height;
} OptionMenuCallbacks;
typedef struct OptionMenuColors {
GColor background;
GColor foreground;
} OptionMenuColors;
typedef enum OptionMenuContentType {
//! Content consists of title subtitle or single-line title with ample vertical spacing.
OptionMenuContentType_Default,
//! Content consists of a single line.
OptionMenuContentType_SingleLine,
//! Content consists of two lines.
OptionMenuContentType_DoubleLine,
OptionMenuContentTypeCount
} OptionMenuContentType;
struct OptionMenu {
Window window;
StatusBarLayer status_layer;
MenuLayer menu_layer;
const char *title;
GFont title_font;
OptionMenuContentType content_type;
GBitmap chosen_image;
GBitmap not_chosen_image;
bool icons_enabled;
OptionMenuCallbacks callbacks;
void *context;
int choice;
OptionMenuColors status_colors;
OptionMenuColors normal_colors;
OptionMenuColors highlight_colors;
};
typedef struct {
const char *title;
int choice;
OptionMenuContentType content_type;
OptionMenuColors status_colors;
OptionMenuColors highlight_colors;
bool icons_enabled;
} OptionMenuConfig;
uint16_t option_menu_default_cell_height(OptionMenuContentType content_type, bool selected);
void option_menu_set_status_colors(OptionMenu *option_menu, GColor background, GColor foreground);
void option_menu_set_normal_colors(OptionMenu *option_menu, GColor background, GColor foreground);
void option_menu_set_highlight_colors(OptionMenu *option_menu, GColor background,
GColor foreground);
//! @internal
//! This is currently the only way to set callbacks, which follows (calling it now) 4.x conventions.
//! If option menu must be exported to 3.x, a pass-by value wrapper must be created.
void option_menu_set_callbacks(OptionMenu *option_menu, const OptionMenuCallbacks *callbacks,
void *context);
void option_menu_set_title(OptionMenu *option_menu, const char *title);
void option_menu_set_choice(OptionMenu *option_menu, int choice);
void option_menu_set_content_type(OptionMenu *option_menu, OptionMenuContentType content_type);
// enable or disable radio button icons
void option_menu_set_icons_enabled(OptionMenu *option_menu, bool icons_enabled);
void option_menu_reload_data(OptionMenu *option_menu);
//! @internal
//! Use this to set common initialization parameters rather than a group of the particular setters.
void option_menu_configure(OptionMenu *option_menu, const OptionMenuConfig *config);
void option_menu_init(OptionMenu *option_menu);
void option_menu_deinit(OptionMenu *option_menu);
OptionMenu *option_menu_create(void);
void option_menu_destroy(OptionMenu *option_menu);
void option_menu_system_draw_row(OptionMenu *option_menu, GContext *ctx, const Layer *cell_layer,
const GRect *cell_frame, const char *title, bool selected,
void *context);

View File

@@ -0,0 +1,63 @@
/*
* 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 "path_layer.h"
#include "applib/graphics/graphics.h"
void path_layer_update_proc(PathLayer *path_layer, GContext* ctx) {
if (!gcolor_is_transparent(path_layer->fill_color)) {
graphics_context_set_fill_color(ctx, path_layer->fill_color);
gpath_draw_filled(ctx, &path_layer->path);
}
if (!gcolor_is_transparent(path_layer->stroke_color)) {
graphics_context_set_stroke_color(ctx, path_layer->stroke_color);
gpath_draw_outline(ctx, &path_layer->path);
}
}
void path_layer_init(PathLayer *path_layer, const GPathInfo *path_info) {
gpath_init(&path_layer->path, path_info);
const GRect outer_rect = gpath_outer_rect(&path_layer->path);
layer_init(&path_layer->layer, &outer_rect);
path_layer->stroke_color = GColorWhite;
path_layer->fill_color = GColorBlack;
path_layer->layer.update_proc = (LayerUpdateProc)path_layer_update_proc;
}
void path_layer_deinit(PathLayer *path_layer, const GPathInfo *path_info) {
layer_deinit(&path_layer->layer);
}
void path_layer_set_stroke_color(PathLayer *path_layer, GColor color) {
if (gcolor_equal(color, path_layer->stroke_color)) {
return;
}
path_layer->stroke_color = color;
layer_mark_dirty(&(path_layer->layer));
}
void path_layer_set_fill_color(PathLayer *path_layer, GColor color) {
if (gcolor_equal(color, path_layer->fill_color)) {
return;
}
path_layer->fill_color = color;
layer_mark_dirty(&(path_layer->layer));
}
Layer* path_layer_get_layer(const PathLayer *path_layer) {
return &((PathLayer *)path_layer)->layer;
}

View File

@@ -0,0 +1,44 @@
/*
* 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"
#include "applib/graphics/gpath.h"
#include "applib/ui/layer.h"
typedef struct PathLayer {
Layer layer;
GPath path;
GColor stroke_color;
GColor fill_color;
} PathLayer;
void path_layer_init(PathLayer *path_layer, const GPathInfo *path_info);
void path_layer_deinit(PathLayer *path_layer, const GPathInfo *path_info);
void path_layer_set_stroke_color(PathLayer *path_layer, GColor color);
void path_layer_set_fill_color(PathLayer *path_layer, GColor color);
//! Gets the "root" Layer of the path layer, which is the parent for the sub-
//! layers used for its implementation.
//! @param path_layer Pointer to the PathLayer for which to get the "root" Layer
//! @return The "root" Layer of the path layer.
//! @internal
//! @note The result is always equal to `(Layer *) path_layer`.
Layer* path_layer_get_layer(const PathLayer *path_layer);

View File

@@ -0,0 +1,23 @@
/*
* 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 "preferred_durations.h"
#include "dialogs/dialog.h"
uint32_t preferred_result_display_duration(void) {
return DIALOG_TIMEOUT_DEFAULT;
}

View File

@@ -0,0 +1,37 @@
/*
* 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 <stdint.h>
//! @file preferred_durations.h
//! @addtogroup UI
//! @{
//! @addtogroup Preferences
//!
//! \brief Values recommended by the system
//!
//! @{
//! Get the recommended amount of milliseconds a result window should be visible before it should
//! automatically close.
//! @note It is the application developer's responsibility to automatically close a result window.
//! @return The recommended result window timeout duration in milliseconds
uint32_t preferred_result_display_duration(void);
//! @} // end addtogroup Preferences
//! @} // end addtogroup UI

View File

@@ -0,0 +1,84 @@
/*
* 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 "progress_layer.h"
#include "system/passert.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/layer.h"
#include "util/math.h"
#include <string.h>
static int16_t scale_progress_bar_width_px(unsigned int progress_percent, int16_t rect_width_px) {
return ((progress_percent * (rect_width_px)) / 100);
}
void progress_layer_set_progress(ProgressLayer* progress_layer, unsigned int progress_percent) {
// Can't use the clip macro here because it fails with uints
progress_layer->progress_percent = MIN(100, progress_percent);
layer_mark_dirty(&progress_layer->layer);
}
void progress_layer_update_proc(ProgressLayer* progress_layer, GContext* ctx) {
const GRect *bounds = &progress_layer->layer.bounds;
int16_t progress_bar_width_px = scale_progress_bar_width_px(progress_layer->progress_percent,
bounds->size.w);
const GRect progress_bar = GRect(bounds->origin.x, bounds->origin.y, progress_bar_width_px,
bounds->size.h);
const int16_t corner_radius = progress_layer->corner_radius;
graphics_context_set_fill_color(ctx, progress_layer->background_color);
graphics_fill_round_rect(ctx, bounds, corner_radius, GCornersAll);
// Draw the progress bar
graphics_context_set_fill_color(ctx, progress_layer->foreground_color);
graphics_fill_round_rect(ctx, &progress_bar, corner_radius, GCornersAll);
#if SCREEN_COLOR_DEPTH_BITS == 1
graphics_context_set_stroke_color(ctx, progress_layer->foreground_color);
graphics_draw_round_rect(ctx, bounds, corner_radius);
#endif
}
void progress_layer_init(ProgressLayer* progress_layer, const GRect *frame) {
*progress_layer = (ProgressLayer){};
layer_init(&progress_layer->layer, frame);
progress_layer->layer.update_proc = (LayerUpdateProc) progress_layer_update_proc;
progress_layer->foreground_color = GColorBlack;
progress_layer->background_color = GColorWhite;
progress_layer->corner_radius = 1;
}
void progress_layer_deinit(ProgressLayer* progress_layer) {
layer_deinit(&progress_layer->layer);
}
void progress_layer_set_foreground_color(ProgressLayer* progress_layer, GColor color) {
progress_layer->foreground_color = color;
}
void progress_layer_set_background_color(ProgressLayer* progress_layer, GColor color) {
progress_layer->background_color = color;
}
void progress_layer_set_corner_radius(ProgressLayer* progress_layer, uint16_t corner_radius) {
progress_layer->corner_radius = corner_radius;
}

View File

@@ -0,0 +1,55 @@
/*
* 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"
#include "applib/graphics/graphics.h"
#include "applib/ui/layer.h"
#define MIN_PROGRESS_PERCENT 0
#define MAX_PROGRESS_PERCENT 100
#define PROGRESS_SUGGESTED_HEIGHT PBL_IF_COLOR_ELSE(6, 7)
#define PROGRESS_SUGGESTED_CORNER_RADIUS PBL_IF_COLOR_ELSE(2, 3)
//! Note: Do NOT modify the first two elements of this struct since type punning
//! is used to grab the progress_percent during the layer's update_proc
typedef struct {
Layer layer;
unsigned int progress_percent;
GColor foreground_color;
GColor background_color;
int16_t corner_radius;
} ProgressLayer;
//! Draw a progress bar inside the given frame
//!
//! Note: the frame *must* be at least 8 pixels wide and 8 pixels tall.
//! This is because 2 pixels of white padding are placed around the progress
//! bar, and the progress bar itself is bounded by a 2 pixel black rounded rect.
//! For greatest sex appeal, make the progress bar larger than 8x8.
void progress_layer_init(ProgressLayer* progress_layer, const GRect *frame);
void progress_layer_deinit(ProgressLayer* progress_layer);
void progress_layer_set_foreground_color(ProgressLayer* progress_layer, GColor color);
void progress_layer_set_background_color(ProgressLayer* progress_layer, GColor color);
//! Convenience function to set the progress layer's progress and mark the
//! layer dirty.
void progress_layer_set_progress(ProgressLayer* progress_layer, unsigned int progress_percent);
void progress_layer_set_corner_radius(ProgressLayer* progress_layer, uint16_t corner_radius);

View File

@@ -0,0 +1,340 @@
/*
* 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 "progress_window.h"
#include "applib/applib_malloc.auto.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/window_private.h"
#include "applib/ui/window_stack.h"
#include "kernel/pbl_malloc.h"
#include "services/common/compositor/compositor_transitions.h"
#include "services/normal/timeline/timeline_resources.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#define SCROLL_OUT_MS 250
#define BAR_HEIGHT PROGRESS_SUGGESTED_HEIGHT
#define BAR_WIDTH 80
#define BAR_TO_TRANS_MS 160
#define TRANS_TO_DOT_MS 90
#define DOT_TRANSITION_RADIUS 13
#define DOT_COMPOSITOR_RADIUS 7
#define DOT_OFFSET 25
#define FAKE_PROGRESS_UPDATE_INTERVAL 200
#define FAKE_PROGRESS_UPDATE_AMOUNT 2
#define INITIAL_PERCENT 0
static void prv_finished(ProgressWindow *data, bool success) {
if (data->callbacks.finished) {
data->callbacks.finished(data, success, data->context);
}
}
///////////////////////////////
// Animation Related Functions
///////////////////////////////
static void prv_animation_stopped_success(Animation *animation, bool finished, void *context) {
ProgressWindow *data = context;
const bool success = true;
prv_finished(data, success);
}
static void prv_finished_failure_callback(void *data) {
const bool success = false;
prv_finished(data, success);
}
static void prv_show_peek_layer(ProgressWindow *data) {
if (data->is_peek_layer_used) {
Layer *root_layer = window_get_root_layer(&data->window);
PeekLayer *peek_layer = &data->peek_layer;
peek_layer_play(peek_layer);
layer_add_child(root_layer, (Layer *)peek_layer);
const int standing_ms = 1 * MS_PER_SECOND;
data->peek_layer_timer = evented_timer_register(PEEK_LAYER_UNFOLD_DURATION + standing_ms,
false, prv_finished_failure_callback, data);
} else {
prv_finished_failure_callback(data);
}
}
static void prv_animation_stopped_failure(Animation *animation, bool finished, void *context) {
ProgressWindow *data = context;
prv_show_peek_layer(data);
}
static void prv_schedule_progress_success_animation(ProgressWindow *data) {
#if !PLATFORM_TINTIN
GRect beg = data->progress_layer.layer.bounds;
GRect mid = beg;
GRect end = beg;
// Morph from progress_layer to a large transition dot to the compositor dot
// by changing the bounds of the progress_layer using 2 animations
layer_set_clips((Layer *)&data->progress_layer, false); // Extending bounds to grow the dot
progress_layer_set_corner_radius(&data->progress_layer, DOT_TRANSITION_RADIUS);
mid.size.w = DOT_TRANSITION_RADIUS * 2;
mid.size.h = DOT_TRANSITION_RADIUS * 2;
mid.origin.x = DOT_OFFSET - DOT_TRANSITION_RADIUS + 2;
mid.origin.y = BAR_HEIGHT - DOT_TRANSITION_RADIUS + 1; // shift to accommodate growing radius
end.size.w = DOT_COMPOSITOR_RADIUS * 2;
end.size.h = DOT_COMPOSITOR_RADIUS * 2;
end.origin.x = DOT_OFFSET - DOT_COMPOSITOR_RADIUS - 1;
end.origin.y = BAR_HEIGHT - DOT_COMPOSITOR_RADIUS - 2; // shift to accommodate growing radius
PropertyAnimation *prop_anim =
property_animation_create_layer_bounds((Layer *)&data->progress_layer, &beg, &mid);
Animation *animation1 = property_animation_get_animation(prop_anim);
animation_set_duration(animation1, BAR_TO_TRANS_MS);
animation_set_curve(animation1, AnimationCurveEaseIn);
prop_anim = property_animation_create_layer_bounds((Layer *)&data->progress_layer, &mid, &end);
Animation *animation2 = property_animation_get_animation(prop_anim);
animation_set_duration(animation2, TRANS_TO_DOT_MS);
animation_set_curve(animation2, AnimationCurveLinear);
animation_set_handlers(animation2, (AnimationHandlers) {
.stopped = prv_animation_stopped_success,
}, data);
Animation *animation = animation_sequence_create(animation1, animation2, NULL);
data->result_animation = animation;
animation_schedule(animation);
#else
// Don't animate to a dot on old platforms, just finish immediately.
static const bool success = true;
prv_finished(data, success);
#endif
}
static void prv_schedule_progress_failure_animation(ProgressWindow *data, uint32_t timeline_res_id,
const char *message, uint32_t delay) {
// Initialize the peek layer
if (timeline_res_id || message) {
Layer *root_layer = window_get_root_layer(&data->window);
PeekLayer *peek_layer = &data->peek_layer;
peek_layer_init(peek_layer, &root_layer->frame);
peek_layer_set_title_font(peek_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
TimelineResourceInfo timeline_res = {
.res_id = timeline_res_id,
};
peek_layer_set_icon(peek_layer, &timeline_res);
peek_layer_set_title(peek_layer, message);
peek_layer_set_background_color(peek_layer, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
data->is_peek_layer_used = true;
}
#if !PLATFORM_TINTIN
// Animate the progress bar out, by shrinking it's width from it's current size down to 0.
// When this completes, prv_animation_stopped_failure will show the peek layer.
GRect *start = &data->progress_layer.layer.frame;
GRect stop = *start;
stop.size.w = 0;
PropertyAnimation *prop_anim =
property_animation_create_layer_frame((Layer *)&data->progress_layer, start, &stop);
Animation *animation = property_animation_get_animation(prop_anim);
// If we failed, pause on the screen for a little.
animation_set_delay(animation, delay);
animation_set_duration(animation, SCROLL_OUT_MS);
animation_set_curve(animation, AnimationCurveEaseOut);
animation_set_handlers(animation, (AnimationHandlers) {
.stopped = prv_animation_stopped_failure,
}, data);
data->result_animation = animation;
animation_schedule(animation);
#else
// Don't animate, just show the peek layer immediately.
prv_show_peek_layer(data);
#endif
}
////////////////////////////
// Internal Helper Functions
////////////////////////////
//! Used to clean up the application's data before exiting
static void prv_cancel_fake_progress_timer(ProgressWindow *data) {
if (data->fake_progress_timer != EVENTED_TIMER_INVALID_ID) {
evented_timer_cancel(data->fake_progress_timer);
data->fake_progress_timer = EVENTED_TIMER_INVALID_ID;
}
}
static void prv_set_progress(ProgressWindow *data, int16_t progress) {
data->progress_percent = CLIP(progress, data->progress_percent, MAX_PROGRESS_PERCENT);
progress_layer_set_progress(&data->progress_layer, data->progress_percent);
}
static void prv_fake_update_progress(void *context) {
ProgressWindow *data = context;
prv_set_progress(data, data->progress_percent + FAKE_PROGRESS_UPDATE_AMOUNT);
if (data->progress_percent >= data->max_fake_progress_percent) {
// Hit the max, we're done
data->fake_progress_timer = EVENTED_TIMER_INVALID_ID;
} else {
data->fake_progress_timer = evented_timer_register(FAKE_PROGRESS_UPDATE_INTERVAL, false,
prv_fake_update_progress, data);
}
}
////////////////////////////
// Public API
////////////////////////////
void progress_window_set_max_fake_progress(ProgressWindow *window,
int16_t max_fake_progress_percent) {
window->max_fake_progress_percent = CLIP(max_fake_progress_percent, 0, MAX_PROGRESS_PERCENT);
}
void progress_window_set_progress(ProgressWindow *window, int16_t progress) {
if (window->state == ProgressWindowState_FakeProgress) {
// We've seen our first bit of real progress, stop faking it.
prv_cancel_fake_progress_timer(window);
window->state = ProgressWindowState_RealProgress;
}
prv_set_progress(window, progress);
}
void progress_window_set_result_success(ProgressWindow *window) {
if (window->state == ProgressWindowState_Result) {
// Ignore requests to change the result once we already have one
return;
}
window->state = ProgressWindowState_Result;
prv_cancel_fake_progress_timer(window);
prv_set_progress(window, MAX_PROGRESS_PERCENT);
prv_schedule_progress_success_animation(window);
}
void progress_window_set_result_failure(ProgressWindow *window, uint32_t timeline_res,
const char *message, uint32_t delay) {
if (window->state == ProgressWindowState_Result) {
// Ignore requests to change the result once we already have one
return;
}
window->state = ProgressWindowState_Result;
prv_cancel_fake_progress_timer(window);
prv_schedule_progress_failure_animation(window, timeline_res, message, delay);
}
void progress_window_set_callbacks(ProgressWindow *window, ProgressWindowCallbacks callbacks,
void *context) {
window->context = context;
window->callbacks = callbacks;
}
void progress_window_set_back_disabled(ProgressWindow *window, bool disabled) {
window_set_overrides_back_button(&window->window, disabled);
}
void progress_window_push(ProgressWindow *window, WindowStack *window_stack) {
const bool animated = true;
window_stack_push(window_stack, (Window *)window, animated);
}
void app_progress_window_push(ProgressWindow *window) {
const bool animated = true;
app_window_stack_push((Window *)window, animated);
}
void progress_window_pop(ProgressWindow *window) {
const bool animated = true;
window_stack_remove((Window *)window, animated);
}
void progress_window_init(ProgressWindow *data) {
// Create and set up the window
Window *window = &data->window;
window_init(window, WINDOW_NAME("Progress Window"));
window_set_background_color(window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
const GRect *bounds = &window->layer.bounds;
GPoint center = grect_center_point(bounds);
const GRect progress_bounds = (GRect) {
.origin = { center.x - (BAR_WIDTH / 2), center.y - (BAR_HEIGHT / 2) },
.size = { BAR_WIDTH, BAR_HEIGHT },
};
ProgressLayer *progress_layer = &data->progress_layer;
progress_layer_init(progress_layer, &progress_bounds);
#if PBL_COLOR
progress_layer_set_foreground_color(progress_layer, GColorWhite);
progress_layer_set_background_color(progress_layer, GColorBlack);
#endif
progress_layer_set_corner_radius(progress_layer, PROGRESS_SUGGESTED_CORNER_RADIUS);
layer_add_child(&window->layer, (Layer *)progress_layer);
data->max_fake_progress_percent = PROGRESS_WINDOW_DEFAULT_FAKE_PERCENT;
data->state = ProgressWindowState_FakeProgress;
data->is_peek_layer_used = false;
data->fake_progress_timer = evented_timer_register(FAKE_PROGRESS_UPDATE_INTERVAL, false,
prv_fake_update_progress, data);
prv_set_progress(data, INITIAL_PERCENT);
}
void progress_window_deinit(ProgressWindow *data) {
if (!data) {
return;
}
animation_unschedule(data->result_animation);
peek_layer_deinit(&data->peek_layer);
data->is_peek_layer_used = false;
prv_cancel_fake_progress_timer(data);
if (data->peek_layer_timer != EVENTED_TIMER_INVALID_ID) {
evented_timer_cancel(data->peek_layer_timer);
data->peek_layer_timer = EVENTED_TIMER_INVALID_ID;
}
}
ProgressWindow *progress_window_create(void) {
ProgressWindow *window = applib_zalloc(sizeof(ProgressWindow));
progress_window_init(window);
return window;
}
void progress_window_destroy(ProgressWindow *window) {
if (window) {
progress_window_pop(window);
}
progress_window_deinit(window);
applib_free(window);
}

View File

@@ -0,0 +1,130 @@
/*
* 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/ui/animation.h"
#include "applib/ui/progress_layer.h"
#include "applib/ui/window.h"
#include "applib/ui/window_stack.h"
#include "apps/system_apps/timeline/peek_layer.h"
#include "services/common/evented_timer.h"
//! @file progress_window.h
//!
//! A UI component that is a window that contains a progress bar. The state of the progress bar
//! is updated using progress_window_set_progress. When the window is first pushed, the progress
//! bar will fill on it's own, faking progress until the max_fake_progress_percent threshold is
//! hit. Once the client wishes to indicate success or failure, calling
//! progress_window_set_progress_success or progress_window_set_progress_failure will cause the
//! UI to animate out to indicate the result, followed by calling the .finished callback if
//! provided. Once progress_window_set_progress_success or progress_window_set_progress_failure
//! has been called, subsequent calls will be ignored.
#define PROGRESS_WINDOW_DEFAULT_FAKE_PERCENT 15
#define PROGRESS_WINDOW_DEFAULT_FAILURE_DELAY_MS 1000
typedef struct ProgressWindow ProgressWindow;
typedef void (*ProgressWindowFinishedCallback)(ProgressWindow *window, bool success, void *context);
typedef struct {
//! Callback for when the window has finished any animations that are triggered by
//! progress_window_set_progress_success or progress_window_set_progress_failure.
ProgressWindowFinishedCallback finished;
} ProgressWindowCallbacks;
typedef enum {
ProgressWindowState_FakeProgress,
ProgressWindowState_RealProgress,
ProgressWindowState_Result
} ProgressWindowState;
struct ProgressWindow {
//! UI
Window window;
ProgressLayer progress_layer;
//! In the event of a failure, shows a client supplied timeline resource and message.
//! see progress_window_set_progress_failure
PeekLayer peek_layer;
Animation *result_animation;
ProgressWindowCallbacks callbacks;
void *context; //!< context for above callbacks
//! What state we're in.
ProgressWindowState state;
//! Timer to fill the bar with fake progress at the beginning
EventedTimerID fake_progress_timer;
//! Timer to keep the failure peek layer on screen for a bit before finishing
EventedTimerID peek_layer_timer;
//! The progress we've indicated so far
int16_t progress_percent;
//! Maximum fake progress
int16_t max_fake_progress_percent;
//! Whether the peek layer was used to indicate failure. We only use it if the client specifies
//! a timeline resource or a message, otherwise we skip showing the peek layer.
bool is_peek_layer_used;
};
void progress_window_init(ProgressWindow *data);
void progress_window_deinit(ProgressWindow *data);
ProgressWindow *progress_window_create(void);
void progress_window_destroy(ProgressWindow *window);
void progress_window_push(ProgressWindow *window, WindowStack *window_stack);
//! Helper function to push a progress window to the app window stack.
void app_progress_window_push(ProgressWindow *window);
void progress_window_pop(ProgressWindow *window);
//! Set the maximum percentage we should fake progress to until real progress is required.
void progress_window_set_max_fake_progress(ProgressWindow *window,
int16_t max_fake_progress_percent);
//! Update the progress to a given percentage. This will stop any further fake progress being shown
//! the first time this is called. Note that setting progress to 100 is not the same as calling
//! one of the progress_windw_set_result_* methods.
void progress_window_set_progress(ProgressWindow *window, int16_t progress);
//! Tell the ProgressWindow it should animate in a way to show success. When the animation is
//! complete, .callbacks.finished will be called if previously provided.
void progress_window_set_result_success(ProgressWindow *window);
//! Tell the ProgressWindow it should animate in a way to show failure. When the animation is
//! complete, .callbacks.finished will be called if previously provided.
//!
//! @param timeline_res_id optional timeline resource, can be 0 if not desired
//! @param message optional message, can be NULL
//! @param delay duration of the progress bar shrinking animation in milliseconds
void progress_window_set_result_failure(ProgressWindow *window, uint32_t timeline_res_id,
const char *message, uint32_t delay);
void progress_window_set_callbacks(ProgressWindow *window, ProgressWindowCallbacks callbacks,
void *context);
//! @internal
void progress_window_set_back_disabled(ProgressWindow *window, bool disabled);

View File

@@ -0,0 +1,617 @@
/*
* 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 "property_animation_private.h"
#include "animation_interpolate.h"
#include "animation_private.h"
#include "animation_timing.h"
#include "applib/legacy2/ui/property_animation_legacy2.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "system/passert.h"
#include "system/logging.h"
#include "util/size.h"
#include "layer.h"
/////////////////////
// Property Animation
//
static const PropertyAnimationImplementation s_frame_layer_implementation = {
.base = {
.update = (AnimationUpdateImplementation) property_animation_update_grect,
},
.accessors = {
.setter = { .grect = (const GRectSetter) layer_set_frame_by_value, },
.getter = { .grect = (const GRectGetter) layer_get_frame_by_value, },
},
};
static const PropertyAnimationImplementation s_bounds_layer_implementation = {
.base = {
.update = (AnimationUpdateImplementation) property_animation_update_grect,
},
.accessors = {
.setter = { .grect = (const GRectSetter) layer_set_bounds_by_value, },
.getter = { .grect = (const GRectGetter) layer_get_bounds_by_value, },
},
};
// -----------------------------------------------------------------------------------------
static inline PropertyAnimationPrivate *prv_find_property_animation(PropertyAnimation *handle) {
return (PropertyAnimationPrivate *)animation_private_animation_find((Animation *)handle);
}
// -----------------------------------------------------------------------------------------
void property_animation_update_int16(PropertyAnimation *property_animation_h,
const uint32_t distance_normalized) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
property_animation_legacy2_update_int16((PropertyAnimationLegacy2 *)property_animation_h,
distance_normalized);
return;
}
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return;
}
int16_t result = interpolate_int16(distance_normalized,
property_animation->values.from.int16,
property_animation->values.to.int16);
((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.setter.int16(property_animation->subject, result);
}
// -----------------------------------------------------------------------------------------
void property_animation_update_uint32(PropertyAnimation *property_animation_h,
const uint32_t distance_normalized) {
PBL_ASSERTN(!animation_private_using_legacy_2(NULL));
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return;
}
uint32_t result = interpolate_uint32(distance_normalized,
property_animation->values.from.uint32,
property_animation->values.to.uint32);
((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.setter.uint32(property_animation->subject, result);
}
// -----------------------------------------------------------------------------------------
void property_animation_update_gpoint(PropertyAnimation *property_animation_h,
const uint32_t distance_normalized) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
property_animation_legacy2_update_gpoint((PropertyAnimationLegacy2 *)property_animation_h,
distance_normalized);
return;
}
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return;
}
GPoint result;
result.x = interpolate_int16(distance_normalized,
property_animation->values.from.gpoint.x,
property_animation->values.to.gpoint.x);
result.y = interpolate_int16(distance_normalized,
property_animation->values.from.gpoint.y,
property_animation->values.to.gpoint.y);
((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.setter.gpoint(property_animation->subject, result);
}
// -----------------------------------------------------------------------------------------
void property_animation_update_grect(PropertyAnimation *property_animation_h,
const uint32_t distance_normalized) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
property_animation_legacy2_update_grect((PropertyAnimationLegacy2 *)property_animation_h,
distance_normalized);
return;
}
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return;
}
GRect result;
result.origin.x = interpolate_int16(
distance_normalized,
property_animation->values.from.grect.origin.x,
property_animation->values.to.grect.origin.x);
result.origin.y = interpolate_int16(
distance_normalized,
property_animation->values.from.grect.origin.y,
property_animation->values.to.grect.origin.y);
result.size.w = interpolate_int16(
distance_normalized,
property_animation->values.from.grect.size.w,
property_animation->values.to.grect.size.w);
result.size.h = interpolate_int16(
distance_normalized,
property_animation->values.from.grect.size.h,
property_animation->values.to.grect.size.h);
((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.setter.grect(property_animation->subject, result);
}
// -----------------------------------------------------------------------------------------
#if !defined(PLATFORM_TINTIN)
void property_animation_update_gtransform(PropertyAnimation *property_animation_h,
const uint32_t distance_normalized) {
PBL_ASSERTN(!animation_private_using_legacy_2(NULL));
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return;
}
GTransform result;
result.a = interpolate_fixed32(
distance_normalized,
property_animation->values.from.gtransform.a,
property_animation->values.to.gtransform.a);
result.b = interpolate_fixed32(
distance_normalized,
property_animation->values.from.gtransform.b,
property_animation->values.to.gtransform.b);
result.c = interpolate_fixed32(
distance_normalized,
property_animation->values.from.gtransform.c,
property_animation->values.to.gtransform.c);
result.d = interpolate_fixed32(
distance_normalized,
property_animation->values.from.gtransform.d,
property_animation->values.to.gtransform.d);
result.tx = interpolate_fixed32(
distance_normalized,
property_animation->values.from.gtransform.tx,
property_animation->values.to.gtransform.tx);
result.ty = interpolate_fixed32(
distance_normalized,
property_animation->values.from.gtransform.ty,
property_animation->values.to.gtransform.ty);
// NOTE: We are not exposing the GTransform in the public SDK, so the setter and getter
// must be typecast
GTransformSetter setter = (GTransformSetter)((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.setter.int16;
setter(property_animation->subject, result);
}
#endif
// -----------------------------------------------------------------------------------------
void property_animation_update_gcolor8(PropertyAnimation *property_animation_h,
const uint32_t distance_normalized) {
PBL_ASSERTN(!animation_private_using_legacy_2(NULL));
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return;
}
GColor8 result;
result.a = interpolate_int16(
distance_normalized,
property_animation->values.from.gcolor8.a,
property_animation->values.to.gcolor8.a);
result.r = interpolate_int16(
distance_normalized,
property_animation->values.from.gcolor8.r,
property_animation->values.to.gcolor8.r);
result.g = interpolate_int16(
distance_normalized,
property_animation->values.from.gcolor8.g,
property_animation->values.to.gcolor8.g);
result.b = interpolate_int16(
distance_normalized,
property_animation->values.from.gcolor8.b,
property_animation->values.to.gcolor8.b);
((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.setter.gcolor8(property_animation->subject, result);
}
// -----------------------------------------------------------------------------------------
void property_animation_update_fixed_s32_16(PropertyAnimation *property_animation_h,
const uint32_t distance_normalized) {
PBL_ASSERTN(!animation_private_using_legacy_2(NULL));
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return;
}
Fixed_S32_16 result = interpolate_fixed32(distance_normalized,
property_animation->values.from.fixed_s32_16,
property_animation->values.to.fixed_s32_16);
// NOTE: We are not exposing the Fixed_S32_16 in the public SDK, so the setter and getter
// must be typecast
Fixed_S32_16Setter setter = (Fixed_S32_16Setter)((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.setter.int16;
setter(property_animation->subject, result);
}
// -----------------------------------------------------------------------------------------
static void prv_init(PropertyAnimationPrivate *property_animation,
const PropertyAnimationImplementation *implementation,
void *subject, void *from_value, void *to_value) {
property_animation->animation.is_property_animation = true;
memset(&property_animation->values, 0xff, sizeof(property_animation->values));
property_animation->animation.implementation = (AnimationImplementation*) implementation;
property_animation->subject = subject;
if (implementation->accessors.getter.int16) {
if (property_animation->animation.implementation->update
== (AnimationUpdateImplementation) property_animation_update_int16) {
property_animation->values.to.int16 = to_value ? *((int16_t *)to_value)
: implementation->accessors.getter.int16(subject);
property_animation->values.from.int16 = from_value ? *((int16_t*)from_value)
: implementation->accessors.getter.int16(subject);
} else if (property_animation->animation.implementation->update
== (AnimationUpdateImplementation) property_animation_update_uint32) {
property_animation->values.to.uint32 = to_value ? *((uint32_t *)to_value)
: implementation->accessors.getter.uint32(subject);
property_animation->values.from.uint32 = from_value ? *((uint32_t *)from_value)
: implementation->accessors.getter.uint32(subject);
} else if (property_animation->animation.implementation->update
== (AnimationUpdateImplementation) property_animation_update_gpoint) {
property_animation->values.to.gpoint = to_value ? *((GPoint*)to_value)
: implementation->accessors.getter.gpoint(subject);
property_animation->values.from.gpoint = from_value ? *((GPoint*)from_value)
: implementation->accessors.getter.gpoint(subject);
} else if (property_animation->animation.implementation->update
== (AnimationUpdateImplementation)property_animation_update_grect) {
property_animation->values.to.grect = to_value ? *((GRect*)to_value)
: implementation->accessors.getter.grect(subject);
property_animation->values.from.grect = from_value ? *((GRect*)from_value)
: implementation->accessors.getter.grect(subject);
#if !PLATFORM_TINTIN
} else if (property_animation->animation.implementation->update
== (AnimationUpdateImplementation)property_animation_update_gtransform) {
// NOTE: We are not exposing the GTransform in the public SDK, so the setter and getter
// must be typecast
GTransformGetter getter = (GTransformGetter)((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.getter.int16;
property_animation->values.to.gtransform = to_value ? *((GTransform*)to_value)
: getter(subject);
property_animation->values.from.gtransform = from_value ? *((GTransform*)from_value)
: getter(subject);
#endif
} else if (property_animation->animation.implementation->update
== (AnimationUpdateImplementation)property_animation_update_gcolor8) {
property_animation->values.to.gcolor8 = to_value ? *((GColor8*)to_value)
: implementation->accessors.getter.gcolor8(subject);
property_animation->values.from.gcolor8 = from_value ? *((GColor8*)from_value)
: implementation->accessors.getter.gcolor8(subject);
} else if (property_animation->animation.implementation->update
== (AnimationUpdateImplementation)property_animation_update_fixed_s32_16) {
// NOTE: We are not exposing the Fixed_S32_16 in the public SDK, so the setter and getter
// must be typecast
Fixed_S32_16Getter getter = (Fixed_S32_16Getter)((PropertyAnimationImplementation*)
property_animation->animation.implementation)
->accessors.getter.int16;
property_animation->values.to.fixed_s32_16 = to_value ? *((Fixed_S32_16*)to_value)
: getter(subject);
property_animation->values.from.fixed_s32_16 = from_value ? *((Fixed_S32_16*)from_value)
: getter(subject);
}
}
}
// -----------------------------------------------------------------------------------------
PropertyAnimation *property_animation_create(
const PropertyAnimationImplementation *implementation,
void *subject, void *from_value, void *to_value) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
return (PropertyAnimation *)property_animation_legacy2_create(
(PropertyAnimationLegacy2Implementation *)implementation, subject, from_value, to_value);
}
PropertyAnimationPrivate* property_animation = applib_type_malloc(PropertyAnimationPrivate);
if (!property_animation) {
return NULL;
}
memset(property_animation, 0, sizeof(*property_animation));
Animation *handle = animation_private_animation_init(&property_animation->animation);
prv_init(property_animation, implementation, subject, from_value, to_value);
return (PropertyAnimation *)handle;
}
// -----------------------------------------------------------------------------------------
// Create a new property animation structure, copying just the property animation unique fields
PropertyAnimationPrivate *property_animation_private_clone(PropertyAnimationPrivate *from) {
PBL_ASSERTN(!animation_private_using_legacy_2(NULL));
PropertyAnimationPrivate* property_animation = applib_type_malloc(PropertyAnimationPrivate);
if (!property_animation) {
return NULL;
}
memset(property_animation, 0, sizeof(*property_animation));
uint8_t *dst = (uint8_t *)property_animation;
uint8_t *src = (uint8_t *)from;
uint32_t offset = sizeof(AnimationPrivate); // Skip the base class fields
uint32_t size = sizeof(PropertyAnimationPrivate) - offset;
memcpy(dst + offset, src + offset, size);
return property_animation;
}
// -----------------------------------------------------------------------------------------
bool property_animation_init(PropertyAnimation *animation_h,
const PropertyAnimationImplementation *implementation,
void *subject, void *from_value, void *to_value) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
property_animation_legacy2_init((PropertyAnimationLegacy2 *)animation_h,
(PropertyAnimationLegacy2Implementation *)implementation,
subject, from_value, to_value);
return true;
}
PropertyAnimationPrivate *property_animation = prv_find_property_animation(animation_h);
if (!property_animation) {
return false;
}
// It is an error to call this if the animation is scheduled
PBL_ASSERTN(!animation_is_scheduled((Animation *)animation_h));
prv_init(property_animation, implementation, subject, from_value, to_value);
return true;
}
// -----------------------------------------------------------------------------------------
PropertyAnimation* property_animation_create_layer_frame(struct Layer *layer, GRect *from_frame,
GRect *to_frame) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
return (PropertyAnimation *)property_animation_legacy2_create_layer_frame(layer,
from_frame, to_frame);
}
return property_animation_create(&s_frame_layer_implementation, layer, from_frame, to_frame);
}
// -----------------------------------------------------------------------------------------
PropertyAnimation* property_animation_create_layer_bounds(struct Layer *layer, GRect *from_bounds,
GRect *to_bounds) {
// no legacy2 support as this was never exposed on 2.x
return property_animation_create(&s_bounds_layer_implementation, layer, from_bounds, to_bounds);
}
// -----------------------------------------------------------------------------------------
bool property_animation_init_layer_frame(PropertyAnimation *animation_h,
struct Layer *layer, GRect *from_frame, GRect *to_frame) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
property_animation_legacy2_init_layer_frame((PropertyAnimationLegacy2 *)animation_h, layer,
from_frame, to_frame);
return true;
}
return property_animation_init(animation_h, &s_frame_layer_implementation, layer, from_frame,
to_frame);
}
// -----------------------------------------------------------------------------------------
PropertyAnimation *property_animation_create_bounds_origin(Layer *layer, GPoint *from, GPoint *to) {
// no legacy2 support as this was never exposed on 2.x
PropertyAnimation *result = property_animation_create(&s_bounds_layer_implementation,
layer, NULL, NULL);
GRect value = layer->bounds;
if (from) {
value.origin = *from;
}
property_animation_set_from_grect(result, &value);
value = layer->bounds;
if (to) {
value.origin = *to;
}
property_animation_set_to_grect(result, &value);
return result;
}
// -----------------------------------------------------------------------------------------
static void property_animation_update_mark_dirty(Animation* animation,
const AnimationProgress normalized) {
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
Layer *subject;
if (property_animation_get_subject(prop_anim, (void**)&subject) && subject) {
layer_mark_dirty(subject);
}
}
static const PropertyAnimationImplementation s_dirty_layer_implementation = {
.base = {
.update = property_animation_update_mark_dirty,
},
};
PropertyAnimation *property_animation_create_mark_dirty(struct Layer *layer) {
// no legacy2 support as this was never exposed on 2.x
PropertyAnimation *result = property_animation_create(&s_dirty_layer_implementation,
layer, NULL, NULL);
return result;
}
// -----------------------------------------------------------------------------------------
void property_animation_destroy(PropertyAnimation* property_animation_h) {
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
property_animation_legacy2_destroy((PropertyAnimationLegacy2 *)property_animation_h);
return;
}
animation_destroy((Animation *)property_animation_h);
}
// -----------------------------------------------------------------------------------------
Animation *property_animation_get_animation(PropertyAnimation *property_animation) {
return (Animation *)property_animation;
}
// -----------------------------------------------------------------------------------------
bool property_animation_subject(PropertyAnimation *property_animation_h, void **value, bool set) {
if (!value) {
return false;
}
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
if (set) {
((PropertyAnimationLegacy2 *)property_animation_h)->subject = *value;
} else {
*value = ((PropertyAnimationLegacy2 *)property_animation_h)->subject;
}
return true;
}
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return false;
}
if (set) {
property_animation->subject = *value;
} else {
*value = property_animation->subject;
}
return true;
}
// -----------------------------------------------------------------------------------------
bool property_animation_from(PropertyAnimation *property_animation_h, void *value, size_t size,
bool set) {
if (!value) {
return false;
}
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
PropertyAnimationLegacy2 *legacy = (PropertyAnimationLegacy2 *)property_animation_h;
if (set) {
memcpy(&legacy->values.from, value, size);
} else {
memcpy(value, &legacy->values.from, size);
}
return true;
}
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return false;
}
if (size > MEMBER_SIZE(PropertyAnimationPrivate, values.from)) {
APP_LOG(APP_LOG_LEVEL_WARNING, "invalid size");
return false;
}
if (set) {
memcpy(&property_animation->values.from, value, size);
} else {
memcpy(value, &property_animation->values.from, size);
}
return true;
}
// -----------------------------------------------------------------------------------------
bool property_animation_to(PropertyAnimation *property_animation_h, void *value, size_t size,
bool set) {
if (!value) {
return false;
}
if (animation_private_using_legacy_2(NULL)) {
// We need to enable other applib modules like sroll_layer, menu_layer, etc. which are
// compiled to use the 3.0 animation API to work with 2.0 apps.
PropertyAnimationLegacy2 *legacy = (PropertyAnimationLegacy2 *)property_animation_h;
if (set) {
memcpy(&legacy->values.to, value, size);
} else {
memcpy(value, &legacy->values.to, size);
}
return true;
}
PropertyAnimationPrivate *property_animation = prv_find_property_animation(property_animation_h);
if (!property_animation) {
return false;
}
if (size > MEMBER_SIZE(PropertyAnimationPrivate, values.to)) {
APP_LOG(APP_LOG_LEVEL_WARNING, "invalid size");
return false;
}
if (set) {
memcpy(&property_animation->values.to, value, size);
} else {
memcpy(value, &property_animation->values.to, size);
}
return true;
}

View File

@@ -0,0 +1,683 @@
/*
* 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 "animation.h"
#include "applib/graphics/gtypes.h"
//////////////////////
// Property Animations
//
//! @file property_animation.h
//! @addtogroup UI
//! @{
//! @addtogroup Animation
//! @{
//! @addtogroup PropertyAnimation
//! \brief A ProperyAnimation animates the value of a "property" of a "subject" over time.
//!
//! <h3>Animating a Layer's frame property</h3>
//! Currently there is only one specific type of property animation offered off-the-shelf, namely
//! one to change the frame (property) of a layer (subject), see \ref
//! property_animation_create_layer_frame().
//!
//! <h3>Implementing a custom PropertyAnimation</h3>
//! It is fairly simple to create your own variant of a PropertyAnimation.
//!
//! Please refer to \htmlinclude UiFramework.html (chapter "Property Animations") for a conceptual
//! overview of the animation framework and make sure you understand the underlying \ref Animation,
//! in case you are not familiar with it, before trying to implement a variation on
//! PropertyAnimation.
//!
//! To implement a custom property animation, use \ref property_animation_create() and provide a
//! function pointers to the accessors (getter and setter) and setup, update and teardown callbacks
//! in the implementation argument. Note that the type of property to animate with \ref
//! PropertyAnimation is limited to int16_t, GPoint or GRect.
//!
//! For each of these types, there are implementations provided for the necessary `.update` handler
//! of the animation: see \ref property_animation_update_int16(), \ref
//! property_animation_update_gpoint() and \ref property_animation_update_grect().
//! These update functions expect the `.accessors` to conform to the following interface:
//! Any getter needs to have the following function signature: `__type__ getter(void *subject);`
//! Any setter needs to have to following function signature: `void setter(void *subject,
//! __type__ value);`
//! See \ref Int16Getter, \ref Int16Setter, \ref GPointGetter, \ref GPointSetter,
//! \ref GRectGetter, \ref GRectSetter for the typedefs that accompany the update fuctions.
//!
//! \code{.c}
//! static const PropertyAnimationImplementation my_implementation = {
//! .base = {
//! // using the "stock" update callback:
//! .update = (AnimationUpdateImplementation) property_animation_update_gpoint,
//! },
//! .accessors = {
//! // my accessors that get/set a GPoint from/onto my subject:
//! .setter = { .gpoint = my_layer_set_corner_point, },
//! .getter = { .gpoint = (const GPointGetter) my_layer_get_corner_point, },
//! },
//! };
//! static PropertyAnimation* s_my_animation_ptr = NULL;
//! static GPoint s_to_point = GPointZero;
//! ...
//! // Use NULL as 'from' value, this will make the animation framework call the getter
//! // to get the current value of the property and use that as the 'from' value:
//! s_my_animation_ptr = property_animation_create(&my_implementation, my_layer, NULL, &s_to_point);
//! animation_schedule(property_animation_get_animation(s_my_animation_ptr));
//! \endcode
//! @{
struct PropertyAnimation;
typedef struct PropertyAnimation PropertyAnimation;
struct Layer;
//! Function signature of a setter function to set a property of type int16_t onto the subject.
//! @see \ref property_animation_update_int16()
//! @see \ref PropertyAnimationAccessors
typedef void (*Int16Setter)(void *subject, int16_t int16);
//! Function signature of a getter function to get the current property of type int16_t of the
//! subject.
//! @see \ref property_animation_create()
//! @see \ref PropertyAnimationAccessors
typedef int16_t (*Int16Getter)(void *subject);
//! Function signature of a setter function to set a property of type uint32_t onto the subject.
//! @see \ref property_animation_update_int16()
//! @see \ref PropertyAnimationAccessors
typedef void (*UInt32Setter)(void *subject, uint32_t uint32);
//! Function signature of a getter function to get the current property of type uint32_t of the
//! subject.
//! @see \ref property_animation_create()
//! @see \ref PropertyAnimationAccessors
typedef uint32_t (*UInt32Getter)(void *subject);
//! Function signature of a setter function to set a property of type GPoint onto the subject.
//! @see \ref property_animation_update_gpoint()
//! @see \ref PropertyAnimationAccessors
typedef void (*GPointSetter)(void *subject, GPoint gpoint);
//! Function signature of a getter function to get the current property of type GPoint of the subject.
//! @see \ref property_animation_create()
//! @see \ref PropertyAnimationAccessors
typedef GPointReturn (*GPointGetter)(void *subject);
//! Function signature of a setter function to set a property of type GRect onto the subject.
//! @see \ref property_animation_update_grect()
//! @see \ref PropertyAnimationAccessors
typedef void (*GRectSetter)(void *subject, GRect grect);
//! Function signature of a getter function to get the current property of type GRect of the subject.
//! @see \ref property_animation_create()
//! @see \ref PropertyAnimationAccessors
typedef GRectReturn (*GRectGetter)(void *subject);
//! Function signature of a setter function to set a property of type GTransform onto the subject.
//! @see \ref property_animation_update_gtransform()
//! @see \ref PropertyAnimationAccessors
typedef void (*GTransformSetter)(void *subject, GTransform gtransform);
//! Function signature of a getter function to get the current property of type GTransform of the
//! subject.
//! @see \ref property_animation_create()
//! @see \ref PropertyAnimationAccessors
typedef GTransformReturn (*GTransformGetter)(void *subject);
//! Function signature of a setter function to set a property of type GColor8 onto the subject.
//! @see \ref property_animation_update_gcolor8()
//! @see \ref PropertyAnimationAccessors
typedef void (*GColor8Setter)(void *subject, GColor8 gcolor);
//! Function signature of a getter function to get the current property of type GColor8 of the
//! subject.
//! @see \ref property_animation_create()
//! @see \ref PropertyAnimationAccessors
typedef GColor8 (*GColor8Getter)(void *subject);
//! Function signature of a setter function to set a property of type Fixed_S32_16 onto the subject.
//! @see \ref property_animation_update_fixed_s32_16()
//! @see \ref PropertyAnimationAccessors
typedef void (*Fixed_S32_16Setter)(void *subject, Fixed_S32_16 fixed_s32_16);
//! Function signature of a getter function to get the current property of type Fixed_S32_16 of the
//! subject.
//! @see \ref property_animation_create()
//! @see \ref PropertyAnimationAccessors
typedef Fixed_S32_16Return (*Fixed_S32_16Getter)(void *subject);
//! Data structure containing the setter and getter function pointers that the property animation
//! should use.
//! The specified setter function will be used by the animation's update callback. <br/> Based on
//! the type of the property (int16_t, GPoint or GRect), the accompanying update callback should be
//! used, see \ref property_animation_update_int16(), \ref property_animation_update_gpoint() and
//! \ref property_animation_update_grect(). <br/>
//! The getter function is used when the animation is initialized, to assign the current value of
//! the subject's property as "from" or "to" value, see \ref property_animation_create().
typedef struct PropertyAnimationAccessors {
//! Function pointer to the implementation of the function that __sets__ the updated property
//! value. This function will be called repeatedly for each animation frame.
//! @see PropertyAnimationAccessors
union {
//! Use if the property to animate is of int16_t type
Int16Setter int16;
//! Use if the property to animate is of GPoint type
GPointSetter gpoint;
//! Use if the property to animate is of GRect type
GRectSetter grect;
//! Use if the property to animate is of GColor8 type
GColor8Setter gcolor8;
//! Use if the property to animate is of uint32_t type
UInt32Setter uint32;
} setter;
//! Function pointer to the implementation of the function that __gets__ the current property
//! value. This function will be called during \ref property_animation_create(), to get the current
//! property value, in case the `from_value` or `to_value` argument is `NULL`.
//! @see PropertyAnimationAccessors
union {
//! Use if the property to animate is of int16_t type
Int16Getter int16;
//! Use if the property to animate is of GPoint type
GPointGetter gpoint;
//! Use if the property to animate is of GRect type
GRectGetter grect;
//! Use if the property to animate is of GColor8 type
GColor8Getter gcolor8;
//! Use if the property to animate is of uint32_t type
UInt32Getter uint32;
} getter;
} PropertyAnimationAccessors;
//! Data structure containing a collection of function pointers that form the implementation of the
//! property animation.
//! See the code example at the top (\ref PropertyAnimation).
typedef struct PropertyAnimationImplementation {
//! The "inherited" fields from the Animation "base class".
AnimationImplementation base;
//! The accessors to set/get the property to be animated.
PropertyAnimationAccessors accessors;
} PropertyAnimationImplementation;
//! Convenience function to create and initialize a property animation that animates the frame of a
//! Layer. It sets up the PropertyAnimation to use \ref layer_set_frame() and \ref layer_get_frame()
//! as accessors and uses the `layer` parameter as the subject for the animation.
//! The same defaults are used as with \ref animation_create().
//! @param layer the layer that will be animated
//! @param from_frame the frame that the layer should animate from
//! @param to_frame the frame that the layer should animate to
//! @note Pass in `NULL` as one of the frame arguments to have it set automatically to the layer's
//! current frame. This will result in a call to \ref layer_get_frame() to get the current frame of
//! the layer.
//! @return A handle to the property animation. `NULL` if animation could not be created
PropertyAnimation *property_animation_create_layer_frame(struct Layer *layer, GRect *from_frame,
GRect *to_frame);
//! Convenience function to create and initialize a property animation that animates the bounds of a
//! Layer. It sets up the PropertyAnimation to use \ref layer_set_bounds()
//! and \ref layer_get_bounds() as accessors and uses the `layer` parameter
//! as the subject for the animation.
//! The same defaults are used as with \ref animation_create().
//! @param layer the layer that will be animated
//! @param from_bounds the bounds that the layer should animate from
//! @param to_bounds the bounds that the layer should animate to
//! @note Pass in `NULL` as one of the frame arguments to have it set automatically to the layer's
//! current bounds. This will result in a call to \ref layer_get_bounds() to get the current
//! frame of the layer.
//! @return A handle to the property animation. `NULL` if animation could not be created
PropertyAnimation *property_animation_create_layer_bounds(struct Layer *layer, GRect *from_bounds,
GRect *to_bounds);
//! Convenience function to create and initialize a property animation that animates the bound's
//! origin of a Layer. It sets up the PropertyAnimation to use layer_set_bounds() and
//! layer_get_bounds() as accessors and uses the `layer` parameter as the subject for the animation.
//! The same defaults are used as with \ref animation_create().
//! @param layer the layer that will be animated
//! @param from_origin the origin that the bounds should animate from
//! @param to_origin the origin that the layer should animate to
//! @return A handle to the property animation. `NULL` if animation could not be created
PropertyAnimation *property_animation_create_bounds_origin(struct Layer *layer, GPoint *from,
GPoint *to);
PropertyAnimation *property_animation_create_mark_dirty(struct Layer *layer);
//! @internal
//! Convenience function to re-initialize an already instantiated layer frame animation.
//! @param layer the layer that will be animated
//! @param from_frame the frame that the layer should animate from
//! @param to_frame the frame that the layer should animate to
//! @note Pass in `NULL` as one of the frame arguments to have it set automatically to the layer's
//! current frame. This will result in a call to \ref layer_get_frame() to get the current frame of
//! the layer.
//! @return true if successful
bool property_animation_init_layer_frame(PropertyAnimation *animation_h,
struct Layer *layer, GRect *from_frame, GRect *to_frame);
//! Destroy a property animation allocated by property_animation_create() or relatives.
//! @param property_animation the return value from property_animation_create
void property_animation_destroy(PropertyAnimation* property_animation);
//! Convenience function to retrieve an animation instance from a property animation instance
//! @param property_animation The property animation
//! @return The \ref Animation within this PropertyAnimation
Animation *property_animation_get_animation(PropertyAnimation *property_animation);
//! Convenience function to clone a property animation instance
//! @param property_animation The property animation
//! @return A clone of the original Animation
#define property_animation_clone(property_animation) \
(PropertyAnimation *)animation_clone((Animation *)property_animation)
//! Convenience function to retrieve the 'from' GRect value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_from_grect(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GRect), false)
//! Convenience function to set the 'from' GRect value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_from_grect(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GRect), true)
//! Convenience function to retrieve the 'from' GPoint value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_from_gpoint(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GPoint), false)
//! Convenience function to set the 'from' GPoint value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_from_gpoint(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GPoint), true)
//! Convenience function to retrieve the 'from' int16_t value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_from_int16(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(int16_t), false)
//! Convenience function to set the 'from' int16_t value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_from_int16(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(int16_t), true)
//! Convenience function to retrieve the 'from' uint32_t value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_from_uint32(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(uint32_t), false)
//! Convenience function to set the 'from' uint32_t value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_from_uint32(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(uint32_t), true)
//! Convenience function to retrieve the 'from' GTransform value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_from_gtransform(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GTransform), false)
//! Convenience function to set the 'from' GTransform value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_from_gtransform(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GTransform), true)
//! Convenience function to retrieve the 'from' GColor8 value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_from_gcolor8(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GColor8), false)
//! Convenience function to set the 'from' GColor8 value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_from_gcolor8(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(GColor8), true)
//! Convenience function to retrieve the 'from' Fixed_S32_16 value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_from_fixed_s32_16(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(Fixed_S32_16), false)
//! Convenience function to set the 'from' Fixed_S32_16 value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_from_fixed_s32_16(property_animation, value_ptr) \
property_animation_from(property_animation, value_ptr, sizeof(Fixed_S32_16), true)
//! Convenience function to retrieve the 'to' GRect value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_to_grect(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GRect), false)
//! Convenience function to set the 'to' GRect value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_to_grect(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GRect), true)
//! Convenience function to retrieve the 'to' GPoint value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_to_gpoint(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GPoint), false)
//! Convenience function to set the 'to' GPoint value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_to_gpoint(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GPoint), true)
//! Convenience function to retrieve the 'to' int16_t value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_to_int16(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(int16_t), false)
//! Convenience function to set the 'to' int16_t value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_to_int16(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(int16_t), true)
//! Convenience function to retrieve the 'to' uint32_t value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_to_uint32(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(uint32_t), false)
//! Convenience function to set the 'to' uint32_t value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_to_uint32(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(uint32_t), true)
//! Convenience function to retrieve the 'to' GTransform value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_to_gtransform(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GTransform), false)
//! Convenience function to set the 'to' GTransform value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_to_gtransform(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GTransform), true)
//! Convenience function to retrieve the 'to' GColor8 value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_to_gcolor8(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GColor8), false)
//! Convenience function to set the 'to' GColor8 value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_to_gcolor8(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(GColor8), true)
//! Convenience function to retrieve the 'to' Fixed_S32_16 value from property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr The value will be retrieved into this pointer
//! @return true on success, false on failure
#define property_animation_get_to_fixed_s32_16(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(Fixed_S32_16), false)
//! Convenience function to set the 'to' Fixed_S32_16 value of property animation handle
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new value
//! @return true on success, false on failure
#define property_animation_set_to_fixed_s32_16(property_animation, value_ptr) \
property_animation_to(property_animation, value_ptr, sizeof(Fixed_S32_16), true)
//! Retrieve the subject of a property animation
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer used to store the subject of this property animation
//! @return The subject of this PropertyAnimation
#define property_animation_get_subject(property_animation, value_ptr) \
property_animation_subject(property_animation, value_ptr, false)
//! Set the subject of a property animation
//! @param property_animation The PropertyAnimation to be accessed
//! @param value_ptr Pointer to the new subject value
#define property_animation_set_subject(property_animation, value_ptr) \
property_animation_subject(property_animation, value_ptr, true)
////////////////////////////////////////////////////////////////////////////////
// Primitive helper functions for the property_animation_get|set.* macros
//
//! Helper function used by the property_animation_get|set_subject macros
//! @param property_animation Handle to the property animation
//! @param subject The subject to get or set.
//! @param set true to set new subject, false to retrieve existing value
//! @return true if successful, false on failure (usually a bad animation_h)
bool property_animation_subject(PropertyAnimation *property_animation, void **subject, bool set);
//! Helper function used by the property_animation_get|set_from_.* macros
//! @param property_animation Handle to the property animation
//! @param from Pointer to the value
//! @param size Size of the from value
//! @param set true to set new value, false to retrieve existing one
//! @return true if successful, false on failure (usually a bad animation_h)
bool property_animation_from(PropertyAnimation *property_animation, void *from, size_t size,
bool set);
//! Helper function used by the property_animation_get|set_to_.* macros
//! @param property_animation handle to the property animation
//! @param to Pointer to the value
//! @param size Size of the to value
//! @param set true to set new value, false to retrieve existing one
//! @return true if successful, false on failure (usually a bad animation_h)
bool property_animation_to(PropertyAnimation *property_animation, void *to, size_t size,
bool set);
//////////////////////////////////////////
// Implementing custom Property Animations
//
//! Creates a new PropertyAnimation on the heap and and initializes it with the specified values.
//! The same defaults are used as with \ref animation_create().
//! If the `from_value` or the `to_value` is `NULL`, the getter accessor will be called to get the
//! current value of the property and be used instead.
//! @param implementation Pointer to the implementation of the animation. In most cases, it makes
//! sense to pass in a `static const` struct pointer.
//! @param subject Pointer to the "subject" being animated. This will be passed in when the getter/
//! setter accessors are called,
//! see \ref PropertyAnimationAccessors, \ref GPointSetter, and friends. The value of this pointer
//! will be copied into the `.subject` field of the PropertyAnimation struct.
//! @param from_value Pointer to the value that the subject should animate from
//! @param to_value Pointer to the value that the subject should animate to
//! @note Pass in `NULL` as one of the value arguments to have it set automatically to the subject's
//! current property value, as returned by the getter function. Also note that passing in `NULL` for
//! both `from_value` and `to_value`, will result in the animation having the same from- and to-
//! values, effectively not doing anything.
//! @return A handle to the property animation. `NULL` if animation could not be created
PropertyAnimation* property_animation_create(const PropertyAnimationImplementation *implementation,
void *subject, void *from_value, void *to_value);
//! @internal
//! Convenience function to re-initialize an already instantiated property animation.
//! @param implementation Pointer to the implementation of the animation. In most cases, it makes
//! sense to pass in a `static const` struct pointer.
//! @param subject Pointer to the "subject" being animated. This will be passed in when the getter/
//! setter accessors are called,
//! see \ref PropertyAnimationAccessors, \ref GPointSetter, and friends. The value of this pointer
//! will be copied into the `.subject` field of the PropertyAnimation struct.
//! @param from_value Pointer to the value that the subject should animate from
//! @param to_value Pointer to the value that the subject should animate to
//! @note Pass in `NULL` as one of the value arguments to have it set automatically to the subject's
//! current property value, as returned by the getter function. Also note that passing in `NULL` for
//! both `from_value` and `to_value`, will result in the animation having the same from- and to-
//! values, effectively not doing anything.
//! @return true if successful
bool property_animation_init(PropertyAnimation *animation_h,
const PropertyAnimationImplementation *implementation,
void *subject, void *from_value, void *to_value);
//! Default update callback for a property animations to update a property of type int16_t.
//! Assign this function to the `.base.update` callback field of your
//! PropertyAnimationImplementation, in combination with a `.getter` and `.setter` accessors of
//! types \ref Int16Getter and \ref Int16Setter.
//! The implementation of this function will calculate the next value of the animation and call the
//! setter to set the new value upon the subject.
//! @param property_animation The property animation for which the update is requested.
//! @param distance_normalized The current normalized distance. See \ref
//! AnimationUpdateImplementation
//! @note This function is not supposed to be called "manually", but will be called automatically
//! when the animation is being run.
void property_animation_update_int16(PropertyAnimation *property_animation,
const uint32_t distance_normalized);
//! Default update callback for a property animations to update a property of type uint32_t.
//! Assign this function to the `.base.update` callback field of your
//! PropertyAnimationImplementation, in combination with a `.getter` and `.setter` accessors of
//! types \ref UInt32Getter and \ref UInt32Setter.
//! The implementation of this function will calculate the next value of the animation and call the
//! setter to set the new value upon the subject.
//! @param property_animation The property animation for which the update is requested.
//! @param distance_normalized The current normalized distance. See \ref
//! AnimationUpdateImplementation
//! @note This function is not supposed to be called "manually", but will be called automatically
//! when the animation is being run.
void property_animation_update_uint32(PropertyAnimation *property_animation,
const uint32_t distance_normalized);
//! Default update callback for a property animations to update a property of type GPoint.
//! Assign this function to the `.base.update` callback field of your
//! PropertyAnimationImplementation,
//! in combination with a `.getter` and `.setter` accessors of types \ref GPointGetter and \ref
//! GPointSetter.
//! The implementation of this function will calculate the next point of the animation and call the
//! setter to set the new point upon the subject.
//! @param property_animation The property animation for which the update is requested.
//! @param distance_normalized The current normalized distance. See \ref
//! AnimationUpdateImplementation
//! @note This function is not supposed to be called "manually", but will be called automatically
//! when the animation is being run.
void property_animation_update_gpoint(PropertyAnimation *property_animation,
const uint32_t distance_normalized);
//! Default update callback for a property animations to update a property of type GRect.
//! Assign this function to the `.base.update` callback field of your
//! PropertyAnimationImplementation, in combination with a `.getter` and `.setter` accessors of
//! types \ref GRectGetter and \ref GRectSetter. The implementation of this function will calculate
//! the next rectangle of the animation and call the setter to set the new rectangle upon the
//! subject.
//! @param property_animation The property animation for which the update is requested.
//! @param distance_normalized The current normalized distance. See \ref
//! AnimationUpdateImplementation
//! @note This function is not supposed to be called "manually", but will be called automatically
//! when the animation is being run.
void property_animation_update_grect(PropertyAnimation *property_animation,
const uint32_t distance_normalized);
//! @internal
//! GTransform is not exported, so don't include it in tintin. When GTransform will become exported,
//! remove this from here, property_animation_update_gtransform and property_animation_create.
#if !defined(PLATFORM_TINTIN)
//! Default update callback for a property animations to update a property of type GTransform.
//! Assign this function to the `.base.update` callback field of your
//! PropertyAnimationImplementation, in combination with a `.getter` and `.setter` accessors of
//! types \ref GTransformGetter and \ref GTransformSetter. The implementation of this function will
//! calculate the next GTransform of the animation and call the setter to set the new value upon
//! the subject.
//! @param property_animation The property animation for which the update is requested.
//! @param distance_normalized The current normalized distance. See \ref
//! AnimationUpdateImplementation
//! @note This function is not supposed to be called "manually", but will be called automatically
//! when the animation is being run.
void property_animation_update_gtransform(PropertyAnimation *property_animation,
const uint32_t distance_normalized);
#endif
//! Default update callback for a property animations to update a property of type GColor8.
//! Assign this function to the `.base.update` callback field of your
//! PropertyAnimationImplementation, in combination with a `.getter` and `.setter` accessors of
//! types \ref GColor8Getter and \ref GColor8Setter. The implementation of this function will
//! calculate the next rectangle of the animation and call the setter to set the new value upon
//! the subject.
//! @param property_animation The property animation for which the update is requested.
//! @param distance_normalized The current normalized distance. See \ref
//! AnimationUpdateImplementation
//! @note This function is not supposed to be called "manually", but will be called automatically
//! when the animation is being run.
void property_animation_update_gcolor8(PropertyAnimation *property_animation,
const uint32_t distance_normalized);
//! Default update callback for a property animations to update a property of type Fixed_S32_16.
//! Assign this function to the `.base.update` callback field of your
//! PropertyAnimationImplementation, in combination with a `.getter` and `.setter` accessors of
//! types \ref Fixed_S32_16Getter and \ref Fixed_S32_16Setter. The implementation of this function
//! will calculate the next Fixed_S32_16 of the animation and call the setter to set the new value
//! upon the subject.
//! @param property_animation The property animation for which the update is requested.
//! @param distance_normalized The current normalized distance. See \ref
//! AnimationUpdateImplementation
//! @note This function is not supposed to be called "manually", but will be called automatically
//! when the animation is being run.
void property_animation_update_fixed_s32_16(PropertyAnimation *property_animation,
const uint32_t distance_normalized);
//! @} // group PropertyAnimation
//! @} // group Animation
//! @} // group UI

Some files were not shown because too many files have changed in this diff Show More