Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View file

@ -0,0 +1,61 @@
/*
* 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 "rocky_api.h"
#include <stddef.h>
#include "rocky_api_app_message.h"
#include "rocky_api_console.h"
#include "rocky_api_datetime.h"
#include "rocky_api_global.h"
#include "rocky_api_graphics.h"
#include "rocky_api_memory.h"
#include "rocky_api_preferences.h"
#include "rocky_api_tickservice.h"
#include "rocky_api_timers.h"
#include "rocky_api_watchinfo.h"
void rocky_api_watchface_init(void) {
static const RockyGlobalAPI *const apis[] = {
#if !APPLIB_EMSCRIPTEN
&APP_MESSAGE_APIS,
&CONSOLE_APIS,
#endif
&DATETIME_APIS,
&GRAPHIC_APIS,
#if !APPLIB_EMSCRIPTEN
&MEMORY_APIS,
&PREFERENCES_APIS,
#endif
&TICKSERVICE_APIS,
#if !APPLIB_EMSCRIPTEN
&TIMER_APIS,
&WATCHINFO_APIS,
#endif
NULL,
};
rocky_global_init(apis);
}
void rocky_api_deinit(void) {
rocky_global_deinit();
}

View file

@ -0,0 +1,44 @@
/*
* 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.
*/
#pragma once
#include "stdbool.h"
#include "jerry-api.h"
// generic callback per API, e.g. to (de-) initialize
typedef void (*RockyAPIHandler)(void);
// callback to let APIs know when a caller registers an event rocky.on(event_name, handler)
// return true, if you are interested in the given event so that the pair will be stored
typedef bool (*RockyEventedAPIAddHandler)(const char *event_name, jerry_value_t handler);
// callback to let APIs know when a caller unregisters an event rocky.off(event_name, handler)
typedef void (*RockyEventedAPIRemoveHandler)(const char *event_name, jerry_value_t handler);
typedef struct {
RockyAPIHandler init;
RockyAPIHandler deinit;
// responds to .on('someevent', f)
RockyEventedAPIAddHandler add_handler;
// responds to .off('someevent', f);
RockyEventedAPIRemoveHandler remove_handler;
} RockyGlobalAPI;
void rocky_api_watchface_init(void);
void rocky_api_deinit(void);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,123 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
#include <util/attributes.h>
#define POSTMESSAGE_PROTOCOL_MIN_VERSION ((uint8_t)1)
#define POSTMESSAGE_PROTOCOL_MAX_VERSION ((uint8_t)1)
//! Max size in bytes of the largest Chunk payload that can be sent out.
#define POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE ((uint16_t)1000)
//! Max size in bytes of the largest Chunk payload that can be received.
#define POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE ((uint16_t)1000)
//! Message Types, expected values are byte arrays. See structs below.
typedef enum {
PostMessageKeyInvalid = 0,
//! Has no payload.
PostMessageKeyResetRequest = 1,
//! Has PostMessageResetCompletePayload as payload.
PostMessageKeyResetComplete = 2,
//! Has PostMessageChunkPayload as payload.
PostMessageKeyChunk = 3,
//! Has PostMessageUnsupportedErrorPayload as payload.
PostMessageKeyUnsupportedError = 4,
PostMessageKey_Count
} PostMessageKey;
typedef struct PACKED {
//! Lowest supported version (inclusive)
uint8_t min_supported_version;
//! Highest supported version (inclusive)
uint8_t max_supported_version;
//! Maximum Chunk size (little-endian) that the sender of ResetComplete is capable of sending.
uint16_t max_tx_chunk_size;
//! Maximum Chunk size (little-endian) that the sender of ResetComplete is capable of receiving.
uint16_t max_rx_chunk_size;
} PostMessageResetCompletePayload;
_Static_assert(sizeof(PostMessageResetCompletePayload) >= 6,
"Should never be smaller than the V1 payload!");
typedef struct PACKED {
union PACKED {
//! Header for first Chunk in a sequence of chunks. Valid when is_first == true.
struct PACKED {
//! Total size of the object (sum of the lengths of all chunk_data in the Chunk sequence)
//! including a zero byte at the end of the JSON string.
uint32_t total_size_bytes:31;
//! Always set to true for the first Chunk.
bool is_first:1;
};
//! Header for continuation Chunks in a sequence of chunks. Valid when is_first == false.
struct PACKED {
//! The offset of the chunk_data into the fully assembled object.
uint32_t offset_bytes:31;
//! Always set to false for a continuation Chunk.
//! @note the compiler doesn't like it if the field is called the same, even though it's of
//! the same type and occupies the same bits...
bool continuation_is_first:1;
};
};
//! JSON string data (potentially a partial fragment).
//! The final Chunk's chunk_data MUST be zero-terminated!
char chunk_data[0];
} PostMessageChunkPayload;
typedef enum {
PostMessageErrorUnsupportedVersion,
PostMessageErrorMalformedResetComplete,
} PostMessageError;
typedef struct PACKED {
PostMessageError error_code:8;
} PostMessageUnsupportedErrorPayload;
//! See statechart diagram over here:
//! https://pebbletechnology.atlassian.net/wiki/display/PRODUCT/postMessage%28%29+protocol
typedef enum {
//! Transport (AppMessage / PP) Disconnected
PostMessageStateDisconnected = 0,
//! No negotiated state, wait for remote to request reset
PostMessageStateAwaitingResetRequest,
//! Waiting for a reset complete message, the remote side had initiated the ResetRequest.
PostMessageStateAwaitingResetCompleteRemoteInitiated,
//! Waiting for a reset complete message, the local side had initiated the ResetRequest.
PostMessageStateAwaitingResetCompleteLocalInitiated,
//! Transport connected, negotiation complete and ready to send and receive payload chunks.
PostMessageStateSessionOpen,
PostMessageState_Count
} PostMessageState;
extern const RockyGlobalAPI APP_MESSAGE_APIS;

View file

@ -0,0 +1,65 @@
/*
* 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 "rocky_api_console.h"
#include "applib/app_logging.h"
#include "rocky_api_util.h"
#include "system/passert.h"
#define ROCKY_CONSOLE "console"
#define ROCKY_CONSOLE_LOG "log"
#define ROCKY_CONSOLE_WARN "warn"
#define ROCKY_CONSOLE_ERROR "error"
static jerry_value_t prv_log(uint8_t level, jerry_length_t argc, const jerry_value_t argv[]) {
for (size_t i = 0; i < argc; i++) {
char buffer[100] = {0};
jerry_object_to_string_to_utf8_char_buffer(argv[i], (jerry_char_t *)buffer, sizeof(buffer));
app_log(level, "JS", 0, "%s", buffer);
}
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_console_log) {
return prv_log(APP_LOG_LEVEL_INFO, argc, argv);
}
JERRY_FUNCTION(prv_console_warn) {
return prv_log(APP_LOG_LEVEL_WARNING, argc, argv);
}
JERRY_FUNCTION(prv_console_error) {
return prv_log(APP_LOG_LEVEL_ERROR, argc, argv);
}
static void prv_init(void) {
bool was_created;
JS_VAR console =
rocky_get_or_create_object(jerry_create_undefined(), ROCKY_CONSOLE, rocky_creator_object,
NULL, &was_created);
// there must not be a global console object yet
PBL_ASSERTN(was_created);
rocky_add_function(console, ROCKY_CONSOLE_LOG, prv_console_log);
rocky_add_function(console, ROCKY_CONSOLE_WARN, prv_console_warn);
rocky_add_function(console, ROCKY_CONSOLE_ERROR, prv_console_error);
}
const RockyGlobalAPI CONSOLE_APIS = {
.init = prv_init,
};

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
extern const RockyGlobalAPI CONSOLE_APIS;

View file

@ -0,0 +1,393 @@
/*
* 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 "rocky_api_datetime.h"
#include "rocky_api_errors.h"
#include "rocky_api_util.h"
#include "services/common/clock.h"
#include "system/passert.h"
#include "util/size.h"
#include <string.h>
#define ROCKY_DATE_TOLOCALETIMESTRING "toLocaleTimeString"
#define ROCKY_DATE_TOLOCALEDATESTRING "toLocaleDateString"
#define ROCKY_DATE_TOLOCALESTRING "toLocaleString"
#define ROCKY_DATE_FORMAT_NUMERIC "numeric"
#define ROCKY_DATE_FORMAT_2DIGIT "2-digit"
#define ROCKY_DATE_FORMAT_SHORT "short"
#define ROCKY_DATE_FORMAT_LONG "long"
#define BUFFER_LEN_DATE 40
#define BUFFER_LEN_TIME 20
// 2 = strlen(", ")
#define BUFFER_LEN_DATETIME (BUFFER_LEN_DATE + BUFFER_LEN_TIME + 2)
static void prv_tm_from_js_date(jerry_value_t date, struct tm *tm) {
JS_VAR js_seconds = jerry_get_object_getter_result(date, "getSeconds");
JS_VAR js_minutes = jerry_get_object_getter_result(date, "getMinutes");
JS_VAR js_hours = jerry_get_object_getter_result(date, "getHours");
JS_VAR js_mdays = jerry_get_object_getter_result(date, "getDate");
JS_VAR js_month = jerry_get_object_getter_result(date, "getMonth");
JS_VAR js_year = jerry_get_object_getter_result(date, "getFullYear");
JS_VAR js_wday = jerry_get_object_getter_result(date, "getDay");
*tm = (struct tm) {
.tm_sec = (int)jerry_get_number_value(js_seconds),
.tm_min = (int)jerry_get_number_value(js_minutes),
.tm_hour = (int)jerry_get_number_value(js_hours),
.tm_mday = (int)jerry_get_number_value(js_mdays),
.tm_mon = (int)jerry_get_number_value(js_month),
.tm_year = (int)jerry_get_number_value(js_year) - 1900,
.tm_wday = (int)jerry_get_number_value(js_wday),
// seems as we can live without those for now
// .tm_yday = ,
// .tm_isdst = ,
// .tm_gmtoff = ,
// .tm_zone = ,
};
}
static bool prv_matches_system_locale(jerry_value_t locale) {
if (jerry_value_is_undefined(locale)) {
return true;
}
// in the future, we could run a case-insenstive compare against app_get_system_locale()
// but as we want apps to encourage to be i18n, there's no real point to
// receive strings such as 'en-us'. We will ask them to always pass undefined instead
return false;
}
typedef enum {
ToStringFormatUnsupported = 1 << 0,
ToStringFormatLocaleTime = 1 << 1,
ToStringFormatSecondNumeric = 1 << 2,
ToStringFormatSecond2Digit = 1 << 3,
ToStringFormatMinuteNumeric = 1 << 4,
ToStringFormatMinute2Digit = 1 << 5,
ToStringFormatHourNumeric = 1 << 6,
ToStringFormatHour2Digit = 1 << 7,
ToStringFormatLocaleDate = 1 << 8,
ToStringFormatDayNumeric = 1 << 9,
ToStringFormatDay2Digit = 1 << 10,
ToStringFormatDayShort = 1 << 11,
ToStringFormatDayLong = 1 << 12,
ToStringFormatMonthNumeric = 1 << 13,
ToStringFormatMonth2Digit = 1 << 14,
ToStringFormatMonthShort = 1 << 15,
ToStringFormatMonthLong = 1 << 16,
ToStringFormatYearNumeric = 1 << 17,
ToStringFormatYear2Digit = 1 << 18,
ToStringFormatEmpty = 1 << 19,
} ToStringFormat;
static const ToStringFormat ToStringFormatTimeMask = (
ToStringFormatLocaleTime |
ToStringFormatSecondNumeric |
ToStringFormatSecond2Digit |
ToStringFormatMinuteNumeric |
ToStringFormatMinute2Digit |
ToStringFormatHourNumeric |
ToStringFormatHour2Digit
);
static const ToStringFormat ToStringFormatDateMask = (
ToStringFormatLocaleDate |
ToStringFormatDayNumeric |
ToStringFormatDay2Digit |
ToStringFormatDayShort |
ToStringFormatDayLong |
ToStringFormatMonthNumeric |
ToStringFormatMonth2Digit |
ToStringFormatMonthShort |
ToStringFormatMonthLong |
ToStringFormatYearNumeric |
ToStringFormatYear2Digit
);
static ToStringFormat prv_parse_to_string_format(const jerry_value_t options,
ToStringFormat default_format,
ToStringFormat mask,
bool *is_24h_style) {
JS_VAR second = jerry_get_object_field(options, "second");
JS_VAR minute = jerry_get_object_field(options, "minute");
JS_VAR hour = jerry_get_object_field(options, "hour");
JS_VAR day = jerry_get_object_field(options, "day");
JS_VAR month = jerry_get_object_field(options, "month");
JS_VAR year = jerry_get_object_field(options, "year");
JS_VAR hour12 = jerry_get_object_field(options, "hour12");
if (!jerry_value_is_undefined(hour12)) {
*is_24h_style = !jerry_get_boolean_value(hour12);
}
struct {
const jerry_value_t field;
const char *value;
ToStringFormat format;
} option_values[] = {
{.field = second, .value = ROCKY_DATE_FORMAT_NUMERIC, .format = ToStringFormatSecondNumeric},
{.field = second, .value = ROCKY_DATE_FORMAT_2DIGIT, .format = ToStringFormatSecond2Digit},
{.field = minute, .value = ROCKY_DATE_FORMAT_NUMERIC, .format = ToStringFormatMinuteNumeric},
{.field = minute, .value = ROCKY_DATE_FORMAT_2DIGIT, .format = ToStringFormatMinute2Digit},
{.field = hour, .value = ROCKY_DATE_FORMAT_NUMERIC, .format = ToStringFormatHourNumeric},
{.field = hour, .value = ROCKY_DATE_FORMAT_2DIGIT, .format = ToStringFormatHour2Digit},
{.field = day, .value = ROCKY_DATE_FORMAT_NUMERIC, .format = ToStringFormatDayNumeric},
{.field = day, .value = ROCKY_DATE_FORMAT_2DIGIT, .format = ToStringFormatDay2Digit},
{.field = day, .value = ROCKY_DATE_FORMAT_SHORT, .format = ToStringFormatDayShort},
{.field = day, .value = ROCKY_DATE_FORMAT_LONG, .format = ToStringFormatDayLong},
{.field = month, .value = ROCKY_DATE_FORMAT_NUMERIC, .format = ToStringFormatMonthNumeric},
{.field = month, .value = ROCKY_DATE_FORMAT_2DIGIT, .format = ToStringFormatMonth2Digit},
{.field = month, .value = ROCKY_DATE_FORMAT_SHORT, .format = ToStringFormatMonthShort},
{.field = month, .value = ROCKY_DATE_FORMAT_LONG, .format = ToStringFormatMonthLong},
{.field = year, .value = ROCKY_DATE_FORMAT_NUMERIC, .format = ToStringFormatYearNumeric},
{.field = year, .value = ROCKY_DATE_FORMAT_2DIGIT, .format = ToStringFormatYear2Digit},
};
int found_options = 0;
ToStringFormat result = default_format;
for (size_t i = 0; i < ARRAY_LENGTH(option_values); i++) {
if ((option_values[i].format & mask) == 0) {
// skip option values that are irrelevant
continue;
}
if (rocky_str_equal(option_values[i].field, option_values[i].value)) {
result = option_values[i].format;
found_options++;
}
if (found_options > 1) {
// today, we don't support combinations of several options, it's either none or one
return ToStringFormatUnsupported;
}
}
return result;
}
static const char *prv_strftime_format(ToStringFormat format, bool is_24h_style) {
switch (format) {
case ToStringFormatUnsupported:
WTF;
case ToStringFormatLocaleTime:
return is_24h_style ? "%H:%M:%S" : "%I:%M:%S %p";
case ToStringFormatSecondNumeric:
case ToStringFormatSecond2Digit:
return "%S";
case ToStringFormatMinuteNumeric:
case ToStringFormatMinute2Digit:
return "%M";
case ToStringFormatHourNumeric:
case ToStringFormatHour2Digit:
return is_24h_style ? "%H" : "%I %p";
case ToStringFormatLocaleDate:
return "%x";
case ToStringFormatDayNumeric:
case ToStringFormatDay2Digit:
return "%d";
case ToStringFormatDayShort:
return "%a";
case ToStringFormatDayLong:
return "%A";
case ToStringFormatMonthNumeric:
case ToStringFormatMonth2Digit:
return "%m";
case ToStringFormatMonthShort:
return "%b";
case ToStringFormatMonthLong:
return "%B";
case ToStringFormatYearNumeric:
return "%Y";
case ToStringFormatYear2Digit:
return "%y";
case ToStringFormatEmpty:
return "";
}
return "";
}
static bool prv_strip_leading_zero(ToStringFormat format, bool is_24h_style) {
switch (format) {
case ToStringFormatLocaleTime:
// %I adds leading zeros, for single digit hours. We don't want that for 12h
return !is_24h_style;
case ToStringFormatSecondNumeric:
case ToStringFormatMinuteNumeric:
case ToStringFormatHourNumeric:
case ToStringFormatDayNumeric:
case ToStringFormatMonthNumeric:
return true;
case ToStringFormatUnsupported:
case ToStringFormatEmpty:
case ToStringFormatSecond2Digit:
case ToStringFormatMinute2Digit:
case ToStringFormatHour2Digit:
case ToStringFormatLocaleDate:
case ToStringFormatDay2Digit:
case ToStringFormatDayShort:
case ToStringFormatDayLong:
case ToStringFormatMonthShort:
case ToStringFormatMonthLong:
case ToStringFormatMonth2Digit:
return false;
case ToStringFormatYearNumeric:
case ToStringFormatYear2Digit:
// yes, we want to keep leading zeros in both cases for year as we control the format
// exclusively via the strftime format
return false;
}
return false;
}
static size_t prv_to_locale_buffer(jerry_value_t this_val,
jerry_length_t argc, const jerry_value_t *argv,
ToStringFormat default_format,
ToStringFormat mask,
char *buffer, size_t buffer_len,
jerry_value_t *error) {
JS_VAR locale = argc >= 1 ? jerry_acquire_value(argv[0]) : jerry_create_undefined();
JS_VAR options = argc >= 2 ? jerry_acquire_value(argv[1]) : jerry_create_object();
if (!prv_matches_system_locale(locale)) {
if (error) {
*error = rocky_error_argument_invalid("Unsupported locale");
}
return 0;
}
bool is_24h_style = clock_is_24h_style();
const ToStringFormat format = prv_parse_to_string_format(options, default_format, mask,
&is_24h_style);
if (format == ToStringFormatUnsupported) {
if (error) {
*error = rocky_error_argument_invalid("Unsupported options");
}
return 0;
}
struct tm tm;
prv_tm_from_js_date(this_val, &tm);
const char *strftime_format = prv_strftime_format(format, is_24h_style);
const size_t str_len = strftime(buffer, buffer_len, strftime_format, &tm);
const bool strip_leading_char = buffer[0] == '0' && prv_strip_leading_zero(format, is_24h_style);
if (strip_leading_char) {
memmove(buffer, buffer + 1, str_len);
}
return str_len;
}
static jerry_value_t prv_to_locale_time_or_date_string(jerry_value_t this_val,
jerry_length_t argc,
const jerry_value_t argv[],
ToStringFormat date_default_format,
ToStringFormat time_default_format) {
// both, .toLocaleTimeString() and .toLocaleDateString() fall back to "<date>, <time>"
// if clients specify options that are not part of time / date
// similarly, .toLocaleString() falls back to "<date>, <time>" if no known option was specified
// yes, in some code paths, this isn't the most-efficient but it's robust on the other hand
// format date
char date_buffer[BUFFER_LEN_DATE] = {0};
jerry_value_t error = jerry_create_undefined();
const size_t date_len = prv_to_locale_buffer(this_val, argc, argv,
date_default_format, ToStringFormatDateMask,
date_buffer, sizeof(date_buffer), &error);
if (jerry_value_has_error_flag(error)) {
return error;
}
// format time
char time_buffer[BUFFER_LEN_TIME] = {0};
const size_t time_len = prv_to_locale_buffer(this_val, argc, argv,
time_default_format, ToStringFormatTimeMask,
time_buffer, sizeof(time_buffer), &error);
if (jerry_value_has_error_flag(error)) {
return error;
}
// concatenate result
char result_buffer[BUFFER_LEN_DATETIME] = {0};
size_t remaining_buffer_size = sizeof(result_buffer);
strncpy(result_buffer, date_buffer, sizeof(result_buffer));
remaining_buffer_size -= date_len;
if (date_len > 0 && time_len > 0) {
strncat(result_buffer, ", ", remaining_buffer_size);
remaining_buffer_size -= 2; // 2 = strlen(", ")
}
strncat(result_buffer, time_buffer, remaining_buffer_size);
return jerry_create_string_utf8((jerry_char_t *)result_buffer);
}
JERRY_FUNCTION(prv_to_locale_time_string) {
return prv_to_locale_time_or_date_string(this_val, argc, argv,
ToStringFormatEmpty, ToStringFormatLocaleTime);
}
JERRY_FUNCTION(prv_to_locale_date_string) {
return prv_to_locale_time_or_date_string(this_val, argc, argv,
ToStringFormatLocaleDate, ToStringFormatEmpty);
}
JERRY_FUNCTION(prv_to_locale_string) {
jerry_value_t error = jerry_create_undefined();
char buffer[BUFFER_LEN_DATETIME] = {0};
// we allow users to pick from any option to here
const size_t total_len = prv_to_locale_buffer(this_val, argc, argv,
ToStringFormatEmpty,
ToStringFormatDateMask | ToStringFormatTimeMask,
buffer, sizeof(buffer), &error);
if (jerry_value_has_error_flag(error)) {
return error;
}
if (total_len != 0) {
// user picked options, so we formatted something into buffer
return jerry_create_string_utf8((jerry_char_t *) buffer);
}
// if total_len == 0, nothing was formatted and we default to "<date>, <time>"
return prv_to_locale_time_or_date_string(this_val, 0, NULL,
ToStringFormatLocaleDate, ToStringFormatLocaleTime);
}
//
static void prv_rocky_add_date_functions(jerry_value_t global) {
JS_VAR date_constructor = jerry_get_object_field(global, "Date");
JS_VAR date_prototype = jerry_get_object_field(date_constructor, "prototype");
JS_VAR locale_time_string = jerry_create_external_function(prv_to_locale_time_string);
jerry_set_object_field(date_prototype, ROCKY_DATE_TOLOCALETIMESTRING, locale_time_string);
JS_VAR locale_date_string = jerry_create_external_function(prv_to_locale_date_string);
jerry_set_object_field(date_prototype, ROCKY_DATE_TOLOCALEDATESTRING, locale_date_string);
JS_VAR locale_string = jerry_create_external_function(prv_to_locale_string);
jerry_set_object_field(date_prototype, ROCKY_DATE_TOLOCALESTRING, locale_string);
}
static void prv_init(void) {
JS_VAR global = jerry_get_global_object();
prv_rocky_add_date_functions(global);
}
const RockyGlobalAPI DATETIME_APIS = {
.init = prv_init,
};

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
extern const RockyGlobalAPI DATETIME_APIS;

View file

@ -0,0 +1,98 @@
/*
* 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 "rocky_api_errors.h"
#include "rocky_api_util.h"
#include "applib/app_logging.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
#include "system/passert.h"
#include <inttypes.h>
#include <stdint.h>
jerry_value_t rocky_error_arguments_missing(void) {
jerry_char_t *msg = (jerry_char_t *)"Not enough arguments";
return jerry_create_error(JERRY_ERROR_TYPE, msg);
}
jerry_value_t rocky_error_argument_invalid(const char *msg) {
return jerry_create_error(JERRY_ERROR_TYPE, (jerry_char_t *)msg);
}
jerry_value_t rocky_error_argument_invalid_at_index(uint32_t arg_idx, const char *error_msg) {
char buffer[100] = {0};
snprintf(buffer, sizeof(buffer), "Argument at index %"PRIu32" is invalid: %s",
arg_idx, error_msg);
return rocky_error_argument_invalid(buffer);
}
jerry_value_t rocky_error_unexpected_type(uint32_t arg_idx, const char *expected_type_name) {
char buffer[100] = {0};
snprintf(buffer, sizeof(buffer), "Argument at index %"PRIu32" is not a %s",
arg_idx, expected_type_name);
return rocky_error_argument_invalid(buffer);
}
static jerry_value_t prv_error_two_parts(jerry_error_t error_type,
const char *left, const char *right) {
char buffer[100] = {0};
snprintf(buffer, sizeof(buffer), "%s%s", left, right);
return jerry_create_error(error_type, (jerry_char_t *)buffer);
}
jerry_value_t rocky_error_oom(const char *hint) {
return prv_error_two_parts(JERRY_ERROR_RANGE, "Out of memory: ", hint);
}
static char * prv_get_string_from_field(jerry_value_t object, const jerry_char_t *property) {
JS_VAR prop_name = jerry_create_string((const jerry_char_t *) property);
JS_VAR prop_val = jerry_get_property(object, prop_name);
JS_VAR prop_str = jerry_value_to_string(prop_val);
char *result = rocky_string_alloc_and_copy(prop_str);
return result;
}
// from lit-magic-string.inc.h
const jerry_char_t ERROR_NAME_PROPERTY_NAME[] = "name";
const jerry_char_t ERROR_MSG_PROPERTY_NAME[] = "message";
void rocky_error_print(jerry_value_t error_val) {
char *name = NULL;
char *msg = NULL;
if (jerry_value_is_object(error_val)) {
name = prv_get_string_from_field(error_val, ERROR_NAME_PROPERTY_NAME);
msg = prv_get_string_from_field(error_val, ERROR_MSG_PROPERTY_NAME);
} else {
jerry_value_clear_error_flag(&error_val);
JS_VAR error_str = jerry_value_to_string(error_val);
msg = rocky_string_alloc_and_copy(error_str);
}
if (name) {
APP_LOG(LOG_LEVEL_ERROR, "Unhandled %s", name);
} else {
APP_LOG(LOG_LEVEL_ERROR, "Unhandled exception");
}
if (msg) {
APP_LOG(LOG_LEVEL_ERROR, " %s", msg);
}
task_free(name);
task_free(msg);
}

View file

@ -0,0 +1,33 @@
/*
* 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.
*/
#pragma once
#include "jerry-api.h"
// Raise TypeError: Not enough arguments
jerry_value_t rocky_error_arguments_missing(void);
jerry_value_t rocky_error_argument_invalid_at_index(uint32_t arg_idx, const char *error_msg);
jerry_value_t rocky_error_argument_invalid(const char *msg);
jerry_value_t rocky_error_oom(const char *hint);
jerry_value_t rocky_error_unexpected_type(uint32_t arg_idx, const char *expected_type_name);
// Print error type & msg
void rocky_error_print(jerry_value_t error_val);

