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

253 lines
11 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 "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;
}