diff --git a/.changeset/tall-needles-cover.md b/.changeset/tall-needles-cover.md new file mode 100644 index 00000000..9e37ce3f --- /dev/null +++ b/.changeset/tall-needles-cover.md @@ -0,0 +1,5 @@ +--- +'nxjs-runtime': patch +--- + +Add support for 1080p resolution in docked mode diff --git a/apps/canvas/src/main.ts b/apps/canvas/src/main.ts index 28e295c5..99833999 100644 --- a/apps/canvas/src/main.ts +++ b/apps/canvas/src/main.ts @@ -1,20 +1,23 @@ +screen.enable1080p(); + const ctx = screen.getContext('2d'); -ctx.fillStyle = 'white'; -ctx.font = '42px system-ui'; -ctx.textAlign = 'center'; -ctx.textBaseline = 'bottom'; function clock() { const now = new Date(); + const scaleFactor = screen.height / 150; // Reset ctx.clearRect(0, 0, screen.width, screen.height); + ctx.fillStyle = 'white'; // Render time as text + ctx.font = `${8 * scaleFactor}px system-ui`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'bottom'; ctx.fillText(now.toLocaleString(), screen.width / 2, screen.height); ctx.save(); - const scaleFactor = 4.8; + ctx.translate( screen.width / 2 - (150 * scaleFactor) / 2, screen.height / 2 - (150 * scaleFactor) / 2, diff --git a/apps/fonts/src/main.ts b/apps/fonts/src/main.ts index 291e4cd7..0882fa0f 100644 --- a/apps/fonts/src/main.ts +++ b/apps/fonts/src/main.ts @@ -1,13 +1,13 @@ const ctx = screen.getContext('2d'); const fontUrl = new URL('fonts/Alexandria.ttf', Switch.entrypoint); -const fontData = Switch.readFileSync(fontUrl); +const fontData = Switch.readFileSync(fontUrl)!; const font = new FontFace('Alexandria', fontData); fonts.add(font); //const emojiFontUrl = new URL('fonts/NotoColorEmoji-Regular.ttf', Switch.entrypoint); const emojiFontUrl = new URL('fonts/Twemoji.ttf', Switch.entrypoint); -const emojiFontData = Switch.readFileSync(emojiFontUrl); +const emojiFontData = Switch.readFileSync(emojiFontUrl)!; const emojiFont = new FontFace('Twemoji', emojiFontData); fonts.add(emojiFont); diff --git a/packages/runtime/src/$.ts b/packages/runtime/src/$.ts index 6f57f233..bb079058 100644 --- a/packages/runtime/src/$.ts +++ b/packages/runtime/src/$.ts @@ -50,7 +50,7 @@ export interface Init { // applet.c appletIlluminance(): number; appletGetAppletType(): number; - appletGetOperationMode(): number; + appletGetOperationMode(): string; // battery.c batteryInit(): void; @@ -59,6 +59,12 @@ export interface Init { // canvas.c canvasNew(width: number, height: number): Screen | OffscreenCanvas; + canvasResize( + c: Screen, + ctx: CanvasRenderingContext2D | undefined, + width: number, + height: number + ): void; canvasInitClass(c: ClassOf): void; canvasContext2dNew(c: Screen): CanvasRenderingContext2D; canvasContext2dNew(c: OffscreenCanvas): OffscreenCanvasRenderingContext2D; @@ -80,7 +86,7 @@ export interface Init { ): string; canvasContext2dSetFont( ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, - font: FontFace, + font: FontFace | null, size: number, fontString: string, ): number[]; @@ -125,7 +131,6 @@ export interface Init { // font.c fontFaceNew(data: ArrayBuffer): FontFace; - getSystemFont(): ArrayBuffer; // fs.c mkdirSync(path: string, mode: number): number; @@ -172,8 +177,8 @@ export interface Init { unsetenv(name: string): void; envToObject(): Record; onFrame(fn: (kDown: number) => void): void; + onAppletEvent(fn: (type: number) => void): void; onExit(fn: () => void): void; - framebufferInit(screen: Screen): void; hidInitializeTouchScreen(): void; hidGetTouchScreenStates(): Touch[] | undefined; hidInitializeKeyboard(): void; diff --git a/packages/runtime/src/canvas/canvas-rendering-context-2d.ts b/packages/runtime/src/canvas/canvas-rendering-context-2d.ts index 2af727ca..63079ddf 100644 --- a/packages/runtime/src/canvas/canvas-rendering-context-2d.ts +++ b/packages/runtime/src/canvas/canvas-rendering-context-2d.ts @@ -11,7 +11,7 @@ import { stub, returnOnThrow, } from '../utils'; -import { addSystemFont, findFont, fonts } from '../font/font-face-set'; +import { findFont, fonts } from '../font/font-face-set'; import { DOMMatrix, type DOMMatrix2DInit } from '../dommatrix'; import type { Path2D } from './path2d'; import type { Screen } from '../screen'; @@ -47,7 +47,6 @@ export class CanvasRenderingContext2D { const ctx = $.canvasContext2dNew(canvas); Object.setPrototypeOf(ctx, CanvasRenderingContext2D.prototype); _.set(ctx, { canvas }); - ctx.font = '10px system-ui'; return ctx; } @@ -100,10 +99,11 @@ export class CanvasRenderingContext2D { // Invalid font size return; } - let font = findFont(fonts, parsed); + let font: ReturnType | null = findFont(fonts, parsed); if (!font) { if (parsed.family.includes('system-ui')) { - font = addSystemFont(fonts); + // `null` is a special value to use the System font + font = null; } else { return; } diff --git a/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts b/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts index d27f78de..b9ae5e7b 100644 --- a/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts +++ b/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts @@ -11,7 +11,7 @@ import { stub, returnOnThrow, } from '../utils'; -import { addSystemFont, findFont, fonts } from '../font/font-face-set'; +import { findFont, fonts } from '../font/font-face-set'; import { DOMMatrix, type DOMMatrix2DInit } from '../dommatrix'; import type { Path2D } from './path2d'; import type { OffscreenCanvas } from './offscreen-canvas'; @@ -47,7 +47,6 @@ export class OffscreenCanvasRenderingContext2D { const ctx = $.canvasContext2dNew(canvas); Object.setPrototypeOf(ctx, OffscreenCanvasRenderingContext2D.prototype); _.set(ctx, { canvas }); - ctx.font = '10px system-ui'; return ctx; } @@ -98,10 +97,11 @@ export class OffscreenCanvasRenderingContext2D { // Invalid font size return; } - let font = findFont(fonts, parsed); + let font: ReturnType | null = findFont(fonts, parsed); if (!font) { if (parsed.family.includes('system-ui')) { - font = addSystemFont(fonts); + // `null` is a special value to use the System font + font = null; } else { return; } diff --git a/packages/runtime/src/font/font-face-set.ts b/packages/runtime/src/font/font-face-set.ts index b1e62c8a..a98f50b1 100644 --- a/packages/runtime/src/font/font-face-set.ts +++ b/packages/runtime/src/font/font-face-set.ts @@ -1,4 +1,3 @@ -import { $ } from '../$'; import { INTERNAL_SYMBOL } from '../internal'; import { EventTarget } from '../polyfills/event-target'; import { assertInternalConstructor, createInternal, def } from '../utils'; @@ -90,7 +89,7 @@ def(fonts, 'fonts'); export function findFont( fontFaceSet: FontFaceSet, desired: IFont, -): FontFace | null { +): FontFace | undefined { if (!desired.family) { throw new Error('No `font-family` was specified'); } @@ -107,12 +106,4 @@ export function findFont( } } } - return null; -} - -export function addSystemFont(fonts: FontFaceSet): FontFace { - const data = $.getSystemFont(); - const font = new FontFace('system-ui', data); - fonts.add(font); - return font; } diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index e5ef9fa8..172eba19 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -15,6 +15,7 @@ import { UIEvent, PromiseRejectionEvent, } from './polyfills/event'; +import './font/font-face'; export type * from './types'; export type * from './console'; @@ -183,6 +184,12 @@ $.onUnhandledRejection((p, r) => { const btnPlus = 1 << 10; ///< Plus button let previousButtons = 0; +$.onAppletEvent((type) => { + if (type === 1 /* AppletHookType_OnOperationMode */) { + dispatchEvent(new Event('operationmodechange')); + } +}); + $.onFrame((kDown) => { processTimers(); callRafCallbacks(); diff --git a/packages/runtime/src/polyfills/event.ts b/packages/runtime/src/polyfills/event.ts index 4616bf51..7b4aacc3 100644 --- a/packages/runtime/src/polyfills/event.ts +++ b/packages/runtime/src/polyfills/event.ts @@ -74,6 +74,7 @@ export class Event implements globalThis.Event { throw new Error('Method not implemented.'); } } +def(Event); export interface UIEventInit extends EventInit { detail?: number; @@ -93,6 +94,7 @@ export class UIEvent extends Event implements globalThis.UIEvent { throw new Error('Method not implemented.'); } } +def(UIEvent); // Keyboard modifiers bitmasks const CTRL = 1n << 0n; @@ -394,6 +396,7 @@ export class KeyboardEvent extends UIEvent implements globalThis.KeyboardEvent { return code; } } +def(KeyboardEvent); export interface TouchInit { clientX?: number; @@ -520,6 +523,7 @@ export class TouchEvent extends UIEvent implements globalThis.TouchEvent { this.touches = toTouchList(options.touches); } } +def(TouchEvent); export interface ErrorEventInit extends EventInit { colno?: number; @@ -544,6 +548,7 @@ export class ErrorEvent extends Event implements globalThis.ErrorEvent { this.message = this.error?.message ?? ''; } } +def(ErrorEvent); export interface PromiseRejectionEventInit extends EventInit { promise: Promise; @@ -562,10 +567,4 @@ export class PromiseRejectionEvent this.reason = options.reason; } } - -def(Event); -def(ErrorEvent); def(PromiseRejectionEvent); -def(UIEvent); -def(KeyboardEvent); -def(TouchEvent); diff --git a/packages/runtime/src/screen.ts b/packages/runtime/src/screen.ts index 382008ec..d830b925 100644 --- a/packages/runtime/src/screen.ts +++ b/packages/runtime/src/screen.ts @@ -1,13 +1,13 @@ import { $ } from './$'; -import { assertInternalConstructor, createInternal, def } from './utils'; +import { assertInternalConstructor, createInternal, def, proto } from './utils'; import { EventTarget } from './polyfills/event-target'; import { INTERNAL_SYMBOL } from './internal'; import { CanvasRenderingContext2D } from './canvas/canvas-rendering-context-2d'; import { initTouchscreen } from './touchscreen'; -import type { TouchEvent } from './polyfills/event'; +import { Event, type TouchEvent } from './polyfills/event'; interface ScreenInternal { - context2d?: CanvasRenderingContext2D; + ctx?: CanvasRenderingContext2D; } const _ = createInternal(); @@ -19,12 +19,32 @@ export class Screen extends EventTarget implements globalThis.Screen { constructor() { assertInternalConstructor(arguments); super(); - const c = $.canvasNew(1280, 720) as Screen; - Object.setPrototypeOf(c, Screen.prototype); + const c = proto($.canvasNew(1280, 720), Screen); _.set(c, {}); return c; } + /** + * + */ + enableHiRes() { + const i = _(this); + const resize = () => { + let w = 1280, + h = 720; + if ($.appletGetOperationMode() === 'docked') { + w = 1920; + h = 1080; + } + if (w !== this.width || h !== this.height) { + $.canvasResize(this, i.ctx, w, h); + dispatchEvent(new Event('resize')); + } + }; + addEventListener('operationmodechange', resize); + resize(); + } + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Screen/availWidth) */ get availWidth() { return this.width; @@ -51,14 +71,14 @@ export class Screen extends EventTarget implements globalThis.Screen { } /** - * The width of the screen in CSS pixels. + * The width of the screen in pixels. * * @see https://developer.mozilla.org/docs/Web/API/Screen/width */ declare readonly width: number; /** - * The height of the screen in CSS pixels. + * The height of the screen in pixels. * * @see https://developer.mozilla.org/docs/Web/API/Screen/height */ @@ -70,17 +90,15 @@ export class Screen extends EventTarget implements globalThis.Screen { } const i = _(this); - if (!i.context2d) { - i.context2d = new CanvasRenderingContext2D( + if (!i.ctx) { + i.ctx = new CanvasRenderingContext2D( // @ts-expect-error Internal constructor INTERNAL_SYMBOL, this, ); - - $.framebufferInit(this); } - return i.context2d; + return i.ctx; } // @ts-expect-error diff --git a/packages/runtime/src/switch.ts b/packages/runtime/src/switch.ts index 960259dd..75b5ec47 100644 --- a/packages/runtime/src/switch.ts +++ b/packages/runtime/src/switch.ts @@ -21,6 +21,8 @@ export * from './switch/irsensor'; export * from './switch/profile'; export { Socket, Server }; +export type OperationMode = 'docked' | 'handheld'; + export type PathLike = string | URL; export interface Versions { @@ -236,3 +238,10 @@ export function listen(opts: ListenOptions) { } return server; } + +/** + * afd + */ +export function operationMode() { + return $.appletGetOperationMode() as OperationMode; +} diff --git a/packages/runtime/src/window.ts b/packages/runtime/src/window.ts index 0c6d7fda..02d81c38 100644 --- a/packages/runtime/src/window.ts +++ b/packages/runtime/src/window.ts @@ -44,6 +44,19 @@ export function addEventListener( options?: AddEventListenerOptions | boolean, ): void; +/** + * The `resize` event is sent to the global scope when the console changes + * between handheld or docked mode. In handheld mode, only 720p resolution + * is supported. In docked mode, 1080p resolution is used. + * + * @see https://developer.mozilla.org/docs/Web/API/Window/resize_event + */ +export function addEventListener( + type: 'resize', + callback: EventListenerOrEventListenerObject, + options?: AddEventListenerOptions | boolean +): void; + /** * @see https://developer.mozilla.org/docs/Web/API/Element/keydown_event */ diff --git a/source/applet.c b/source/applet.c index a8ac3eea..e0641667 100644 --- a/source/applet.c +++ b/source/applet.c @@ -18,7 +18,7 @@ JSValue nx_appletGetAppletType(JSContext *ctx, JSValueConst this_val, int argc, JSValue nx_appletGetOperationMode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - return JS_NewInt32(ctx, appletGetOperationMode()); + return JS_NewString(ctx, appletGetOperationMode() ? "docked" : "handheld"); } JSValue nx_appletRequestLaunchApplication(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) diff --git a/source/canvas.c b/source/canvas.c index c91eb675..2e854e06 100644 --- a/source/canvas.c +++ b/source/canvas.c @@ -10,6 +10,8 @@ #include "image.h" #include "canvas.h" +static char *default_font = "10px system-ui"; + #define CANVAS_CONTEXT_ARGV0 \ nx_canvas_context_2d_t *context = JS_GetOpaque2(ctx, argv[0], nx_canvas_context_class_id); \ if (!context) \ @@ -98,7 +100,8 @@ static void set_fill_rule(JSContext *ctx, JSValueConst fill_rule, cairo_t *cr) cairo_set_fill_rule(cr, rule); } -static void apply_path(JSContext *ctx, JSValue this_val, JSValue path) { +static void apply_path(JSContext *ctx, JSValue this_val, JSValue path) +{ nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); JSValue apply_path_func = JS_GetPropertyStr(ctx, nx_ctx->init_obj, "applyPath"); JSValue apply_path_argv[] = {this_val, path}; @@ -178,22 +181,26 @@ static JSValue nx_canvas_context_2d_is_point_in_path(JSContext *ctx, JSValueCons { CANVAS_CONTEXT_THIS; JSValue path = JS_NULL; - if (argc > 0 && JS_IsObject(argv[0])) { + if (argc > 0 && JS_IsObject(argv[0])) + { path = argv[0]; argc--; argv++; } bool is_in = false; - if (argc >= 2) { + if (argc >= 2) + { double args[2]; if (js_validate_doubles_args(ctx, argv, args, 2, 0)) return JS_EXCEPTION; - if (argc == 3 && JS_IsString(argv[2])) { + if (argc == 3 && JS_IsString(argv[2])) + { set_fill_rule(ctx, argv[2], cr); } bool needs_restore = false; - if (!JS_IsNull(path)) { + if (!JS_IsNull(path)) + { needs_restore = true; save_path(context); apply_path(ctx, this_val, path); @@ -203,14 +210,16 @@ static JSValue nx_canvas_context_2d_is_point_in_path(JSContext *ctx, JSValueCons matrix.is_2d = true; matrix.values.m11 = matrix.values.m22 = matrix.values.m33 = matrix.values.m44 = 1.; cairo_get_matrix(cr, &matrix.cr_matrix); - if (!nx_dommatrix_is_identity_(&matrix)) { + if (!nx_dommatrix_is_identity_(&matrix)) + { nx_dommatrix_invert_self_(&matrix); double z = 0, w = 1; nx_dommatrix_transform_point_(&matrix, &args[0], &args[1], &z, &w); } is_in = cairo_in_fill(cr, args[0], args[1]); - if (needs_restore) { + if (needs_restore) + { restore_path(context); } } @@ -221,18 +230,21 @@ static JSValue nx_canvas_context_2d_is_point_in_stroke(JSContext *ctx, JSValueCo { CANVAS_CONTEXT_THIS; JSValue path = JS_NULL; - if (argc > 0 && JS_IsObject(argv[0])) { + if (argc > 0 && JS_IsObject(argv[0])) + { path = argv[0]; argc--; argv++; } bool is_in = false; - if (argc >= 2) { + if (argc >= 2) + { double args[2]; if (js_validate_doubles_args(ctx, argv, args, 2, 0)) return JS_EXCEPTION; bool needs_restore = false; - if (!JS_IsNull(path)) { + if (!JS_IsNull(path)) + { needs_restore = true; save_path(context); apply_path(ctx, this_val, path); @@ -242,14 +254,16 @@ static JSValue nx_canvas_context_2d_is_point_in_stroke(JSContext *ctx, JSValueCo matrix.is_2d = true; matrix.values.m11 = matrix.values.m22 = matrix.values.m33 = matrix.values.m44 = 1.; cairo_get_matrix(cr, &matrix.cr_matrix); - if (!nx_dommatrix_is_identity_(&matrix)) { + if (!nx_dommatrix_is_identity_(&matrix)) + { nx_dommatrix_invert_self_(&matrix); double z = 0, w = 1; nx_dommatrix_transform_point_(&matrix, &args[0], &args[1], &z, &w); } is_in = cairo_in_stroke(cr, args[0], args[1]); - if (needs_restore) { + if (needs_restore) + { restore_path(context); } } @@ -807,7 +821,22 @@ static JSValue nx_canvas_context_2d_set_font(JSContext *ctx, JSValueConst this_v { CANVAS_CONTEXT_ARGV0; - context->state->font = argv[1]; + if (JS_IsNull(argv[1])) + { + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); + if (JS_IsUndefined(nx_ctx->system_font)) + { + if (nx_load_system_font(ctx)) + { + return JS_EXCEPTION; + } + } + context->state->font = nx_ctx->system_font; + } + else + { + context->state->font = argv[1]; + } nx_font_face_t *face = nx_get_font_face(ctx, context->state->font); if (!face) return JS_EXCEPTION; @@ -1607,6 +1636,36 @@ nx_canvas_t *nx_get_canvas(JSContext *ctx, JSValueConst obj) return JS_GetOpaque2(ctx, obj, nx_canvas_class_id); } +int initialize_canvas(JSContext *ctx, nx_canvas_t *canvas, int width, int height) +{ + size_t buf_size = width * height * 4; + uint8_t *buffer = js_mallocz(ctx, buf_size); + if (!buffer) + { + return 1; + } + + if (canvas->surface) + { + cairo_surface_destroy(canvas->surface); + } + if (canvas->data) + { + js_free(ctx, canvas->data); + } + + canvas->width = width; + canvas->height = height; + canvas->data = buffer; + canvas->data_size = buf_size; + + // On Switch, the byte order seems to be BGRA + canvas->surface = cairo_image_surface_create_for_data( + buffer, CAIRO_FORMAT_ARGB32, width, height, width * 4); + + return 0; +} + static JSValue nx_canvas_new(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { int width; @@ -1616,35 +1675,59 @@ static JSValue nx_canvas_new(JSContext *ctx, JSValueConst this_val, int argc, JS if (JS_ToInt32(ctx, &height, argv[1])) return JS_EXCEPTION; - size_t buf_size = width * height * 4; - uint8_t *buffer = js_mallocz(ctx, buf_size); - if (!buffer) - return JS_EXCEPTION; - - nx_canvas_t *context = js_mallocz(ctx, sizeof(nx_canvas_t)); - if (!context) + nx_canvas_t *canvas = js_mallocz(ctx, sizeof(nx_canvas_t)); + if (!canvas) return JS_EXCEPTION; JSValue obj = JS_NewObjectClass(ctx, nx_canvas_class_id); if (JS_IsException(obj)) { - js_free(ctx, context); + js_free(ctx, canvas); return obj; } - // On Switch, the byte order seems to be BGRA - cairo_surface_t *surface = cairo_image_surface_create_for_data( - buffer, CAIRO_FORMAT_ARGB32, width, height, width * 4); - - context->width = width; - context->height = height; - context->data = buffer; - context->surface = surface; + if (initialize_canvas(ctx, canvas, width, height)) + { + js_free(ctx, canvas); + return JS_EXCEPTION; + } - JS_SetOpaque(obj, context); + JS_SetOpaque(obj, canvas); return obj; } +static JSValue nx_canvas_resize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + int width, height; + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); + if ( + JS_ToInt32(ctx, &width, argv[2]) || + JS_ToInt32(ctx, &height, argv[3])) + { + return JS_EXCEPTION; + } + nx_canvas_t *canvas = nx_get_canvas(ctx, argv[0]); + if (!canvas) { + return JS_EXCEPTION; + } + if (initialize_canvas(ctx, canvas, width, height)) + { + return JS_EXCEPTION; + } + if (nx_ctx->rendering_mode == NX_RENDERING_MODE_CANVAS && !JS_IsUndefined(argv[1])) { + nx_canvas_context_2d_t *context = nx_get_canvas_context_2d(ctx, argv[1]); + if (!context) { + return JS_EXCEPTION; + } + if (initialize_canvas_context_2d(ctx, context)) + { + return JS_EXCEPTION; + } + //nx_framebuffer_init_(nx_ctx); + } + return JS_UNDEFINED; +} + static JSValue nx_canvas_get_width(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { nx_canvas_t *canvas = nx_get_canvas(ctx, this_val); @@ -1866,6 +1949,10 @@ static JSValue nx_canvas_context_2d_restore(JSContext *ctx, JSValueConst this_va js_free(ctx, prev); nx_font_face_t *face = nx_get_font_face(ctx, context->state->font); + if (!face) + { + return JS_EXCEPTION; + } cairo_set_font_face(cr, face->cairo_font); set_font_size(context, context->state->font_size); } @@ -2696,8 +2783,8 @@ static JSValue nx_canvas_context_2d_set_transform(JSContext *ctx, JSValueConst t JS_ToFloat64(ctx, &m.xy, argv[2]) || JS_ToFloat64(ctx, &m.yy, argv[3]) || JS_ToFloat64(ctx, &m.x0, argv[4]) || - JS_ToFloat64(ctx, &m.y0, argv[5]) - ) { + JS_ToFloat64(ctx, &m.y0, argv[5])) + { return JS_EXCEPTION; } cairo_set_matrix(cr, &m); @@ -2780,31 +2867,38 @@ static JSValue nx_canvas_context_2d_init_class(JSContext *ctx, JSValueConst this return JS_UNDEFINED; } -static JSValue nx_canvas_context_2d_new(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +int initialize_canvas_context_2d(JSContext *ctx, nx_canvas_context_2d_t *context) { - nx_canvas_t *canvas = nx_get_canvas(ctx, argv[0]); - - nx_canvas_context_2d_t *context = js_mallocz(ctx, sizeof(nx_canvas_context_2d_t)); + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); nx_canvas_context_2d_state_t *state = js_mallocz(ctx, sizeof(nx_canvas_context_2d_state_t)); - if (!context || !state) - return JS_EXCEPTION; + if (!state) + { + return 1; + } - JSValue obj = JS_NewObjectClass(ctx, nx_canvas_context_class_id); - if (JS_IsException(obj)) + // Free previous state if this is part of a resize + if (context->state) { - js_free(ctx, context); - js_free(ctx, state); - return obj; + finalizer_canvas_context_2d_state(JS_GetRuntime(ctx), context->state); + } + if (context->ctx) + { + cairo_destroy(context->ctx); } - context->canvas = canvas; context->state = state; - context->ctx = cairo_create(canvas->surface); + context->ctx = cairo_create(context->canvas->surface); + + if (JS_IsUndefined(nx_ctx->system_font)) + { + if (nx_load_system_font(ctx)) + { + return 1; + } + } // Match browser defaults state->next = NULL; - state->font = JS_UNDEFINED; - state->font_size = 10.; state->fill.a = 1.; state->stroke.a = 1.; state->global_alpha = 1.; @@ -2814,29 +2908,65 @@ static JSValue nx_canvas_context_2d_new(JSContext *ctx, JSValueConst this_val, i state->text_baseline = TEXT_BASELINE_ALPHABETIC; cairo_set_line_width(context->ctx, 1.); + state->font = nx_ctx->system_font; + state->font_string = strdup(default_font); + state->font_size = 10.; + nx_font_face_t *face = nx_get_font_face(ctx, state->font); + state->ft_face = face->ft_face; + state->hb_font = face->hb_font; + cairo_set_font_face(context->ctx, face->cairo_font); + set_font_size(context, state->font_size); + + return 0; +} + +static JSValue nx_canvas_context_2d_new(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + nx_canvas_t *canvas = nx_get_canvas(ctx, argv[0]); + + nx_canvas_context_2d_t *context = js_mallocz(ctx, sizeof(nx_canvas_context_2d_t)); + if (!context) + return JS_EXCEPTION; + + JSValue obj = JS_NewObjectClass(ctx, nx_canvas_context_class_id); + if (JS_IsException(obj)) + { + js_free(ctx, context); + return obj; + } + + context->canvas = canvas; + + if (initialize_canvas_context_2d(ctx, context)) + { + js_free(ctx, context); + return JS_EXCEPTION; + } + JS_SetOpaque(obj, context); return obj; } static void finalizer_canvas(JSRuntime *rt, JSValue val) { - nx_canvas_t *context = JS_GetOpaque(val, nx_canvas_class_id); - if (context) + nx_canvas_t *canvas = JS_GetOpaque(val, nx_canvas_class_id); + if (canvas) { - if (context->surface) + if (canvas->surface) { - cairo_surface_destroy(context->surface); + cairo_surface_destroy(canvas->surface); } - if (context->data) + if (canvas->data) { - js_free_rt(rt, context->data); + js_free_rt(rt, canvas->data); } - js_free_rt(rt, context); + js_free_rt(rt, canvas); } } static const JSCFunctionListEntry init_function_list[] = { JS_CFUNC_DEF("canvasNew", 0, nx_canvas_new), + JS_CFUNC_DEF("canvasResize", 0, nx_canvas_resize), JS_CFUNC_DEF("canvasInitClass", 0, nx_canvas_init_class), JS_CFUNC_DEF("canvasContext2dNew", 0, nx_canvas_context_2d_new), JS_CFUNC_DEF("canvasContext2dInitClass", 0, nx_canvas_context_2d_init_class), diff --git a/source/canvas.h b/source/canvas.h index 39571d9d..3c910883 100644 --- a/source/canvas.h +++ b/source/canvas.h @@ -6,16 +6,15 @@ /** * `Screen` / `OffscreenCanvas` / `Image` / `ImageBitmap` */ -typedef struct +typedef struct nx_canvas_s { uint32_t width; uint32_t height; uint8_t *data; + size_t data_size; cairo_surface_t *surface; } nx_canvas_t; -nx_canvas_t *nx_get_canvas(JSContext *ctx, JSValueConst obj); - typedef struct nx_rgba_s { double r; @@ -86,6 +85,9 @@ typedef struct nx_canvas_context_2d_s nx_canvas_context_2d_state_t *state; } nx_canvas_context_2d_t; +nx_canvas_t *nx_get_canvas(JSContext *ctx, JSValueConst obj); nx_canvas_context_2d_t *nx_get_canvas_context_2d(JSContext *ctx, JSValueConst obj); +int initialize_canvas(JSContext* ctx, nx_canvas_t* canvas, int width, int height); +int initialize_canvas_context_2d(JSContext *ctx, nx_canvas_context_2d_t *context); void nx_init_canvas(JSContext *ctx, JSValueConst init_obj); diff --git a/source/font.c b/source/font.c index a5d09297..341c3cea 100644 --- a/source/font.c +++ b/source/font.c @@ -5,17 +5,17 @@ static JSClassID nx_font_face_class_id; -static JSValue nx_new_font_face(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +JSValue nx_new_font_face_(JSContext *ctx, u8 *data, size_t bytes) { nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); nx_font_face_t *context = js_mallocz(ctx, sizeof(nx_font_face_t)); if (!context) + { return JS_EXCEPTION; + } - size_t bytes; - FT_Byte *font_data = JS_GetArrayBuffer(ctx, &bytes, argv[0]); context->font_buffer = js_malloc(ctx, bytes); - memcpy(context->font_buffer, font_data, bytes); + memcpy(context->font_buffer, data, bytes); if (nx_ctx->ft_library == NULL) { @@ -50,37 +50,51 @@ static JSValue nx_new_font_face(JSContext *ctx, JSValueConst this_val, int argc, return obj; } -static JSValue nx_get_system_font(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +static JSValue nx_new_font_face(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + size_t bytes; + FT_Byte *font_data = JS_GetArrayBuffer(ctx, &bytes, argv[0]); + return nx_new_font_face_(ctx, font_data, bytes); +} + +int nx_load_system_font(JSContext *ctx) +{ + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); PlFontData font; Result rc = plGetSharedFontByType(&font, PlSharedFontType_Standard); if (R_FAILED(rc)) { JS_ThrowTypeError(ctx, "Failed to load system font"); - return JS_EXCEPTION; + return 1; + } + JSValue system_font = nx_new_font_face_(ctx, font.address, font.size); + if (JS_IsException(system_font)) + { + return 1; } - return JS_NewArrayBufferCopy(ctx, font.address, font.size); + nx_ctx->system_font = JS_DupValue(ctx, system_font); + return 0; } static void finalizer_font_face(JSRuntime *rt, JSValue val) { - nx_font_face_t *context = JS_GetOpaque(val, nx_font_face_class_id); - if (context) + nx_font_face_t *font = JS_GetOpaque(val, nx_font_face_class_id); + if (font) { - if (context->hb_font) + if (font->hb_font) { - hb_font_destroy(context->hb_font); + hb_font_destroy(font->hb_font); } - if (context->cairo_font) + if (font->cairo_font) { - cairo_font_face_destroy(context->cairo_font); + cairo_font_face_destroy(font->cairo_font); } - if (context->ft_face) + if (font->ft_face) { - FT_Done_Face(context->ft_face); + FT_Done_Face(font->ft_face); } - js_free_rt(rt, context->font_buffer); - js_free_rt(rt, context); + js_free_rt(rt, font->font_buffer); + js_free_rt(rt, font); } } @@ -91,7 +105,6 @@ nx_font_face_t *nx_get_font_face(JSContext *ctx, JSValueConst obj) static const JSCFunctionListEntry function_list[] = { JS_CFUNC_DEF("fontFaceNew", 0, nx_new_font_face), - JS_CFUNC_DEF("getSystemFont", 0, nx_get_system_font), }; void nx_init_font(JSContext *ctx, JSValueConst init_obj) diff --git a/source/font.h b/source/font.h index d752bc07..1b62fd9f 100644 --- a/source/font.h +++ b/source/font.h @@ -9,7 +9,7 @@ #define STR(x) STR_HELPER(x) #define FREETYPE_VERSION_STR STR(FREETYPE_MAJOR) "." STR(FREETYPE_MINOR) "." STR(FREETYPE_PATCH) -typedef struct +typedef struct nx_font_face_s { FT_Face ft_face; hb_font_t *hb_font; @@ -17,5 +17,6 @@ typedef struct FT_Byte *font_buffer; } nx_font_face_t; +int nx_load_system_font(JSContext *ctx); nx_font_face_t *nx_get_font_face(JSContext *ctx, JSValueConst obj); void nx_init_font(JSContext *ctx, JSValueConst init_obj); diff --git a/source/main.c b/source/main.c index 91cd89f2..a3c6f152 100644 --- a/source/main.c +++ b/source/main.c @@ -33,59 +33,42 @@ #define LOG_FILENAME "nxjs-debug.log" -// Text renderer -static PrintConsole *print_console = NULL; - -// Framebuffer renderer -static NWindow *win = NULL; -static Framebuffer *framebuffer = NULL; -static uint8_t *js_framebuffer = NULL; - -void nx_console_exit() +void nx_console_exit(nx_context_t *nx_ctx) { - if (print_console != NULL) + if (nx_ctx->print_console != NULL) { - consoleExit(print_console); - print_console = NULL; + consoleExit(nx_ctx->print_console); + nx_ctx->print_console = NULL; } } -static JSValue nx_framebuffer_init(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +void nx_framebuffer_init_(nx_context_t *nx_ctx) { - nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); - nx_console_exit(); - if (win == NULL) + nx_console_exit(nx_ctx); + if (nx_ctx->win == NULL) { // Retrieve the default window - win = nwindowGetDefault(); + nx_ctx->win = nwindowGetDefault(); } - if (framebuffer != NULL) + if (nx_ctx->framebuffer != NULL) { - framebufferClose(framebuffer); - free(framebuffer); + framebufferClose(nx_ctx->framebuffer); + free(nx_ctx->framebuffer); } - u32 width, height; - nx_canvas_t *canvas = nx_get_canvas(ctx, argv[0]); - if (!canvas) - return JS_EXCEPTION; - width = canvas->width; - height = canvas->height; - js_framebuffer = canvas->data; - framebuffer = malloc(sizeof(Framebuffer)); - framebufferCreate(framebuffer, win, width, height, PIXEL_FORMAT_BGRA_8888, 2); - framebufferMakeLinear(framebuffer); + nx_ctx->framebuffer = malloc(sizeof(Framebuffer)); + nx_canvas_t *canvas = nx_ctx->screen_canvas_context->canvas; + framebufferCreate(nx_ctx->framebuffer, nx_ctx->win, canvas->width, canvas->height, PIXEL_FORMAT_BGRA_8888, 2); + framebufferMakeLinear(nx_ctx->framebuffer); nx_ctx->rendering_mode = NX_RENDERING_MODE_CANVAS; - return JS_UNDEFINED; } -void nx_framebuffer_exit() +void nx_framebuffer_exit(nx_context_t *nx_ctx) { - if (framebuffer != NULL) + if (nx_ctx->framebuffer != NULL) { - framebufferClose(framebuffer); - free(framebuffer); - framebuffer = NULL; - js_framebuffer = NULL; + framebufferClose(nx_ctx->framebuffer); + free(nx_ctx->framebuffer); + nx_ctx->framebuffer = NULL; } } @@ -172,10 +155,10 @@ static JSValue js_print(JSContext *ctx, JSValueConst this_val, int argc, JSValue nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); if (nx_ctx->rendering_mode != NX_RENDERING_MODE_CONSOLE) { - nx_framebuffer_exit(); - if (print_console == NULL) + nx_framebuffer_exit(nx_ctx); + if (nx_ctx->print_console == NULL) { - print_console = consoleInit(NULL); + nx_ctx->print_console = consoleInit(NULL); } nx_ctx->rendering_mode = NX_RENDERING_MODE_CONSOLE; } @@ -415,6 +398,13 @@ static JSValue nx_set_frame_handler(JSContext *ctx, JSValueConst this_val, int a return JS_UNDEFINED; } +static JSValue nx_set_applet_event_handler(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); + nx_ctx->applet_event_handler = JS_DupValue(ctx, argv[0]); + return JS_UNDEFINED; +} + static JSValue nx_set_exit_handler(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); @@ -457,12 +447,29 @@ void nx_process_pending_jobs(JSRuntime *rt) } } +void nx_applet_event_hook(AppletHookType type, void *param) +{ + JSContext *ctx = (JSContext *)param; + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); + if (nx_ctx->had_error) return; + + JSValue args[] = { + JS_NewInt32(ctx, type), + }; + JSValue rtn = JS_Call(ctx, nx_ctx->applet_event_handler, JS_NULL, 1, args); + if (JS_IsException(rtn)) + { + return nx_emit_error_event(ctx); + } + JS_FreeValue(ctx, rtn); +} + // Main program entrypoint int main(int argc, char *argv[]) { Result rc; - print_console = consoleInit(NULL); + PrintConsole *print_console = consoleInit(NULL); rc = socketInitializeDefault(); if (R_FAILED(rc)) @@ -496,11 +503,14 @@ int main(int argc, char *argv[]) nx_context_t *nx_ctx = malloc(sizeof(nx_context_t)); memset(nx_ctx, 0, sizeof(nx_context_t)); + nx_ctx->print_console = print_console; nx_ctx->rendering_mode = NX_RENDERING_MODE_CONSOLE; nx_ctx->thpool = thpool_init(4); + nx_ctx->system_font = JS_UNDEFINED; nx_ctx->frame_handler = JS_UNDEFINED; nx_ctx->exit_handler = JS_UNDEFINED; nx_ctx->error_handler = JS_UNDEFINED; + nx_ctx->applet_event_handler = JS_UNDEFINED; nx_ctx->unhandled_rejection_handler = JS_UNDEFINED; pthread_mutex_init(&(nx_ctx->async_done_mutex), NULL); JS_SetContextOpaque(ctx, nx_ctx); @@ -578,9 +588,7 @@ int main(int argc, char *argv[]) JS_CFUNC_DEF("onExit", 1, nx_set_exit_handler), JS_CFUNC_DEF("onFrame", 1, nx_set_frame_handler), - - // framebuffer renderer - JS_CFUNC_DEF("framebufferInit", 1, nx_framebuffer_init), + JS_CFUNC_DEF("onAppletEvent", 1, nx_set_applet_event_handler), // hid JS_CFUNC_DEF("hidInitializeKeyboard", 0, js_hid_initialize_keyboard), @@ -665,6 +673,9 @@ int main(int argc, char *argv[]) free(js_path); } + AppletHookCookie cookie; + appletHook(&cookie, nx_applet_event_hook, ctx); + main_loop: while (appletMainLoop()) { @@ -723,9 +734,12 @@ int main(int argc, char *argv[]) { // Copy the JS framebuffer to the current Switch buffer u32 stride; - u8 *framebuf = (u8 *)framebufferBegin(framebuffer, &stride); - memcpy(framebuf, js_framebuffer, 1280 * 720 * 4); - framebufferEnd(framebuffer); + u8 *framebuf = (u8 *)framebufferBegin(nx_ctx->framebuffer, &stride); + memcpy( + framebuf, + nx_ctx->screen_canvas_context->canvas->data, + nx_ctx->screen_canvas_context->canvas->data_size); + framebufferEnd(nx_ctx->framebuffer); } } @@ -734,26 +748,30 @@ int main(int argc, char *argv[]) thpool_wait(nx_ctx->thpool); thpool_destroy(nx_ctx->thpool); + appletUnhook(&cookie); + // Call exit handler JSValue ret_val = JS_Call(ctx, nx_ctx->exit_handler, JS_NULL, 0, NULL); JS_FreeValue(ctx, ret_val); if (nx_ctx->rendering_mode == NX_RENDERING_MODE_CONSOLE) { - nx_console_exit(); + nx_console_exit(nx_ctx); } else if (nx_ctx->rendering_mode == NX_RENDERING_MODE_CANVAS) { - nx_framebuffer_exit(); + nx_framebuffer_exit(nx_ctx); } fclose(debug_fd); FILE *leaks_fd = freopen(LOG_FILENAME, "a", stdout); JS_FreeValue(ctx, global_obj); + JS_FreeValue(ctx, nx_ctx->system_font); JS_FreeValue(ctx, nx_ctx->frame_handler); JS_FreeValue(ctx, nx_ctx->exit_handler); JS_FreeValue(ctx, nx_ctx->error_handler); + JS_FreeValue(ctx, nx_ctx->applet_event_handler); JS_FreeValue(ctx, nx_ctx->unhandled_rejection_handler); JS_FreeContext(ctx); diff --git a/source/types.h b/source/types.h index 4f932b13..295a7858 100644 --- a/source/types.h +++ b/source/types.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -67,6 +68,8 @@ enum nx_rendering_mode NX_RENDERING_MODE_CANVAS }; +struct nx_canvas_context_2d_s; + typedef struct nx_context_s { int had_error; @@ -78,14 +81,24 @@ typedef struct nx_context_s FT_Library ft_library; HidVibrationDeviceHandle vibration_device_handles[2]; IM3Environment wasm_env; + struct nx_canvas_context_2d_s *screen_canvas_context; + JSValue system_font; JSValue init_obj; JSValue frame_handler; JSValue exit_handler; JSValue error_handler; + JSValue applet_event_handler; JSValue unhandled_rejection_handler; - // mbedtls structures shared by all TLS connections + // `mbedtls` structures shared by all TLS connections bool mbedtls_initialized; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; + + // Text renderer + PrintConsole *print_console; + + // Framebuffer renderer + NWindow *win; + Framebuffer *framebuffer; } nx_context_t;