View file

@ -0,0 +1,351 @@
/*
* 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 "rocky_api_global.h"
#include "applib/app_logging.h"
#include "jerry-api.h"
#include "kernel/pbl_malloc.h"
#include "system/passert.h"
#include "syscall/syscall.h"
#include "util/size.h"
#include "rocky_api.h"
#include "rocky_api_errors.h"
#include "rocky_api_util.h"
#define ROCKY_LISTENERS "_listeners"
#define ROCKY_ON "on"
#define ROCKY_ADD_EVENT_LISTENER "addEventListener"
#define ROCKY_OFF "off"
#define ROCKY_REMOVE_EVENT_LISTENER "removeEventListener"
#define ROCKY_EVENT_CONSTRUCTOR "Event"
#define ROCKY_EVENT_TYPE "type"
// TODO: PBL-35780 make this part of app_state_get_rocky_runtime_context()
SECTION(".rocky_bss") static const RockyGlobalAPI *const *s_global_apis;
#define API_REFS_FOREACH(var_name) \
for (RockyGlobalAPI const *const *var_name = s_global_apis; *var_name != NULL; var_name++)
static jerry_value_t prv_get_or_create_listener_array(const char *event_name) {
JS_VAR rocky = rocky_get_rocky_singleton();
JS_VAR all_listeners = rocky_get_or_create_object(rocky, ROCKY_LISTENERS,
rocky_creator_object,
NULL, NULL);
return rocky_get_or_create_object(all_listeners, event_name,
rocky_creator_empty_array,
NULL, NULL);
}
// callback while iterating over listeners
// @param event_listeners the array this listener is part of
// @param idx position within the array at which the listener exists
// @param listener the JS function that's registered as listener
// @param data pointer provided when calling prv_iterate_event_listeners()
// @return true, if you interested in further iterations. False to stop iterating.
typedef bool (*EventListenerIteratorCb)(jerry_value_t event_listeners, uint32_t idx,
jerry_value_t listener, void *data);
static void prv_iterate_event_listeners(const char *event_name,
EventListenerIteratorCb callback,
void *data) {
PBL_ASSERTN(callback);
JS_VAR rocky = rocky_get_rocky_singleton();
JS_VAR all_listeners = jerry_get_object_field(rocky, "_listeners");
JS_VAR event_listeners = jerry_get_object_field(all_listeners, event_name);
// printf("event_listeners.refcount: %d", jerry_port_)
const uint32_t len = jerry_get_array_length(event_listeners);
for (uint32_t idx = 0; idx < len; idx++) {
JS_VAR listener = jerry_get_property_by_index(event_listeners, idx);
if (jerry_value_is_function(listener)) {
bool wants_more = callback(event_listeners, idx, listener, data);
if (!wants_more) {
break;
}
}
}
}
typedef struct {
jerry_value_t listener;
bool found;
} ListenerQueryData;
static bool prv_find_listener(jerry_value_t event_listeners, uint32_t idx,
jerry_value_t listener, void *data) {
ListenerQueryData *query_data = data;
if (query_data->listener == listener) {
query_data->found = true;
return false;
}
return true;
}
static bool prv_listener_is_registered(const char *event_name, jerry_value_t listener) {
ListenerQueryData data = {
.listener = listener,
};
prv_iterate_event_listeners(event_name, prv_find_listener, &data);
return data.found;
}
T_STATIC void prv_add_event_listener_to_list(const char *event_name, jerry_value_t listener) {
if (prv_listener_is_registered(event_name, listener)) {
// we won't register the same listener twice
return;
}
JS_VAR listeners = prv_get_or_create_listener_array(event_name);
const uint32_t num_entries = jerry_get_array_length(listeners);
JS_UNUSED_VAL = jerry_set_property_by_index(listeners, num_entries, listener);
}
static bool prv_remove_listener(jerry_value_t event_listeners, uint32_t idx,
jerry_value_t listener, void *data) {
ListenerQueryData *query_data = data;
if (query_data->listener == listener) {
// calling `event_listeners.splice(idx, 1)` to remove item at idx
// wow, this is a lot of code for this
JS_VAR splice = jerry_get_object_field(event_listeners, "splice");
const jerry_value_t args[] = {jerry_create_number(idx), jerry_create_number(1)};
JS_VAR remove_result = jerry_call_function(splice, event_listeners,
args, ARRAY_LENGTH(args));
if (jerry_value_has_error_flag(remove_result)) {
rocky_log_exception("removing event listener", remove_result);
}
for (size_t i = 0; i < ARRAY_LENGTH(args); i++) {
jerry_release_value(args[i]);
}
// mark item as removed and stop iterating
query_data->found = true;
return false;
}
return true;
}
T_STATIC bool prv_remove_event_listener_from_list(const char *event_name, jerry_value_t listener) {
ListenerQueryData data = {
.listener = listener,
};
prv_iterate_event_listeners(event_name, prv_remove_listener, &data);
return data.found;
}
// implementation of .on(event_name, handler) and stores them in a new property
//
// rocky._listeners = {
// "event_1" : [ function_1, function_2, ... ],
// "event_2" ; [ ... ] ,
// ...
// }
//
// please note that we ignore events, no API is interested in by asking each of them
// via .add_handler(event_name, func)
static jerry_value_t prv_event_listener_extract_args(const jerry_length_t argc,
const jerry_value_t argv[],
char *event_name, size_t event_name_size,
jerry_value_t *func) {
PBL_ASSERTN(event_name && func);
if (argc < 2) {
return rocky_error_arguments_missing();
}
memset(event_name, 0, event_name_size);
const jerry_size_t len = jerry_string_to_utf8_char_buffer(argv[0],
(jerry_char_t *)event_name,
event_name_size);
if (len == 0 || len == event_name_size) {
return rocky_error_argument_invalid("Not a valid event");
}
if (!jerry_value_is_function(argv[1])) {
return rocky_error_argument_invalid("Not a valid handler");
}
*func = argv[1];
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_add_event_listener) {
char event_name[32];
jerry_value_t func; // out parameter &func will not be acquired
JS_VAR arg_result = prv_event_listener_extract_args(argc, argv,
event_name, sizeof(event_name),
&func);
if (jerry_value_has_error_flag(arg_result)) {
return jerry_acquire_value(arg_result);
}
bool is_relevant = false;
API_REFS_FOREACH(api_ref) {
if ((*api_ref)->add_handler) {
is_relevant = (*api_ref)->add_handler(event_name, func);
if (is_relevant) {
break;
}
}
}
if (!is_relevant) {
APP_LOG(LOG_LEVEL_WARNING, "Unknown event '%s'", event_name);
return jerry_create_undefined();
}
prv_add_event_listener_to_list((char *)event_name, func);
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_remove_event_listener) {
char event_name[32];
jerry_value_t func; // out parameter &func will not be acquired
JS_VAR arg_result = prv_event_listener_extract_args(argc, argv,
event_name, sizeof(event_name),
&func);
if (jerry_value_has_error_flag(arg_result)) {
return jerry_acquire_value(arg_result);
}
const bool removed = prv_remove_event_listener_from_list(event_name, func);
if (removed) {
API_REFS_FOREACH(api_ref) {
if ((*api_ref)->remove_handler) {
(*api_ref)->remove_handler(event_name, func);
}
}
} else {
APP_LOG(LOG_LEVEL_WARNING, "Unknown handler for event '%s'", event_name);
}
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_event_constructor) {
if (argc < 1) {
return rocky_error_arguments_missing();
}
if (!jerry_value_is_string(argv[0])) {
return rocky_error_unexpected_type(0, "String");
}
jerry_set_object_field(this_val, ROCKY_EVENT_TYPE, argv[0]);
return jerry_create_undefined();
}
static void prv_copy_property(const jerry_value_t rocky,
const char *name_from, const char *name_to) {
JS_VAR on = jerry_get_object_field(rocky, name_from);
jerry_set_object_field(rocky, name_to, on);
}
void rocky_global_init(const RockyGlobalAPI *const *global_apis) {
PBL_ASSERTN(global_apis);
s_global_apis = global_apis;
JS_VAR rocky = jerry_create_object();
// this keeps a permanent reference to the singleton
rocky_set_rocky_singleton(rocky);
rocky_add_function(rocky, ROCKY_ON, prv_add_event_listener);
prv_copy_property(rocky, ROCKY_ON, ROCKY_ADD_EVENT_LISTENER);
rocky_add_function(rocky, ROCKY_OFF, prv_remove_event_listener);
prv_copy_property(rocky, ROCKY_OFF, ROCKY_REMOVE_EVENT_LISTENER);
JS_UNUSED_VAL = rocky_add_constructor(ROCKY_EVENT_CONSTRUCTOR, prv_event_constructor);
API_REFS_FOREACH(api_ref) {
if ((*api_ref)->init) {
(*api_ref)->init();
}
}
}
void rocky_global_deinit(void) {
API_REFS_FOREACH(api_ref) {
if ((*api_ref)->deinit) {
(*api_ref)->deinit();
}
}
#if APPLIB_EMSCRIPTEN
rocky_delete_singleton();
#endif
}
static bool prv_has_any(jerry_value_t event_listeners, uint32_t idx,
jerry_value_t listener, void *data) {
*(bool *)data = true;
return false;
}
bool rocky_global_has_event_handlers(const char *event_name) {
bool result = false;
prv_iterate_event_listeners(event_name, prv_has_any, &result);
return result;
}
typedef struct {
const jerry_value_t this_arg;
const jerry_value_t *args_p;
jerry_size_t args_count;
} EventHandlersCallArgs;
static bool prv_call_event_handlers_cb(jerry_value_t event_listeners, uint32_t idx,
jerry_value_t listener, void *data) {
EventHandlersCallArgs * const args = data;
rocky_util_call_user_function_and_log_uncaught_error(listener, args->this_arg, args->args_p,
args->args_count);
return true;
}
void rocky_global_call_event_handlers(jerry_value_t event) {
EventHandlersCallArgs args = {
.this_arg = jerry_create_undefined(),
.args_p = &event,
.args_count = 1,
};
JS_VAR event_str = jerry_get_object_field(event, "type");
char *event_name = rocky_string_alloc_and_copy(event_str);
prv_iterate_event_listeners(event_name, prv_call_event_handlers_cb, &args);
task_free(event_name);
jerry_release_value(args.this_arg);
}
static void prv_call_event_handlers_async_cb(void *ctx) {
jerry_value_t event = (jerry_value_t)(uintptr_t) ctx;
rocky_global_call_event_handlers(event);
jerry_release_value(event); // was acquired in rocky_global_call_event_handlers_async() call
}
void rocky_global_call_event_handlers_async(jerry_value_t event) {
sys_current_process_schedule_callback(prv_call_event_handlers_async_cb,
(void *)(uintptr_t)jerry_acquire_value(event));
}
jerry_value_t rocky_global_create_event(const char *type_str) {
JS_VAR jerry_type_str = jerry_create_string_utf8((const jerry_char_t *)type_str);
JS_VAR event = rocky_create_with_constructor(ROCKY_EVENT_CONSTRUCTOR, &jerry_type_str, 1);
return jerry_acquire_value(event);
}

View file

@ -0,0 +1,36 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
#include "jerry-api.h"
// array of evented APIs, last entry must be NULL
void rocky_global_init(const RockyGlobalAPI *const *evented_apis);
void rocky_global_deinit(void);
bool rocky_global_has_event_handlers(const char *event_name);
void rocky_global_call_event_handlers(jerry_value_t event);
//! Schedules the event to be processed on a later event loop iteration.
void rocky_global_call_event_handlers_async(jerry_value_t event);
// Create a BaseEvent, filling in the type field with the given type string.
// The returned event must be released by the caller.
jerry_value_t rocky_global_create_event(const char *type_str);

View file

@ -0,0 +1,358 @@
/*
* 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 "jerry-api.h"
#include "rocky_api_graphics.h"
#include "rocky_api_graphics_path2d.h"
#include "rocky_api_graphics_text.h"
#include "rocky_api_util.h"
#include "rocky_api_util_args.h"
#include "applib/app.h"
#include "applib/app_logging.h"
#include "applib/ui/ui.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/size.h"
#include "util/trig.h"
#include "process_state/app_state/app_state.h"
#include "rocky_api_errors.h"
#include "rocky_api_global.h"
#include "rocky_api_graphics_color.h"
#define ROCKY_EVENT_DRAW "draw"
#define ROCKY_EVENT_DRAW_CONTEXT "context"
#define ROCKY_REQUESTDRAW "requestDraw"
#define ROCKY_CONTEXT2D_CONSTRUCTOR "CanvasRenderingContext2D"
#define ROCKY_CONTEXT2D_CANVAS "canvas"
#define ROCKY_CONTEXT2D_CLEARRECT "clearRect"
#define ROCKY_CONTEXT2D_FILLRECT "fillRect"
#define ROCKY_CONTEXT2D_FILLRADIAL "rockyFillRadial"
#define ROCKY_CONTEXT2D_STROKERECT "strokeRect"
#define ROCKY_CONTEXT2D_LINEWIDTH "lineWidth"
#define ROCKY_CONTEXT2D_STROKESTYLE "strokeStyle"
#define ROCKY_CONTEXT2D_FILLSTYLE "fillStyle"
#define ROCKY_CONTEXT2D_SAVE "save"
#define ROCKY_CONTEXT2D_RESTORE "restore"
#define ROCKY_CANVAS_CONSTRUCTOR "CanvasElement"
#define ROCKY_CANVAS_CLIENTWIDTH "clientWidth"
#define ROCKY_CANVAS_CLIENTHEIGHT "clientHeight"
#define ROCKY_CANVAS_UNOBSTRUCTEDLEFT "unobstructedLeft"
#define ROCKY_CANVAS_UNOBSTRUCTEDTOP "unobstructedTop"
#define ROCKY_CANVAS_UNOBSTRUCTEDWIDTH "unobstructedWidth"
#define ROCKY_CANVAS_UNOBSTRUCTEDHEIGHT "unobstructedHeight"
typedef struct {
ListNode node;
GDrawState draw_state;
} Context2DStoredState;
// TODO: PBL-35780 make this part of app_state_get_rocky_runtime_context()
SECTION(".rocky_bss") static Context2DStoredState *s_canvas_context_2d_stored_states;
T_STATIC jerry_value_t prv_create_canvas_context_2d_for_layer(Layer *layer) {
JS_VAR context_2d = rocky_create_with_constructor(ROCKY_CONTEXT2D_CONSTRUCTOR,
/* no args: */ NULL, 0);
JS_VAR canvas = jerry_get_object_field(context_2d, ROCKY_CONTEXT2D_CANVAS);
{
JS_VAR client_width = jerry_create_number(layer->bounds.size.w);
JS_VAR client_height = jerry_create_number(layer->bounds.size.h);
jerry_set_object_field(canvas, ROCKY_CANVAS_CLIENTWIDTH, client_width);
jerry_set_object_field(canvas, ROCKY_CANVAS_CLIENTHEIGHT, client_height);
}
{
GRect uo_rect;
layer_get_unobstructed_bounds(layer, &uo_rect);
JS_VAR unobstructed_left = jerry_create_number(uo_rect.origin.x);
JS_VAR unobstructed_right = jerry_create_number(uo_rect.origin.y);
JS_VAR unobstructed_width = jerry_create_number(uo_rect.size.w);
JS_VAR unobstructed_height = jerry_create_number(uo_rect.size.h);
jerry_set_object_field(canvas, ROCKY_CANVAS_UNOBSTRUCTEDLEFT, unobstructed_left);
jerry_set_object_field(canvas, ROCKY_CANVAS_UNOBSTRUCTEDTOP, unobstructed_right);
jerry_set_object_field(canvas, ROCKY_CANVAS_UNOBSTRUCTEDWIDTH, unobstructed_width);
jerry_set_object_field(canvas, ROCKY_CANVAS_UNOBSTRUCTEDHEIGHT, unobstructed_height);
}
return jerry_acquire_value(context_2d);
}
static void prv_rocky_update_proc(Layer *layer, GContext *ctx) {
if (!rocky_global_has_event_handlers(ROCKY_EVENT_DRAW)) {
return;
}
rocky_api_graphics_text_reset_state();
rocky_api_graphics_path2d_reset_state();
JS_VAR event = rocky_global_create_event(ROCKY_EVENT_DRAW);
JS_VAR context_2d = prv_create_canvas_context_2d_for_layer(layer);
jerry_set_object_field(event, ROCKY_EVENT_DRAW_CONTEXT, context_2d);
rocky_global_call_event_handlers(event);
rocky_api_graphics_path2d_reset_state();
}
JERRY_FUNCTION(prv_request_draw) {
Window *const top_window = app_window_stack_get_top_window();
if (!top_window) {
return jerry_create_undefined();
}
layer_mark_dirty(&top_window->layer);
return jerry_create_undefined();
}
GContext *rocky_api_graphics_get_gcontext(void) {
return app_state_get_graphics_context();
}
static jerry_value_t prv_rect_precise_call(const jerry_length_t argc, const jerry_value_t argv[],
void (*func)(GContext *, const GRectPrecise *)) {
GRectPrecise rect;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(ROCKY_ARG(rect));
GContext *const ctx = rocky_api_graphics_get_gcontext();
func(ctx, &rect);
return jerry_create_undefined();
}
static GRect prv_grect_from_precise(const GRectPrecise *rect) {
const int16_t x = Fixed_S16_3_rounded_int(rect->origin.x);
const int16_t y = Fixed_S16_3_rounded_int(rect->origin.y);
const int16_t w = Fixed_S16_3_rounded_int(grect_precise_get_max_x(rect)) - x;
const int16_t h = Fixed_S16_3_rounded_int(grect_precise_get_max_y(rect)) - y;
return GRect(x, y, w, h);
}
static jerry_value_t prv_rect_call(const jerry_length_t argc, const jerry_value_t argv[],
void (*func)(GContext *, const GRect *)) {
GRectPrecise prect;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(
ROCKY_ARG(prect),
);
GContext *const ctx = rocky_api_graphics_get_gcontext();
const GRect rect = prv_grect_from_precise(&prect);
func(ctx, &rect);
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_fill_rect) {
return prv_rect_call(argc, argv, graphics_fill_rect);
}
static void prv_draw_rect_impl(GContext *ctx, const GRectPrecise *rect) {
GRectPrecise adjusted_rect = *rect;
adjusted_rect.origin.x.raw_value -= FIXED_S16_3_HALF.raw_value;
adjusted_rect.origin.y.raw_value -= FIXED_S16_3_HALF.raw_value;
graphics_draw_rect_precise(ctx, &adjusted_rect);
}
JERRY_FUNCTION(prv_stroke_rect) {
return prv_rect_precise_call(argc, argv, prv_draw_rect_impl);
}
JERRY_FUNCTION(prv_clear_rect) {
GContext *const ctx = rocky_api_graphics_get_gcontext();
const GColor prev_color = ctx->draw_state.fill_color;
ctx->draw_state.fill_color = GColorBlack;
JS_VAR result = prv_rect_call(argc, argv, graphics_fill_rect);
ctx->draw_state.fill_color = prev_color;
return jerry_acquire_value(result);
}
JERRY_FUNCTION(prv_set_stroke_width) {
uint8_t width;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(
ROCKY_ARG(width),
);
graphics_context_set_stroke_width(rocky_api_graphics_get_gcontext(), width);
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_get_stroke_width) {
return jerry_create_number((double)rocky_api_graphics_get_gcontext()->draw_state.stroke_width);
}
static jerry_value_t prv_graphics_set_color(const jerry_length_t argc,
const jerry_value_t argv[],
void (*func)(GContext *, GColor)) {
GColor color;
const RockyArgBinding binding = ROCKY_ARG(color);
JS_VAR error_value = rocky_args_assign(argc, argv, &binding, 1);
// Canvas APIs do a no-op if the color string is invalid
if (!jerry_value_has_error_flag(error_value)) {
func(rocky_api_graphics_get_gcontext(), color);
};
return jerry_create_undefined();
}
#define COLOR_BUFFER_LENGTH (12)
T_STATIC void prv_graphics_color_to_char_buffer(GColor8 color, char *buf_out) {
if (color.a <= 1) {
strncpy(buf_out, "transparent", COLOR_BUFFER_LENGTH);
} else {
snprintf(buf_out, COLOR_BUFFER_LENGTH, "#%02X%02X%02X",
color.r * 85, color.g * 85, color.b * 85);
}
}
static jerry_value_t prv_graphics_get_color_string(GColor8 color) {
char buf[COLOR_BUFFER_LENGTH];
prv_graphics_color_to_char_buffer(color, buf);
return jerry_create_string((const jerry_char_t *)buf);
}
JERRY_FUNCTION(prv_set_stroke_style) {
return prv_graphics_set_color(argc, argv, graphics_context_set_stroke_color);
}
JERRY_FUNCTION(prv_get_stroke_style) {
return prv_graphics_get_color_string(rocky_api_graphics_get_gcontext()->draw_state.stroke_color);
}
JERRY_FUNCTION(prv_set_fill_style) {
return prv_graphics_set_color(argc, argv, graphics_context_set_fill_color);
}
JERRY_FUNCTION(prv_get_fill_style) {
return prv_graphics_get_color_string(rocky_api_graphics_get_gcontext()->draw_state.fill_color);
}
JERRY_FUNCTION(prv_fill_radial) {
// pblFillRadial(cx, cy, radius1, radius2, angle1, angle2)
// TODO: PBL-40555 consolidate angle handling here and in rocky_api_path2d.c
GPointPrecise center;
Fixed_S16_3 radius1, radius2;
double angle_1, angle_2;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(
ROCKY_ARG(center.x),
ROCKY_ARG(center.y),
ROCKY_ARG(radius1),
ROCKY_ARG(radius2),
ROCKY_ARG_ANGLE(angle_1),
ROCKY_ARG_ANGLE(angle_2),
);
// adjust for coordinate system
center.x.raw_value -= FIXED_S16_3_HALF.raw_value;
center.y.raw_value -= FIXED_S16_3_HALF.raw_value;
radius1.raw_value = MAX(0, radius1.raw_value);
radius2.raw_value = MAX(0, radius2.raw_value);
const Fixed_S16_3 inner_radius = Fixed_S16_3(MIN(radius1.raw_value, radius2.raw_value));
const Fixed_S16_3 outer_radius = Fixed_S16_3(MAX(radius1.raw_value, radius2.raw_value));
GContext *const ctx = rocky_api_graphics_get_gcontext();
graphics_fill_radial_precise_internal(ctx, center, inner_radius, outer_radius,
(int32_t)angle_1, (int32_t)angle_2);
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_save) {
Context2DStoredState *const new_head = task_zalloc(sizeof(*new_head));
new_head->draw_state = rocky_api_graphics_get_gcontext()->draw_state;
list_insert_before(&s_canvas_context_2d_stored_states->node, &new_head->node);
s_canvas_context_2d_stored_states = new_head;
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_restore) {
Context2DStoredState *const head = s_canvas_context_2d_stored_states;
if (head) {
rocky_api_graphics_get_gcontext()->draw_state = head->draw_state;
list_remove(&head->node, (ListNode **)&s_canvas_context_2d_stored_states, NULL);
task_free(head);
}
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_canvas_rendering_context_2d_constructor) {
JS_VAR canvas = rocky_create_with_constructor(ROCKY_CANVAS_CONSTRUCTOR,
/* no args: */ NULL, 0);
jerry_set_object_field(this_val, ROCKY_CONTEXT2D_CANVAS, canvas);
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_canvas_constructor) {
return jerry_create_undefined();
}
static void prv_configure_top_window_and_create_constructors(void) {
Window *const window = app_window_stack_get_top_window();
// rocky graphics require a window to already be on the current window stack
PBL_ASSERTN(window);
window->layer.update_proc = prv_rocky_update_proc;
// Create the CanvasRenderingContext2D constructor:
JS_VAR ctx_prototype =
rocky_add_constructor(ROCKY_CONTEXT2D_CONSTRUCTOR,
prv_canvas_rendering_context_2d_constructor);
jerry_set_object_field(ctx_prototype, ROCKY_CONTEXT2D_CANVAS, jerry_create_undefined());
rocky_add_function(ctx_prototype, ROCKY_CONTEXT2D_CLEARRECT, prv_clear_rect);
rocky_add_function(ctx_prototype, ROCKY_CONTEXT2D_FILLRECT, prv_fill_rect);
rocky_add_function(ctx_prototype, ROCKY_CONTEXT2D_FILLRADIAL, prv_fill_radial);
rocky_add_function(ctx_prototype, ROCKY_CONTEXT2D_STROKERECT, prv_stroke_rect);
rocky_add_function(ctx_prototype, ROCKY_CONTEXT2D_SAVE, prv_save);
rocky_add_function(ctx_prototype, ROCKY_CONTEXT2D_RESTORE, prv_restore);
rocky_define_property(ctx_prototype, ROCKY_CONTEXT2D_LINEWIDTH,
prv_get_stroke_width, prv_set_stroke_width);
rocky_define_property(ctx_prototype, ROCKY_CONTEXT2D_STROKESTYLE,
prv_get_stroke_style, prv_set_stroke_style);
rocky_define_property(ctx_prototype, ROCKY_CONTEXT2D_FILLSTYLE,
prv_get_fill_style, prv_set_fill_style);
rocky_api_graphics_path2d_add_canvas_methods(ctx_prototype);
rocky_api_graphics_text_add_canvas_methods(ctx_prototype);
// Create the CanvasElement constructor:
JS_UNUSED_VAL = rocky_add_constructor(ROCKY_CANVAS_CONSTRUCTOR, prv_canvas_constructor);
}
static void prv_init_apis(void) {
prv_configure_top_window_and_create_constructors();
JS_VAR rocky = rocky_get_rocky_singleton();
rocky_add_function(rocky, ROCKY_REQUESTDRAW, prv_request_draw);
rocky_api_graphics_text_init();
rocky_api_graphics_path2d_reset_state(); // does not have an init, so we call reset_state()
}
static void prv_deinit_apis(void) {
while (s_canvas_context_2d_stored_states) {
Context2DStoredState *state = s_canvas_context_2d_stored_states;
s_canvas_context_2d_stored_states =
(Context2DStoredState *)s_canvas_context_2d_stored_states->node.next;
task_free(state);
}
rocky_api_graphics_text_deinit();
rocky_api_graphics_path2d_reset_state();
}
static bool prv_add_handler(const char *event_name, jerry_value_t handler) {
return strcmp(ROCKY_EVENT_DRAW, event_name) == 0;
}
const RockyGlobalAPI GRAPHIC_APIS = {
.init = prv_init_apis,
.deinit = prv_deinit_apis,
.add_handler = prv_add_handler,
};

