mirror of
https://github.com/google/pebble.git
synced 2025-06-14 21:53:12 +00:00
579 lines
19 KiB
C
579 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_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 <string.h>
|
|
|
|
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;
|
|
}
|