/* * 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; }