View file

@ -0,0 +1,26 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
#include "jerry-api.h"
#include "applib/graphics/gtypes.h"
extern const RockyGlobalAPI GRAPHIC_APIS;
GContext *rocky_api_graphics_get_gcontext(void);

View file

@ -0,0 +1,362 @@
/*
* 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 "rocky_api_graphics_color.h"
#include "rocky_api_util.h"
#include "string.h"
#include "util/size.h"
#define GColorARGB8FromRGBA(red, green, blue, alpha) \
(uint8_t)( \
((((alpha) >> 6) & 0b11) << 6) | \
((((red) >> 6) & 0b11) << 4) | \
((((green) >> 6) & 0b11) << 2) | \
((((blue) >> 6) & 0b11) << 0) \
)
#define GColorARGB8FromRGB(red, green, blue) GColorARGB8FromRGBA(red, green, blue, 255)
#define GColorARGB8FromHEX(v) \
GColorARGB8FromRGB(((v) >> 16) & 0xff, ((v) >> 8) & 0xff, ((v) & 0xff))
// if performance ever becomes an issue with this, we can sort the names and to a binary search
T_STATIC const RockyAPIGraphicsColorDefinition s_color_definitions[] = {
// taken from https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
{"black", GColorARGB8FromHEX(0x000000)},
{"silver", GColorARGB8FromHEX(0xc0c0c0)},
{"gray", GColorARGB8FromHEX(0x808080)},
{"white", GColorARGB8FromHEX(0xffffff)},
{"maroon", GColorARGB8FromHEX(0x800000)},
{"red", GColorARGB8FromHEX(0xff0000)},
// {"purple", GColorARGBFromHEX(0x800080)}, inconsistent with Pebble color
{"fuchsia", GColorARGB8FromHEX(0xff00ff)},
// {"green", GColorARGBFromHEX(0x008000)}, inconsistent with Pebble color
{"lime", GColorARGB8FromHEX(0x00ff00)},
{"olive", GColorARGB8FromHEX(0x808000)},
{"yellow", GColorARGB8FromHEX(0xffff00)},
{"navy", GColorARGB8FromHEX(0x000080)},
{"blue", GColorARGB8FromHEX(0x0000ff)},
{"teal", GColorARGB8FromHEX(0x008080)},
{"aqua", GColorARGB8FromHEX(0x00ffff)},
{"antiquewhite", GColorARGB8FromHEX(0xfaebd7)},
{"aquamarine", GColorARGB8FromHEX(0x7fffd4)},
{"azure", GColorARGB8FromHEX(0xf0ffff)},
{"beige", GColorARGB8FromHEX(0xf5f5dc)},
{"bisque", GColorARGB8FromHEX(0xffe4c4)},
{"blanchedalmond", GColorARGB8FromHEX(0xffebcd)},
{"blueviolet", GColorARGB8FromHEX(0x8a2be2)},
{"brown", GColorARGB8FromHEX(0xa52a2a)},
{"burlywood", GColorARGB8FromHEX(0xdeb887)},
// {"cadetblue", GColorARGBFromHEX(0x5f9ea0)}, inconsistent with Pebble color
{"chartreuse", GColorARGB8FromHEX(0x7fff00)},
{"chocolate", GColorARGB8FromHEX(0xd2691e)},
{"coral", GColorARGB8FromHEX(0xff7f50)},
{"cornflowerblue", GColorARGB8FromHEX(0x6495ed)},
{"cornsilk", GColorARGB8FromHEX(0xfff8dc)},
{"crimson", GColorARGB8FromHEX(0xdc143c)},
{"darkblue", GColorARGB8FromHEX(0x00008b)},
{"darkcyan", GColorARGB8FromHEX(0x008b8b)},
{"darkgoldenrod", GColorARGB8FromHEX(0xb8860b)},
// {"darkgray", GColorARGBFromHEX(0xa9a9a9)}, inconsistent with Pebble color
// {"darkgreen", GColorARGBFromHEX(0x006400)}, inconsistent with Pebble color
// {"darkgrey", GColorARGBFromHEX(0xa9a9a9)}, inconsistent with Pebble color
{"darkkhaki", GColorARGB8FromHEX(0xbdb76b)},
{"darkmagenta", GColorARGB8FromHEX(0x8b008b)},
{"darkolivegreen", GColorARGB8FromHEX(0x556b2f)},
{"darkorange", GColorARGB8FromHEX(0xff8c00)},
{"darkorchid", GColorARGB8FromHEX(0x9932cc)},
{"darkred", GColorARGB8FromHEX(0x8b0000)},
{"darksalmon", GColorARGB8FromHEX(0xe9967a)},
{"darkseagreen", GColorARGB8FromHEX(0x8fbc8f)},
{"darkslateblue", GColorARGB8FromHEX(0x483d8b)},
{"darkslategray", GColorARGB8FromHEX(0x2f4f4f)},
{"darkslategrey", GColorARGB8FromHEX(0x2f4f4f)},
{"darkturquoise", GColorARGB8FromHEX(0x00ced1)},
{"darkviolet", GColorARGB8FromHEX(0x9400d3)},
{"deeppink", GColorARGB8FromHEX(0xff1493)},
{"deepskyblue", GColorARGB8FromHEX(0x00bfff)},
{"dimgray", GColorARGB8FromHEX(0x696969)},
{"dimgrey", GColorARGB8FromHEX(0x696969)},
{"dodgerblue", GColorARGB8FromHEX(0x1e90ff)},
{"firebrick", GColorARGB8FromHEX(0xb22222)},
{"floralwhite", GColorARGB8FromHEX(0xfffaf0)},
{"forestgreen", GColorARGB8FromHEX(0x228b22)},
{"gainsboro", GColorARGB8FromHEX(0xdcdcdc)},
{"ghostwhite", GColorARGB8FromHEX(0xf8f8ff)},
{"gold", GColorARGB8FromHEX(0xffd700)},
{"goldenrod", GColorARGB8FromHEX(0xdaa520)},
{"greenyellow", GColorARGB8FromHEX(0xadff2f)},
{"grey", GColorARGB8FromHEX(0x808080)},
{"honeydew", GColorARGB8FromHEX(0xf0fff0)},
{"hotpink", GColorARGB8FromHEX(0xff69b4)},
{"indianred", GColorARGB8FromHEX(0xcd5c5c)},
// {"indigo", GColorARGBFromHEX(0x4b0082)}, inconsistent with Pebble color
{"ivory", GColorARGB8FromHEX(0xfffff0)},
{"khaki", GColorARGB8FromHEX(0xf0e68c)},
{"lavender", GColorARGB8FromHEX(0xe6e6fa)},
{"lavenderblush", GColorARGB8FromHEX(0xfff0f5)},
{"lawngreen", GColorARGB8FromHEX(0x7cfc00)},
{"lemonchiffon", GColorARGB8FromHEX(0xfffacd)},
{"lightblue", GColorARGB8FromHEX(0xadd8e6)},
{"lightcoral", GColorARGB8FromHEX(0xf08080)},
{"lightcyan", GColorARGB8FromHEX(0xe0ffff)},
{"lightgoldenrodyellow", GColorARGB8FromHEX(0xfafad2)},
// {"lightgray", GColorARGBFromHEX(0xd3d3d3)}, inconsistent with Pebble color
{"lightgreen", GColorARGB8FromHEX(0x90ee90)},
// {"lightgrey", GColorARGBFromHEX(0xd3d3d3)}, inconsistent with Pebble color
{"lightpink", GColorARGB8FromHEX(0xffb6c1)},
{"lightsalmon", GColorARGB8FromHEX(0xffa07a)},
{"lightseagreen", GColorARGB8FromHEX(0x20b2aa)},
{"lightskyblue", GColorARGB8FromHEX(0x87cefa)},
{"lightslategray", GColorARGB8FromHEX(0x778899)},
{"lightslategrey", GColorARGB8FromHEX(0x778899)},
{"lightsteelblue", GColorARGB8FromHEX(0xb0c4de)},
{"lightyellow", GColorARGB8FromHEX(0xffffe0)},
{"limegreen", GColorARGB8FromHEX(0x32cd32)},
{"linen", GColorARGB8FromHEX(0xfaf0e6)},
// {"mediumaquamarine", GColorARGBFromHEX(0x66cdaa)}, inconsistent with Pebble color
{"mediumblue", GColorARGB8FromHEX(0x0000cd)},
{"mediumorchid", GColorARGB8FromHEX(0xba55d3)},
{"mediumpurple", GColorARGB8FromHEX(0x9370db)},
{"mediumseagreen", GColorARGB8FromHEX(0x3cb371)},
{"mediumslateblue", GColorARGB8FromHEX(0x7b68ee)},
// {"mediumspringgreen", GColorARGBFromHEX(0x00fa9a)}, inconsistent with Pebble color
{"mediumturquoise", GColorARGB8FromHEX(0x48d1cc)},
{"mediumvioletred", GColorARGB8FromHEX(0xc71585)},
{"midnightblue", GColorARGB8FromHEX(0x191970)},
{"mintcream", GColorARGB8FromHEX(0xf5fffa)},
{"mistyrose", GColorARGB8FromHEX(0xffe4e1)},
{"moccasin", GColorARGB8FromHEX(0xffe4b5)},
{"navajowhite", GColorARGB8FromHEX(0xffdead)},
{"oldlace", GColorARGB8FromHEX(0xfdf5e6)},
{"olivedrab", GColorARGB8FromHEX(0x6b8e23)},
{"orangered", GColorARGB8FromHEX(0xff4500)},
{"orchid", GColorARGB8FromHEX(0xda70d6)},
{"palegoldenrod", GColorARGB8FromHEX(0xeee8aa)},
{"palegreen", GColorARGB8FromHEX(0x98fb98)},
{"paleturquoise", GColorARGB8FromHEX(0xafeeee)},
{"palevioletred", GColorARGB8FromHEX(0xdb7093)},
{"papayawhip", GColorARGB8FromHEX(0xffefd5)},
{"peachpuff", GColorARGB8FromHEX(0xffdab9)},
{"peru", GColorARGB8FromHEX(0xcd853f)},
{"pink", GColorARGB8FromHEX(0xffc0cb)},
{"plum", GColorARGB8FromHEX(0xdda0dd)},
{"powderblue", GColorARGB8FromHEX(0xb0e0e6)},
{"rosybrown", GColorARGB8FromHEX(0xbc8f8f)},
{"royalblue", GColorARGB8FromHEX(0x4169e1)},
{"saddlebrown", GColorARGB8FromHEX(0x8b4513)},
{"salmon", GColorARGB8FromHEX(0xfa8072)},
{"sandybrown", GColorARGB8FromHEX(0xf4a460)},
{"seagreen", GColorARGB8FromHEX(0x2e8b57)},
{"seashell", GColorARGB8FromHEX(0xfff5ee)},
{"sienna", GColorARGB8FromHEX(0xa0522d)},
{"skyblue", GColorARGB8FromHEX(0x87ceeb)},
{"slateblue", GColorARGB8FromHEX(0x6a5acd)},
{"slategray", GColorARGB8FromHEX(0x708090)},
{"slategrey", GColorARGB8FromHEX(0x708090)},
{"snow", GColorARGB8FromHEX(0xfffafa)},
{"springgreen", GColorARGB8FromHEX(0x00ff7f)},
{"steelblue", GColorARGB8FromHEX(0x4682b4)},
{"tan", GColorARGB8FromHEX(0xd2b48c)},
{"thistle", GColorARGB8FromHEX(0xd8bfd8)},
{"tomato", GColorARGB8FromHEX(0xff6347)},
{"turquoise", GColorARGB8FromHEX(0x40e0d0)},
{"violet", GColorARGB8FromHEX(0xee82ee)},
{"wheat", GColorARGB8FromHEX(0xf5deb3)},
{"whitesmoke", GColorARGB8FromHEX(0xf5f5f5)},
{"yellowgreen", GColorARGB8FromHEX(0x9acd32)},
// CSS compatibility
{"darkgrey", GColorDarkGrayARGB8},
{"lightgrey", GColorLightGrayARGB8},
// special cases
{"transparent", GColorClearARGB8},
{"clear", GColorClearARGB8},
// Pebble colors taken from gcolor_defitions.h
{"black", GColorBlackARGB8},
{"oxfordblue", GColorOxfordBlueARGB8},
{"dukeblue", GColorDukeBlueARGB8},
{"blue", GColorBlueARGB8},
{"darkgreen", GColorDarkGreenARGB8},
{"midnightgreen", GColorMidnightGreenARGB8},
{"cobaltblue", GColorCobaltBlueARGB8},
{"bluemoon", GColorBlueMoonARGB8},
{"islamicgreen", GColorIslamicGreenARGB8},
{"jaegergreen", GColorJaegerGreenARGB8},
{"tiffanyblue", GColorTiffanyBlueARGB8},
{"vividcerulean", GColorVividCeruleanARGB8},
{"green", GColorGreenARGB8},
{"malachite", GColorMalachiteARGB8},
{"mediumspringgreen", GColorMediumSpringGreenARGB8},
{"cyan", GColorCyanARGB8},
{"bulgarianrose", GColorBulgarianRoseARGB8},
{"imperialpurple", GColorImperialPurpleARGB8},
{"indigo", GColorIndigoARGB8},
{"electricultramarine", GColorElectricUltramarineARGB8},
{"armygreen", GColorArmyGreenARGB8},
{"darkgray", GColorDarkGrayARGB8},
{"liberty", GColorLibertyARGB8},
{"verylightblue", GColorVeryLightBlueARGB8},
{"kellygreen", GColorKellyGreenARGB8},
{"maygreen", GColorMayGreenARGB8},
{"cadetblue", GColorCadetBlueARGB8},
{"pictonblue", GColorPictonBlueARGB8},
{"brightgreen", GColorBrightGreenARGB8},
{"screamingreen", GColorScreaminGreenARGB8},
{"mediumaquamarine", GColorMediumAquamarineARGB8},
{"electricblue", GColorElectricBlueARGB8},
{"darkcandyapplered", GColorDarkCandyAppleRedARGB8},
{"jazzberryjam", GColorJazzberryJamARGB8},
{"purple", GColorPurpleARGB8},
{"vividviolet", GColorVividVioletARGB8},
{"windsortan", GColorWindsorTanARGB8},
{"rosevale", GColorRoseValeARGB8},
{"purpureus", GColorPurpureusARGB8},
{"lavenderindigo", GColorLavenderIndigoARGB8},
{"limerick", GColorLimerickARGB8},
{"brass", GColorBrassARGB8},
{"lightgray", GColorLightGrayARGB8},
{"babyblueeyes", GColorBabyBlueEyesARGB8},
{"springbud", GColorSpringBudARGB8},
{"inchworm", GColorInchwormARGB8},
{"mintgreen", GColorMintGreenARGB8},
{"celeste", GColorCelesteARGB8},
{"red", GColorRedARGB8},
{"folly", GColorFollyARGB8},
{"fashionmagenta", GColorFashionMagentaARGB8},
{"magenta", GColorMagentaARGB8},
{"orange", GColorOrangeARGB8},
{"sunsetorange", GColorSunsetOrangeARGB8},
{"brilliantrose", GColorBrilliantRoseARGB8},
{"shockingpink", GColorShockingPinkARGB8},
{"chromeyellow", GColorChromeYellowARGB8},
{"rajah", GColorRajahARGB8},
{"melon", GColorMelonARGB8},
{"richbrilliantlavender", GColorRichBrilliantLavenderARGB8},
{"yellow", GColorYellowARGB8},
{"icterine", GColorIcterineARGB8},
{"pastelyellow", GColorPastelYellowARGB8},
{"white", GColorWhiteARGB8},
// terminator for unit-test
{0},
};
static bool prv_parse_name(const char *color_value, GColor8 *parsed_color) {
for (size_t i = 0; i < ARRAY_LENGTH(s_color_definitions); i++) {
if (s_color_definitions[i].name && strcmp(s_color_definitions[i].name, color_value) == 0) {
if (parsed_color) {
*parsed_color = (GColor8) {.argb = s_color_definitions[i].value};
}
return true;
}
}
return false;
}
static bool prv_parse_hex_comp(const char *color_value, size_t len, int32_t *value_out) {
// we re-implement strtol here be able to limit the length of the str
int32_t value = 0;
for (size_t i = 0; i < len; i++) {
char ch = color_value[i];
if (ch >= '0' && ch <= '9') {
ch = ch - '0';
} else if (ch >= 'A' && ch <= 'F') {
ch = ch - 'A' + 10;
} else if (ch >= 'a' && ch <= 'f') {
ch = ch - 'a' + 10;
} else { // This will also catch '\0'
return false;
}
value = value * 16 + ch;
}
*value_out = value;
return true;
}
static bool prv_parse_hex_comps(const char *color_value, size_t len,
int32_t *r, int32_t *g, int32_t *b, int32_t *a) {
return prv_parse_hex_comp(color_value + 0 * len, len, r) &&
prv_parse_hex_comp(color_value + 1 * len, len, g) &&
prv_parse_hex_comp(color_value + 2 * len, len, b) &&
(a == NULL || prv_parse_hex_comp(color_value + 3 * len, len, a));
}
static bool prv_parse_hex(const char *color_value, GColor8 *parsed_color) {
const size_t len = strlen(color_value);
if (len < 1 || color_value[0] != '#') {
return false;
}
int32_t r, g, b, a;
switch (len) {
case 4: { // #RGB
if (prv_parse_hex_comps(color_value + 1, 1, &r, &g, &b, NULL)) {
*parsed_color = GColorFromRGB(r * (255/15), g * (255/15), b * (255/15));
return true;
}
}
case 5: { // #RGBA
if (prv_parse_hex_comps(color_value + 1, 1, &r, &g, &b, &a)) {
*parsed_color = GColorFromRGBA(r * (255/15), g * (255/15), b * (255/15), a * (255/15));
if (parsed_color->a == 0) {
*parsed_color = GColorClear;
}
return true;
}
}
case 7: { // #RRGGBB
if (prv_parse_hex_comps(color_value + 1, 2, &r, &g, &b, NULL)) {
*parsed_color = GColorFromRGB(r, g, b);
return true;
}
}
case 9: { // #RRGGBBAA
if (prv_parse_hex_comps(color_value + 1, 2, &r, &g, &b, &a)) {
*parsed_color = GColorFromRGBA(r, g, b, a);
if (parsed_color->a == 0) {
*parsed_color = GColorClear;
}
return true;
}
}
}
return false;
}
bool rocky_api_graphics_color_parse(const char *color_value, GColor8 *parsed_color) {
return prv_parse_name(color_value, parsed_color) ||
prv_parse_hex(color_value, parsed_color);
}
bool rocky_api_graphics_color_from_value(jerry_value_t value, GColor *result) {
if (jerry_value_is_number(value)) {
*result = (GColor) {.argb = jerry_get_int32_value(value)};
return true;
}
if (jerry_value_is_string(value)) {
char color_str[50] = {0};
jerry_string_to_utf8_char_buffer(value, (jerry_char_t *)color_str, sizeof(color_str));
return rocky_api_graphics_color_parse(color_str, result);
}
return false;
}

View file

@ -0,0 +1,31 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
#include "applib/graphics/gtypes.h"
#include "jerry-api.h"
typedef struct RockyAPIGraphicsColorDefinition {
const char *name;
const uint8_t value;
} RockyAPIGraphicsColorDefinition;
bool rocky_api_graphics_color_parse(const char *color_value, GColor8 *parsed_color);
bool rocky_api_graphics_color_from_value(jerry_value_t value, GColor *parsed_color);

View file

@ -0,0 +1,391 @@
/*
* 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 "rocky_api_graphics_path2d.h"
#include "rocky_api_graphics.h"
#include "rocky_api_errors.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/graphics_circle.h"
#include "applib/graphics/graphics_line.h"
#include "kernel/pbl_malloc.h"
#include "rocky_api_util.h"
#include "rocky_api_util_args.h"
#include "system/passert.h"
#include "util/size.h"
#include "util/trig.h"
#define PATH2D_ARC "arc"
#define PATH2D_RECT "rect"
#define PATH2D_BEGINPATH "beginPath"
#define PATH2D_MOVETO "moveTo"
#define PATH2D_LINETO "lineTo"
#define PATH2D_CLOSEPATH "closePath"
#define ROCKY_CONTEXT2D_STROKE "stroke"
#define ROCKY_CONTEXT2D_FILL "fill"
#define MINIMUM_ARRAY_LEN (8)
// TODO: PBL-35780 make this part of app_state_get_rocky_runtime_context()
SECTION(".rocky_bss") T_STATIC RockyAPIPathStep *s_rocky_path_steps;
SECTION(".rocky_bss") T_STATIC size_t s_rocky_path_steps_array_len;
SECTION(".rocky_bss") T_STATIC size_t s_rocky_path_steps_num;
void rocky_api_graphics_path2d_reset_state(void) {
s_rocky_path_steps_num = 0;
task_free(s_rocky_path_steps);
s_rocky_path_steps = NULL;
s_rocky_path_steps_array_len = 0;
}
JERRY_FUNCTION(prv_begin_path) {
rocky_api_graphics_path2d_reset_state();
return jerry_create_undefined();
}
static size_t prv_get_realloc_array_len(const size_t required_array_len) {
size_t len = s_rocky_path_steps_array_len ?: MINIMUM_ARRAY_LEN;
while (required_array_len > len) {
len *= 2;
}
return len;
}
static jerry_value_t prv_try_allocate_steps(const size_t num_steps_increment) {
const size_t required_array_len = (s_rocky_path_steps_num + num_steps_increment);
if (required_array_len <= s_rocky_path_steps_array_len) {
goto success;
}
const size_t new_array_len = prv_get_realloc_array_len(required_array_len);
void *new_steps_array = task_realloc(s_rocky_path_steps,
sizeof(RockyAPIPathStep) * new_array_len);
if (!new_steps_array) {
return rocky_error_oom("can't create more path steps");
}
s_rocky_path_steps = new_steps_array;
s_rocky_path_steps_array_len = new_array_len;
success:
return jerry_create_undefined();
}
#define TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(num_steps_increment) \
ROCKY_RETURN_IF_ERROR(prv_try_allocate_steps(num_steps_increment))
static jerry_value_t prv_add_pt(jerry_length_t argc, const jerry_value_t *argv,
RockyAPIPathStepType step_type) {
const double raw_x = argc > 0 ? (jerry_get_number_value(argv[0]) - 0.5) * FIXED_S16_3_FACTOR : 0;
const double raw_y = argc > 1 ? (jerry_get_number_value(argv[1]) - 0.5) * FIXED_S16_3_FACTOR : 0;
if (raw_x < INT16_MIN || raw_x > INT16_MAX || raw_y < INT16_MIN || raw_x > INT16_MAX) {
return rocky_error_argument_invalid("Value out of bounds");
}
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(1);
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = step_type,
.pt.xy = GPointPrecise((int16_t)raw_x, (int16_t)raw_y),
};
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_move_to) {
return prv_add_pt(argc, argv, RockyAPIPathStepType_MoveTo);
}
JERRY_FUNCTION(prv_line_to) {
return prv_add_pt(argc, argv, RockyAPIPathStepType_LineTo);
}
JERRY_FUNCTION(prv_stroke) {
GContext *const ctx = rocky_api_graphics_get_gcontext();
GPoint p = {0};
GPointPrecise pp = {.x = {0}, .y = {0}};
bool moved_already = false;
#define ASSIGN_P(new_p) do { \
moved_already = true; \
p = GPointFromGPointPrecise(new_p); \
pp = new_p; \
} while (0)
for (size_t i = 0; i < s_rocky_path_steps_num; i++) {
RockyAPIPathStep *const step = &s_rocky_path_steps[i];
switch (step->type) {
case RockyAPIPathStepType_MoveTo: {
ASSIGN_P(step->pt.xy);
break;
}
case RockyAPIPathStepType_LineTo: {
if (moved_already) {
graphics_line_draw_precise_stroked(ctx, pp, step->pt.xy);
}
ASSIGN_P(step->pt.xy);
break;
}
case RockyAPIPathStepType_Arc: {
if (moved_already) {
const GPointPrecise pt_from = gpoint_from_polar_precise(
&step->arc.center, (uint16_t)step->arc.radius.raw_value, step->arc.angle_start);
graphics_line_draw_precise_stroked(ctx, pp, pt_from);
}
int32_t angle_start = step->arc.angle_start;
int32_t angle_end = step->arc.angle_end;
if (step->arc.anti_clockwise) {
const int32_t t = angle_start;
angle_start = angle_end;
angle_end = t;
}
while (angle_end < angle_start) {
angle_end += TRIG_MAX_ANGLE;
}
graphics_draw_arc_precise_internal(ctx, step->arc.center, step->arc.radius,
angle_start, angle_end);
const GPointPrecise pt_to = gpoint_from_polar_precise(
&step->arc.center, (uint16_t)step->arc.radius.raw_value, step->arc.angle_end);
ASSIGN_P(pt_to);
break;
}
default:
WTF;
}
}
#undef ASSIGN_P
return jerry_create_undefined();
}
static void prv_fill_points(GPoint *points, size_t num) {
if (num < 3) {
return;
}
GPath path = {
.num_points = num,
.points = points,
};
GContext *const ctx = rocky_api_graphics_get_gcontext();
gpath_draw_filled(ctx, &path);
}
static GPointPrecise prv_point_add_vector_precise(GPointPrecise *pt, GVectorPrecise *v) {
return GPointPrecise(
pt->x.raw_value + v->dx.raw_value,
pt->y.raw_value + v->dy.raw_value);
}
JERRY_FUNCTION(prv_fill) {
GPoint *const points = task_zalloc(sizeof(*points) * s_rocky_path_steps_num);
if (!points) {
return rocky_error_oom("too many points to fill");
}
size_t points_num = 0;
jerry_value_t rv;
#define ADD_P(pt) \
PBL_ASSERTN(points_num < s_rocky_path_steps_num); \
points[points_num++] = GPointFromGPointPrecise( \
prv_point_add_vector_precise(&(pt).xy, &(pt).fill_delta));
for (size_t i = 0; i < s_rocky_path_steps_num; i++) {
RockyAPIPathStep *const step = &s_rocky_path_steps[i];
switch (step->type) {
case RockyAPIPathStepType_MoveTo: {
prv_fill_points(points, points_num);
points_num = 0;
ADD_P(step->pt);
break;
}
case RockyAPIPathStepType_LineTo: {
ADD_P(step->pt);
break;
}
case RockyAPIPathStepType_Arc: {
rv = rocky_error_argument_invalid("fill() does not support arc()");
goto cleanup;
}
}
}
rv = jerry_create_undefined();
prv_fill_points(points, points_num);
cleanup:
task_free(points);
return rv;
}
JERRY_FUNCTION(prv_arc) {
GPointPrecise center;
Fixed_S16_3 radius;
double angle_1, angle_2;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(
ROCKY_ARG(center.x),
ROCKY_ARG(center.y),
ROCKY_ARG(radius),
ROCKY_ARG_ANGLE(angle_1),
ROCKY_ARG_ANGLE(angle_2));
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(1);
const bool anti_clockwise = (argc >= 6) ? jerry_get_boolean_value(argv[5]) : false;
// adjust for coordinate system
center.x.raw_value -= FIXED_S16_3_HALF.raw_value;
center.y.raw_value -= FIXED_S16_3_HALF.raw_value;
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_Arc,
.arc = (RockyAPIPathStepArc) {
.center = center,
.radius = radius,
// TODO: PBL-40555 consolidate angle handling here and in rocky_api_graphics.c
.angle_start = jerry_get_angle_value(argv[3]),
.angle_end = jerry_get_angle_value(argv[4]),
.anti_clockwise = anti_clockwise,
},
};
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_rect) {
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(5);
if (argc >= 4) {
GRectPrecise rect;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(ROCKY_ARG(rect));
grect_precise_standardize(&rect);
// shift rectangle into coordinate system
const int16_t half_pt = FIXED_S16_3_HALF.raw_value;
rect.origin.x.raw_value -= half_pt;
rect.origin.y.raw_value -= half_pt;
// special casing for our filling algorithm to match fillRect()
const int16_t full_pt = FIXED_S16_3_ONE.raw_value;
const int16_t delta_t = full_pt;
const int16_t delta_r = full_pt;
const int16_t delta_b = 0;
const int16_t delta_l = 0;
const GVectorPrecise delta_tl = GVectorPrecise(delta_l, delta_t);
const GVectorPrecise delta_tr = GVectorPrecise(delta_r, delta_t);
const GVectorPrecise delta_br = GVectorPrecise(delta_r, delta_b);
const GVectorPrecise delta_bl = GVectorPrecise(delta_l, delta_b);
const Fixed_S16_3 right = grect_precise_get_max_x(&rect);
const Fixed_S16_3 bottom = grect_precise_get_max_y(&rect);
// top left
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_MoveTo,
.pt.xy = rect.origin,
.pt.fill_delta = delta_tl,
};
// top right
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = GPointPrecise(right.raw_value, rect.origin.y.raw_value),
.pt.fill_delta = delta_tr,
};
// bottom right
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = GPointPrecise(right.raw_value, bottom.raw_value),
.pt.fill_delta = delta_br,
};
// bottom left
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = GPointPrecise(rect.origin.x.raw_value, bottom.raw_value),
.pt.fill_delta = delta_bl,
};
// top left again, to close path
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = rect.origin,
.pt.fill_delta = delta_tl,
};
}
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_close_path) {
// lineTo() back to most-recent .moveTo()
if (s_rocky_path_steps_num < 2) {
return jerry_create_undefined();
}
RockyAPIPathStep *step = &s_rocky_path_steps[s_rocky_path_steps_num - 1];
// if the last step was a moveTo(), there's nothing to do
if (step->type == RockyAPIPathStepType_MoveTo) {
return jerry_create_undefined();
}
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(1);
do {
step--;
if (step->type == RockyAPIPathStepType_MoveTo) {
// add a lintTo() at the end
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt = step->pt,
};
break;
}
} while (step > &s_rocky_path_steps[0]);
return jerry_create_undefined();
}
void rocky_api_graphics_path2d_add_canvas_methods(jerry_value_t obj) {
rocky_add_function(obj, PATH2D_BEGINPATH, prv_begin_path);
rocky_add_function(obj, PATH2D_MOVETO, prv_move_to);
rocky_add_function(obj, PATH2D_LINETO, prv_line_to);
rocky_add_function(obj, PATH2D_ARC, prv_arc);
rocky_add_function(obj, PATH2D_RECT, prv_rect);
rocky_add_function(obj, PATH2D_CLOSEPATH, prv_close_path);
rocky_add_function(obj, ROCKY_CONTEXT2D_STROKE, prv_stroke);
rocky_add_function(obj, ROCKY_CONTEXT2D_FILL, prv_fill);
}
//! For unit testing
jerry_value_t rocky_api_graphics_path2d_try_allocate_steps(size_t inc_steps) {
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(inc_steps);
return jerry_create_undefined();
}
size_t rocky_api_graphics_path2d_min_array_len(void) {
return MINIMUM_ARRAY_LEN;
}
size_t rocky_api_graphics_path2d_array_len(void) {
return s_rocky_path_steps_array_len;
}
jerry_value_t rocky_api_graphics_path2d_call_fill(void) {
// Args aren't correct, but it doesn't matter right now because the function doesn't use them:
return prv_fill(jerry_create_undefined(), jerry_create_undefined(), NULL, 0);
}

View file

@ -0,0 +1,56 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
#include "jerry-api.h"
#include "applib/graphics/gtypes.h"
void rocky_api_graphics_path2d_add_canvas_methods(jerry_value_t obj);
//! Resets the internal state and frees any memory associated with it.
void rocky_api_graphics_path2d_reset_state(void);
// these types exist so that we can unit test the internal state
typedef enum {
RockyAPIPathStepType_MoveTo,
RockyAPIPathStepType_LineTo,
RockyAPIPathStepType_Arc,
} RockyAPIPathStepType;
typedef struct RockyAPIPathStepPoint {
GPointPrecise xy;
// to be applied to xy when calling .fill() (not .stroke()) as a workaround for some of our
// rendering quirks, needs to be solved for real
GVectorPrecise fill_delta;
} RockyAPIPathStepPoint;
typedef struct RockyAPIPathStepArc {
GPointPrecise center;
Fixed_S16_3 radius;
int32_t angle_start;
int32_t angle_end;
bool anti_clockwise;
} RockyAPIPathStepArc;
typedef struct RockyAPIPathStep {
RockyAPIPathStepType type;
union {
RockyAPIPathStepPoint pt;
RockyAPIPathStepArc arc;
};
} RockyAPIPathStep;

View file

@ -0,0 +1,281 @@
/*
* 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 <applib/graphics/gcontext.h>
#include <applib/graphics/gtypes.h>
#include "rocky_api_graphics_text.h"
#include "applib/fonts/fonts.h"
#include "kernel/pbl_malloc.h"
#include "rocky_api_graphics.h"
#include "rocky_api_util_args.h"
#include "rocky_api_util.h"
#include <math.h>
#include <string.h>
#define ROCKY_CONTEXT2D_FILLTEXT "fillText"
#define ROCKY_CONTEXT2D_FONT "font"
#define ROCKY_CONTEXT2D_MEASURETEXT "measureText"
#define ROCKY_CONTEXT2D_TEXTALIGN "textAlign"
// TODO: PBL-35780 use app_state_get_rocky_runtime_context().context_binding instead
SECTION(".rocky_bss") T_STATIC RockyAPITextState s_rocky_text_state;
SECTION(".rocky_bss") static GFont s_default_font;
static GSize prv_get_max_used_size(GContext *ctx, const char *str_buffer, const GRect box) {
return graphics_text_layout_get_max_used_size(ctx, str_buffer,
s_rocky_text_state.font,
box, s_rocky_text_state.overflow_mode,
s_rocky_text_state.alignment, NULL);
}
// fillText(text, x, y [, maxWidth])
JERRY_FUNCTION(prv_fill_text) {
char *str_buffer;
int16_t x;
int16_t y;
// we don't use INT16_MAX as this seems to leads to overflows deep down in our code
const int16_t large_int = 10000;
int16_t box_width;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(
ROCKY_ARG(str_buffer),
ROCKY_ARG(x),
ROCKY_ARG(y),
);
if (argc >= 4) {
// we use this to get range checks and rounding for free
JS_VAR result = rocky_args_assign(argc - 3, &argv[3],
&(RockyArgBinding){.ptr = &box_width, .type = RockyArgTypeInt16}, 1);
if (jerry_value_has_error_flag(result)) {
return jerry_acquire_value(result);
}
} else {
box_width = large_int;
}
GContext *const ctx = rocky_api_graphics_get_gcontext();
GRect box = {
.origin.x = x,
.origin.y = y,
.size.w = box_width,
.size.h = large_int,
};
// adjust box to accommodate for alignment
switch (s_rocky_text_state.alignment) {
case GTextAlignmentCenter: {
box.origin.x -= box.size.w / 2;
break;
}
case GTextAlignmentRight: {
box.origin.x -= box.size.w;
break;
}
default: {} // do nothing
}
ctx->draw_state.text_color = ctx->draw_state.fill_color;
graphics_draw_text(ctx, str_buffer, s_rocky_text_state.font,
box,
s_rocky_text_state.overflow_mode,
s_rocky_text_state.alignment,
s_rocky_text_state.text_attributes);
task_free(str_buffer);
return jerry_create_undefined();
}
static bool prv_text_align_from_value(jerry_value_t value, GTextAlignment *result) {
char str[10] = {0};
jerry_string_to_utf8_char_buffer(value, (jerry_char_t *)str, sizeof(str));
#define HANDLE_CASE(identifer, value) \
if (strcmp(identifer, str) == 0) { \
*result = value; \
return true; \
}
HANDLE_CASE("left", GTextAlignmentLeft);
HANDLE_CASE("right", GTextAlignmentRight);
HANDLE_CASE("center", GTextAlignmentCenter);
// assuming left-to-right
HANDLE_CASE("start", GTextAlignmentLeft);
HANDLE_CASE("end", GTextAlignmentRight);
#undef HANDLE_CASE
// unknown value
return false;
}
JERRY_FUNCTION(prv_set_text_align) {
GTextAlignment alignment;
if (argc >= 1 && prv_text_align_from_value(argv[0], &alignment)) {
s_rocky_text_state.alignment = alignment;
}
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_get_text_align) {
char *align_str = NULL;
switch (s_rocky_text_state.alignment) {
case GTextAlignmentLeft:
align_str = "left";
break;
case GTextAlignmentRight:
align_str = "right";
break;
case GTextAlignmentCenter:
align_str = "center";
break;
}
return jerry_create_string((const jerry_char_t *)align_str);
}
// we speed this up, e.g. by sorting and doing binary search if this ever becomes an issue
T_STATIC const RockyAPISystemFontDefinition s_font_definitions[] = {
{.js_name = "18px bold Gothic", .res_key = FONT_KEY_GOTHIC_18_BOLD},
{.js_name = "14px Gothic", .res_key = FONT_KEY_GOTHIC_14},
{.js_name = "14px bold Gothic", .res_key = FONT_KEY_GOTHIC_14_BOLD},
{.js_name = "18px Gothic", .res_key = FONT_KEY_GOTHIC_18},
{.js_name = "24px Gothic", .res_key = FONT_KEY_GOTHIC_24},
{.js_name = "24px bold Gothic", .res_key = FONT_KEY_GOTHIC_24_BOLD},
{.js_name = "28px Gothic", .res_key = FONT_KEY_GOTHIC_28},
{.js_name = "28px bold Gothic", .res_key = FONT_KEY_GOTHIC_28_BOLD},
{.js_name = "30px bolder Bitham", .res_key = FONT_KEY_BITHAM_30_BLACK},
{.js_name = "42px bold Bitham", .res_key = FONT_KEY_BITHAM_42_BOLD},
{.js_name = "42px light Bitham", .res_key = FONT_KEY_BITHAM_42_LIGHT},
{.js_name = "42px Bitham-numeric", .res_key = FONT_KEY_BITHAM_42_MEDIUM_NUMBERS},
{.js_name = "34px Bitham-numeric", .res_key = FONT_KEY_BITHAM_34_MEDIUM_NUMBERS},
{.js_name = "21px Roboto", .res_key = FONT_KEY_ROBOTO_CONDENSED_21},
{.js_name = "49px Roboto-subset", .res_key = FONT_KEY_ROBOTO_BOLD_SUBSET_49},
{.js_name = "28px bold Droid-serif", .res_key = FONT_KEY_DROID_SERIF_28_BOLD},
{.js_name = "20px bold Leco-numbers", .res_key = FONT_KEY_LECO_20_BOLD_NUMBERS},
{.js_name = "26px bold Leco-numbers-am-pm", .res_key = FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM},
{.js_name = "32px bold numbers Leco-numbers", .res_key = FONT_KEY_LECO_32_BOLD_NUMBERS},
{.js_name = "36px bold numbers Leco-numbers", .res_key = FONT_KEY_LECO_36_BOLD_NUMBERS},
{.js_name = "38px bold numbers Leco-numbers", .res_key = FONT_KEY_LECO_38_BOLD_NUMBERS},
{.js_name = "42px bold numbers Leco-numbers", .res_key = FONT_KEY_LECO_42_NUMBERS},
{.js_name = "28px light numbers Leco-numbers", .res_key = FONT_KEY_LECO_28_LIGHT_NUMBERS},
{ 0 }, // element to support unit-testing
};
//! The index to the default font in s_font_definitions
#define DEFAULT_FONT_DEFINITION (s_font_definitions[2])
T_STATIC bool prv_font_definition_from_value(
jerry_value_t value, RockyAPISystemFontDefinition const **result) {
char str[50] = {0};
jerry_string_to_utf8_char_buffer(value, (jerry_char_t *)str, sizeof(str));
const RockyAPISystemFontDefinition *def = s_font_definitions;
while (def->js_name) {
if (strcmp(str, def->js_name) == 0) {
*result = def;
return true;
}
def++;
}
return false;
}
JERRY_FUNCTION(prv_set_font) {
const RockyAPISystemFontDefinition *font_definition = NULL;
if (argc >= 1 && prv_font_definition_from_value(argv[0], &font_definition)) {
s_rocky_text_state.font = fonts_get_system_font(font_definition->res_key);
s_rocky_text_state.font_name = font_definition->js_name;
}
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_get_font) {
return jerry_create_string_utf8((const jerry_char_t *)s_rocky_text_state.font_name);
}
JERRY_FUNCTION(prv_measure_text) {
char *str_buffer;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(
ROCKY_ARG(str_buffer),
);
GContext *const ctx = rocky_api_graphics_get_gcontext();
const int16_t box_x = argc >= 2 ? (int16_t)jerry_get_int32_value(argv[1]) : 0;
const int16_t box_y = argc >= 3 ? (int16_t)jerry_get_int32_value(argv[2]) : 0;
const int16_t box_width = argc >= 4 ? (int16_t)jerry_get_int32_value(argv[3]) : INT16_MAX;
const GRect box = {
.origin.x = box_x,
.origin.y = box_y,
.size.w = box_width,
.size.h = INT16_MAX,
};
const GSize size = prv_get_max_used_size(ctx, str_buffer, box);
task_free(str_buffer);
// return a TextMetrics object
JS_VAR result = jerry_create_object();
JS_VAR result_width = jerry_create_number(size.w);
JS_VAR result_height = jerry_create_number(size.h);
JS_VAR result_abbl = jerry_create_number(-1);
JS_VAR result_abbr = jerry_create_number(-2);
jerry_set_object_field(result, "width", result_width);
jerry_set_object_field(result, "height", result_height);
return jerry_acquire_value(result);
}
void rocky_api_graphics_text_add_canvas_methods(jerry_value_t obj) {
rocky_add_function(obj, ROCKY_CONTEXT2D_FILLTEXT, prv_fill_text);
rocky_add_function(obj, ROCKY_CONTEXT2D_MEASURETEXT, prv_measure_text);
rocky_define_property(obj, ROCKY_CONTEXT2D_TEXTALIGN, prv_get_text_align, prv_set_text_align);
rocky_define_property(obj, ROCKY_CONTEXT2D_FONT, prv_get_font, prv_set_font);
}
static void prv_text_state_deinit(void) {
if (s_rocky_text_state.text_attributes) {
graphics_text_attributes_destroy(s_rocky_text_state.text_attributes);
s_rocky_text_state.text_attributes = NULL;
}
}
void rocky_api_graphics_text_reset_state(void) {
prv_text_state_deinit();
s_rocky_text_state = (RockyAPITextState) {
.font = s_default_font,
.font_name = DEFAULT_FONT_DEFINITION.js_name,
.overflow_mode = GTextOverflowModeWordWrap,
.alignment = GTextAlignmentLeft,
.text_attributes = NULL,
};
}
void rocky_api_graphics_text_init(void) {
s_default_font = fonts_get_system_font(DEFAULT_FONT_DEFINITION.res_key);
rocky_api_graphics_text_reset_state();
}
void rocky_api_graphics_text_deinit(void) {
prv_text_state_deinit();
}

View file

@ -0,0 +1,41 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
#include "jerry-api.h"
#include "applib/graphics/text.h"
void rocky_api_graphics_text_init(void);
void rocky_api_graphics_text_deinit(void);
void rocky_api_graphics_text_add_canvas_methods(jerry_value_t obj);
void rocky_api_graphics_text_reset_state(void);
// these structs are exposed here so that we can unit-test the internal state
typedef struct RockyAPISystemFontDefinition {
const char *js_name;
const char *res_key;
} RockyAPISystemFontDefinition;
typedef struct RockyAPITextState {
GFont font;
const char *font_name;
GTextOverflowMode overflow_mode;
GTextAlignment alignment;
GTextAttributes *text_attributes;
} RockyAPITextState;

View file

@ -0,0 +1,203 @@
/*
* 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 "rocky_api_memory.h"
#include "rocky_api_global.h"
#include "rocky_api_util.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include <ecma/base/ecma-gc.h>
#include <jmem/jmem-allocator.h>
#include <jmem/jmem-heap.h>
#include <jmem/jmem-poolman.h>
#include <util/math.h>
#include <stdbool.h>
#include <string.h>
#define ROCKY_EVENT_MEMORYPRESSURE "memorypressure"
#define ROCKY_EVENT_MEMORYPRESSURE_LEVEL "level"
#define ROCKY_EVENT_MEMORYPRESSURE_LEVEL_HIGH "high"
// #define ROCKY_EVENT_MEMORYPRESSURE_LEVEL_NORMAL "normal" // NYI: PBL-42081
// #define ROCKY_EVENT_MEMORYPRESSURE_LEVEL_LOW "low" // NYI: PBL-42081
#define HEADROOM_MIN_SIZE_BYTES (128)
//! This struct should only be accessed from the app task, so no locking is required.
typedef struct RockyMemoryAPIContext {
//! Reserved headroom that will be made available just before calling into the
//! 'memorypressure' event handler.
void *headroom;
size_t headroom_size;
//! True if we're currently calling the 'memorypressure' event handler.
bool is_calling_memory_callback;
} RockyMemoryAPIContext;
static bool prv_is_headroom_allocated(const RockyMemoryAPIContext *ctx) {
return (ctx->headroom != NULL);
}
static void prv_allocate_headroom_or_die(RockyMemoryAPIContext *ctx) {
// It's highly likely that while executing a the handler for the 'memorypressure' event,
// new objects have been created on the heap. Therefore, it's unlikely we'll be able to reclaim
// the desired headroom immediately after returning from the handler. Try to grab as much as we
// can and resize it later on, see prv_resize_headroom_if_needed().
jmem_heap_stats_t stats = {};
jmem_heap_get_stats(&stats);
if (stats.largest_free_block_bytes < HEADROOM_MIN_SIZE_BYTES) {
jerry_port_fatal(ERR_OUT_OF_MEMORY, __builtin_return_address(0));
return;
}
const size_t headroom_size = MIN(stats.largest_free_block_bytes,
ROCKY_API_MEMORY_HEADROOM_DESIRED_SIZE_BYTES);
// This will jerry_port_fatal() if the size isn't available:
ctx->headroom = jmem_heap_alloc_block(headroom_size);
ctx->headroom_size = headroom_size;
}
static void prv_deallocate_headroom(RockyMemoryAPIContext *ctx) {
jmem_heap_free_block(ctx->headroom, ctx->headroom_size);
ctx->headroom = NULL;
ctx->headroom_size = 0;
}
static void prv_resize_headroom_if_needed(RockyMemoryAPIContext *ctx) {
// If needed, try to get our headroom back at the level where we want it to be.
if (ctx->headroom &&
ctx->headroom_size < ROCKY_API_MEMORY_HEADROOM_DESIRED_SIZE_BYTES) {
prv_deallocate_headroom(ctx);
prv_allocate_headroom_or_die(ctx);
}
}
static void prv_collect_all_garbage(void) {
ecma_free_unused_memory(JMEM_FREE_UNUSED_MEMORY_SEVERITY_HIGH, 0, true);
jmem_pools_collect_empty();
}
static void prv_memorypressure_app_log(const char *level, const jmem_heap_stats_t *stats) {
APP_LOG(LOG_LEVEL_WARNING, "Memory pressure level: %s", level);
APP_LOG(LOG_LEVEL_WARNING,
"heap size: %zu, alloc'd: %zu, waste: %zu, largest free block: %zu,",
stats->size, stats->allocated_bytes, stats->waste_bytes, stats->largest_free_block_bytes);
APP_LOG(LOG_LEVEL_WARNING, "used blocks: %zu, free blocks: %zu",
stats->alloc_count, stats->free_count);
}
static void prv_call_memorypressure_handler(RockyMemoryAPIContext *ctx,
const char *level, jmem_heap_stats_t *stats,
bool fatal_if_not_freed) {
if (ctx->is_calling_memory_callback && fatal_if_not_freed) {
// If this happens, the event handler wasn't able to run because there wasn't enough memory
// and triggered the OOM callback again -- basically this means our headroom was too small to
// execute the handler...
sys_analytics_inc(ANALYTICS_APP_METRIC_MEM_ROCKY_RECURSIVE_MEMORYPRESSURE_EVENT_COUNT,
AnalyticsClient_CurrentTask);
return;
}
ctx->is_calling_memory_callback = true;
// TODO: PBL-41990 -- Release caches internal to Rocky's API implementation
prv_memorypressure_app_log(level, stats);
prv_deallocate_headroom(ctx);
prv_collect_all_garbage();
{ // New scope to cleanup the event immediately after the event handler call.
JS_VAR memory_pressure_event = rocky_global_create_event(ROCKY_EVENT_MEMORYPRESSURE);
JS_VAR level_val = jerry_create_string_utf8((const jerry_char_t *)level);
jerry_set_object_field(memory_pressure_event, ROCKY_EVENT_MEMORYPRESSURE_LEVEL, level_val);
rocky_global_call_event_handlers(memory_pressure_event);
}
prv_collect_all_garbage();
prv_allocate_headroom_or_die(ctx);
ctx->is_calling_memory_callback = false;
}
static void prv_memory_callback(jmem_free_unused_memory_severity_t severity,
size_t requested_size_bytes,
bool fatal_if_not_freed) {
RockyMemoryAPIContext *ctx = app_state_get_rocky_memory_api_context();
if (!fatal_if_not_freed || severity < JMEM_FREE_UNUSED_MEMORY_SEVERITY_HIGH) {
ecma_free_unused_memory(severity, requested_size_bytes, fatal_if_not_freed);
if (!ctx->is_calling_memory_callback) {
// It's likely memory has just been free'd, try resizing now.
// See comment at the top of prv_allocate_headroom_or_die() why this is needed.
prv_resize_headroom_if_needed(ctx);
}
return;
}
// Trigger agressive garbage collection, force property hashmaps to be dropped:
prv_collect_all_garbage();
jmem_heap_stats_t stats = {};
jmem_heap_get_stats(&stats);
if (stats.largest_free_block_bytes >= requested_size_bytes + sizeof(jmem_heap_free_t)) {
return;
}
prv_call_memorypressure_handler(ctx, ROCKY_EVENT_MEMORYPRESSURE_LEVEL_HIGH, &stats,
fatal_if_not_freed);
}
static void prv_init(void) {
RockyMemoryAPIContext *ctx = task_zalloc_check(sizeof(RockyMemoryAPIContext));
app_state_set_rocky_memory_api_context(ctx);
jmem_unregister_free_unused_memory_callback(ecma_free_unused_memory);
jmem_register_free_unused_memory_callback(prv_memory_callback);
}
static void prv_deinit(void) {
RockyMemoryAPIContext *ctx = app_state_get_rocky_memory_api_context();
if (prv_is_headroom_allocated(ctx)) {
prv_deallocate_headroom(ctx);
}
jmem_unregister_free_unused_memory_callback(prv_memory_callback);
jmem_register_free_unused_memory_callback(ecma_free_unused_memory);
task_free(ctx);
app_state_set_rocky_memory_api_context(NULL);
}
static bool prv_add_handler(const char *event_name, jerry_value_t handler) {
if (strcmp(event_name, ROCKY_EVENT_MEMORYPRESSURE) == 0) {
RockyMemoryAPIContext *ctx = app_state_get_rocky_memory_api_context();
if (!prv_is_headroom_allocated(ctx)) {
prv_allocate_headroom_or_die(ctx);
}
return true;
}
return false;
}
const RockyGlobalAPI MEMORY_APIS = {
.init = prv_init,
.deinit = prv_deinit,
.add_handler = prv_add_handler,
};

View file

@ -0,0 +1,23 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
#define ROCKY_API_MEMORY_HEADROOM_DESIRED_SIZE_BYTES (256)
extern const RockyGlobalAPI MEMORY_APIS;

View file

@ -0,0 +1,77 @@
/*
* 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 "rocky_api_preferences.h"
#include "applib/preferred_content_size.h"
#include "rocky_api_util.h"
#include "system/passert.h"
#define ROCKY_USERPREFERENCES "userPreferences"
#define ROCKY_USERPREFERENCES_CONTENTSIZE "contentSize"
static const char *prv_get_content_size_string(void) {
PreferredContentSize size = preferred_content_size();
// make sure enum is among known values
switch (size) {
case PreferredContentSizeSmall:
case PreferredContentSizeMedium:
case PreferredContentSizeLarge:
case PreferredContentSizeExtraLarge:
break;
default:
size = PreferredContentSizeDefault;
}
switch (size) {
case PreferredContentSizeSmall:
return "small";
case PreferredContentSizeMedium:
return "medium";
case PreferredContentSizeLarge:
return "large";
case PreferredContentSizeExtraLarge:
return "x-large";
case NumPreferredContentSizes: {}
}
// unreachable
return "unknown";
}
static jerry_value_t prv_get_content_size(void) {
return jerry_create_string(
(const jerry_char_t *)prv_get_content_size_string());
}
static void prv_fill_preferences(jerry_value_t preferences) {
JS_VAR content_size = prv_get_content_size();
jerry_set_object_field(preferences, ROCKY_USERPREFERENCES_CONTENTSIZE, content_size);
}
static void prv_init(void) {
bool was_created = false;
JS_VAR rocky = rocky_get_rocky_singleton();
JS_VAR preferences = rocky_get_or_create_object(rocky, ROCKY_USERPREFERENCES,
rocky_creator_object, NULL, &was_created);
PBL_ASSERTN(was_created);
prv_fill_preferences(preferences);
}
const RockyGlobalAPI PREFERENCES_APIS = {
.init = prv_init,
};

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
extern const RockyGlobalAPI PREFERENCES_APIS;

View file

@ -0,0 +1,105 @@
/*
* 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 "rocky_api_errors.h"
#include "rocky_api_tickservice.h"
#include "rocky_api_util.h"
#include <string.h>
#include "applib/tick_timer_service.h"
#include "rocky_api_global.h"
#include "rocky_api_util.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/size.h"
#define ROCKY_EVENT_SECONDCHANGE "secondchange"
#define ROCKY_EVENT_MINUTECHANGE "minutechange"
#define ROCKY_EVENT_HOURCHANGE "hourchange"
#define ROCKY_EVENT_DAYCHANGE "daychange"
#define ROCKY_FIELD_EVENT_DATE "date"
// TODO: PBL-35780 use app_state_get_rocky_runtime_context().context_binding instead
SECTION(".rocky_bss") static TimeUnits s_units;
static void prv_init(void) {
s_units = (TimeUnits)0;
}
static jerry_value_t prv_create_event(const char *event_name, struct tm *tick_time) {
JS_VAR event = rocky_global_create_event(event_name);
JS_VAR date_obj = rocky_util_create_date(tick_time);
jerry_set_object_field(event, ROCKY_FIELD_EVENT_DATE, date_obj);
return jerry_acquire_value(event);
}
static const struct {
const char *event_name;
TimeUnits time_units;
} s_events[] = {
{ROCKY_EVENT_SECONDCHANGE,
// In some scenarios, our C-API doesn't trigger callbacks with just SECOND_UNIT or MINUTE_UNIT
// if the hour changes. To make the JS-API more conveniently to use without changing the
// existing C behavior, we subscribe to all "higher" units as well.
SECOND_UNIT | MINUTE_UNIT | HOUR_UNIT | DAY_UNIT | MONTH_UNIT | YEAR_UNIT},
{ROCKY_EVENT_MINUTECHANGE,
MINUTE_UNIT | HOUR_UNIT | DAY_UNIT | MONTH_UNIT | YEAR_UNIT},
{ROCKY_EVENT_HOURCHANGE,
HOUR_UNIT | DAY_UNIT | MONTH_UNIT | YEAR_UNIT},
{ROCKY_EVENT_DAYCHANGE,
DAY_UNIT | MONTH_UNIT | YEAR_UNIT},
};
T_STATIC void prv_tick_handler(struct tm *tick_time, TimeUnits units_changed) {
for (size_t i = 0; i < ARRAY_LENGTH(s_events); i++) {
if (units_changed & s_events[i].time_units) {
JS_VAR event = prv_create_event(s_events[i].event_name, tick_time);
rocky_global_call_event_handlers(event);
}
}
}
static bool prv_add_handler(const char *event_name, jerry_value_t handler) {
TimeUnits added_units = (TimeUnits)0;
for (size_t i = 0; i < ARRAY_LENGTH(s_events); i++) {
if (strcmp(s_events[i].event_name, event_name) == 0) {
added_units |= s_events[i].time_units;
break;
}
}
if (added_units == 0) {
return false;
}
s_units |= added_units;
tick_timer_service_subscribe(s_units, prv_tick_handler);
// contract is: we call handler immediately after subscribe once
JS_VAR event = prv_create_event(event_name, NULL);
rocky_util_call_user_function_and_log_uncaught_error(handler, jerry_create_undefined(),
&event, 1);
return true;
}
const RockyGlobalAPI TICKSERVICE_APIS = {
.init = prv_init,
.add_handler = prv_add_handler,
// TODO: PBL-43380 apparently, we never unsubsrcibed from tick events…
};

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
extern const RockyGlobalAPI TICKSERVICE_APIS;

View file

@ -0,0 +1,140 @@
/*
* 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 "rocky_api_errors.h"
#include "jerry-api.h"
#include "rocky_api_util.h"
#include "applib/app_timer.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
#include "rocky_api.h"
#define ROCKY_SETINTERVAL "setInterval"
#define ROCKY_SETTIMEOUT "setTimeout"
#define ROCKY_CLEARTIMEOUT "clearTimeout"
#define ROCKY_CLEARINTERVAL "clearInterval"
typedef struct {
bool is_repeating;
jerry_value_t callback;
AppTimer *timer;
jerry_length_t argc;
jerry_value_t argv[];
} RockyTimerCbData;
static void prv_timer_cleanup(RockyTimerCbData *timer_data) {
jerry_release_value(timer_data->callback);
for (unsigned i = 0; i < timer_data->argc; i++) {
jerry_release_value(timer_data->argv[i]);
}
task_free(timer_data);
}
static void prv_timer_callback(void *data) {
RockyTimerCbData *timer_data = data;
if (jerry_value_is_function(timer_data->callback)) {
rocky_util_call_user_function_and_log_uncaught_error(
timer_data->callback, jerry_create_undefined(), timer_data->argv, timer_data->argc);
} else if (jerry_value_is_string(timer_data->callback)) {
char *source_buf = rocky_string_alloc_and_copy(timer_data->callback);
rocky_util_eval_and_log_uncaught_error((const jerry_char_t *)source_buf, strlen(source_buf));
task_free(source_buf);
}
if (!timer_data->is_repeating) {
prv_timer_cleanup(timer_data);
}
}
static jerry_value_t prv_create_timer(const jerry_value_t *argv,
const jerry_length_t argc,
bool is_repeating) {
if (argc < 1) {
return rocky_error_arguments_missing();
}
jerry_value_t callback = argv[0];
if (!jerry_value_is_function(callback) &&
!jerry_value_is_string(callback)) {
// Nothing to call, but somehow this is valid ¯\_(ツ)_/¯, no-op
return jerry_create_number(0);
}
jerry_acquire_value(callback);
uint32_t timeout = 0;
jerry_length_t cb_argc = 0;
if (argc >= 2) {
// both numbers (123) and strings ('123') are valid
// all others are 0
timeout = rocky_util_uint_from_value(argv[1]);
cb_argc = argc - 2;
}
RockyTimerCbData *cb_data = task_zalloc_check(sizeof(RockyTimerCbData) +
cb_argc * sizeof(jerry_value_t));
*cb_data = (RockyTimerCbData){
.is_repeating = is_repeating,
.callback = callback,
.argc = cb_argc
};
// copy arguments over to cb_data
for (unsigned i = 0; i < cb_argc; i++) {
cb_data->argv[i] = argv[i + 2];
jerry_acquire_value(cb_data->argv[i]);
}
cb_data->timer = app_timer_register_repeatable(timeout,
prv_timer_callback,
cb_data,
is_repeating);
return jerry_create_number((uintptr_t) cb_data->timer);
}
JERRY_FUNCTION(setInterval_handler) {
return prv_create_timer(argv, argc, true /*is_repeating*/);
}
JERRY_FUNCTION(setTimeout_handler) {
return prv_create_timer(argv, argc, false /*is_repeating*/);
}
JERRY_FUNCTION(clearTimer_handler) {
if (argc < 1 || !jerry_value_is_number(argv[0])) {
// Somehow this is valid ¯\_(ツ)_/¯, no-op
return jerry_create_undefined();
}
AppTimer *timer =
(AppTimer *)(uintptr_t)rocky_util_uint_from_value(argv[0]);
RockyTimerCbData *timer_data = app_timer_get_data(timer);
app_timer_cancel(timer);
if (timer_data) {
prv_timer_cleanup(timer_data);
}
return jerry_create_undefined();
}
static void prv_init_apis(void) {
rocky_add_global_function(ROCKY_SETINTERVAL, setInterval_handler);
rocky_add_global_function(ROCKY_SETTIMEOUT, setTimeout_handler);
rocky_add_global_function(ROCKY_CLEARTIMEOUT, clearTimer_handler);
rocky_add_global_function(ROCKY_CLEARINTERVAL, clearTimer_handler);
}
const RockyGlobalAPI TIMER_APIS = {
.init = prv_init_apis,
};

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
extern const RockyGlobalAPI TIMER_APIS;

