mirror of
https://github.com/google/pebble.git
synced 2025-11-21 15:02:18 -05:00
Import of the watch repository from Pebble
This commit is contained in:
396
src/fw/applib/ui/action_bar_layer.c
Normal file
396
src/fw/applib/ui/action_bar_layer.c
Normal 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));
|
||||
}
|
||||
289
src/fw/applib/ui/action_bar_layer.h
Normal file
289
src/fw/applib/ui/action_bar_layer.h
Normal 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
|
||||
//!
|
||||
//! 
|
||||
//! 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
|
||||
51
src/fw/applib/ui/action_button.c
Normal file
51
src/fw/applib/ui/action_button.c
Normal 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);
|
||||
}
|
||||
30
src/fw/applib/ui/action_button.h
Normal file
30
src/fw/applib/ui/action_button.h
Normal 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);
|
||||
125
src/fw/applib/ui/action_menu_hierarchy.c
Normal file
125
src/fw/applib/ui/action_menu_hierarchy.c
Normal 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);
|
||||
}
|
||||
}
|
||||
108
src/fw/applib/ui/action_menu_hierarchy.h
Normal file
108
src/fw/applib/ui/action_menu_hierarchy.h
Normal 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
|
||||
847
src/fw/applib/ui/action_menu_layer.c
Normal file
847
src/fw/applib/ui/action_menu_layer.c
Normal 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);
|
||||
}
|
||||
98
src/fw/applib/ui/action_menu_layer.h
Normal file
98
src/fw/applib/ui/action_menu_layer.h
Normal 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);
|
||||
359
src/fw/applib/ui/action_menu_window.c
Normal file
359
src/fw/applib/ui/action_menu_window.c
Normal 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);
|
||||
}
|
||||
137
src/fw/applib/ui/action_menu_window.h
Normal file
137
src/fw/applib/ui/action_menu_window.h
Normal 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
|
||||
81
src/fw/applib/ui/action_menu_window_private.h
Normal file
81
src/fw/applib/ui/action_menu_window_private.h
Normal 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[];
|
||||
};
|
||||
202
src/fw/applib/ui/action_toggle.c
Normal file
202
src/fw/applib/ui/action_toggle.c
Normal 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);
|
||||
}
|
||||
}
|
||||
89
src/fw/applib/ui/action_toggle.h
Normal file
89
src/fw/applib/ui/action_toggle.h
Normal 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
1932
src/fw/applib/ui/animation.c
Normal file
File diff suppressed because it is too large
Load Diff
532
src/fw/applib/ui/animation.h
Normal file
532
src/fw/applib/ui/animation.h
Normal 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”.
|
||||
//! 
|
||||
//!
|
||||
//! @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
|
||||
179
src/fw/applib/ui/animation_interpolate.c
Normal file
179
src/fw/applib/ui/animation_interpolate.c
Normal 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);
|
||||
}
|
||||
142
src/fw/applib/ui/animation_interpolate.h
Normal file
142
src/fw/applib/ui/animation_interpolate.h
Normal 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);
|
||||
168
src/fw/applib/ui/animation_private.h
Normal file
168
src/fw/applib/ui/animation_private.h
Normal 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);
|
||||
161
src/fw/applib/ui/animation_timing.c
Normal file
161
src/fw/applib/ui/animation_timing.c
Normal 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;
|
||||
}
|
||||
62
src/fw/applib/ui/animation_timing.h
Normal file
62
src/fw/applib/ui/animation_timing.h
Normal 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);
|
||||
51
src/fw/applib/ui/app_window_click_glue.c
Normal file
51
src/fw/applib/ui/app_window_click_glue.c
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
32
src/fw/applib/ui/app_window_click_glue.h
Normal file
32
src/fw/applib/ui/app_window_click_glue.h
Normal 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);
|
||||
|
||||
120
src/fw/applib/ui/app_window_stack.c
Normal file
120
src/fw/applib/ui/app_window_stack.c
Normal 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);
|
||||
}
|
||||
101
src/fw/applib/ui/app_window_stack.h
Normal file
101
src/fw/applib/ui/app_window_stack.h
Normal 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
|
||||
124
src/fw/applib/ui/bitmap_layer.c
Normal file
124
src/fw/applib/ui/bitmap_layer.c
Normal 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));
|
||||
}
|
||||
163
src/fw/applib/ui/bitmap_layer.h
Normal file
163
src/fw/applib/ui/bitmap_layer.h
Normal 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.
|
||||
//!
|
||||
//! 
|
||||
//! 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
386
src/fw/applib/ui/click.c
Normal 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
235
src/fw/applib/ui/click.h
Normal 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
|
||||
89
src/fw/applib/ui/click_internal.h
Normal file
89
src/fw/applib/ui/click_internal.h
Normal 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);
|
||||
|
||||
381
src/fw/applib/ui/content_indicator.c
Normal file
381
src/fw/applib/ui/content_indicator.c
Normal 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);
|
||||
}
|
||||
118
src/fw/applib/ui/content_indicator.h
Normal file
118
src/fw/applib/ui/content_indicator.h
Normal 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
|
||||
82
src/fw/applib/ui/content_indicator_private.h
Normal file
82
src/fw/applib/ui/content_indicator_private.h
Normal 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);
|
||||
200
src/fw/applib/ui/crumbs_layer.c
Normal file
200
src/fw/applib/ui/crumbs_layer.c
Normal 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);
|
||||
}
|
||||
45
src/fw/applib/ui/crumbs_layer.h
Normal file
45
src/fw/applib/ui/crumbs_layer.h
Normal 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);
|
||||
109
src/fw/applib/ui/date_time_selection_window_private.c
Normal file
109
src/fw/applib/ui/date_time_selection_window_private.c
Normal 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;
|
||||
}
|
||||
}
|
||||
52
src/fw/applib/ui/date_time_selection_window_private.h
Normal file
52
src/fw/applib/ui/date_time_selection_window_private.h
Normal 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);
|
||||
252
src/fw/applib/ui/dialogs/actionable_dialog.c
Normal file
252
src/fw/applib/ui/dialogs/actionable_dialog.c
Normal 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;
|
||||
}
|
||||
75
src/fw/applib/ui/dialogs/actionable_dialog.h
Normal file
75
src/fw/applib/ui/dialogs/actionable_dialog.h
Normal 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);
|
||||
57
src/fw/applib/ui/dialogs/actionable_dialog_private.h
Normal file
57
src/fw/applib/ui/dialogs/actionable_dialog_private.h
Normal 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;
|
||||
117
src/fw/applib/ui/dialogs/bt_conn_dialog.c
Normal file
117
src/fw/applib/ui/dialogs/bt_conn_dialog.c
Normal 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);
|
||||
}
|
||||
52
src/fw/applib/ui/dialogs/bt_conn_dialog.h
Normal file
52
src/fw/applib/ui/dialogs/bt_conn_dialog.h
Normal 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);
|
||||
112
src/fw/applib/ui/dialogs/confirmation_dialog.c
Normal file
112
src/fw/applib/ui/dialogs/confirmation_dialog.c
Normal 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);
|
||||
}
|
||||
65
src/fw/applib/ui/dialogs/confirmation_dialog.h
Normal file
65
src/fw/applib/ui/dialogs/confirmation_dialog.h
Normal 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);
|
||||
105
src/fw/applib/ui/dialogs/dialog.c
Normal file
105
src/fw/applib/ui/dialogs/dialog.c
Normal 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);
|
||||
}
|
||||
}
|
||||
145
src/fw/applib/ui/dialogs/dialog.h
Normal file
145
src/fw/applib/ui/dialogs/dialog.h
Normal 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);
|
||||
176
src/fw/applib/ui/dialogs/dialog_private.c
Normal file
176
src/fw/applib/ui/dialogs/dialog_private.c
Normal 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);
|
||||
}
|
||||
72
src/fw/applib/ui/dialogs/dialog_private.h
Normal file
72
src/fw/applib/ui/dialogs/dialog_private.h
Normal 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);
|
||||
443
src/fw/applib/ui/dialogs/expandable_dialog.c
Normal file
443
src/fw/applib/ui/dialogs/expandable_dialog.c
Normal 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);
|
||||
}
|
||||
148
src/fw/applib/ui/dialogs/expandable_dialog.h
Normal file
148
src/fw/applib/ui/dialogs/expandable_dialog.h
Normal 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);
|
||||
227
src/fw/applib/ui/dialogs/simple_dialog.c
Normal file
227
src/fw/applib/ui/dialogs/simple_dialog.c
Normal 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;
|
||||
}
|
||||
68
src/fw/applib/ui/dialogs/simple_dialog.h
Normal file
68
src/fw/applib/ui/dialogs/simple_dialog.h
Normal 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);
|
||||
113
src/fw/applib/ui/inverter_layer.c
Normal file
113
src/fw/applib/ui/inverter_layer.c
Normal 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;
|
||||
}
|
||||
|
||||
82
src/fw/applib/ui/inverter_layer.h
Normal file
82
src/fw/applib/ui/inverter_layer.h
Normal 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".
|
||||
//!
|
||||
//! 
|
||||
//! 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
|
||||
187
src/fw/applib/ui/kino/kino_layer.c
Normal file
187
src/fw/applib/ui/kino/kino_layer.c
Normal 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;
|
||||
}
|
||||
86
src/fw/applib/ui/kino/kino_layer.h
Normal file
86
src/fw/applib/ui/kino/kino_layer.h
Normal 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);
|
||||
218
src/fw/applib/ui/kino/kino_player.c
Normal file
218
src/fw/applib/ui/kino/kino_player.c
Normal 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);
|
||||
}
|
||||
79
src/fw/applib/ui/kino/kino_player.h
Normal file
79
src/fw/applib/ui/kino/kino_player.h
Normal 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);
|
||||
179
src/fw/applib/ui/kino/kino_reel.c
Normal file
179
src/fw/applib/ui/kino/kino_reel.c
Normal 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;
|
||||
}
|
||||
120
src/fw/applib/ui/kino/kino_reel.h
Normal file
120
src/fw/applib/ui/kino/kino_reel.h
Normal 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);
|
||||
76
src/fw/applib/ui/kino/kino_reel/morph_square.c
Normal file
76
src/fw/applib/ui/kino/kino_reel/morph_square.c
Normal 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;
|
||||
}
|
||||
27
src/fw/applib/ui/kino/kino_reel/morph_square.h
Normal file
27
src/fw/applib/ui/kino/kino_reel/morph_square.h
Normal 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);
|
||||
295
src/fw/applib/ui/kino/kino_reel/scale_segmented.c
Normal file
295
src/fw/applib/ui/kino/kino_reel/scale_segmented.c
Normal 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);
|
||||
}
|
||||
114
src/fw/applib/ui/kino/kino_reel/scale_segmented.h
Normal file
114
src/fw/applib/ui/kino/kino_reel/scale_segmented.h
Normal 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);
|
||||
423
src/fw/applib/ui/kino/kino_reel/transform.c
Normal file
423
src/fw/applib/ui/kino/kino_reel/transform.c
Normal 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;
|
||||
}
|
||||
}
|
||||
120
src/fw/applib/ui/kino/kino_reel/transform.h
Normal file
120
src/fw/applib/ui/kino/kino_reel/transform.h
Normal 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);
|
||||
88
src/fw/applib/ui/kino/kino_reel/unfold.c
Normal file
88
src/fw/applib/ui/kino/kino_reel/unfold.c
Normal 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);
|
||||
}
|
||||
54
src/fw/applib/ui/kino/kino_reel/unfold.h
Normal file
54
src/fw/applib/ui/kino/kino_reel/unfold.h
Normal 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);
|
||||
166
src/fw/applib/ui/kino/kino_reel_custom.c
Normal file
166
src/fw/applib/ui/kino/kino_reel_custom.c
Normal 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;
|
||||
}
|
||||
23
src/fw/applib/ui/kino/kino_reel_custom.h
Normal file
23
src/fw/applib/ui/kino/kino_reel_custom.h
Normal 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);
|
||||
131
src/fw/applib/ui/kino/kino_reel_gbitmap.c
Normal file
131
src/fw/applib/ui/kino/kino_reel_gbitmap.c
Normal 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);
|
||||
}
|
||||
25
src/fw/applib/ui/kino/kino_reel_gbitmap.h
Normal file
25
src/fw/applib/ui/kino/kino_reel_gbitmap.h
Normal 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);
|
||||
25
src/fw/applib/ui/kino/kino_reel_gbitmap_private.h
Normal file
25
src/fw/applib/ui/kino/kino_reel_gbitmap_private.h
Normal 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);
|
||||
149
src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c
Normal file
149
src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c
Normal 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);
|
||||
}
|
||||
26
src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.h
Normal file
26
src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.h
Normal 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);
|
||||
102
src/fw/applib/ui/kino/kino_reel_pdci.c
Normal file
102
src/fw/applib/ui/kino/kino_reel_pdci.c
Normal 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);
|
||||
}
|
||||
25
src/fw/applib/ui/kino/kino_reel_pdci.h
Normal file
25
src/fw/applib/ui/kino/kino_reel_pdci.h
Normal 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);
|
||||
146
src/fw/applib/ui/kino/kino_reel_pdcs.c
Normal file
146
src/fw/applib/ui/kino/kino_reel_pdcs.c
Normal 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);
|
||||
}
|
||||
25
src/fw/applib/ui/kino/kino_reel_pdcs.h
Normal file
25
src/fw/applib/ui/kino/kino_reel_pdcs.h
Normal 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
634
src/fw/applib/ui/layer.c
Normal 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, ¤t_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
431
src/fw/applib/ui/layer.h
Normal 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
|
||||
|
||||
24
src/fw/applib/ui/layer_private.h
Normal file
24
src/fw/applib/ui/layer_private.h
Normal 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);
|
||||
176
src/fw/applib/ui/menu_cell_layer.h
Normal file
176
src/fw/applib/ui/menu_cell_layer.h
Normal 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
|
||||
1308
src/fw/applib/ui/menu_layer.c
Normal file
1308
src/fw/applib/ui/menu_layer.c
Normal file
File diff suppressed because it is too large
Load Diff
632
src/fw/applib/ui/menu_layer.h
Normal file
632
src/fw/applib/ui/menu_layer.h
Normal 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.
|
||||
//!
|
||||
//! 
|
||||
//! <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
|
||||
|
||||
44
src/fw/applib/ui/menu_layer_private.h
Normal file
44
src/fw/applib/ui/menu_layer_private.h
Normal 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;
|
||||
618
src/fw/applib/ui/menu_layer_system_cells.c
Normal file
618
src/fw/applib/ui/menu_layer_system_cells.c
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
244
src/fw/applib/ui/number_window.c
Normal file
244
src/fw/applib/ui/number_window.c
Normal 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);
|
||||
}
|
||||
150
src/fw/applib/ui/number_window.h
Normal file
150
src/fw/applib/ui/number_window.h
Normal 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
|
||||
//!
|
||||
//! 
|
||||
//! @{
|
||||
|
||||
//! 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
|
||||
|
||||
337
src/fw/applib/ui/option_menu_window.c
Normal file
337
src/fw/applib/ui/option_menu_window.c
Normal 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(¬_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);
|
||||
}
|
||||
}
|
||||
126
src/fw/applib/ui/option_menu_window.h
Normal file
126
src/fw/applib/ui/option_menu_window.h
Normal 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);
|
||||
63
src/fw/applib/ui/path_layer.c
Normal file
63
src/fw/applib/ui/path_layer.c
Normal 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;
|
||||
}
|
||||
|
||||
44
src/fw/applib/ui/path_layer.h
Normal file
44
src/fw/applib/ui/path_layer.h
Normal 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);
|
||||
|
||||
23
src/fw/applib/ui/preferred_durations.c
Normal file
23
src/fw/applib/ui/preferred_durations.c
Normal 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;
|
||||
}
|
||||
37
src/fw/applib/ui/preferred_durations.h
Normal file
37
src/fw/applib/ui/preferred_durations.h
Normal 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
|
||||
84
src/fw/applib/ui/progress_layer.c
Normal file
84
src/fw/applib/ui/progress_layer.c
Normal 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;
|
||||
}
|
||||
55
src/fw/applib/ui/progress_layer.h
Normal file
55
src/fw/applib/ui/progress_layer.h
Normal 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);
|
||||
340
src/fw/applib/ui/progress_window.c
Normal file
340
src/fw/applib/ui/progress_window.c
Normal 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);
|
||||
}
|
||||
130
src/fw/applib/ui/progress_window.h
Normal file
130
src/fw/applib/ui/progress_window.h
Normal 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);
|
||||
617
src/fw/applib/ui/property_animation.c
Normal file
617
src/fw/applib/ui/property_animation.c
Normal 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;
|
||||
}
|
||||
683
src/fw/applib/ui/property_animation.h
Normal file
683
src/fw/applib/ui/property_animation.h
Normal 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
Reference in New Issue
Block a user