/* * 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_private.h" #include "applib/app_logging.h" #include "applib/graphics/graphics.h" #include "applib/ui/app_window_click_glue.h" #include "applib/ui/app_window_stack.h" #include "applib/ui/click.h" #include "applib/ui/layer.h" #include "applib/ui/layer_private.h" #include "applib/ui/window_manager.h" #include "applib/ui/window_stack.h" #include "applib/applib_malloc.auto.h" #include "applib/legacy2/ui/status_bar_legacy2.h" #include "kernel/ui/kernel_ui.h" #include "kernel/ui/modals/modal_manager.h" #include "process_management/process_manager.h" #include "process_state/app_state/app_state.h" #include "system/logging.h" #include "system/passert.h" #include "syscall/syscall.h" #include "status_bar_layer.h" #include typedef enum { WindowHandlerOffsetLoad = offsetof(WindowHandlers, load), WindowHandlerOffsetAppear = offsetof(WindowHandlers, appear), WindowHandlerOffsetDisappear = offsetof(WindowHandlers, disappear), WindowHandlerOffsetUnload = offsetof(WindowHandlers, unload), } WindowHandlerOffset; void window_do_layer_update_proc(Layer *layer, GContext* ctx) { Window *window = layer_get_window(layer); const GColor bg_color = window->background_color; if (!gcolor_is_transparent(bg_color)) { GDrawState prev_state = graphics_context_get_drawing_state(ctx); graphics_context_set_fill_color(ctx, bg_color); graphics_fill_rect(ctx, &layer->bounds); graphics_context_set_drawing_state(ctx, prev_state); } } //! helper struct to move displacement logic out of window_render() typedef struct { GPoint drawing_box_origin; GRect clip_box; } DrawingStateOrigins; static void prv_adjust_drawing_state_for_legacy2_apps(DrawingStateOrigins *saved_state, GContext *ctx, Window *window) { GDrawState * const draw_state = &ctx->draw_state; *saved_state = (DrawingStateOrigins){ .drawing_box_origin = draw_state->drawing_box.origin, .clip_box = draw_state->clip_box, }; const int16_t full_screen_displacement = window->is_fullscreen ? 0 : STATUS_BAR_HEIGHT; draw_state->drawing_box.origin.y += full_screen_displacement; draw_state->clip_box.origin.y += full_screen_displacement; WindowStack *stack = window->parent_window_stack; if (window_transition_context_has_legacy_window_to(stack, window)) { // for 2.x apps, we cannot animate the window frame during a transition but need to use this // externalized state const GPoint displacement = stack->transition_context.window_to_displacement; gpoint_add_eq(&draw_state->drawing_box.origin, displacement); gpoint_add_eq(&draw_state->clip_box.origin, displacement); } // clip_box must respect screen boundaries grect_clip(&draw_state->clip_box, &saved_state->clip_box); } static void prv_restore_drawing_state(DrawingStateOrigins *saved_state, GContext *ctx) { ctx->draw_state.drawing_box.origin = saved_state->drawing_box_origin; ctx->draw_state.clip_box = saved_state->clip_box; } void prv_render_legacy2_system_status_bar(GContext *ctx, Window *window) { if (!window->is_fullscreen) { // adjust clipping rectangle so that rendering doesn't happen outside of the window // this prevents instant colors changes when going from one window to another GRect saved_clip_box = ctx->draw_state.clip_box; grect_clip(&ctx->draw_state.clip_box, &window->layer.frame); StatusBarLayerConfig config = { .foreground_color = GColorWhite, .background_color = GColorBlack, .mode = StatusBarLayerModeClock, }; GRect frame = window->layer.frame; // window.frame.origin.y is 0 already (for 2.x compatibility reasons) // see prv_adjust_drawing_state_for_legacy2_apps() // so all we need to alter is the height of the frame frame.size.h = STATUS_BAR_HEIGHT; if (window_stack_is_animating_with_fixed_status_bar(window->parent_window_stack)) { frame.origin.x = 0; } status_bar_layer_render(ctx, &frame, &config); ctx->draw_state.clip_box = saved_clip_box; } } void window_render(Window *window, GContext *ctx) { PBL_ASSERTN(window); if (window->on_screen == false) { window->is_render_scheduled = false; return; } // workaround for 3rd-party apps // if a window is configured as non-fullscreen, it's frame needs to start at .origin={0,0} // to compensate for cases where clients configure a layer hierarchy with // my_layer = layer_create(window.root_layer.frame) // ! wrong, should be .bounds // Of course on the screen, it still needs to start at {0, 16}. We adjust for that by // moving the GContext's draw_state before we traverse the layer hierarchy to render it. // Also see window_calc_frame() DrawingStateOrigins saved_state; prv_adjust_drawing_state_for_legacy2_apps(&saved_state, ctx, window); layer_render_tree(&window->layer, ctx); prv_restore_drawing_state(&saved_state, ctx); prv_render_legacy2_system_status_bar(ctx, window); window->is_render_scheduled = false; } void window_call_handler(Window *window, WindowHandlerOffset handler_offset) { if (window == NULL) { return; } WindowHandler handler = *(WindowHandler*)(((uint8_t*)&window->window_handlers) + handler_offset); if (handler) { handler(window); } } void window_schedule_render(Window *window) { window->is_render_scheduled = true; } GRect window_calc_frame(bool fullscreen) { GContext *ctx = graphics_context_get_current_context(); GRect result = (GRect) { .origin = { 0, 0 }, .size = graphics_context_get_framebuffer_size(ctx) }; result.size.h -= fullscreen ? 0 : STATUS_BAR_HEIGHT; return result; } // FIXME: there is a problem in this function: // This function initializes the root layer to be the screen size. So, on a // new window with a status bar, unless otherwise forced to with something // like window_set_on_screen(), the window is first rendered at position (0,0); // then this function shifts it to its correct position of (0, STATUS_BAR_HEIGHT). // Either this function should set the window not on screen, or we should provide // an alternate function for initializing the window that takes a frame dimension too. void window_init(Window *window, const char* debug_name) { if (window == NULL) { PBL_LOG(LOG_LEVEL_ERROR, "Tried to init a NULL window"); return; } *window = (Window){}; #ifndef RELEASE window->debug_name = debug_name; #else (void)debug_name; #endif bool fullscreen = !process_manager_compiled_with_legacy2_sdk(); const GRect frame = window_calc_frame(fullscreen); layer_init(&window->layer, &frame); window->is_fullscreen = fullscreen; window->layer.window = window; window->layer.update_proc = window_do_layer_update_proc; window->background_color = GColorWhite; window->in_click_config_provider = false; window->is_waiting_for_click_config = false; window->parent_window_stack = NULL; } Window* window_create(void) { Window* window = applib_type_malloc(Window); if (window) { window_init(window, ""); } return window; } void window_destroy(Window* window) { if (window == NULL) { return; } window_deinit(window); applib_free(window); } void window_deinit(Window *window) { PBL_ASSERTN(window); // FIXME: is there a way to cancel a pending render event? window_set_on_screen(window, false, true); layer_remove_child_layers(&window->layer); window_unload(window); } void window_set_overrides_back_button(Window *window, bool overrides_back_button) { if (overrides_back_button == window->overrides_back_button) { return; } window->overrides_back_button = overrides_back_button; } static ClickManager* prv_get_current_click_manager(void) { return window_manager_get_window_click_manager(window_manager_get_top_window()); } static void prv_call_click_provider(Window *window) { window->is_waiting_for_click_config = false; app_click_config_setup_with_window(prv_get_current_click_manager(), window); window->is_click_configured = true; } static void prv_check_is_in_click_config_provider(Window *window, char *type) { PBL_ASSERT(window->in_click_config_provider, "Click %s must be set from click config provider (Window %p)", type, window); } void window_setup_click_config_provider(Window *window) { prv_call_click_provider(window); } void window_set_click_config_provider_with_context( Window *window, ClickConfigProvider click_config_provider, void *context) { PBL_ASSERTN(window); window->click_config_provider = click_config_provider; window->click_config_context = context; if (window->on_screen && !window->is_unfocusable) { // We're already on screen, make the config provider get called. prv_call_click_provider(window); } else { window->is_waiting_for_click_config = true; } } void window_set_click_config_provider(Window *window, ClickConfigProvider click_config_provider) { window_set_click_config_provider_with_context(window, click_config_provider, NULL); } void window_set_click_context(ButtonId button_id, void *context) { prv_check_is_in_click_config_provider(window_manager_get_top_window(), "context"); ClickManager *mgr = prv_get_current_click_manager(); ClickConfig *cfg = &mgr->recognizers[button_id].config; cfg->context = context; } void window_single_click_subscribe(ButtonId button_id, ClickHandler handler) { Window *window = window_manager_get_top_window(); prv_check_is_in_click_config_provider(window, "subscribe"); ClickManager *mgr = prv_get_current_click_manager(); ClickConfig *cfg = &mgr->recognizers[button_id].config; cfg->click.repeat_interval_ms = 0; cfg->click.handler = handler; if (button_id == BUTTON_ID_BACK) { window_set_overrides_back_button(window, true); } } void window_single_repeating_click_subscribe(ButtonId button_id, uint16_t repeat_interval_ms, ClickHandler handler) { prv_check_is_in_click_config_provider(window_manager_get_top_window(), "subscribe"); if (button_id == BUTTON_ID_BACK) { return; } ClickManager *mgr = prv_get_current_click_manager(); ClickConfig *cfg = &mgr->recognizers[button_id].config; cfg->click.repeat_interval_ms = repeat_interval_ms; cfg->click.handler = handler; } void window_multi_click_subscribe(ButtonId button_id, uint8_t min_clicks, uint8_t max_clicks, uint16_t timeout, bool last_click_only, ClickHandler handler) { Window *window = window_manager_get_top_window(); prv_check_is_in_click_config_provider(window, "subscribe"); ClickManager *mgr = prv_get_current_click_manager(); ClickConfig *cfg = &mgr->recognizers[button_id].config; cfg->multi_click.min = (min_clicks == 0) ? 2 : min_clicks; cfg->multi_click.max = (max_clicks == 0) ? min_clicks : max_clicks; cfg->multi_click.timeout = (timeout == 0) ? 300 : timeout; cfg->multi_click.last_click_only = last_click_only; cfg->multi_click.handler = handler; if (button_id == BUTTON_ID_BACK) { window_set_overrides_back_button(window, true); } } void window_long_click_subscribe(ButtonId button_id, uint16_t delay_ms, ClickHandler down_handler, ClickHandler up_handler) { prv_check_is_in_click_config_provider(window_manager_get_top_window(), "subscribe"); if (button_id == BUTTON_ID_BACK) { // We only want system apps to be able to override the back button for long // clicks. Allowing third-party apps to override the back button would make // long-pressing the back button a normal interaction method, and users may // unintentionally hold the button too long and force-quit the app. if (app_install_id_from_app_db(sys_process_manager_get_current_process_id())) { return; } else { Window *window = window_manager_get_top_window(); window_set_overrides_back_button(window, true); } } ClickManager *mgr = prv_get_current_click_manager(); ClickConfig *cfg = &mgr->recognizers[button_id].config; cfg->long_click.delay_ms = (delay_ms == 0) ? 500 : delay_ms; cfg->long_click.handler = down_handler; cfg->long_click.release_handler = up_handler; } void window_raw_click_subscribe(ButtonId button_id, ClickHandler down_handler, ClickHandler up_handler, void *context) { prv_check_is_in_click_config_provider(window_manager_get_top_window(), "subscribe"); if (button_id == BUTTON_ID_BACK) { PBL_LOG(LOG_LEVEL_DEBUG, "Cannot register BUTTON_ID_BACK raw handler"); return; } ClickManager *mgr = prv_get_current_click_manager(); ClickConfig *cfg = &mgr->recognizers[button_id].config; cfg->raw.up_handler = up_handler; cfg->raw.down_handler = down_handler; cfg->raw.context = context; } ClickConfigProvider window_get_click_config_provider(const Window *window) { return window->click_config_provider; } void *window_get_click_config_context(Window *window) { return window->click_config_context; } void window_set_window_handlers(Window *window, const WindowHandlers *handlers) { if (handlers) { window->window_handlers = *handlers; } } void window_set_window_handlers_by_value(Window *window, WindowHandlers handlers) { window_set_window_handlers(window, &handlers); } void window_set_user_data(Window *window, void *data) { window->user_data = data; } void* window_get_user_data(const Window *window) { return window->user_data; } struct Layer* window_get_root_layer(const Window *window) { return &((Window *)window)->layer; } static void prv_window_load(Window *window) { if (window->is_loaded) { return; } window_call_handler(window, WindowHandlerOffsetLoad); window->is_loaded = true; } void window_unload(Window *window) { if (!window->is_loaded) { return; } window->is_loaded = false; window_call_handler(window, WindowHandlerOffsetUnload); // Don't touch window after calling it's unload handler. We allow windows to free themselves on unload. } // TODO PBL-1769: deal with window unload. In app deinit? When low memory? void window_set_on_screen(Window *window, bool new_on_screen, bool call_window_appear_handlers) { PBL_ASSERTN(window != NULL); // This tripped me up for about a day if (new_on_screen == window->on_screen) { return; } // Window went from offscreen to onscreen (or vice versa) // Provides internal signaling to ui elements of appear/disappear layer_property_changed_tree(&window->layer); window->on_screen = new_on_screen; if (window->on_screen) { window_schedule_render(window); // The click provider was set but not updated if (window->is_waiting_for_click_config && !window->is_unfocusable) { prv_call_click_provider(window); } } else { window->is_render_scheduled = false; window->is_waiting_for_click_config = false; window->is_click_configured = false; } if (call_window_appear_handlers) { if (window->on_screen) { prv_window_load(window); // In our load handler, we may unload ourselves; this is perfectly fine! However, // if we do that, we never appear on the screen! In that case, window->on_screen // may have changed between the time we checked and after we called prv_window_load, // so we need to check it again! if (window->on_screen) { // Window has no cache, so when it appears, schedule (re)render: window_call_handler(window, WindowHandlerOffsetAppear); } } else if (window->is_loaded) { // We have to have loaded (and consequently appeared) to actually disappear because // we can actually set ourselves off-screen before we've ever been on-screen (this // happens if we unload ourselves in our load handler), so we have to double check. window_call_handler(window, WindowHandlerOffsetDisappear); } } } void window_set_background_color(Window *window, GColor background_color) { const GColor window_bg_color = window->background_color; if (gcolor_equal(background_color, window_bg_color)) { return; } window->background_color = background_color; layer_mark_dirty(&window->layer); } void window_set_background_color_2bit(Window *window, GColor2 background_color) { window_set_background_color(window, get_native_color(background_color)); } void window_set_fullscreen(Window *window, bool enabled) { if (window->is_fullscreen == enabled) { return; } window->is_fullscreen = enabled; window->layer.frame = window_calc_frame(enabled); window->layer.bounds.size = window->layer.frame.size; layer_mark_dirty(&window->layer); } bool window_get_fullscreen(const Window *window) { return window->is_fullscreen; } void window_set_status_bar_icon(Window *window, const GBitmap *icon) { return; } bool window_is_on_screen(Window *window) { return (window->on_screen); } bool window_is_loaded(Window *window) { return (window->is_loaded); } void window_set_transparent(Window *window, bool transparent) { window->is_transparent = transparent; } bool window_is_transparent(Window *window) { return window->is_transparent; } void window_set_focusable(Window *window, bool focusable) { window->is_unfocusable = !focusable; } bool window_is_focusable(Window *window) { return !window->is_unfocusable; } const char* window_get_debug_name(Window *window) { #ifndef RELEASE return window->debug_name; #else return "?"; (void)window; #endif } // A simple wrapper so feedback can be given to developers if click config subscriptions are made from outside of the // click config configuration callback. void window_call_click_config_provider(Window *window, void *context) { window->in_click_config_provider = true; window->click_config_provider(context); window->in_click_config_provider = false; } static bool prv_find_status_bar_layer(Layer *layer, void *ctx) { if (layer_is_status_bar_layer(layer)) { *((StatusBarLayer **)ctx) = (StatusBarLayer *)layer; return false; // prevent further iterating } return true; } bool window_has_status_bar(Window *window) { if (!window) { return false; } if (!window->is_fullscreen) { return true; } StatusBarLayer *status_bar = NULL; layer_process_tree(&window->layer, &status_bar, prv_find_status_bar_layer); return status_bar && !layer_get_hidden(&status_bar->layer); } void window_attach_recognizer(Window *window, Recognizer *recognizer) { if (!window) { return; } layer_attach_recognizer(window_get_root_layer(window), recognizer); } void window_detach_recognizer(Window *window, Recognizer *recognizer) { if (!window) { return; } layer_detach_recognizer(window_get_root_layer(window), recognizer); } RecognizerList *window_get_recognizer_list(Window *window) { if (!window) { return NULL; } return layer_get_recognizer_list(window_get_root_layer(window)); } RecognizerManager *window_get_recognizer_manager(Window *window) { // TODO return the app's recognizer manager // https://pebbletechnology.atlassian.net/browse/PBL-30957 return NULL; }