View file

@ -0,0 +1,260 @@
/*
* 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 "rocky_api_util.h"
#include "rocky_api_errors.h"
#include "applib/app_logging.h"
#include "kernel/pbl_malloc.h"
#include "system/passert.h"
#include "util/size.h"
#include "util/trig.h"
#include "vendor/jerryscript/jerry-libm/include/math.h" // easiest way to get M_PI
#include <stdlib.h>
#include <string.h>
#define ROCKY_SINGLETON "_rocky"
// [MT] including <math.h> causes the jerry-libm header to get included again :(
extern double round(double d);
uintptr_t rocky_util_uint_from_value(const jerry_value_t value) {
uintptr_t rv = 0;
if (jerry_value_is_number(value)) {
rv = jerry_get_number_value(value);
} else if (jerry_value_is_string(value)) {
uint32_t sz = jerry_get_utf8_string_size(value);
char buf[sz + 1];
memset(buf, 0, sz + 1);
jerry_string_to_utf8_char_buffer(value, (jerry_char_t *)buf, sz);
rv = strtoul(buf, NULL, 0);
}
return rv;
}
int32_t jerry_get_int32_value(jerry_value_t value) {
return (int32_t)(round(jerry_get_number_value(value)));
}
int32_t jerry_get_angle_value(jerry_value_t value) {
return (int32_t)
(jerry_get_number_value(value) * TRIG_MAX_ANGLE / (2 * M_PI)) + TRIG_MAX_ANGLE / 4;
}
char *rocky_string_alloc_and_copy(const jerry_value_t string) {
char *out_str = NULL;
if (jerry_value_is_string(string)) {
uint32_t sz = jerry_get_utf8_string_size(string);
out_str = task_zalloc_check(sz + 1);
jerry_string_to_utf8_char_buffer(string, (jerry_char_t *)out_str, sz);
}
return out_str;
}
void rocky_log_exception(const char *message, jerry_value_t exception) {
// using APP_LOG in this function so that 3rd-parties will know what went wrong with their JS
APP_LOG(APP_LOG_LEVEL_ERROR, "Exception while %s", message);
jerry_char_t buffer[100] = {};
const ssize_t written = jerry_object_to_string_to_utf8_char_buffer(exception, buffer,
sizeof(buffer) - 1);
if (written > 0) {
APP_LOG(APP_LOG_LEVEL_ERROR, "%s", buffer);
} else {
APP_LOG(APP_LOG_LEVEL_ERROR, "no further info.");
}
}
void jerry_set_object_field(jerry_value_t object, const char *field, jerry_value_t value) {
JS_VAR prop_name = jerry_create_string((jerry_char_t *)field);
JS_UNUSED_VAL = jerry_set_property(object, prop_name, value);
}
jerry_value_t jerry_get_object_field(jerry_value_t object, const char* field) {
JS_VAR prop_name = jerry_create_string((jerry_char_t *) field);
const bool has_property = jerry_has_property(object, prop_name);
JS_VAR value = (has_property) ? jerry_get_property(object, prop_name) : jerry_create_undefined();
return jerry_acquire_value(value);
}
bool rocky_str_equal(jerry_value_t str_js, const char *str) {
char buffer[40] = {0};
PBL_ASSERTN(strlen(str) < sizeof(buffer));
jerry_string_to_utf8_char_buffer(str_js, (jerry_char_t *) buffer, sizeof(buffer));
return strcmp(buffer, str) == 0;
}
jerry_value_t jerry_get_object_getter_result(jerry_value_t object, const char *getter_name) {
JS_VAR getter = jerry_get_object_field(object, getter_name);
const bool is_function = jerry_value_is_function(getter);
JS_VAR result = (is_function) ? jerry_call_function(getter, object, NULL, 0)
: jerry_create_undefined();
return jerry_acquire_value(result);
}
jerry_value_t rocky_creator_object(void *ignore) {
return jerry_create_object();
}
jerry_value_t rocky_creator_empty_array(void *ignore) {
return jerry_create_array(0);
}
jerry_value_t rocky_get_or_create_object(jerry_value_t parent, const char *name,
RockyObjectCreatorFunc creator_func, void *data,
bool *was_created) {
if (jerry_value_is_undefined(parent)) {
parent = jerry_get_global_object();
// it's safe to release the global object here already, it will never be destroyed
jerry_release_value(parent);
}
// check the object doesn't already exist
JS_VAR val = jerry_get_object_field(parent, name);
if (!jerry_value_is_undefined(val)) {
if (was_created) {
*was_created = false;
}
return jerry_acquire_value(val);
}
JS_VAR result = creator_func(data);
jerry_set_object_field(parent, name, result);
if (was_created) {
*was_created = true;
}
return jerry_acquire_value(result);
}
static jerry_value_t prv_create_function(void *c_function_ptr) {
return jerry_create_external_function(c_function_ptr);
}
bool rocky_add_function(jerry_value_t parent, char *name, jerry_external_handler_t handler) {
bool result = false;
JS_UNUSED_VAL = rocky_get_or_create_object(parent, name, prv_create_function, handler, &result);
return result;
}
bool rocky_add_global_function(char *name, jerry_external_handler_t handler) {
return rocky_add_function(jerry_create_undefined(), name, handler);
}
jerry_value_t rocky_add_constructor(char *name, jerry_external_handler_t handler) {
JS_VAR prototype = jerry_create_object();
JS_VAR rocky_object = rocky_get_rocky_singleton();
JS_VAR constructor = rocky_get_or_create_object(rocky_object, name,
prv_create_function, handler, NULL);
// JerryScript doesn't create a prototype for external/C functions :( probably to save memory?
jerry_set_object_field(prototype, "constructor", constructor);
jerry_set_object_field(constructor, "prototype", prototype);
return jerry_acquire_value(prototype);
}
jerry_value_t rocky_create_with_constructor(const char *rocky_constructor_name,
const jerry_value_t args_p[],
jerry_size_t args_count) {
JS_VAR rocky_object = rocky_get_rocky_singleton();
JS_VAR constructor = jerry_get_object_field(rocky_object, rocky_constructor_name);
JS_VAR object = jerry_construct_object(constructor, args_p, args_count);
return jerry_acquire_value(object);
}
// TODO: PBL-35780 make this part of app_state_get_rocky_runtime_context()
SECTION(".rocky_bss") static jerry_value_t s_rocky_singleton;
void rocky_set_rocky_singleton(jerry_value_t v) {
s_rocky_singleton = jerry_acquire_value(v);
JS_VAR global = jerry_get_global_object();
jerry_set_object_field(global, ROCKY_SINGLETON, v);
}
jerry_value_t rocky_get_rocky_singleton(void) {
return jerry_acquire_value(s_rocky_singleton);
}
void rocky_delete_singleton(void) {
JS_VAR rocky_str = jerry_create_string((const jerry_char_t *)ROCKY_SINGLETON);
JS_VAR global = jerry_get_global_object();
jerry_delete_property(global, rocky_str);
}
void rocky_define_property(jerry_value_t parent, const char *prop_name,
jerry_external_handler_t getter,
jerry_external_handler_t setter) {
jerry_property_descriptor_t prop_desc = {};
jerry_init_property_descriptor_fields(&prop_desc);
prop_desc.is_get_defined = getter != NULL;
prop_desc.getter = jerry_create_external_function(getter);
prop_desc.is_set_defined = setter != NULL;
prop_desc.setter = jerry_create_external_function(setter);
JS_VAR prop_name_js = jerry_create_string((const jerry_char_t *) prop_name);
JS_UNUSED_VAL = jerry_define_own_property(parent, prop_name_js, &prop_desc);
jerry_release_value(prop_desc.getter);
jerry_release_value(prop_desc.setter);
}
// If result has an error flag set, log the error.
// Note: this function releases the passed in value.
T_STATIC void prv_log_uncaught_error(const jerry_value_t result) {
if (jerry_value_has_error_flag(result)) {
rocky_error_print(result);
}
jerry_release_value(result);
}
void rocky_util_eval_and_log_uncaught_error(const jerry_char_t *source_p, size_t source_size) {
prv_log_uncaught_error(jerry_eval(source_p, source_size, false /*strict*/));
}
void rocky_util_call_user_function_and_log_uncaught_error(const jerry_value_t func_obj_val,
const jerry_value_t this_val,
const jerry_value_t args_p[],
jerry_size_t args_count) {
prv_log_uncaught_error(jerry_call_function(func_obj_val, this_val, args_p, args_count));
}
jerry_value_t rocky_util_create_date(struct tm *tick_time) {
JS_VAR date_constructor = jerry_get_global_builtin((const jerry_char_t *)"Date");
if (!jerry_value_is_constructor(date_constructor)) {
return jerry_create_undefined();
}
jerry_value_t date_obj;
if (tick_time) {
jerry_value_t args[] = {
jerry_create_number(1900 + tick_time->tm_year), jerry_create_number(tick_time->tm_mon),
jerry_create_number(tick_time->tm_mday), jerry_create_number(tick_time->tm_hour),
jerry_create_number(tick_time->tm_min), jerry_create_number(tick_time->tm_sec)
};
date_obj = jerry_construct_object(date_constructor, args, ARRAY_LENGTH(args));
for (unsigned i = 0; i < ARRAY_LENGTH(args); ++i) {
jerry_release_value(args[i]);
}
} else {
date_obj = jerry_construct_object(date_constructor, NULL, 0);
}
return date_obj;
}
void rocky_cleanup_js_var(const jerry_value_t *var) {
jerry_release_value(*var);
}

