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

525 lines
19 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "window_stack.h"
#include "animation_private.h"
#include "app_window_click_glue.h"
#include "window_manager.h"
#include "window_private.h"
#include "window_stack_animation.h"
#include "window_stack_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/legacy2/ui/status_bar_legacy2.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/kernel_ui.h"
#include "process_management/app_manager.h"
#include "process_state/app_state/app_state.h"
#include "services/common/compositor/compositor.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/struct.h"
// Private API
////////////////////////////////////
static bool prv_filter_window_item_for_window(ListNode *node, void *data) {
WindowStackItem *item = (WindowStackItem *)node;
return item->window == data;
}
static WindowStackItem *prv_next_item(WindowStackItem *item) {
return (WindowStackItem *)list_get_next(&item->list_node);
}
static WindowStackItem *prv_find_window_stack_item_for_window(WindowStack *window_stack,
Window *window) {
if (!window_stack) {
// Window can't be on a NULL window stack.
return NULL;
}
WindowStackItem *item = (WindowStackItem *)list_find(window_stack->list_head,
prv_filter_window_item_for_window, window);
return item;
}
static void prv_set_new_window_on_screen(Window *appeared_window) {
GRect bounds = window_calc_frame(window_get_fullscreen(appeared_window));
Layer *root_layer = window_get_root_layer(appeared_window);
layer_set_bounds(root_layer, &bounds);
window_set_on_screen(appeared_window, true /* new window */, true /* call handlers */);
click_manager_clear(window_manager_get_window_click_manager(appeared_window));
}
static void prv_unload_removed_windows(WindowStack *window_stack) {
// Copy the removed windows list into a local array and then call the unload callback on
// each removed window.
WindowStackItem *items_to_unload[WINDOW_STACK_ITEMS_MAX];
unsigned int num_items = 0;
while (window_stack->removed_list_head) {
if (num_items >= WINDOW_STACK_ITEMS_MAX) {
break;
}
WindowStackItem *removed_item = (WindowStackItem *)window_stack->removed_list_head;
items_to_unload[num_items++] = removed_item;
window_stack->removed_list_head = list_pop_head(window_stack->removed_list_head);
}
WindowTransitioningContext *context = &window_stack->transition_context;
for (unsigned int i = 0; i < num_items; ++i) {
// The update routine for the transition_to animation relies on the
// window_to being present. If we are unloading the window we should really
// unschedule the animation so we don't touch free'd memory!
//
// For now, rely on our animation transition routines having checks for
// NULL windows since our animation subsystem can't cope with these
// unschedules in some cases (See PBL-25460 for more details)
if (context->window_to == items_to_unload[i]->window) {
context->window_to = NULL;
}
if (context->window_from == items_to_unload[i]->window) {
context->window_from = NULL;
}
window_unload(items_to_unload[i]->window);
applib_free(items_to_unload[i]);
}
}
static void prv_transition_to(Window *window_from, Window *window_to,
const WindowTransitionImplementation *transition) {
PBL_ASSERTN(window_to && transition);
WindowStack *window_stack = window_to->parent_window_stack;
WindowTransitioningContext *context = &window_stack->transition_context;
if (context->animation) {
// If we currently have an animation, run it to completion immediately before starting
// another transition.
// For 2.x apps, just unschedule the animation because there is no equivalent call for
// animation_set_elapsed (which is not available to 2.x apps)
if (process_manager_compiled_with_legacy2_sdk()) {
animation_unschedule(context->animation);
applib_free(context->animation);
} else {
animation_set_elapsed(context->animation,
animation_get_duration(context->animation, true, true));
}
}
*context = (WindowTransitioningContext) {
.window_to = window_to,
.window_to_last_x = INT16_MAX,
.window_from = window_from,
.window_from_last_x = INT16_MAX,
.implementation = transition,
};
// TODO: PBL-17806 in future, store frames and config values as well
if (transition->create_animation) {
context->animation = transition->create_animation(context);
}
// If we haven't set an animation, either because create_animation() was NULL or it returned
// NULL, use g_window_transition_none_implementation
if (!context->animation) {
context->animation = g_window_transition_none_implementation.create_animation(context);
}
PBL_ASSERTN(context->animation);
// TODO: PBL-17645 setup container view
animation_schedule(context->animation);
// TODO: PBL-17645 cleanup container view in animation.stopped
}
static void prv_next_inserter(WindowStackItem *stack_item) {
WindowStack *window_stack = stack_item->window->parent_window_stack;
WindowStackItem *prev_item = (WindowStackItem *)window_stack->list_head;
// Insert after the found previous item:
list_insert_after(&prev_item->list_node, &stack_item->list_node);
}
static void prv_push_inserter(WindowStackItem *stack_item) {
WindowStack *window_stack = stack_item->window->parent_window_stack;
WindowStackItem *prev_item = (WindowStackItem *)(window_stack->list_head);
// Insert before the found previous item, provided that it exists.
ListNode *list_node_closest_to_head = &stack_item->list_node;
list_init(list_node_closest_to_head);
if (prev_item) {
ListNode *prev_list_node = &prev_item->list_node;
list_node_closest_to_head = list_insert_before(prev_list_node, list_node_closest_to_head);
}
// Update the reference to list head in case the window that was inserted is
// the new list head.
window_stack->list_head = list_get_head(list_node_closest_to_head);
}
static void prv_insert_with_function(WindowStack *window_stack_to, Window *window,
void(*inserter)(WindowStackItem *),
const WindowTransitionImplementation *transition_insert,
const WindowTransitionImplementation *transition_pop) {
PBL_ASSERTN(window_stack_to);
WindowStack *window_stack_from = window->parent_window_stack;
Window *window_from = window_manager_get_top_window();
// Assign the new stack for the window
window->parent_window_stack = window_stack_to;
if (!window_from) {
// We do not animate the first window, but instead let the compositor animate.
transition_insert = &g_window_transition_none_implementation;
transition_pop = &g_window_transition_none_implementation;
}
// This is a backwards compatibility hack for legacy2 applications. By default, watchface
// windows are not fullscreen until they're pushed onto the window stack, whereas legacy2
// watchfaces assume they are already full screen before being pushed.
bool is_app_window = window_manager_is_app_window(window);
if (is_app_window && sys_app_is_watchface()) {
window_set_fullscreen(window, true);
}
// If on the list of removed items for a window stack, remove from the removed items list
// as we want to add it back to the window stack.
if (window_stack_from) {
ListNode *node = list_find(window_stack_from->removed_list_head,
prv_filter_window_item_for_window, window);
if (node != NULL) {
list_remove(node, &window_stack_from->removed_list_head, NULL);
}
}
WindowStackItem *item = NULL;
if (window_stack_contains_window(window_stack_from, window)) {
// If the item is on the list of window items, then we remove it as we're going to
// re-insert it.
item = prv_find_window_stack_item_for_window(window_stack_from, window);
list_remove(&item->list_node, &window_stack_from->list_head, NULL);
} else {
// If the item is not yet on the window stack's list, then we allocate space for it
// on the heap.
item = applib_type_malloc(WindowStackItem);
*item = (WindowStackItem) {
.window = window,
.pop_transition_implementation = transition_pop,
};
}
inserter(item);
if (window_from == NULL || !window_manager_is_window_visible(window_from)) {
prv_transition_to(window_from, window, transition_insert);
}
PBL_LOG(LOG_LEVEL_DEBUG, "(+) %s=%p <%s>", is_app_window ? "window" : "modal window",
window, window_get_debug_name(window));
}
static Window *prv_remove_item(WindowStackItem *pop_item,
const WindowTransitionImplementation *transition) {
PBL_ASSERTN(pop_item->window);
WindowStack *window_stack = pop_item->window->parent_window_stack;
// Do a transition from element that needs to be removed only if it was on the
// top of the visible window stack.
Window *window_from = NULL;
// If this window is currently transitioning and it is a modal window
if ((window_stack->transition_context.window_to == pop_item->window) &&
!window_manager_is_app_window(pop_item->window)) {
compositor_transition_cancel();
}
if (window_manager_is_window_visible(pop_item->window)) {
window_from = pop_item->window;
// If no transition is explicitly provided, use the one specified when pushed.
transition = transition ?: pop_item->pop_transition_implementation;
PBL_ASSERTN(transition);
} else {
// We don't intentionally clean up the .pop_transition of a previous element if
// a client actively messes with the window stack; they need to take care of this
// in any potential custom transition. The default transitions cannot handle this.
window_from = NULL;
}
// Remove the item from the window stack
list_remove(&pop_item->list_node, &window_stack->list_head, NULL);
WindowStackItem *stack_item = (WindowStackItem *)window_stack->list_head;
Window *window_to = stack_item ? stack_item->window : NULL;
// Add the removed item to the 'removed' list
window_stack->removed_list_head = list_insert_before(window_stack->removed_list_head,
&pop_item->list_node);
// Store the window here, as we're potentially free'ing the item later on.
Window *pop_item_window = pop_item->window;
bool is_app_window = window_manager_is_app_window(pop_item_window);
PBL_LOG(LOG_LEVEL_DEBUG, "(-) %s=%p <%s>", is_app_window ? "window" : "modal window",
pop_item_window, window_get_debug_name(pop_item_window));
// Only animate if the window was previously at the top of the stack and there's a
// window we can transition to.
if (window_from && window_to) {
prv_transition_to(window_from, window_to, transition);
} else {
// We don't fire a transition in this case, but to ensure that all window-callbacks
// will still be called and the click handler is managed correctly, we call the
// appropriate helper functions manually with a fake transitioning context.
WindowTransitioningContext ctx = {
.window_from = pop_item_window,
};
window_transition_context_disappear(&ctx);
}
return pop_item_window;
}
// Public API
////////////////////////////////////
Window *window_stack_get_top_window(WindowStack *window_stack) {
WindowStackItem *item = (WindowStackItem *)NULL_SAFE_FIELD_ACCESS(window_stack, list_head, NULL);
return NULL_SAFE_FIELD_ACCESS(item, window, NULL);
}
void window_stack_push(WindowStack *window_stack, Window *window, bool animated) {
const WindowTransitionImplementation *transition_insert =
animated ? window_transition_get_default_push_implementation() :
&g_window_transition_none_implementation;
const WindowTransitionImplementation *transition_pop =
animated ? window_transition_get_default_pop_implementation() :
&g_window_transition_none_implementation;
window_stack_push_with_transition(window_stack, window, transition_insert, transition_pop);
}
void window_stack_push_with_transition(WindowStack *window_stack, Window *window,
const WindowTransitionImplementation *push_transition,
const WindowTransitionImplementation *pop_transition) {
if (window_stack->lock_push) {
return;
}
PBL_ASSERTN(push_transition && pop_transition);
prv_insert_with_function(window_stack, window, prv_push_inserter, push_transition,
pop_transition);
}
void window_stack_insert_next(WindowStack *window_stack, Window *window) {
if (window_stack->lock_push) {
return;
}
const WindowTransitionImplementation *transition_to = &g_window_transition_none_implementation;
const WindowTransitionImplementation *transition_pop =
window_transition_get_default_pop_implementation();
prv_insert_with_function(window_stack, window, prv_next_inserter, transition_to, transition_pop);
}
Window *window_stack_pop(WindowStack *window_stack, bool animated) {
// Transition NULL will default to the registered pop_transition to the stack item
const WindowTransitionImplementation *transition =
animated ? NULL : &g_window_transition_none_implementation;
return window_stack_pop_with_transition(window_stack, transition);
}
Window *window_stack_pop_with_transition(WindowStack *window_stack,
const WindowTransitionImplementation *transition) {
if (window_stack->list_head == NULL) {
PBL_LOG(LOG_LEVEL_DEBUG, "Nothing to pop.");
return NULL;
}
WindowStackItem *pop_item = (WindowStackItem *)window_stack->list_head;
Window *window = NULL;
if (pop_item) {
window = prv_remove_item(pop_item, transition);
}
return window;
}
void window_stack_pop_all(WindowStack *window_stack, const bool animated) {
if (window_stack->list_head == NULL) {
return;
}
WindowStackItem *top_item = (WindowStackItem *)window_stack->list_head;
WindowStackItem *next_item;
do {
// We manually remove each item to ensure that we do not call prv_remove_unloaded_windows
// until we have cleaned up the list. This prevents us from running into the issue where
// an unload handler pushes a window onto the stack, but that window is subsequently
// popped from the stack on another iteration of the while loop.
next_item = prv_next_item(top_item);
if (next_item == NULL) {
break;
}
list_remove(&next_item->list_node, &window_stack->list_head, NULL);
window_stack->removed_list_head = list_insert_before(window_stack->removed_list_head,
&next_item->list_node);
window_set_on_screen(next_item->window, false /* not new */, true /* call handlers */);
} while (true);
window_stack_pop(window_stack, animated);
}
bool window_stack_remove(Window *window, bool animated) {
if (window == NULL) {
return false;
}
WindowStackItem *item = prv_find_window_stack_item_for_window(window->parent_window_stack,
window);
if (item == NULL) {
return false;
}
const WindowTransitionImplementation *transition =
animated ? window_transition_get_default_pop_implementation() :
&g_window_transition_none_implementation;
window = prv_remove_item(item, transition);
return window != NULL;
}
bool window_stack_remove_with_transition(Window *window,
const WindowTransitionImplementation *transition) {
if (window == NULL) {
return false;
}
WindowStack *stack = window->parent_window_stack;
WindowStackItem *item = prv_find_window_stack_item_for_window(stack, window);
if (item == NULL) {
return false;
}
window = prv_remove_item(item, transition);
return window != NULL;
}
bool window_stack_contains_window(WindowStack *window_stack, Window *window) {
return (prv_find_window_stack_item_for_window(window_stack, window) != NULL);
}
uint32_t window_stack_count(WindowStack *window_stack) {
return list_count(window_stack->list_head);
}
// Stack status
////////////////////////////////////
void window_stack_lock_push(WindowStack *window_stack) {
window_stack->lock_push = true;
}
void window_stack_unlock_push(WindowStack *window_stack) {
window_stack->lock_push = false;
}
bool window_stack_is_animating(WindowStack *window_stack) {
return window_stack && animation_is_scheduled(window_stack->transition_context.animation);
}
bool window_stack_is_animating_with_fixed_status_bar(WindowStack *window_stack) {
WindowTransitioningContext *context = &window_stack->transition_context;
return window_stack_is_animating(window_stack) &&
window_has_status_bar(context->window_from) &&
window_has_status_bar(context->window_to);
}
// Transitioning Context Functions
////////////////////////////////////
// @note: This function prototypes exist in window_stack_private.h
bool window_transition_context_has_legacy_window_to(WindowStack *stack, Window *window) {
return (stack->transition_context.window_to == window) &&
process_manager_compiled_with_legacy2_sdk();
}
void window_transition_context_disappear(WindowTransitioningContext *context) {
Window *window_from = context->window_from;
if (!window_from || window_manager_is_window_visible(window_from)) {
PBL_LOG(LOG_LEVEL_DEBUG, "No windows to unload from stack.");
context->window_from = NULL;
return;
}
// Remove window reference from context to prevent future calls to it (e.g. "is dirty?")
context->window_from = NULL;
if (!window_manager_is_window_visible(window_from)) {
window_set_on_screen(window_from, false /* not new */, true /* call handlers */);
}
prv_unload_removed_windows(window_from->parent_window_stack);
}
void window_transition_context_appear(WindowTransitioningContext *context) {
Window *window_to = context->window_to;
if (!window_manager_is_window_visible(window_to)) {
return;
}
prv_set_new_window_on_screen(window_to);
ClickManager *click_manager = window_manager_get_window_click_manager(window_to);
if (window_manager_is_window_focused(window_to)) {
// TODO: PBL-37477 Window Stack directly calls click config
// Either this or app_click_config_setup_with_window should be calling
// window_setup_click_config_provider instead
app_click_config_setup_with_window(click_manager, window_to);
}
}
// Debug and Test Functions
/////////////////////////////
size_t window_stack_dump(WindowStack *stack, WindowStackDump **dump) {
*dump = NULL;
size_t count = window_stack_count(stack);
size_t idx = 0;
if (count > 0) {
*dump = kernel_calloc(count, sizeof(WindowStackDump));
if (*dump) {
WindowStackItem *item = (WindowStackItem *)stack->list_head;
while (item) {
(*dump)[idx++] = (WindowStackDump) {
.addr = item->window,
.name = window_get_debug_name(item->window),
};
item = (WindowStackItem *)list_get_next(&item->list_node);
}
PBL_ASSERTN(idx == count);
}
}
return count;
}