View file

@ -0,0 +1,111 @@
/*
* 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.
*/
#pragma once
#include "jerry-api.h"
#include "util/macro.h"
#include "util/time/time.h"
#include <stdlib.h>
// Create a const jerry value that will be released when it goes out of scope
#define JS_VAR const jerry_value_t __attribute__((cleanup(rocky_cleanup_js_var)))
// Create a temporary jerry value with a unique name that will be released when it goes out of scope
#define JS_UNUSED_VAL \
const jerry_value_t __attribute__((unused,cleanup(rocky_cleanup_js_var))) MACRO_CONCAT(js, __COUNTER__)
void rocky_cleanup_js_var(const jerry_value_t *var);
uintptr_t rocky_util_uint_from_value(const jerry_value_t value);
//! Note: you need to free the return value explicitly
char *rocky_string_alloc_and_copy(const jerry_value_t string);
void rocky_log_exception(const char *message, jerry_value_t exception);
#define ROCKY_RETURN_IF_ERROR(expr) \
do { \
const jerry_value_t rv = (expr); \
if (jerry_value_has_error_flag(rv)) { \
return rv; \
} \
} while (0)
#define JERRY_FUNCTION(name) static jerry_value_t name(const jerry_value_t function_obj_p, \
const jerry_value_t this_val, \
const jerry_value_t argv[], \
const jerry_length_t argc)
void jerry_set_object_field(jerry_value_t object, const char *field, jerry_value_t value);
jerry_value_t jerry_get_object_field(jerry_value_t object, const char *field);
jerry_value_t jerry_get_object_getter_result(jerry_value_t object, const char *getter_name);
bool rocky_add_function(jerry_value_t parent, char *name, jerry_external_handler_t handler);
bool rocky_add_global_function(char *name, jerry_external_handler_t handler);
//! Adds a constructor function object to rocky.name (it sets up the prototype of the function
//! which JerryScript normally does not do for external functions).
//! @return the prototype object
jerry_value_t rocky_add_constructor(char *name, jerry_external_handler_t handler);
// Creates an object using a global constructor, in other words: `new constructor_name(args)`
jerry_value_t rocky_create_with_constructor(const char *rocky_constructor_name,
const jerry_value_t args_p[],
jerry_size_t args_count);
// does rounding to avoid Math.sin(2*Math.PI) issues and related problems
int32_t jerry_get_int32_value(jerry_value_t value);
// converts JS angle (0 degrees at 3 o'clock, 360 degrees = 2 * PI)
// to Pebble angle (0 degrees at 12 o'clock, 360 degrees = TRIG_MAX_ANGLE)
int32_t jerry_get_angle_value(jerry_value_t value);
typedef jerry_value_t (*RockyObjectCreatorFunc)(void *data);
// implementations of RockyObjectCreatorFunc for convenience of rocky_get_or_create_object
// these functions simply ignore the parameter
jerry_value_t rocky_creator_object(void *ignore);
jerry_value_t rocky_creator_empty_array(void *ignore);
jerry_value_t rocky_get_or_create_object(jerry_value_t parent, const char *name,
RockyObjectCreatorFunc creator_func, void *data,
bool *was_created);
// True, if jerry value represents a string that is equal to a given char buffer
bool rocky_str_equal(jerry_value_t str_js, const char *str);
void rocky_set_rocky_singleton(jerry_value_t v);
// caller needs to call jerry_release_value() on return value
jerry_value_t rocky_get_rocky_singleton(void);
void rocky_delete_singleton(void);
void rocky_define_property(jerry_value_t parent, const char *prop_name,
jerry_external_handler_t getter,
jerry_external_handler_t setter);
void rocky_util_eval_and_log_uncaught_error(const jerry_char_t *source_p, size_t source_size);
void rocky_util_call_user_function_and_log_uncaught_error(const jerry_value_t func_obj_val,
const jerry_value_t this_val,
const jerry_value_t args_p[],
jerry_size_t args_count);
jerry_value_t rocky_util_create_date(struct tm *tick_time);

View file

@ -0,0 +1,340 @@
/*
* 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 "rocky_api_util_args.h"
#include "rocky_api_errors.h"
#include "rocky_api_graphics_color.h"
#include "rocky_api_util.h"
#include "applib/graphics/gtypes.h"
#include "system/passert.h"
#include <math.h>
#include <util/math.h>
// from lit-magic-string.inc.h
static const char ECMA_STRING_TYPE_NUMBER[] = "Number";
static const char COLOR_TYPES[] = "String ('color name' or '#hex') or Number";
static const char COLOR_ERROR_MSG[] = "Expecting String ('color name' or '#hex') or Number";
typedef struct {
const char *expected_type_name;
int arg_offset;
} RockyArgTypeCheckError;
typedef struct {
const char *error_msg;
int arg_offset;
} RockyArgValueCheckError;
typedef struct {
bool (* check_value_and_assign)(const RockyArgBinding *binding, const jerry_value_t argv[],
RockyArgValueCheckError *val_error_out);
bool (* check_type)(const jerry_value_t argv[], RockyArgTypeCheckError *type_error_out);
uint8_t expected_num_args;
} RockyArgAssignImp;
static bool prv_check_value_number_within_bounds(RockyArgType type, const double *val,
RockyArgValueCheckError *value_error_out) {
const struct {
double min;
double max;
} bounds[] = {
[RockyArgTypeUInt8] = {0.0, 255.0},
[RockyArgTypeUInt16] = {0.0, 65535.0},
[RockyArgTypeUInt32] = {0.0, 4294967295.0},
[RockyArgTypeUInt64] = {0.0, 9223372036854775807.0},
[RockyArgTypeInt8] = {-128.0, 127.0},
[RockyArgTypeInt16] = {-32768.0, 32767.0},
[RockyArgTypeInt32] = {-2147483648.0, 2147483647.0},
[RockyArgTypeInt64] = {-9223372036854775808.0, 9223372036854775807.0},
[RockyArgTypeDouble] = {-1.7976931348623157e+308, 1.7976931348623157e+308},
[RockyArgTypeFixedS16_3] = {-32768.0 / FIXED_S16_3_FACTOR, 32767.0 / FIXED_S16_3_FACTOR},
};
const bool is_within_bounds = WITHIN(*val, bounds[type].min, bounds[type].max);
if (!is_within_bounds) {
*value_error_out = (RockyArgValueCheckError) {
.error_msg = "Value out of bounds for native type",
.arg_offset = 0,
};
}
return is_within_bounds;
}
static Fixed_S16_3 prv_fixed_s3_from_double(double d) {
return (Fixed_S16_3 ){.raw_value = round(d * FIXED_S16_3_FACTOR)};
}
static bool prv_assign_number(const RockyArgBinding *binding, const jerry_value_t argv[],
RockyArgValueCheckError *value_error_out) {
double val = jerry_get_number_value(argv[0]);
if (!prv_check_value_number_within_bounds(binding->type, &val, value_error_out)) {
return false;
}
if (binding->type != RockyArgTypeDouble && binding->type != RockyArgTypeFixedS16_3) {
val = round(val);
}
void *const dest_ptr = binding->ptr;
switch (binding->type) {
case RockyArgTypeUInt8:
*((uint8_t *)dest_ptr) = val;
return true;
case RockyArgTypeUInt16:
*((uint16_t *)dest_ptr) = val;
return true;
case RockyArgTypeUInt32:
*((uint32_t *)dest_ptr) = val;
return true;
case RockyArgTypeUInt64:
*((uint64_t *)dest_ptr) = val;
return true;
case RockyArgTypeInt8:
*((int8_t *)dest_ptr) = val;
return true;
case RockyArgTypeInt16:
*((int16_t *)dest_ptr) = val;
return true;
case RockyArgTypeInt32:
*((int32_t *)dest_ptr) = val;
return true;
case RockyArgTypeInt64:
*((int64_t *)dest_ptr) = val;
return true;
case RockyArgTypeDouble:
*((double *)dest_ptr) = val;
return true;
case RockyArgTypeFixedS16_3:
*((Fixed_S16_3 *)dest_ptr) = prv_fixed_s3_from_double(val);
return true;
default:
WTF;
return false;
}
}
static bool prv_assign_bool(const RockyArgBinding *binding, const jerry_value_t argv[],
RockyArgValueCheckError *value_error_out) {
*((bool *)binding->ptr) = jerry_value_to_boolean(argv[0]);
return true;
}
static void prv_convert_to_string_and_apply(const jerry_value_t val,
void (*apply)(const jerry_value_t,
const RockyArgBinding *),
const RockyArgBinding *binding) {
jerry_value_t str_val;
bool should_release = false;
if (jerry_value_is_string(val)) {
str_val = val;
} else {
str_val = jerry_value_to_string(val);
should_release = true;
}
apply(str_val, binding);
if (should_release) {
jerry_release_value(str_val);
}
}
static void prv_malloc_and_assign_string_applier(const jerry_value_t str,
const RockyArgBinding *binding) {
*((char **)binding->ptr) = rocky_string_alloc_and_copy(str);
}
static bool prv_malloc_and_assign_string(const RockyArgBinding *binding, const jerry_value_t argv[],
RockyArgValueCheckError *value_error_out) {
prv_convert_to_string_and_apply(argv[0], prv_malloc_and_assign_string_applier, binding);
return true;
}
static void prv_copy_string_applier(const jerry_value_t str, const RockyArgBinding *binding) {
const size_t buffer_size = binding->options.string.buffer_size;
const size_t copied_size = jerry_string_to_utf8_char_buffer(str, (jerry_char_t *)binding->ptr,
buffer_size);
((char *)binding->ptr)[copied_size] = '\0';
}
static bool prv_copy_string_no_malloc(const RockyArgBinding *binding, const jerry_value_t argv[],
RockyArgValueCheckError *value_error_out) {
prv_convert_to_string_and_apply(argv[0], prv_copy_string_applier, binding);
return true;
}
static bool prv_convert_and_assign_angle(const RockyArgBinding *binding, const jerry_value_t argv[],
RockyArgValueCheckError *value_error_out) {
*((double *)binding->ptr) = jerry_get_angle_value(argv[0]);
return true;
}
static bool prv_assign_grect_precise(const RockyArgBinding *binding, const jerry_value_t argv[],
RockyArgValueCheckError *value_error_out) {
Fixed_S16_3 v[4];
for (int i = 0; i < 4; ++i) {
const double d = jerry_get_number_value(argv[i]);
if (!prv_check_value_number_within_bounds(RockyArgTypeFixedS16_3, &d, value_error_out)) {
value_error_out->arg_offset = i;
return false;
}
v[i] = prv_fixed_s3_from_double(d);
}
*((GRectPrecise *)binding->ptr) = (GRectPrecise) {
.origin.x = v[0],
.origin.y = v[1],
.size.w = v[2],
.size.h = v[3],
};
return true;
}
static bool prv_convert_and_assign_gcolor(const RockyArgBinding *binding,
const jerry_value_t argv[],
RockyArgValueCheckError *value_error_out) {
if (rocky_api_graphics_color_from_value(argv[0], (GColor *)binding->ptr)) {
return true;
}
*value_error_out = (RockyArgValueCheckError) {
.error_msg = COLOR_ERROR_MSG,
.arg_offset = 0,
};
return false;
}
static bool prv_check_type_is_number(const jerry_value_t argv[],
RockyArgTypeCheckError *type_error_out) {
if (jerry_value_is_number(argv[0])) {
return true;
};
*type_error_out = (RockyArgTypeCheckError) {
.expected_type_name = ECMA_STRING_TYPE_NUMBER,
.arg_offset = 0,
};
return false;
}
static bool prv_check_type_any(const jerry_value_t argv[],
RockyArgTypeCheckError *type_error_out) {
return true;
}
static bool prv_check_4x_number(const jerry_value_t argv[],
RockyArgTypeCheckError *type_error_out) {
for (uint32_t i = 0; i < 4; ++i) {
if (!prv_check_type_is_number(&argv[i], type_error_out)) {
type_error_out->arg_offset = i;
return false;
}
}
return true;
}
static bool prv_check_color_type(const jerry_value_t argv[],
RockyArgTypeCheckError *type_error_out) {
if (jerry_value_is_number(argv[0]) || jerry_value_is_string(argv[0])) {
return true;
}
*type_error_out = (RockyArgTypeCheckError) {
.expected_type_name = COLOR_TYPES,
.arg_offset = 0,
};
return false;
}
static void prv_init_arg_assign_imp(RockyArgType arg_type, RockyArgAssignImp *imp_out) {
// Defaults:
imp_out->expected_num_args = 1;
switch (arg_type) {
case RockyArgTypeUInt8 ... RockyArgTypeFixedS16_3:
imp_out->check_type = prv_check_type_is_number;
imp_out->check_value_and_assign = prv_assign_number;
break;
case RockyArgTypeBool:
imp_out->check_type = prv_check_type_any;
imp_out->check_value_and_assign = prv_assign_bool;
break;
case RockyArgTypeStringMalloc:
imp_out->check_type = prv_check_type_any;
imp_out->check_value_and_assign = prv_malloc_and_assign_string;
break;
case RockyArgTypeStringArray:
imp_out->check_type = prv_check_type_any;
imp_out->check_value_and_assign = prv_copy_string_no_malloc;
break;
case RockyArgTypeAngle:
imp_out->check_type = prv_check_type_is_number;
imp_out->check_value_and_assign = prv_convert_and_assign_angle;
break;
case RockyArgTypeGRectPrecise:
imp_out->expected_num_args = 4;
imp_out->check_type = prv_check_4x_number;
imp_out->check_value_and_assign = prv_assign_grect_precise;
break;
case RockyArgTypeGColor:
imp_out->check_type = prv_check_color_type;
imp_out->check_value_and_assign = prv_convert_and_assign_gcolor;
break;
default:
WTF;
break;
}
}
jerry_value_t rocky_args_assign(const jerry_length_t argc, const jerry_value_t argv[],
const RockyArgBinding *arg_bindings, size_t num_arg_bindings) {
for (uint32_t i = 0; i < num_arg_bindings; ++i) {
const RockyArgBinding *binding = &arg_bindings[i];
RockyArgAssignImp imp;
prv_init_arg_assign_imp(binding->type, &imp);
// Check number of arguments:
// TODO: PBL-40644: support optional bindings
if (i + imp.expected_num_args > argc) {
return rocky_error_arguments_missing();
}
// Type check:
RockyArgTypeCheckError type_error;
if (!imp.check_type(&argv[i], &type_error)) {
return rocky_error_unexpected_type(i + type_error.arg_offset,
type_error.expected_type_name);
}
// Check value, transform & assign:
RockyArgValueCheckError value_error;
if (!imp.check_value_and_assign(&arg_bindings[i], &argv[i], &value_error)) {
return rocky_error_argument_invalid_at_index(i + value_error.arg_offset,
value_error.error_msg);
}
}
// Just ignore any surplus args
return jerry_create_undefined();
}

View file

@ -0,0 +1,114 @@
/*
* 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.
*/
#pragma once
#include <util/size.h>
#include <stdint.h>
#include "jerry-api.h"
typedef struct GRect GRect;
typedef enum {
RockyArgTypeUnsupported = -1,
RockyArgTypeUInt8,
RockyArgTypeUInt16,
RockyArgTypeUInt32,
RockyArgTypeUInt64,
RockyArgTypeInt8,
RockyArgTypeInt16,
RockyArgTypeInt32,
RockyArgTypeInt64,
RockyArgTypeDouble,
RockyArgTypeFixedS16_3,
RockyArgTypeBool,
RockyArgTypeStringArray,
RockyArgTypeStringMalloc,
RockyArgTypeGRectPrecise,
RockyArgTypeGColor,
RockyArgTypeAngle,
RockyArgType_Count,
} RockyArgType;
typedef struct {
void *ptr;
RockyArgType type;
union {
//! Valid when type == RockyArgTypeStringArray
struct {
size_t buffer_size;
} string;
} options;
} RockyArgBinding;
#define ROCKY_ARG_MAKE(v, binding_type, opts) \
((const RockyArgBinding) { .ptr = (v), .type = binding_type, .options = opts })
//! Binds a JS string argument to a C-string for which a buffer is provided by the client code and
//! for which the size of the buffer cannot be derived from the variable type.
//! @note For char[] arrays (with a static size), use ROCKY_ARG() instead.
//! @note If the buffer is too small, nothing will be copied!
#define ROCKY_ARG_STR(char_buf, _buffer_size) \
ROCKY_ARG_MAKE(char_buf, RockyArgTypeStringArray, { .string = { .buffer_size = _buffer_size }})
#define ROCKY_ARG_ANGLE(var) ROCKY_ARG_MAKE(&(var), RockyArgTypeAngle, {})
#define RockyIfCTypeElse(var, c_type, then, else) \
__builtin_choose_expr(__builtin_types_compatible_p(__typeof__(var), c_type), (then), (else))
#define ROCKY_ARG(var) \
RockyIfCTypeElse(var, uint8_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeUInt8, {}), \
RockyIfCTypeElse(var, uint16_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeUInt16, {}), \
RockyIfCTypeElse(var, uint32_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeUInt32, {}), \
RockyIfCTypeElse(var, uint32_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeUInt32, {}), \
RockyIfCTypeElse(var, uint64_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeUInt64, {}), \
RockyIfCTypeElse(var, int8_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeInt8, {}), \
RockyIfCTypeElse(var, int16_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeInt16, {}), \
RockyIfCTypeElse(var, int32_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeInt32, {}), \
RockyIfCTypeElse(var, int32_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeInt32, {}), \
RockyIfCTypeElse(var, int64_t, ROCKY_ARG_MAKE(&(var), RockyArgTypeInt64, {}), \
RockyIfCTypeElse(var, double, ROCKY_ARG_MAKE(&(var), RockyArgTypeDouble, {}), \
RockyIfCTypeElse(var, Fixed_S16_3, ROCKY_ARG_MAKE(&(var), RockyArgTypeFixedS16_3, {}), \
RockyIfCTypeElse(var, bool, ROCKY_ARG_MAKE(&(var), RockyArgTypeBool, {}), \
RockyIfCTypeElse(var, char *, ROCKY_ARG_MAKE(&(var), RockyArgTypeStringMalloc, {}), \
RockyIfCTypeElse(var, char[], ROCKY_ARG_STR(&(var), sizeof(var)), \
RockyIfCTypeElse(var, GRectPrecise, ROCKY_ARG_MAKE(&(var), RockyArgTypeGRectPrecise, {}), \
RockyIfCTypeElse(var, GColor, ROCKY_ARG_MAKE(&(var), RockyArgTypeGColor, {}), \
ROCKY_ARG_MAKE(&(var), RockyArgTypeUnsupported, {}))))))))))))))))))
//! Helper that uses arg_bindings to check whether all mandatory arguments are given, of the
//! expected type and the input values are within the limits of the C type. If the checks pass,
//! the function will transform the JerryScript values to the native equivalents and assign them
//! to the storage as specified by the arg_bindings.
//! @return `undefined` on success and an error object in case of a problem.
jerry_value_t rocky_args_assign(const jerry_length_t argc, const jerry_value_t argv[],
const RockyArgBinding *arg_bindings, size_t num_arg_bindings);
#define ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(...) \
do { \
const RockyArgBinding bindings[] = { \
__VA_ARGS__ \
}; \
const jerry_value_t error_value = \
rocky_args_assign(argc, argv, bindings, ARRAY_LENGTH(bindings)); \
if (jerry_value_has_error_flag(error_value)) { \
return error_value; \
} \
} while (0)

View file

@ -0,0 +1,264 @@
/*
* 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 "rocky_api_watchinfo.h"
#include "applib/app_watch_info.h"
#include "applib/i18n.h"
#include "mfg/mfg_info.h"
#include "rocky_api_util.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "system/version.h"
// rocky.watchInfo = {
// platform: "basalt",
// model: "pebble_time_red",
// language: "en_US",
// firmware: {
// major: 4,
// minor: 0,
// patch: 1,
// suffix: "beta3"
// }
// }
#define ROCKY_WATCHINFO "watchInfo"
#define ROCKY_WATCHINFO_PLATFORM "platform"
#define ROCKY_WATCHINFO_MODEL "model"
#define ROCKY_WATCHINFO_LANG "language"
#define ROCKY_WATCHINFO_FW "firmware"
#define ROCKY_WATCHINFO_FW_MAJOR "major"
#define ROCKY_WATCHINFO_FW_MINOR "minor"
#define ROCKY_WATCHINFO_FW_PATCH "patch"
#define ROCKY_WATCHINFO_FW_SUFFIX "suffix"
static const char *prv_get_platform_name_string(void) {
const PlatformType platform = sys_get_current_app_sdk_platform();
return platform_type_get_name(platform);
}
static jerry_value_t prv_get_platform_name(void) {
return jerry_create_string(
(const jerry_char_t *)prv_get_platform_name_string());
}
#if PLATFORM_TINTIN
# define TINTIN_MODEL(model_str) model_str
#else
# define TINTIN_MODEL(model_str) NULL
#endif // PLATFORM_TINTIN
#if PLATFORM_SNOWY
# define SNOWY_MODEL(model_str) model_str
#else
# define SNOWY_MODEL(model_str) NULL
#endif // PLATFORM_SNOWY
#if PLATFORM_SPALDING
# define SPALDING_MODEL(model_str) model_str
#else
# define SPALDING_MODEL(model_str) NULL
#endif // PLATFORM_SPALDING
#if PLATFORM_SILK
# define SILK_MODEL(model_str) model_str
#else
# define SILK_MODEL(model_str) NULL
#endif // PLATFORM_SPALDING
#if PLATFORM_ROBERT
# define ROBERT_MODEL(model_str) model_str
#else
# define ROBERT_MODEL(model_str) NULL
#endif // PLATFORM_ROBERT
static jerry_value_t prv_get_model_name(void) {
WatchInfoColor color = sys_watch_info_get_color();
char *model_name = NULL;
if (color < WATCH_INFO_COLOR__MAX) {
switch (color) {
case WATCH_INFO_COLOR_BLACK:
model_name = TINTIN_MODEL("pebble_black");
break;
case WATCH_INFO_COLOR_WHITE:
model_name = TINTIN_MODEL("pebble_white");
break;
case WATCH_INFO_COLOR_RED:
model_name = TINTIN_MODEL("pebble_red");
break;
case WATCH_INFO_COLOR_ORANGE:
model_name = TINTIN_MODEL("pebble_orange");
break;
case WATCH_INFO_COLOR_GRAY:
model_name = TINTIN_MODEL("pebble_gray");
break;
case WATCH_INFO_COLOR_STAINLESS_STEEL:
model_name = TINTIN_MODEL("pebble_steel_silver");
break;
case WATCH_INFO_COLOR_MATTE_BLACK:
model_name = TINTIN_MODEL("pebble_steel_black");
break;
case WATCH_INFO_COLOR_BLUE:
model_name = TINTIN_MODEL("pebble_blue");
break;
case WATCH_INFO_COLOR_GREEN:
model_name = TINTIN_MODEL("pebble_green");
break;
case WATCH_INFO_COLOR_PINK:
model_name = TINTIN_MODEL("pebble_pink");
break;
case WATCH_INFO_COLOR_TIME_WHITE:
model_name = SNOWY_MODEL("pebble_time_white");
break;
case WATCH_INFO_COLOR_TIME_BLACK:
model_name = SNOWY_MODEL("pebble_time_black");
break;
case WATCH_INFO_COLOR_TIME_RED:
model_name = SNOWY_MODEL("pebble_time_red");
break;
case WATCH_INFO_COLOR_TIME_STEEL_SILVER:
model_name = SNOWY_MODEL("pebble_time_steel_silver");
break;
case WATCH_INFO_COLOR_TIME_STEEL_BLACK:
model_name = SNOWY_MODEL("pebble_time_steel_black");
break;
case WATCH_INFO_COLOR_TIME_STEEL_GOLD:
model_name = SNOWY_MODEL("pebble_time_steel_gold");
break;
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_14:
model_name = SPALDING_MODEL("pebble_time_round_silver_14mm");
break;
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_14:
model_name = SPALDING_MODEL("pebble_time_round_black_14mm");
break;
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_20:
model_name = SPALDING_MODEL("pebble_time_round_silver_20mm");
break;
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_20:
model_name = SPALDING_MODEL("pebble_time_round_black_20mm");
break;
case WATCH_INFO_COLOR_TIME_ROUND_ROSE_GOLD_14:
model_name = SPALDING_MODEL("pebble_time_round_rose_gold_14mm");
break;
case WATCH_INFO_COLOR_PEBBLE_2_HR_BLACK:
model_name = SILK_MODEL("pebble_2_hr_black");
break;
case WATCH_INFO_COLOR_PEBBLE_2_HR_LIME:
model_name = SILK_MODEL("pebble_2_hr_lime");
break;
case WATCH_INFO_COLOR_PEBBLE_2_HR_FLAME:
model_name = SILK_MODEL("pebble_2_hr_flame");
break;
case WATCH_INFO_COLOR_PEBBLE_2_HR_WHITE:
model_name = SILK_MODEL("pebble_2_hr_white");
break;
case WATCH_INFO_COLOR_PEBBLE_2_HR_AQUA:
model_name = SILK_MODEL("pebble_2_hr_aqua");
break;
case WATCH_INFO_COLOR_PEBBLE_2_SE_BLACK:
model_name = SILK_MODEL("pebble_2_se_black");
break;
case WATCH_INFO_COLOR_PEBBLE_2_SE_WHITE:
model_name = SILK_MODEL("pebble_2_se_white");
break;
case WATCH_INFO_COLOR_PEBBLE_TIME_2_BLACK:
model_name = ROBERT_MODEL("pebble_time_2_black");
break;
case WATCH_INFO_COLOR_PEBBLE_TIME_2_SILVER:
model_name = ROBERT_MODEL("pebble_time_2_silver");
break;
case WATCH_INFO_COLOR_PEBBLE_TIME_2_GOLD:
model_name = ROBERT_MODEL("pebble_time_2_gold");
break;
case WATCH_INFO_COLOR_UNKNOWN:
case WATCH_INFO_COLOR__MAX:
model_name = NULL;
break;
}
}
if (model_name) {
return jerry_create_string((const jerry_char_t *)model_name);
} else {
// Assume we're running on QEMU
const char *platform_name = prv_get_platform_name_string();
const char *qemu_prefix = "qemu_platform_";
char combined_string[strlen(qemu_prefix) + strlen(platform_name) + 1];
strcpy(combined_string, qemu_prefix);
strcat(combined_string, platform_name);
return jerry_create_string((const jerry_char_t *)combined_string);
}
}
static jerry_value_t prv_get_language(void) {
return jerry_create_string((const jerry_char_t *)app_get_system_locale());
}
static jerry_value_t prv_get_fw_version(void) {
WatchInfoVersion fw_version = watch_info_get_firmware_version();
JS_VAR version_major = jerry_create_number(fw_version.major);
JS_VAR version_minor = jerry_create_number(fw_version.minor);
JS_VAR version_patch = jerry_create_number(fw_version.patch);
// Parse the suffix out of the version tag
FirmwareMetadata md;
version_copy_running_fw_metadata(&md);
char *suffix_str = strstr(md.version_tag, "-");
if (suffix_str) {
// Skip the '-' character
suffix_str++;
} else {
suffix_str = "";
}
JS_VAR version_suffix = jerry_create_string((const jerry_char_t *)suffix_str);
JS_VAR version_object = jerry_create_object();
jerry_set_object_field(version_object, ROCKY_WATCHINFO_FW_MAJOR, version_major);
jerry_set_object_field(version_object, ROCKY_WATCHINFO_FW_MINOR, version_minor);
jerry_set_object_field(version_object, ROCKY_WATCHINFO_FW_PATCH, version_patch);
jerry_set_object_field(version_object, ROCKY_WATCHINFO_FW_SUFFIX, version_suffix);
// TODO: PBL-40413: Support .toString() on the fwversion field
return jerry_acquire_value(version_object);
}
static void prv_fill_watchinfo(jerry_value_t watchinfo) {
JS_VAR platform_name = prv_get_platform_name();
JS_VAR model_name = prv_get_model_name();
JS_VAR language = prv_get_language();
JS_VAR fw_version = prv_get_fw_version();
jerry_set_object_field(watchinfo, ROCKY_WATCHINFO_PLATFORM, platform_name);
jerry_set_object_field(watchinfo, ROCKY_WATCHINFO_MODEL, model_name);
jerry_set_object_field(watchinfo, ROCKY_WATCHINFO_LANG, language);
jerry_set_object_field(watchinfo, ROCKY_WATCHINFO_FW, fw_version);
}
static void prv_init_apis(void) {
bool was_created = false;
JS_VAR rocky = rocky_get_rocky_singleton();
JS_VAR watchinfo =
rocky_get_or_create_object(rocky, ROCKY_WATCHINFO, rocky_creator_object, NULL, &was_created);
PBL_ASSERTN(was_created);
prv_fill_watchinfo(watchinfo);
}
const RockyGlobalAPI WATCHINFO_APIS = {
.init = prv_init_apis,
};

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
#pragma once
#include "rocky_api.h"
extern const RockyGlobalAPI WATCHINFO_APIS;

View file

@ -0,0 +1,36 @@
/*
* 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.
*/
#pragma once
#include "util/attributes.h"
#include <stdint.h>
/**
* As we are running pretty low on space on some platforms, we want to reserve some code space
* for future use on the rocky project so that we can ensure there is space to implement APIs
* uniformly on all platforms. Start with 15K and we will dwindle this down as we continue to
* work on the rocky / jerryscript support.
*
* Modification Log:
* 15360 : Starting point
* 13232 : Jerryscript upstream merge on Sept 14
* 12620 : postMessage() re-implementation
*
*/
USED const uint8_t ROCKY_RESERVED_CODE_SPACE[12620] = {};

View file

@ -0,0 +1,121 @@
/*
* 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 "pbl_jcontext.inc.h"
#include "jerry-port.h"
#include "applib/app_heap_analytics.h"
#include "applib/app_logging.h"
#include "applib/pbl_std/pbl_std.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/logging.h"
#include "system/passert.h"
#include <stdarg.h>
#include <stdint.h>
static uint8_t prv_pbl_log_level_from_jerry_log_level(const jerry_log_level_t level) {
switch (level) {
case JERRY_LOG_LEVEL_ERROR:
return LOG_LEVEL_ERROR;
case JERRY_LOG_LEVEL_WARNING:
return LOG_LEVEL_WARNING;
case JERRY_LOG_LEVEL_TRACE:
return LOG_LEVEL_DEBUG_VERBOSE;
case JERRY_LOG_LEVEL_DEBUG:
default:
return LOG_LEVEL_DEBUG;
break;
}
}
/**
* Provide log message implementation for the engine.
*/
void jerry_port_log(jerry_log_level_t level, const char* format, ...) {
const uint8_t log_level = prv_pbl_log_level_from_jerry_log_level(level);
if (log_level > LOG_LEVEL_DEBUG) {
return;
}
va_list args;
va_start(args, format);
app_log_vargs(log_level, "JERRY-LOG", 0, format, args);
va_end(args);
}
/**
* Provide console message implementation for the engine.
*/
void jerry_port_console(const char *format, ...) {
if (format[0] == '\n' && strlen(format) == 1) {
return;
}
va_list args;
va_start(args, format);
app_log_vargs(LOG_LEVEL_DEBUG, "JERRY-CONSOLE", 0, format, args);
va_end(args);
}
void jerry_port_fatal(jerry_fatal_code_t code, void *lr) {
if (ERR_OUT_OF_MEMORY == code) {
app_heap_analytics_log_rocky_heap_oom_fault();
}
jerry_port_log(JERRY_LOG_LEVEL_ERROR, "Fatal Error: %d", code);
PBL_ASSERTN_LR(false, (uint32_t)lr);
}
RockyRuntimeContext * rocky_runtime_context_get(void) {
return app_state_get_rocky_runtime_context();
}
#define ALIGNED_HEAP(ptr) (void *)JERRY_ALIGNUP((uintptr_t)(ptr), JMEM_ALIGNMENT)
void rocky_runtime_context_init(void) {
uint8_t *unaligned_buffer = task_zalloc(sizeof(RockyRuntimeContext) + JMEM_ALIGNMENT);
RockyRuntimeContext * const ctx = ALIGNED_HEAP(unaligned_buffer);
app_state_set_rocky_runtime_context(unaligned_buffer, ctx);
}
void rocky_runtime_context_deinit(void) {
task_free(app_state_get_rocky_runtime_context_buffer());
app_state_set_rocky_runtime_context(NULL, NULL);
}
double jerry_port_get_current_time (void) {
time_t seconds;
uint16_t millis;
time_ms(&seconds, &millis);
return ((double)seconds * 1000.0) + millis;
} /* jerry_port_get_current_time */
DEFINE_SYSCALL(bool, jerry_port_get_time_zone, jerry_time_zone_t *tz_p) {
if (PRIVILEGE_WAS_ELEVATED) {
syscall_assert_userspace_buffer(tz_p, sizeof(*tz_p));
}
time_t utc_now;
time_ms(&utc_now, NULL);
int32_t dstoffset = time_get_isdst(utc_now) ? time_get_dstoffset() : 0;
tz_p->daylight_saving_time = dstoffset / 3600;
tz_p->offset = -1 * time_get_gmtoffset() / 60;
return true;
}

View file

@ -0,0 +1,41 @@
/*
* 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.
*/
#pragma once
// NOTE: This file is uses as an -include file when compiling each jerry-core source file, so
// be careful what you add here!
#define JERRY_CONTEXT(field) (rocky_runtime_context_get()->jerry_global_context.field)
#define JERRY_HEAP_CONTEXT(field) (rocky_runtime_context_get()->jerry_global_heap.field)
#define JERRY_HASH_TABLE_CONTEXT(field) (rocky_runtime_context_get()->jerry_global_hash_table.field)
#include "jcontext.h"
typedef struct RockyRuntimeContext {
jerry_context_t jerry_global_context;
jmem_heap_t jerry_global_heap;
#ifndef CONFIG_ECMA_LCACHE_DISABLE
jerry_hash_table_t jerry_global_hash_table;
#endif
} RockyRuntimeContext;
_Static_assert(
((offsetof(RockyRuntimeContext, jerry_global_heap) +
offsetof(jmem_heap_t, area)) % JMEM_ALIGNMENT) == 0,
"jerry_global_heap.area must be aligned to JMEM_ALIGNMENT!");
extern RockyRuntimeContext * rocky_runtime_context_get(void);

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
#pragma once
void rocky_runtime_context_init(void);
void rocky_runtime_context_deinit(void);

View file

@ -0,0 +1,158 @@
/*
* 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 "rocky.h"
#include "jmem-heap.h"
#include "applib/rockyjs/api/rocky_api.h"
#include "applib/rockyjs/api/rocky_api_util.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "applib/app.h"
#include "applib/applib_resource_private.h"
#include "applib/app_heap_analytics.h"
#include "applib/app_heap_util.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_manager.h"
#include "system/passert.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "code_space_reservation.h"
const RockySnapshotHeader ROCKY_EXPECTED_SNAPSHOT_HEADER = {
.signature = {'P', 'J', 'S', 0}, // C-string terminator in case somebody treats this as source
#if CAPABILITY_HAS_JAVASCRIPT
.version = (uint8_t)CAPABILITY_JAVASCRIPT_BYTECODE_VERSION,
#endif
};
static void prv_rocky_init(void) {
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
rocky_api_watchface_init();
}
bool rocky_is_snapshot(const uint8_t *buffer, size_t buffer_size) {
#if CAPABILITY_HAS_JAVASCRIPT
const size_t header_length = sizeof(ROCKY_EXPECTED_SNAPSHOT_HEADER);
if (buffer_size < header_length ||
memcmp(ROCKY_EXPECTED_SNAPSHOT_HEADER.signature,
buffer,
sizeof(ROCKY_EXPECTED_SNAPSHOT_HEADER.signature)) != 0) {
return false;
}
const uint8_t actual_version = buffer[offsetof(RockySnapshotHeader, version)];
const uint8_t expected_version = ROCKY_EXPECTED_SNAPSHOT_HEADER.version;
if (expected_version != actual_version) {
PBL_LOG(LOG_LEVEL_WARNING, "incompatible JS snapshot version %"PRIu8" (expected: %"PRIu8")",
actual_version, expected_version);
return false;
}
return jerry_is_snapshot(buffer + header_length, buffer_size - header_length);
#else
return false;
#endif
}
static bool prv_rocky_eval_buffer(const uint8_t *buffer, size_t buffer_size) {
jerry_value_t rv;
if (rocky_is_snapshot(buffer, buffer_size)) {
buffer += sizeof(ROCKY_EXPECTED_SNAPSHOT_HEADER);
buffer_size -= sizeof(ROCKY_EXPECTED_SNAPSHOT_HEADER);
PBL_ASSERTN((uintptr_t)buffer % 8 == 0);
rv = jerry_exec_snapshot(buffer, buffer_size, false);
} else {
PBL_LOG(LOG_LEVEL_INFO, "Not a snapshot, interpreting buffer as JS source code");
rv = jerry_eval((jerry_char_t *) buffer, buffer_size, false);
}
bool error_occurred = jerry_value_has_error_flag(rv);
if (error_occurred) {
jerry_value_clear_error_flag(&rv);
rocky_log_exception("Evaluating JS", rv);
}
jerry_release_value(rv);
return !(error_occurred);
}
static void prv_rocky_deinit(void) {
app_heap_analytics_log_stats_to_app_heartbeat(true /* is_rocky_app */);
rocky_api_deinit();
jerry_cleanup();
rocky_runtime_context_deinit();
}
bool rocky_event_loop_with_string_or_snapshot(const void *buffer, size_t buffer_size) {
#if CAPABILITY_HAS_JAVASCRIPT
prv_rocky_init();
const bool result = prv_rocky_eval_buffer(buffer, buffer_size);
if (result) {
app_event_loop_common();
}
prv_rocky_deinit();
return result;
#else
return false;
#endif
}
static bool prv_rocky_event_loop_with_resource(ResAppNum app_num, uint32_t resource_id) {
#if CAPABILITY_HAS_JAVASCRIPT
if (!sys_get_current_app_is_rocky_app()) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Cannot execute JavaScript, insufficient meta data.");
return false;
}
bool rv = false;
size_t sz = sys_resource_size(app_num, resource_id);
char *script = applib_resource_mmap_or_load(app_num,
resource_id,
0, sz, true);
if (script) {
// TODO: PBL-40010 clean this up
// hotfix: we're either dealing with mmap, which is 8 byte aligned already
// or malloc`ed buffer which has 7 additional bytes at the end.
// We're are moving over the bytes so that they are 8-byte aligned
// and pass that pointer to rocky instead
char *aligned_script = (char *)((uintptr_t)(script + 7) & ~7);
if (aligned_script != script) {
// don't write if it's aligned, to avoid writing to mmapped data
memmove(aligned_script, script, sz);
}
rv = rocky_event_loop_with_string_or_snapshot(aligned_script, sz);
applib_resource_munmap_or_free(script);
}
return rv;
#else
return false;
#endif
}
bool rocky_event_loop_with_system_resource(uint32_t resource_id) {
return prv_rocky_event_loop_with_resource(SYSTEM_APP, resource_id);
}
bool rocky_event_loop_with_resource(uint32_t resource_id) {
return prv_rocky_event_loop_with_resource(sys_get_current_resource_num(),
resource_id);
}

View file

@ -0,0 +1,48 @@
/*
* 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.
*/
#pragma once
#include "jerry-api.h"
#include "resource/resource.h"
typedef union {
uint8_t data[8];
struct {
char signature[4];
uint8_t version;
uint8_t padding[3];
};
} RockySnapshotHeader;
#ifndef __clang__
_Static_assert(sizeof(RockySnapshotHeader) == 8, "RockyJS snapshot header size");
#endif
extern const RockySnapshotHeader ROCKY_EXPECTED_SNAPSHOT_HEADER;
bool rocky_add_global_function(char *name, jerry_external_handler_t handler);
bool rocky_add_function(jerry_value_t parent, char *name, jerry_external_handler_t handler);
jerry_value_t rocky_get_rocky_namespace(void);
jerry_value_t rocky_get_rocky_singleton(void);
bool rocky_event_loop_with_resource(uint32_t resource_id);
bool rocky_event_loop_with_system_resource(uint32_t resource_id);
bool rocky_event_loop_with_string_or_snapshot(const void *buffer, size_t buffer_size);
bool rocky_is_snapshot(const uint8_t *buffer, size_t buffer_size);

View file

@ -0,0 +1,58 @@
/*
* 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 "rocky_res.h"
#include "process_management/process_manager.h"
#include "resource/resource_storage.h"
#include "rocky.h"
bool rocky_app_has_compatible_bytecode_res(ResAppNum app_num) {
// we will iterate over each resource to detect any compatible JS byte code
// if there's any, we can assume that there's also a resource with ID 1.
const uint32_t entries = resource_storage_get_num_entries(app_num, 1);
for (uint32_t entry_id = 1; entry_id <= entries; entry_id++) {
// this buffer needs to be large enough to carry
// RockySnapshotHeader + the relevant data JerryScript verifies
uint8_t snapshot_start[sizeof(RockySnapshotHeader) + sizeof(uint64_t)];
const size_t bytes_read = resource_load_byte_range_system(app_num, entry_id, 0,
snapshot_start,
sizeof(snapshot_start));
if (rocky_is_snapshot(snapshot_start, bytes_read)) {
return true;
}
}
return false;
}
RockyResourceValidation rocky_app_validate_resources(const PebbleProcessMd *md) {
if (!md || !md->is_rocky_app) {
// it's not a rocky app, so it cannot have incompatible byte code
return RockyResourceValidation_NotRocky;
}
const ResAppNum app_num = (ResAppNum)process_metadata_get_res_bank_num(md);
if (app_num == (ResAppNum)SYSTEM_APP_BANK_ID) {
// in case of the FW, we can assume that our JS is valid
return RockyResourceValidation_Valid;
}
if (rocky_app_has_compatible_bytecode_res(app_num)) {
return RockyResourceValidation_Valid;
} else {
return RockyResourceValidation_Invalid;
}
}

View file

@ -0,0 +1,32 @@
/*
* 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.
*/
#pragma once
#include "process_management/pebble_process_md.h"
#include "resource/resource.h"
// True, if there's any resource with a compatible snapshot
bool rocky_app_has_compatible_bytecode_res(ResAppNum app_num);
typedef enum {
RockyResourceValidation_NotRocky,
RockyResourceValidation_Invalid,
RockyResourceValidation_Valid,
} RockyResourceValidation;
// True if md describes rocky app and resources contain invalid bytecode
RockyResourceValidation rocky_app_validate_resources(const PebbleProcessMd *md);