diff --git a/examples/pong/entry-point.cpp b/examples/pong/entry-point.cpp index 2fbadaed..4aa5332b 100644 --- a/examples/pong/entry-point.cpp +++ b/examples/pong/entry-point.cpp @@ -11,39 +11,44 @@ static int ANIMATION_LOOP_EVENT_ID = -1; static int KEY_DOWN_EVENT_ID = -1; static int KEY_UP_EVENT_ID = -1; extern "C" { - __attribute__((import_name("registerKeyUpEvent"))) - void register_key_up_event(int event_id); + // __attribute__((import_name("registerKeyUpEvent"))) + // void register_key_up_event(int event_id); - __attribute__((import_name("registerKeyDownEvent"))) - void register_key_down_event(int event_id); + // __attribute__((import_name("registerKeyDownEvent"))) + // void register_key_down_event(int event_id); - __attribute__((import_name("registerAnimationLoop"))) - void register_animation_loop(int event_id); + // __attribute__((import_name("registerAnimationLoop"))) + // void register_animation_loop(int event_id); - __attribute__((import_name("registerMouseMoveEvent"))) - void register_mouse_move_event(int event_id, const char* element_id, bool relative); + // __attribute__((import_name("registerMouseMoveEvent"))) + // void register_mouse_move_event(int event_id, const char* element_id, bool relative); - __attribute__((import_name("registerMousePressEvent"))) - void register_mouse_press_event(int event_id, const char* element_id, bool relative); + // __attribute__((import_name("registerMousePressEvent"))) + // void register_mouse_press_event(int event_id, const char* element_id, bool relative); __attribute__((export_name("initMenu"))) void init_menu() { MOUSE_MOVE_EVENT_ID = twr_register_callback("menuMouseMoveCallback"); - register_mouse_move_event(MOUSE_MOVE_EVENT_ID, "twr_d2dcanvas", true); + // register_mouse_move_event(MOUSE_MOVE_EVENT_ID, "twr_d2dcanvas", true); + d2d_register_event(D2D_MOUSE_MOVE, MOUSE_MOVE_EVENT_ID); MOUSE_PRESS_EVENT_ID = twr_register_callback("menuMousePressCallback"); - register_mouse_press_event(MOUSE_PRESS_EVENT_ID, "twr_d2dcanvas", true); + // register_mouse_press_event(MOUSE_PRESS_EVENT_ID, "twr_d2dcanvas", true); + d2d_register_event(D2D_MOUSE_DOWN, MOUSE_PRESS_EVENT_ID); ANIMATION_LOOP_EVENT_ID = twr_register_callback("menuAnimationLoopCallback"); - register_animation_loop(ANIMATION_LOOP_EVENT_ID); + // register_animation_loop(ANIMATION_LOOP_EVENT_ID); + d2d_register_event(D2D_ANIMATION_FRAME, ANIMATION_LOOP_EVENT_ID); KEY_DOWN_EVENT_ID = twr_register_callback("menuKeyDownCallback"); - register_key_down_event(KEY_DOWN_EVENT_ID); + // register_key_down_event(KEY_DOWN_EVENT_ID); + d2d_register_event(D2D_KEY_DOWN, KEY_DOWN_EVENT_ID); KEY_UP_EVENT_ID = twr_register_callback("menuKeyUpCallback"); - register_key_up_event(KEY_UP_EVENT_ID); + // register_key_up_event(KEY_UP_EVENT_ID); + d2d_register_event(D2D_KEY_UP, KEY_UP_EVENT_ID); menu.setBounds(d2d_get_canvas_prop("canvasWidth"), d2d_get_canvas_prop("canvasHeight")); } diff --git a/examples/pong/index.html b/examples/pong/index.html index 4c11b71b..fc8b84fd 100644 --- a/examples/pong/index.html +++ b/examples/pong/index.html @@ -12,7 +12,7 @@ -
+
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/examples/window/package.json b/examples/window/package.json new file mode 100644 index 00000000..3e452583 --- /dev/null +++ b/examples/window/package.json @@ -0,0 +1,11 @@ +{ + "@parcel/resolver-default": { + "packageExports": true + }, + "alias": { + "twr-wasm": "../../lib-js/index.js" + }, + "dependencies": { + "twr-wasm": "^2.0.0" + } +} diff --git a/examples/window/window.c b/examples/window/window.c new file mode 100644 index 00000000..6e4e6608 --- /dev/null +++ b/examples/window/window.c @@ -0,0 +1,1002 @@ +#include "twr-window.h" +#include "twr-draw2d.h" +#include "twr-crt.h" +#include "stdlib.h" +#include "string.h" +#include + +twr_ioconsole_t* window_con = NULL; +twr_ioconsole_t* canvas_con = NULL; + +long canvas_width = 0; +long canvas_height = 0; + + +struct twr_window_widget box_color_menu; +struct twr_window_widget test_two_menu; +struct twr_window_widget extra_menu; +struct twr_window_widget prop_menu; +struct twr_window_widget menu_prop_menu; +unsigned long box_color = 0xFF0000FF; +const long WIDGET_HEIGHT = 15; +int box_border = 0; + +#define COLORS_LEN 76 +const char* COLORS[COLORS_LEN] = { + "Salmon", + "Red", + "DarkRed", + "Pink", + "DeepPink", + "MediumVioletRed", + "Coral", + "Tomato", + "OrangeRed", + "Orange", + "Gold", + "Yellow", + "LightYellow", + "DarkKhaki", + "Lavender", + "Thistle", + "Violet", + "Fuchsia", + "Magenta", + "MediumOrchid", + "MediumPurple", + "RebeccaPurple", + "DarkMagenta", + "Purple", + "Indigo", + "SlateBlue", + "DarkSlateBlue", + "GreenYellow", + "LimeGreen", + "LightGreen", + "MediumSpringGreen", + "MediumSeaGreen", + "Green", + "OliveDrab", + "Olive", + "MediumAquamarine", + "LightSeaGreen", + "DarkCyan", + "Aqua", + "PaleTurquiose", + "Aquamarine", + "CadetBlue", + "SteelBlue", + "LightSteelBlue", + "DeepSkyBlue", + "DodgetBlue", + "MediumSlateBlue", + "RoyalBlue", + "Blue", + "Navy", + "BurlyWood", + "RosyBrown", + "SandyBrown", + "Goldenrod", + "DarkGoldenrod", + "Peru", + "Chocolate", + "SaddleBrown", + "Sienna", + "Brown", + "Maroon", + "White", + "GhostWhite", + "SeaShell", + "Biege", + "Ivory", + "MistyRose", + "Gainsboro", + "LightGray", + "Silver", + "Gray", + "DimGray", + "LightSlateGray", + "SlateGray", + "DarkSlateGray", + "Black" +}; +unsigned long box_center_dot_color_index = 0; +int box_center_dot = 0; + +struct twr_window_widget spawn_button; +int delete_button_callback; + +struct DynamicWidgetArray { + struct twr_window_widget* arr; + unsigned long len; +}; + +enum MOUSE_EVENT_TYPE { + MOUSE_EVENT_MOVE, + MOUSE_EVENT_LEFT_CLICK, + MOUSE_EVENT_DOUBLE_CLICK, +}; +int mouse_event_ids[3] = {}; +enum MOUSE_EVENT_TYPE box_move_event_type = MOUSE_EVENT_MOVE; + +void setup_box_options_menu(struct twr_window_widget *box_options_menu); +void setup_test_two_menu(struct twr_window_widget *test_two_menu); +void setup_extra_menu(struct twr_window_widget *extra_menu); +void setup_prop_menu(struct twr_window_widget *prop_menu); +void setup_menu_prop_menu(struct twr_window_widget *menu_prop_menu); +__attribute__((export_name("init"))) +void init() { + if (window_con) + free(window_con); + + if (canvas_con) + free(canvas_con); + + window_con = twr_get_console("window"); + canvas_con = twr_window_get_draw_canvas(window_con); + + twr_set_std2d_con(canvas_con); + + canvas_width = io_get_prop(canvas_con, "canvasWidth"); + canvas_height = io_get_prop(canvas_con, "canvasHeight"); + + printf("canvas size: %ld, %ld\n", canvas_width, canvas_height); + + int ANIMATION_EVENT = twr_register_callback("animationFrame"); + d2d_register_event(D2D_ANIMATION_FRAME, ANIMATION_EVENT); + + + + int MOUSE_MOVE_EVENT = twr_register_callback("mouseMoveHandler"); + d2d_register_event(D2D_MOUSE_MOVE, MOUSE_MOVE_EVENT); + mouse_event_ids[MOUSE_EVENT_MOVE] = MOUSE_MOVE_EVENT; + + int MOUSE_LEFT_CLICK_EVENT = twr_register_callback("mouseMoveHandler"); + d2d_register_event(D2D_MOUSE_CLICK, MOUSE_LEFT_CLICK_EVENT); + mouse_event_ids[MOUSE_EVENT_LEFT_CLICK] = MOUSE_LEFT_CLICK_EVENT; + + int MOUSE_DBL_CLICK_EVENT = twr_register_callback("mouseMoveHandler"); + d2d_register_event(D2D_MOUSE_DBLCLICK, MOUSE_DBL_CLICK_EVENT); + mouse_event_ids[MOUSE_EVENT_DOUBLE_CLICK] = MOUSE_DBL_CLICK_EVENT; + + int KEY_PRESS_EVENT = twr_register_callback("keyEventHandler"); + d2d_register_event(D2D_KEY_DOWN, KEY_PRESS_EVENT); + + int WINDOW_RESIZE_EVENT = twr_register_callback("windowResizeHandler"); + twr_window_register_event(window_con, TWR_WINDOW_RESIZE_EVENT, WINDOW_RESIZE_EVENT); + + + + box_color_menu = twr_window_add_menu(window_con, "Box Options"); + test_two_menu = twr_window_add_menu(window_con, "test_two"); + extra_menu = twr_window_add_menu(window_con, "extra"); + prop_menu = twr_window_add_menu(window_con, "prop_modifiers"); + menu_prop_menu = twr_window_add_menu(window_con, "Menu_Prop_Menu"); + + setup_box_options_menu(&box_color_menu); + setup_test_two_menu(&test_two_menu); + setup_extra_menu(&extra_menu); + setup_prop_menu(&prop_menu); + setup_menu_prop_menu(&menu_prop_menu); +} + +int square_x = 75; +int square_y = 75; +int SQUARE_WIDTH = 50; +int SQUARE_HEIGHT = 50; +void force_square_into_bounds() { + //ensure it's within the bottom right bounds + if (square_x + SQUARE_WIDTH > canvas_width) { + square_x = canvas_width - SQUARE_WIDTH; + } + if (square_y + SQUARE_HEIGHT >canvas_height) { + square_y = canvas_height - SQUARE_HEIGHT; + } + + //ensure if it's within the top left bounds + // top left is checked seperately so it's prioritized over the bottom right bounds + if (square_x < 0) { + square_x = 0; + } + if (square_y < 0) { + square_y = 0; + } +} +__attribute__((export_name("windowResizeHandler"))) +void window_resize_handler(int event_id, long width, long height) { + canvas_width = io_get_prop(canvas_con, "canvasWidth"); + canvas_height = io_get_prop(canvas_con, "canvasHeight"); + + printf("new canvas size: %ld, %ld\n", canvas_width, canvas_height); + + force_square_into_bounds(); +} + +struct prop_menu_data { + struct twr_window_widget* widget; + struct twr_widget_prop_details* prop_details; +}; + + +enum prop_menu_state { + PROP_MENU_UNOPENED, + PROP_MENU_SET, + PROP_MENU_GET +}; +enum prop_menu_selected { + PROP_MENU_SELECTED_TRUE, + PROP_MENU_SELECTED_FALSE, + PROP_MENU_SELECTED_UNDEFINED, + PROP_MENU_SELECTED_NUMBER, + PROP_MENU_SELECTED_STRING +}; +enum prop_menu_target { + PROP_MENU_TARGET_WIDGET, + PROP_MENU_TARGET_MENU +}; +struct text_fill_buffer { + char text[100]; + int length; +}; +void append_to_text_fill_buffer(struct text_fill_buffer* buffer, char ch) { + assert(buffer->length < 99); + if (buffer->length < 99) { + buffer->text[buffer->length] = ch; + buffer->length++; + buffer->text[buffer->length] = '\0'; + } +} +bool try_pop_from_text_fill_buffer(struct text_fill_buffer* buffer, char* ret_ch) { + if (buffer->length > 0) { + *ret_ch = buffer->text[buffer->length-1]; + buffer->length--; + buffer->text[buffer->length] = '\0'; + return TRUE; + } else { + return FALSE; + } +} +struct prop_menu_popup { + const char* prop_name; + enum prop_menu_state state; + enum prop_menu_target target; + struct text_fill_buffer text_buffer; + struct text_fill_buffer number_buffer; + bool number_buffer_has_dot; + enum prop_menu_selected accepted_types[5]; + int accepted_types_len; + int selected_type; + struct twr_window_widget* widget; + long width, height; +}; +const int BASE_PROP_MENU_HEIGHT = 25; +const int PROP_MENU_HEIGHT_PER_FIELD = 25; +const int POPUP_TITLE_HEIGHT = 20; +struct prop_menu_popup prop_menu_data = { + .state = PROP_MENU_UNOPENED +}; +void set_prop_menu_data_to_getter(const char* prop_name, const struct twr_widget_prop_value* val, enum prop_menu_target target) { + prop_menu_data.state = PROP_MENU_GET; + prop_menu_data.target = target; + // prop_menu_data.selected = PROP_MENU_SELECTED_NONE; + prop_menu_data.prop_name = prop_name; + prop_menu_data.width = 200; + prop_menu_data.height = BASE_PROP_MENU_HEIGHT + PROP_MENU_HEIGHT_PER_FIELD; + switch (val->type) { + case WINDOW_WIDGET_PROP_BOOLEAN: + sprintf(prop_menu_data.text_buffer.text, "%s", val->boolean ? "true" : "false"); + break; + case WINDOW_WIDGET_PROP_NUMBER: + sprintf(prop_menu_data.text_buffer.text, "%f", val->number); + break; + case WINDOW_WIDGET_PROP_STRING: + sprintf(prop_menu_data.text_buffer.text, "%s", val->string); + break; + case WINDOW_WIDGET_PROP_UNDEFINED: + sprintf(prop_menu_data.text_buffer.text, "undefined"); + break; + } +} +__attribute__((export_name("windowPropMenuGetEvent"))) +void window_prop_menu_get_event(int event_id, struct prop_menu_data* prop_data) { + struct twr_widget_prop_value* val = twr_window_menu_widget_get_prop(prop_data->widget, prop_data->prop_details->name); + + set_prop_menu_data_to_getter(prop_data->prop_details->name, val, PROP_MENU_TARGET_WIDGET); + + free(val); +} +unsigned int count_ones(unsigned int n) { + int i = 0; + for (; n > 0; n &= (n - 1), i++); + return i; +} +void set_prop_menu_data_to_setter(const char* prop_name, struct twr_window_widget* widget, enum WindowWidgetPropVal combined_types, enum prop_menu_target target) { + prop_menu_data = (struct prop_menu_popup){ + .prop_name = prop_name, + .state = PROP_MENU_SET, + .target = target, + .text_buffer = (struct text_fill_buffer){ + .text = "", + .length = 0 + }, + .number_buffer = (struct text_fill_buffer){ + .text = "", + .length = 0 + }, + .number_buffer_has_dot = FALSE, + // .accepted_types = accepted_types, + // .accepted_types_len = accepted_types_length, + .accepted_types_len = 0, + .selected_type = 0, + .widget = widget, + .width = 200, + .height = BASE_PROP_MENU_HEIGHT + PROP_MENU_HEIGHT_PER_FIELD * count_ones((unsigned int)combined_types), + }; + const int num_types = 4; + const enum WindowWidgetPropVal types[] = { + WINDOW_WIDGET_PROP_STRING, + WINDOW_WIDGET_PROP_BOOLEAN, + WINDOW_WIDGET_PROP_NUMBER, + WINDOW_WIDGET_PROP_UNDEFINED + }; + for (int i = 0; i < num_types; i++) { + if (combined_types & types[i]) { + enum prop_menu_selected to_add; + switch (types[i]) { + case WINDOW_WIDGET_PROP_STRING: + to_add = PROP_MENU_SELECTED_STRING; + break; + case WINDOW_WIDGET_PROP_BOOLEAN: + prop_menu_data.accepted_types[prop_menu_data.accepted_types_len] = PROP_MENU_SELECTED_TRUE; + prop_menu_data.accepted_types_len++; + to_add = PROP_MENU_SELECTED_FALSE; + break; + case WINDOW_WIDGET_PROP_NUMBER: + to_add = PROP_MENU_SELECTED_NUMBER; + break; + case WINDOW_WIDGET_PROP_UNDEFINED: + to_add = PROP_MENU_SELECTED_UNDEFINED; + break; + } + prop_menu_data.accepted_types[prop_menu_data.accepted_types_len] = to_add; + prop_menu_data.accepted_types_len++; + } + } +} +__attribute__((export_name("windowPropMenuSetEvent"))) +void window_prop_menu_set_event(int event_id, struct prop_menu_data* prop_data) { + set_prop_menu_data_to_setter(prop_data->prop_details->name, prop_data->widget, prop_data->prop_details->type, PROP_MENU_TARGET_WIDGET); +} +void setup_prop_menu(struct twr_window_widget *prop_menu) { + struct twr_window_widget modified_prop = twr_window_menu_add_check_box_widget(prop_menu, WIDGET_HEIGHT, "Test Widget"); + struct twr_window_widget* modified_prop_heap = (struct twr_window_widget*)malloc(sizeof(struct twr_window_widget)); + *modified_prop_heap = modified_prop; + + struct twr_window_widget setter_menu = twr_window_menu_add_sub_menu_widget_reduced(prop_menu, WIDGET_HEIGHT, "Setters"); + struct twr_window_widget getter_menu = twr_window_menu_add_sub_menu_widget_reduced(prop_menu, WIDGET_HEIGHT, "Getters"); + + + long prop_len; + struct twr_widget_prop_details* props = twr_window_menu_widget_list_props(&modified_prop, &prop_len); + int getter_event_id = twr_register_callback("windowPropMenuGetEvent"); + int setter_event_id = twr_register_callback("windowPropMenuSetEvent"); + // printf("C alloc: %d\nC Length: %ld\n", (int)props, prop_len); + for (long i = 0; i < prop_len; i++) { + struct prop_menu_data* prop_data = (struct prop_menu_data*)malloc(sizeof(struct prop_menu_data)); + *prop_data = (struct prop_menu_data){ + .prop_details = &(props[i]), + .widget = modified_prop_heap, + }; + if (prop_data->prop_details->access & WINDOW_WIDGET_PROP_GET) { + struct twr_window_widget get_button = twr_window_menu_add_button_widget( + &getter_menu, + WIDGET_HEIGHT, + prop_data->prop_details->name + ); + twr_window_menu_widget_add_callback(&get_button, getter_event_id, (void*)prop_data); + } + if (prop_data->prop_details->access & WINDOW_WIDGET_PROP_SET) { + struct twr_window_widget set_button = twr_window_menu_add_button_widget( + &setter_menu, + WIDGET_HEIGHT, + prop_data->prop_details->name + ); + twr_window_menu_widget_add_callback(&set_button, setter_event_id, (void*)prop_data); + } + } + // free(props); +} + + +__attribute__((export_name("boxColorChanged"))) +void box_color_changed(int event_id, void* extra, int selected) { + if (selected) + box_color = (int)extra; +} + +void setup_box_color_sub_menu(struct twr_window_widget* box_color_sub_menu) { + const char* BOX_COLOR_NAMES[20] = { + "Red", + "Blue", + "Magenta", + "Yellow", + "Cyan", + "Teal" + }; + const int BOX_COLOR_VALUES[] = { + 0xFF0000FF, + 0x0000FFFF, + 0xFF00FFFF, + 0xFFFF00FF, + 0x00FFFFFF, + 0x008080FF + }; + int box_color_event = twr_register_callback("boxColorChanged"); + struct twr_window_widget root_radio_item; + for (int i = 0; i < 6; i++) { + struct twr_window_widget radioItem = twr_window_menu_add_radio_item_widget(box_color_sub_menu, WIDGET_HEIGHT, BOX_COLOR_NAMES[i]); + twr_window_menu_widget_add_callback(&radioItem, box_color_event, (void*)(BOX_COLOR_VALUES[i])); + + if (i == 0) { + root_radio_item = radioItem; + } else { + twr_window_menu_radio_item_merge(&root_radio_item, &radioItem); + } + } +} +void setup_box_options_menu(struct twr_window_widget* box_options_menu) { + struct twr_window_widget box_color_sub_menu = twr_window_menu_add_sub_menu_widget_reduced(box_options_menu, WIDGET_HEIGHT, "Box Color"); + setup_box_color_sub_menu(&box_color_sub_menu); + + struct twr_window_widget check_box = twr_window_menu_add_check_box_widget(box_options_menu, WIDGET_HEIGHT, "Box Outline"); + int check_box_event = twr_register_callback("boxBorderChanged"); + twr_window_menu_widget_add_callback(&check_box, check_box_event, (void*)0); +} + + +__attribute__((export_name("spawnButtonPressed"))) +void spawn_button_pressed(int event_id, void* ptr) { + struct twr_window_widget button = twr_window_menu_add_button_widget( + &test_two_menu, + WIDGET_HEIGHT, + "Delete!" + ); + struct twr_window_widget* heap_button = (struct twr_window_widget*)malloc(sizeof(struct twr_window_widget)); + memcpy(heap_button, &button, sizeof(struct twr_window_widget)); + + printf("spawned new button: %d\n", button.widget_id); + + char name[30] = ""; + sprintf(name, "Delete Button: %d", button.widget_id); + twr_window_menu_widget_set_string(&button, "text", name); + + twr_window_menu_widget_add_callback(&button, delete_button_callback, (void*)heap_button); +} +__attribute__((export_name("deleteButtonPressed"))) +void delete_button_pressed(int event_id, struct twr_window_widget* button) { + char name[30] = ""; + sprintf(name, "Delete Button: %d", button->widget_id); + + char* real_name; + assert(twr_window_menu_widget_get_prop_string(button, "text", &real_name)); + assert(strcmp(name, real_name) == 0); + + free(real_name); + twr_window_menu_delete_widget(button); + free(button); +} + +void setup_test_two_menu(struct twr_window_widget *test_two_menu) { + spawn_button = twr_window_menu_add_button_widget( + test_two_menu, + WIDGET_HEIGHT, + "Spawn New Button" + ); + + int spawn_button_callback = twr_register_callback("spawnButtonPressed"); + twr_window_menu_widget_add_callback(&spawn_button, spawn_button_callback, (void*)0); + delete_button_callback = twr_register_callback("deleteButtonPressed"); + + twr_window_menu_add_seperator_widget(test_two_menu, WIDGET_HEIGHT, "-", NULL); +} + +void set_widget_visibility(struct twr_window_widget* widget, int visibility) { + twr_window_menu_widget_set_bool(widget, "isVisible", visibility); +} + +void setup_extra_box_movement_sub_menu(struct twr_window_widget* box_movement_menu) { + struct DynamicWidgetArray* extra_box_movement_items = (struct DynamicWidgetArray*)malloc(sizeof(struct DynamicWidgetArray)); + extra_box_movement_items->len = 4; + extra_box_movement_items->arr = (struct twr_window_widget*)malloc(sizeof(struct twr_window_widget) * extra_box_movement_items->len); + + struct twr_window_widget extra_box_movement_show_box = twr_window_menu_add_check_box_widget(box_movement_menu, WIDGET_HEIGHT, "Show"); + + extra_box_movement_items->arr[0] = twr_window_menu_add_seperator_widget(box_movement_menu, WIDGET_HEIGHT, "-", NULL); + + const char* MOUSE_MOVE_METHOD_NAMES[3] = { + "On Mouse Movement", + "On Left Click", + "On Double Click" + }; + const enum MOUSE_EVENT_TYPE MOUSE_MOVE_METHOD_TYPES[] = { + MOUSE_EVENT_MOVE, + MOUSE_EVENT_LEFT_CLICK, + MOUSE_EVENT_DOUBLE_CLICK + }; + struct twr_window_widget root_radio_item; + int extra_box_movement_options_event_id = twr_register_callback("extraBoxMovementOptionCallback"); + for (int i = 0; i < 3; i++) { + struct twr_window_widget radio_item = twr_window_menu_add_radio_item_widget(box_movement_menu, WIDGET_HEIGHT, MOUSE_MOVE_METHOD_NAMES[i]); + twr_window_menu_widget_add_callback(&radio_item, extra_box_movement_options_event_id, (void*)MOUSE_MOVE_METHOD_TYPES[i]); + if (i == 0) { + root_radio_item = radio_item; + } else { + twr_window_menu_radio_item_merge(&root_radio_item, &radio_item); + } + extra_box_movement_items->arr[i+1] = radio_item; + } + + for (int i = 0; i < extra_box_movement_items->len; i++) { + set_widget_visibility(&extra_box_movement_items->arr[i], 0); + } + + int visibility_event_id = twr_register_callback("extraCheckBoxCallback"); + twr_window_menu_widget_add_callback(&extra_box_movement_show_box, visibility_event_id, (void*)extra_box_movement_items); + +} + +void setup_extra_menu(struct twr_window_widget *extra_menu) { + struct twr_window_widget extra_check_box = twr_window_menu_add_check_box_widget(extra_menu, WIDGET_HEIGHT, "Show Extra Options"); + int extra_checkbox_event_id = twr_register_callback("extraCheckBoxCallback"); + + struct DynamicWidgetArray* extra_widget_array = (struct DynamicWidgetArray*)malloc(sizeof(struct DynamicWidgetArray)); + extra_widget_array->len = 4; + extra_widget_array->arr = (struct twr_window_widget*)malloc(sizeof(struct twr_window_widget) * extra_widget_array->len); + + extra_widget_array->arr[0] = twr_window_menu_add_seperator_widget(extra_menu, WIDGET_HEIGHT, "-", NULL); + + struct twr_window_widget extra_center_dot_checkbox = twr_window_menu_add_check_box_widget(extra_menu, WIDGET_HEIGHT, "Centered Dot"); + int extra_center_dot_event_id = twr_register_callback("extraCenterDotCallback"); + twr_window_menu_widget_add_callback(&extra_center_dot_checkbox, extra_center_dot_event_id, (void*)0); + extra_widget_array->arr[1] = extra_center_dot_checkbox; + + struct twr_window_widget randomize_center_dot_color = twr_window_menu_add_button_widget( + extra_menu, + WIDGET_HEIGHT, + "Randomize Center Dot Color" + ); + int randomize_center_dot_color_event_id = twr_register_callback("randomizeCenterDotColorCallback"); + twr_window_menu_widget_add_callback(&randomize_center_dot_color, randomize_center_dot_color_event_id, (void*)0); + extra_widget_array->arr[2] = randomize_center_dot_color; + + struct twr_window_widget extra_box_movement_menu = twr_window_menu_add_sub_menu_widget_reduced(extra_menu, WIDGET_HEIGHT, "Box Movement"); + setup_extra_box_movement_sub_menu(&extra_box_movement_menu); + extra_widget_array->arr[3] = extra_box_movement_menu; + + twr_window_menu_widget_add_callback(&extra_check_box, extra_checkbox_event_id, (void*)extra_widget_array); + + for (int i = 0; i < extra_widget_array->len; i++) { + set_widget_visibility(&extra_widget_array->arr[i], 0); + } +} + +void setup_menu_prop_menu(struct twr_window_widget *menu_prop_menu) { + struct twr_window_widget getters = twr_window_menu_add_sub_menu_widget_reduced(menu_prop_menu, WIDGET_HEIGHT, "Getters"); + struct twr_window_widget setters = twr_window_menu_add_sub_menu_widget_reduced(menu_prop_menu, WIDGET_HEIGHT, "Setters"); + + long length = 0; + struct twr_menu_prop_details* prop_list = twr_window_menu_list_props(window_con, &length); + + int getter_event_id = twr_register_callback("menuPropMenuGetterCallback"); + int setter_event_id = twr_register_callback("menuPropMenuSetterCallback"); + for (long i = 0; i < length; i++) { + struct twr_window_widget getter = twr_window_menu_add_button_widget(&getters, WIDGET_HEIGHT, prop_list[i].name); + struct twr_window_widget setter = twr_window_menu_add_button_widget(&setters, WIDGET_HEIGHT, prop_list[i].name); + + twr_window_menu_widget_add_callback(&getter, getter_event_id, (void*)&prop_list[i]); + twr_window_menu_widget_add_callback(&setter, setter_event_id, (void*)&prop_list[i]); + } +} + +__attribute__((export_name("menuPropMenuSetterCallback"))) +void menu_prop_menu_setter_callback(int event_id, struct twr_menu_prop_details* details) { + set_prop_menu_data_to_setter(details->name, NULL, details->type, PROP_MENU_TARGET_MENU); +} +__attribute__((export_name("menuPropMenuGetterCallback"))) +void menu_prop_menu_getter_callback(int event_id, struct twr_menu_prop_details* details) { + struct twr_widget_prop_value* val = twr_window_menu_get_prop(window_con, details->name); + + set_prop_menu_data_to_getter(details->name, val, PROP_MENU_TARGET_MENU); + + free(val); +} +__attribute__((export_name("extraBoxMovementOptionCallback"))) +void extra_box_movement_option_callback(int event_id, void* extra, int selected) { + if (selected) + box_move_event_type = (enum MOUSE_EVENT_TYPE)extra; +} +__attribute__((export_name("randomizeCenterDotColorCallback"))) +void randomize_center_dot_color_callback(int event_id, void* _) { + box_center_dot_color_index = rand()%COLORS_LEN; +} +__attribute__((export_name("extraCenterDotCallback"))) +void extra_center_dot_callback(int event_id, void* _, int new_state) { + printf("center box change! %d\n", new_state); + box_center_dot = new_state; +} + +__attribute__((export_name("extraCheckBoxCallback"))) +void extra_check_box_callback(int event_id, struct DynamicWidgetArray* arr, int new_state) { + for (int i = 0; i < arr->len; i++) { + set_widget_visibility(&arr->arr[i], new_state); + } +} +__attribute__((export_name("boxBorderChanged"))) +void box_border_changed(int event_id, void* _, int new_state) { + printf("new box border update! %d\n", new_state); + box_border = new_state; +} + + + +__attribute__((export_name("animationFrame"))) +void animation_frame(int id, int delta) { + struct d2d_draw_seq* ds = d2d_start_draw_sequence(100); + + d2d_clearrect(ds, 0, 0, 1000, 1000); + + d2d_setfillstylergba(ds, 0x00FF00FF); + d2d_fillrect(ds, 0, 0, canvas_width, canvas_height); + + d2d_setfillstylergba(ds, box_color); + d2d_fillrect(ds, square_x, square_y, SQUARE_WIDTH, SQUARE_HEIGHT); + + d2d_setstrokestylergba(ds, 0x000000FF); + d2d_setlinewidth(ds, 4.0); + if (box_border) { + d2d_strokerect(ds, square_x, square_y, SQUARE_WIDTH, SQUARE_HEIGHT); + } + + d2d_setfillstyle(ds, COLORS[box_center_dot_color_index]); + if (box_center_dot) { + d2d_fillrect(ds, square_x + SQUARE_WIDTH/2.0 - 5.0, square_y + SQUARE_HEIGHT/2.0 - 5.0, 10.0, 10.0); + } + + if (prop_menu_data.state != PROP_MENU_UNOPENED) { + int m_x = (canvas_width - prop_menu_data.width)/2; + int m_y = (canvas_height - prop_menu_data.height)/2; + + d2d_setcanvaspropstring(ds, "textBaseline", "top"); + + // printf("((%ld, %ld) + (%d, %d))/2 = (%d, %d)\n", canvas_width, canvas_height, prop_menu_data.width, prop_menu_data.height, m_x, m_y); + d2d_setfillstylergba(ds, 0xC0C0C0FF); + d2d_fillrect(ds, m_x, m_y, prop_menu_data.width, prop_menu_data.height); + + d2d_setfillstylergba(ds, 0x808080FF); + d2d_fillrect(ds, m_x, m_y, prop_menu_data.width, POPUP_TITLE_HEIGHT); + + const int text_offset = 5; + d2d_setfont(ds, "16px Seriph"); + d2d_setfillstylergba(ds, 0xFFFFFFFF); + d2d_filltext(ds, prop_menu_data.prop_name, m_x, m_y+text_offset); + + d2d_setfont(ds, "12px Seriph"); + d2d_setfillstylergba(ds, 0x000000FF); + const int x_offset = 5; + if (prop_menu_data.state == PROP_MENU_GET) { + char buffer[110]; + sprintf(buffer, "value: %s", prop_menu_data.text_buffer.text); + d2d_filltext( + ds, + buffer, + m_x + x_offset, + m_y + (prop_menu_data.height + POPUP_TITLE_HEIGHT)/2 + ); + } else { + int row = 0; + + int height_per_row = (prop_menu_data.height - POPUP_TITLE_HEIGHT)/prop_menu_data.accepted_types_len; + const int EXTRA_TEXT_OFFSET = 5; + int height_offset = m_y + POPUP_TITLE_HEIGHT + EXTRA_TEXT_OFFSET; + for (int i = 0; i < prop_menu_data.accepted_types_len; i++) { + enum prop_menu_selected selected_type = prop_menu_data.accepted_types[prop_menu_data.selected_type]; + switch (prop_menu_data.accepted_types[i]) { + case PROP_MENU_SELECTED_TRUE: + //don't run for true, only false since it renders both + row -= 1; + break; + case PROP_MENU_SELECTED_FALSE: + { + char buffer[30]; + sprintf( + buffer, + "%s true\t%s false", + selected_type == PROP_MENU_SELECTED_TRUE ? "*" : " ", + selected_type == PROP_MENU_SELECTED_FALSE ? "*" : " " + ); + + d2d_filltext( + ds, + buffer, + m_x + x_offset, + height_offset + height_per_row*row + text_offset + ); + } + break; + case PROP_MENU_SELECTED_UNDEFINED: + { + char buffer[30]; + sprintf( + buffer, + "%s undefined", + selected_type == PROP_MENU_SELECTED_UNDEFINED ? "*" : " " + ); + + d2d_filltext( + ds, + buffer, + m_x + x_offset, + height_offset + height_per_row*row + text_offset + ); + } + break; + case PROP_MENU_SELECTED_NUMBER: + { + d2d_filltext( + ds, + selected_type == PROP_MENU_SELECTED_NUMBER ? "* number: " : " number: ", + m_x + x_offset, + height_offset + height_per_row*row + text_offset + ); + const int text_box_offset = 60; + const int inner_text_offset = 5; + d2d_setfillstylergba(ds, 0xFFFFFFFF); + d2d_fillrect( + ds, + m_x + x_offset + text_box_offset, + height_offset + height_per_row*row + 2 - EXTRA_TEXT_OFFSET, + prop_menu_data.width - x_offset*2 - text_box_offset, + height_per_row - 2 + ); + d2d_setfillstylergba(ds, 0x000000FF); + d2d_filltext( + ds, + prop_menu_data.number_buffer.text, + m_x + x_offset + text_box_offset + inner_text_offset, + height_offset + height_per_row*row + text_offset + ); + } + break; + + case PROP_MENU_SELECTED_STRING: + { + d2d_filltext( + ds, + selected_type == PROP_MENU_SELECTED_STRING ? "* string: " : " string: ", + m_x + x_offset, + height_offset + height_per_row*row + text_offset + ); + + const int text_box_offset = 60; + const int inner_text_offset = 5; + d2d_setfillstylergba(ds, 0xFFFFFFFF); + d2d_fillrect( + ds, + m_x + x_offset + text_box_offset, + height_offset + height_per_row*row + 2 - EXTRA_TEXT_OFFSET, + prop_menu_data.width - x_offset*2 - text_box_offset, + height_per_row - 2 + ); + d2d_setfillstylergba(ds, 0x000000FF); + d2d_filltext( + ds, + prop_menu_data.text_buffer.text, + m_x + x_offset + text_box_offset + inner_text_offset, + height_offset + height_per_row*row + text_offset + ); + } + break; + } + row += 1; + } + } + } + + d2d_end_draw_sequence(ds); +} + +__attribute__((export_name("mouseMoveHandler"))) +void mouse_move_handler(int id, int x, int y, int button) { + int m_x = (canvas_width - prop_menu_data.width)/2; + int m_y = (canvas_height - prop_menu_data.height)/2; + + if ( + prop_menu_data.state != PROP_MENU_UNOPENED + && ( + m_x < x && x < m_x + prop_menu_data.width + && m_y < y && y < m_y + prop_menu_data.height + ) + ) { + return; + } + if (mouse_event_ids[MOUSE_EVENT_MOVE] != id) { + prop_menu_data.state = PROP_MENU_UNOPENED; + } + if (mouse_event_ids[box_move_event_type] != id) + return; + if (box_move_event_type == MOUSE_EVENT_LEFT_CLICK && button != 0) + return; + square_x = x - SQUARE_WIDTH/2.0; + square_y = y - SQUARE_HEIGHT/2.0; + force_square_into_bounds(); +} +__attribute__((export_name("keyEventHandler"))) +void key_event_handler(int id, int key) { + printf("pressed key: %d\n", key); + if (prop_menu_data.state == PROP_MENU_UNOPENED) + return; + if (key == 27) { //escape + prop_menu_data.state = PROP_MENU_UNOPENED; + return; + } + if (prop_menu_data.state == PROP_MENU_GET) + return; + + enum prop_menu_selected selected = prop_menu_data.accepted_types[prop_menu_data.selected_type]; + enum prop_menu_target target = prop_menu_data.target; + + struct text_fill_buffer* text_buffer = &prop_menu_data.text_buffer; + struct text_fill_buffer* number_buffer = &prop_menu_data.number_buffer; + switch (key) { + + case 8593: //up arrow + { + if (prop_menu_data.selected_type > 0) { + prop_menu_data.selected_type--; + } + } + break; + + case 8595: //down arrow + { + if (prop_menu_data.selected_type < prop_menu_data.accepted_types_len-1) { + prop_menu_data.selected_type++; + } + } + break; + + case 8: //backspace + { + char deleted; + if (selected == PROP_MENU_SELECTED_STRING) { + try_pop_from_text_fill_buffer(text_buffer, &deleted); + } else if (selected == PROP_MENU_SELECTED_NUMBER) { + if (try_pop_from_text_fill_buffer(number_buffer, &deleted)) { + if (deleted == '.') { + prop_menu_data.number_buffer_has_dot = FALSE; + } + } + } + } + break; + case 127: //delete + { + if (selected == PROP_MENU_SELECTED_STRING) { + text_buffer->text[0] = '\0'; + text_buffer->length = 0; + } else if (selected == PROP_MENU_SELECTED_NUMBER) { + number_buffer->text[0] = '\0'; + number_buffer->length = 0; + prop_menu_data.number_buffer_has_dot = FALSE; + } + } + break; + + case 10: //enter + { + switch (selected) { + case PROP_MENU_SELECTED_TRUE: + case PROP_MENU_SELECTED_FALSE: + if (target == PROP_MENU_TARGET_WIDGET) { + twr_window_menu_widget_set_bool( + prop_menu_data.widget, + prop_menu_data.prop_name, + selected == PROP_MENU_SELECTED_TRUE ? TRUE : FALSE + ); + } else if (target == PROP_MENU_TARGET_MENU) { + twr_window_menu_set_prop_boolean( + window_con, + prop_menu_data.prop_name, + selected == PROP_MENU_SELECTED_TRUE ? TRUE : FALSE + ); + } + break; + case PROP_MENU_SELECTED_UNDEFINED: + assert(target == PROP_MENU_TARGET_WIDGET); + twr_window_menu_widget_set_undefined( + prop_menu_data.widget, + prop_menu_data.prop_name + ); + break; + case PROP_MENU_SELECTED_STRING: + if (target == PROP_MENU_TARGET_WIDGET) { + twr_window_menu_widget_set_string( + prop_menu_data.widget, + prop_menu_data.prop_name, + prop_menu_data.text_buffer.text + ); + } else if (target == PROP_MENU_TARGET_MENU) { + twr_window_menu_set_prop_string( + window_con, + prop_menu_data.prop_name, + prop_menu_data.text_buffer.text + ); + } + break; + case PROP_MENU_SELECTED_NUMBER: + { + assert(prop_menu_data.number_buffer.length > 0); + double res = strtod(prop_menu_data.number_buffer.text, NULL); + if (target == PROP_MENU_TARGET_WIDGET) { + twr_window_menu_widget_set_number( + prop_menu_data.widget, + prop_menu_data.prop_name, + res + ); + } else { + twr_window_menu_set_prop_number( + window_con, + prop_menu_data.prop_name, + res + ); + } + } + break; + default: + assert(FALSE); + } + prop_menu_data.state = PROP_MENU_UNOPENED; + } + break; + + case '-': + if (selected == PROP_MENU_SELECTED_NUMBER) { + if (number_buffer->length == 0) { + append_to_text_fill_buffer(number_buffer, '-'); + } + break; //only break if it was a number, otherwise propogates down to default + } + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '9': + if (selected == PROP_MENU_SELECTED_NUMBER) { + append_to_text_fill_buffer(number_buffer, key); + break; + } + + case '.': + if (selected == PROP_MENU_SELECTED_NUMBER) { + if (!prop_menu_data.number_buffer_has_dot) { + prop_menu_data.number_buffer_has_dot = TRUE; + + append_to_text_fill_buffer(number_buffer, key); + } + + break; + } + + default: + if (selected == PROP_MENU_SELECTED_STRING) { + if (key <= 0xFF && isprint(key)) + append_to_text_fill_buffer(text_buffer, key); + } + break; + } +} \ No newline at end of file diff --git a/source/twr-c/draw2d.c b/source/twr-c/draw2d.c index 62b615a5..2df7120d 100644 --- a/source/twr-c/draw2d.c +++ b/source/twr-c/draw2d.c @@ -706,4 +706,25 @@ long d2d_doesidexist(struct d2d_draw_seq* ds, long id) { d2d_flush(ds); return exists; +} + + +void d2d_register_event(enum D2DEvent eventType, int eventID) { + d2d_register_event_with_con(eventType, eventID, twr_get_std2d_con()); +} +void d2d_register_event_with_con(enum D2DEvent eventType, int eventID, twr_ioconsole_t * con) { + twrRegisterEvent(__twr_get_jsid(con), (int)eventType, eventID); +} + +void d2d_unregister_event(enum D2DEvent eventType, int eventID) { + d2d_unregister_event_with_con(eventType, eventID, twr_get_std2d_con()); +} +void d2d_unregister_event_with_con(enum D2DEvent eventType, int eventID, twr_ioconsole_t * con) { + twrUnregisterEvent(__twr_get_jsid(con), (int)eventType, eventID); +} +void d2d_unregister_all_events() { + d2d_unregister_all_events_with_con(twr_get_std2d_con()); +} +void d2d_unregister_all_events_with_con(twr_ioconsole_t * con) { + twrUnregisterAllEvents(__twr_get_jsid(con)); } \ No newline at end of file diff --git a/source/twr-c/twr-canvas-events.h b/source/twr-c/twr-canvas-events.h new file mode 100644 index 00000000..9d7750af --- /dev/null +++ b/source/twr-c/twr-canvas-events.h @@ -0,0 +1,16 @@ +#ifndef __TWR_CANVAS_EVENTS_H__ +#define __TWR_CANVAS_EVENTS_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +__attribute__((import_name("twrRegisterEvent"))) void twrRegisterEvent(int jsid, int eventType, int eventID); +__attribute__((import_name("twrUnregisterEvent"))) void twrUnregisterEvent(int jsid, int eventType, int eventID); +__attribute__((import_name("twrUnregisterAllEvents"))) void twrUnregisterAllEvents(int jsid); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/source/twr-c/twr-draw2d.h b/source/twr-c/twr-draw2d.h index f6cddc96..1fdcfb20 100644 --- a/source/twr-c/twr-draw2d.h +++ b/source/twr-c/twr-draw2d.h @@ -3,6 +3,7 @@ #include #include "twr-io.h" +#include "twr-canvas-events.h" #ifdef __cplusplus extern "C" { @@ -437,6 +438,21 @@ struct d2d_2d_matrix { __attribute__((import_name("twrConDrawSeq"))) void twrConDrawSeq(int jsid, struct d2d_draw_seq *); __attribute__((import_name("twrConLoadImage"))) bool twrConLoadImage(int jsid, const char* url, long id); +enum D2DEvent { + D2D_KEY_DOWN, + D2D_KEY_UP, + + D2D_MOUSE_DOWN, + D2D_MOUSE_UP, + D2D_MOUSE_CLICK, + D2D_MOUSE_DBLCLICK, + D2D_MOUSE_MOVE, + + D2D_WHEEL, + + D2D_ANIMATION_FRAME +}; + struct d2d_draw_seq* d2d_start_draw_sequence(int flush_at_ins_count); struct d2d_draw_seq* d2d_start_draw_sequence_with_con(int flush_at_ins_count, twr_ioconsole_t * con); void d2d_end_draw_sequence(struct d2d_draw_seq* ds); @@ -518,6 +534,13 @@ void d2d_setcanvaspropstring(struct d2d_draw_seq* ds, const char* prop_name, con long d2d_doesidexist(struct d2d_draw_seq* ds, long id); +void d2d_register_event(enum D2DEvent eventType, int eventID); +void d2d_register_event_with_con(enum D2DEvent eventType, int eventID, twr_ioconsole_t * con); +void d2d_unregister_event(enum D2DEvent eventType, int eventID); +void d2d_unregister_event_with_con(enum D2DEvent eventType, int eventID, twr_ioconsole_t * con); +void d2d_unregister_all_events(); +void d2d_unregister_all_events_with_con(twr_ioconsole_t * con); + #ifdef __cplusplus } #endif diff --git a/source/twr-c/twr-window.c b/source/twr-c/twr-window.c new file mode 100644 index 00000000..c3ef2195 --- /dev/null +++ b/source/twr-c/twr-window.c @@ -0,0 +1,312 @@ +#include "twr-window.h" +#include "twr-crt.h" +#include +#include +#include + +twr_ioconsole_t* twr_window_get_draw_canvas(twr_ioconsole_t* con) { + int id = twrGetDrawCanvasJSID(__twr_get_jsid(con)); + return twr_jscon(id); +} + +struct twr_window_widget twr_window_add_menu(twr_ioconsole_t * con, const char* text) { + int id = __twr_get_jsid(con); + int menu_id = twrWindowAddMenu(id, text); + return (struct twr_window_widget){ + .jsid = id, + .widget_id = menu_id, + }; +} + +struct twr_window_widget twr_window_menu_add_widget(const struct twr_window_widget* menu, const struct twr_widget_constructor* widget) { + int widget_id = twrWindowMenuAddWidget(menu->jsid, menu->widget_id, widget); + return (struct twr_window_widget){ + .jsid = menu->jsid, + .widget_id = widget_id + }; +} + +void twr_window_menu_widget_add_callback(const struct twr_window_widget* widget, int event_id, void* extraPtr) { + twrWindowMenuWidgetAddCallback(widget->jsid, widget->widget_id, event_id, extraPtr); +} + +void twr_window_menu_delete_widget(const struct twr_window_widget* widget) { + twrWindowMenuDeleteWidget(widget->jsid, widget->widget_id); +} + +void twr_window_menu_radio_item_merge(const struct twr_window_widget* widget1, const struct twr_window_widget* widget2) { + assert(widget1->jsid == widget2->jsid); + twrWindowMenuRadioItemMerge(widget1->jsid, widget1->widget_id, widget2->widget_id); +} + +void twr_window_menu_widget_set_string(const struct twr_window_widget* widget, const char* prop_name, const char* val) { + // struct twr_widget_prop_value { + // enum WindowWidgetPropVal type; + // const void* val; + // }; + struct twr_widget_prop_value prop_val = { + .string = val, + .type = WINDOW_WIDGET_PROP_STRING, + }; + twrWindowMenuWidgetSetProp(widget->jsid, widget->widget_id, prop_name, &prop_val); +} +void twr_window_menu_widget_set_bool(const struct twr_window_widget* widget, const char* prop_name, int val) { + struct twr_widget_prop_value prop_val = { + .boolean = val, + .type = WINDOW_WIDGET_PROP_BOOLEAN, + }; + twrWindowMenuWidgetSetProp(widget->jsid, widget->widget_id, prop_name, &prop_val); +} +void twr_window_menu_widget_set_number(const struct twr_window_widget* widget, const char* prop_name, double val) { + struct twr_widget_prop_value prop_val = { + .number = val, + .type = WINDOW_WIDGET_PROP_NUMBER, + }; + twrWindowMenuWidgetSetProp(widget->jsid, widget->widget_id, prop_name, &prop_val); +} +void twr_window_menu_widget_set_undefined(const struct twr_window_widget* widget, const char* prop_name) { + struct twr_widget_prop_value prop_val = { + .type = WINDOW_WIDGET_PROP_UNDEFINED, + }; + twrWindowMenuWidgetSetProp(widget->jsid, widget->widget_id, prop_name, &prop_val); +} + +struct twr_widget_prop_value* twr_window_menu_widget_get_prop(const struct twr_window_widget* widget, const char* prop_name) { + return twrWindowMenuWidgetGetProp(widget->jsid, widget->widget_id, prop_name); +} + +int twr_window_menu_widget_get_prop_string(const struct twr_window_widget* widget, const char* prop_name, char** ret_str) { + struct twr_widget_prop_value* val = twr_window_menu_widget_get_prop(widget, prop_name); + if (val->type == WINDOW_WIDGET_PROP_STRING) { + // *ret_str = (char*)val->string; + *ret_str = strdup(val->string); + free(val); + return 1; + } else { + free(val); + *ret_str = NULL; + return 0; + } +} +char* twr_window_menu_widget_get_prop_string_or_null(const struct twr_window_widget* widget, const char* prop_name) { + char* str; + if (twr_window_menu_widget_get_prop_string(widget, prop_name, &str)) { + return str; + } else { + return NULL; + } +} +int twr_window_menu_widget_get_prop_boolean(const struct twr_window_widget* widget, const char* prop_name, int* ret_bool) { + struct twr_widget_prop_value* val = twr_window_menu_widget_get_prop(widget, prop_name); + if (val->type == WINDOW_WIDGET_PROP_BOOLEAN) { + *ret_bool = val->boolean; + free(val); + return 1; + } else { + free(val); + *ret_bool = false; + return 0; + } +} +int twr_window_menu_widget_get_prop_boolean_or_default(const struct twr_window_widget* widget, const char* prop_name, int default_val) { + int ret = 0; + if (twr_window_menu_widget_get_prop_boolean(widget, prop_name, &ret)) { + return ret; + } else { + return default_val; + } +} +int twr_window_menu_widget_get_prop_number(const struct twr_window_widget* widget, const char* prop_name, double *ret_number) { + struct twr_widget_prop_value* val = twr_window_menu_widget_get_prop(widget, prop_name); + if (val->type == WINDOW_WIDGET_PROP_NUMBER) { + *ret_number = val->number; + free(val); + return 1; + } else { + free(val); + *ret_number = -1.0; + return 0; + } +} +double twr_window_menu_widget_get_prop_number_or_default(const struct twr_window_widget* widget, const char* prop_name, double default_val) { + double ret = 0; + if (twr_window_menu_widget_get_prop_number(widget, prop_name, &ret)) { + return ret; + } else { + return default_val; + } +} + +void twr_window_menu_widget_fill_in_details(const struct twr_window_widget* widget, struct twr_widget_prop_details* details) { + assert(details->name != (void*)0); + twrWindowMenuWidgetGetPropDetails(widget->jsid, widget->widget_id, details); +} +struct twr_widget_prop_details* twr_window_menu_widget_list_props(const struct twr_window_widget* widget, long* length) { + return twrWindowMenuWidgetListProps(widget->jsid, widget->widget_id, length); +} + + +struct twr_menu_prop_details* twr_window_menu_list_props(twr_ioconsole_t* window, long* length) { + return twrWindowMenuListProps(__twr_get_jsid(window), length); +} + + +struct twr_window_widget twr_window_menu_add_button_widget(const struct twr_window_widget* menu, long height, const char* text) { + struct twr_widget_button_constructor button_cons = { + .base = { + .type = WINDOW_WIDGET_BUTTON, + // .width = width, + .height = height + }, + .text = text + }; + + return twr_window_menu_add_widget(menu, &button_cons.base); +} + +struct twr_window_widget twr_window_menu_add_seperator_widget(const struct twr_window_widget* menu, long height, const char* seperator_text, const char* seperator_font) { + struct twr_widget_seperator_constructor seperator_cons = { + .base = { + .type = WINDOW_WIDGET_SEPERATOR, + .height = height, + // .width = width, + }, + .seperator_text = seperator_text, + .seperator_font = seperator_font + }; + + return twr_window_menu_add_widget(menu, &seperator_cons.base); +} + +struct twr_window_widget twr_window_menu_add_radio_item_widget(const struct twr_window_widget* menu, long height, const char* text) { + struct twr_widget_radio_item_constructor radio_item_cons = { + .base = { + .type = WINDOW_WIDGET_RADIO_ITEM, + .height = height, + // .width = width, + }, + .text = text + }; + + return twr_window_menu_add_widget(menu, &radio_item_cons.base); +} + +struct twr_window_widget twr_window_menu_add_sub_menu_widget(const struct twr_window_widget* menu, long height, const char* button_text, long minimum_menu_width, long minimum_menu_height) { + struct twr_widget_sub_menu_constructor sub_menu_cons = { + .base = { + .type = WINDOW_WIDGET_SUB_MENU, + .height = height, + // .width = width + }, + .button_text = button_text, + .minimum_menu_width = minimum_menu_width, + .minimum_menu_height = minimum_menu_height, + }; + + return twr_window_menu_add_widget(menu, &sub_menu_cons.base); +} +struct twr_window_widget twr_window_menu_add_sub_menu_widget_reduced(const struct twr_window_widget* menu, long height, const char* button_text) { + return twr_window_menu_add_sub_menu_widget(menu, height, button_text, -1, -1); +} + +struct twr_window_widget twr_window_menu_add_check_box_widget(const struct twr_window_widget* menu, long height, const char* text) { + struct twr_widget_check_box_constructor check_box_cons = { + .base = { + .type = WINDOW_WIDGET_CHECK_BOX, + .height = height, + // .width = width, + }, + .text = text, + }; + + return twr_window_menu_add_widget(menu, &check_box_cons.base); +} + + +struct twr_widget_prop_value* twr_window_menu_get_prop(twr_ioconsole_t* window, const char* prop_name) { + return twrWindowMenuGetProp(__twr_get_jsid(window), prop_name); +} +int twr_window_menu_get_prop_boolean(twr_ioconsole_t* window, const char* prop_name, int* ret_bool) { + struct twr_widget_prop_value* ret_val = twr_window_menu_get_prop(window, prop_name); + int success = ret_val->type == WINDOW_WIDGET_PROP_BOOLEAN; + if (success) + *ret_bool = ret_val->boolean; + + free(ret_val); + return 0; +} +int twr_window_menu_get_prop_boolean_or_default(twr_ioconsole_t* window, const char* prop_name, int def) { + int ret; + if (twr_window_menu_get_prop_boolean(window, prop_name, &ret)) { + return ret; + } else { + return def; + } +} +int twr_window_menu_get_prop_number(twr_ioconsole_t* window, const char* prop_name, double* ret_number) { + struct twr_widget_prop_value* ret_val = twr_window_menu_get_prop(window, prop_name); + int success = ret_val->type == WINDOW_WIDGET_PROP_NUMBER; + if (success) + *ret_number = ret_val->number; + + free(ret_val); + return success; +} +double twr_window_menu_get_prop_number_or_default(twr_ioconsole_t* window, const char* prop_name, double def) { + double ret; + if (twr_window_menu_get_prop_number(window, prop_name, &ret)) { + return ret; + } else { + return def; + } +} +int twr_window_menu_get_prop_string(twr_ioconsole_t* window, const char* prop_name, char** ret_str) { + struct twr_widget_prop_value* ret_val = twr_window_menu_get_prop(window, prop_name); + int success = ret_val->type == WINDOW_WIDGET_PROP_STRING; + if (success) + *ret_str = strdup(ret_val->string); + + free(ret_val); + return success; +} +char* twr_window_menu_get_prop_string_or_default(twr_ioconsole_t* window, const char* prop_name) { + char* ret; + if (twr_window_menu_get_prop_string(window, prop_name, &ret)) { + return ret; + } else { + return (char*)0; + } +} + +void twr_window_menu_set_prop(twr_ioconsole_t* window, const char* prop_name, struct twr_widget_prop_value* val) { + twrWindowMenuSetProp(__twr_get_jsid(window), prop_name, val); +} +void twr_window_menu_set_prop_boolean(twr_ioconsole_t* window, const char* prop_name, int val) { + twr_window_menu_set_prop(window, prop_name, &(struct twr_widget_prop_value){ + .boolean = val, + .type = WINDOW_WIDGET_PROP_BOOLEAN, + }); +} +void twr_window_menu_set_prop_number(twr_ioconsole_t* window, const char* prop_name, double val) { + twr_window_menu_set_prop(window, prop_name, &(struct twr_widget_prop_value){ + .number = val, + .type = WINDOW_WIDGET_PROP_NUMBER, + }); +} +void twr_window_menu_set_prop_string(twr_ioconsole_t* window, const char* prop_name, const char* val) { + twr_window_menu_set_prop(window, prop_name, &(struct twr_widget_prop_value){ + .string = (char*)val, + .type = WINDOW_WIDGET_PROP_STRING, + }); +} + + +void twr_window_register_event(twr_ioconsole_t* window, enum TwrWindowEvents event, int event_id) { + twrRegisterEvent(__twr_get_jsid(window), event, event_id); +} +void twr_window_unregister_event(twr_ioconsole_t* window, enum TwrWindowEvents event, int event_id) { + twrUnregisterEvent(__twr_get_jsid(window), event, event_id); +} +void twr_window_unregiser_all_events(twr_ioconsole_t* window) { + twrUnregisterAllEvents(__twr_get_jsid(window)); +} \ No newline at end of file diff --git a/source/twr-c/twr-window.h b/source/twr-c/twr-window.h new file mode 100644 index 00000000..12044a5d --- /dev/null +++ b/source/twr-c/twr-window.h @@ -0,0 +1,285 @@ +#ifndef __TWR_WINDOW_H__ +#define __TWR_WINDOW_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "twr-io.h" +#include "twr-canvas-events.h" + +__attribute__((import_name("twrGetDrawCanvasJSID"))) int twrGetDrawCanvasJSID(int jsid); + +twr_ioconsole_t* twr_window_get_draw_canvas(twr_ioconsole_t * con); + +__attribute__((import_name("twrWindowAddMenu"))) int twrWindowAddMenu(int jsid, const char* text); + + +struct twr_window_widget { + int jsid; + int widget_id; +}; + + +enum WindowWidget { + WINDOW_WIDGET_BUTTON, + WINDOW_WIDGET_SEPERATOR, + // WINDOW_WIDGET_RADIO_MENU, + WINDOW_WIDGET_RADIO_ITEM, + WINDOW_WIDGET_SUB_MENU, + WINDOW_WIDGET_CHECK_BOX, +}; + + +struct twr_window_widget twr_window_add_menu(twr_ioconsole_t * con, const char* text); + + +/** + * Base widget constructor for all widgets. + * Any Null or Negative values will be set to their defaults + */ +struct twr_widget_constructor { + /// @brief must be set and matched to the correct widget + enum WindowWidget type; + // / @brief Default depends on widget + // long width; + /// @brief Default depends on widget + long height; +}; + +/** + * Constructor for a button widget: Displays a button that can be clicked on for events. + * Event callbacks are added after construction + * Any Null or Negative values will be set to their defaults + */ +struct twr_widget_button_constructor { + /// @brief Base widget constructor + struct twr_widget_constructor base; + /// @brief defaults to "Lorem Ipsum" + const char* text; +}; + +/** + * @brief Adds a button with the given properties to the given menu + * @param menu: The menu widget this button should be added to + * @param height: Height of widget, -1 sets it to default (might be adjusted automatically by menu) + * @param text: Text displayed on button + * @retval Struct identifying the widget and it's linked window + */ +struct twr_window_widget twr_window_menu_add_button_widget(const struct twr_window_widget* menu, long height, const char* text); + +/** + * Constructor for a seperator widget: Displays a repeating segment of text to seperate sections + * Any Null or Negative values will be set to their defaults + */ +struct twr_widget_seperator_constructor { + /// @brief Base widget constructor + struct twr_widget_constructor base; + /// @brief defaults to "-" + const char* seperator_text; + /// @brief defaults to 16px Seriph + const char* seperator_font; +}; +/** + * @brief Adds a seperator with the given properties to the given menu + * @param menu: The menu widget this seperator should be added to + * @param height: Height of widget, -1 sets it to default (might be adjusted automatically by menu) + * @param seperator_text: Text seperator uses, for instance "-" would fill the width with "-------" + * @param seperator_font: Font that should be used with the seperator text + * @retval Struct identifying the widget and it's linked window + */ +struct twr_window_widget twr_window_menu_add_seperator_widget(const struct twr_window_widget* menu, long height, const char* seperator_text, const char* seperator_font); + +/** + * Constructor for a radio item widget: Items are linked together to have mutually exclusive options + * Any Null or Negative values will be set to their defaults + */ +struct twr_widget_radio_item_constructor { + /// @brief Base widget constructor + struct twr_widget_constructor base; + /// @brief defaults to "Lorem Ipsum" + const char* text; +}; +/** + * @brief Adds a radio item with the given properties to the given menu + * @param menu: The menu widget this radio item should be added to + * @param height: Height of widget, -1 sets it to default (might be adjusted automatically by menu) + * @param text: Text displayed on the radio item + * @retval Struct identifying the widget and it's linked window + */ +struct twr_window_widget twr_window_menu_add_radio_item_widget(const struct twr_window_widget* menu, long height, const char* text); + +/** + * Constructor for a sub-menu widget: Holds a list of widgets + * Any Null or Negative values will be set to their defaults + */ +struct twr_widget_sub_menu_constructor { + /// @brief Base widget constructor + struct twr_widget_constructor base; + /** + * Text for the button that opens this menu + * defaults to Lorem Ipsum + */ + const char* button_text; + /// @brief defaults to 10 + long minimum_menu_width; + /// @brief defaults to 10 + long minimum_menu_height; +}; +/** + * @brief Adds a sub menu button with the given properties to the given menu + * @param menu: The menu widget this sub menu should be added to + * @param height: Height of menu button, -1 sets it to default (might be adjusted automatically by menu) + * @param button_text: Text displayed on the button used to open the sub menu + * @param minimum_menu_width: The minimum width of the menu when it's opened + * @param minimum_menu_height: The minimum height of the menu when it's opened + * @retval Struct identifying the widget and it's linked window + */ +struct twr_window_widget twr_window_menu_add_sub_menu_widget(const struct twr_window_widget* menu, long height, const char* button_text, long minimum_menu_width, long minimum_menu_height); +/** + * @brief Adds a sub menu button with the given properties to the given menu + * @param menu: The menu widget this sub menu should be added to + * @param height: Height of menu button, -1 sets it to default (might be adjusted automatically by menu) + * @param button_text: Text displayed on the button used to open the sub menu + * @retval Struct identifying the widget and it's linked window + */ +struct twr_window_widget twr_window_menu_add_sub_menu_widget_reduced(const struct twr_window_widget* menu, long height, const char* button_text); + +/** + * Constructor for a check box widget: Button with a checkbox that has an event for when it's state changes + * Any Null or Negative values will be set to their defaults + */ +struct twr_widget_check_box_constructor { + /// @brief Base widget constructor + struct twr_widget_constructor base; + /** + * Text for the check box + * defaults to Lorem Ipsum + */ + const char* text; +}; +/** + * @brief Adds a check box with the given properties to the given menu + * @param menu: The menu widget this check box should be added to + * @param height: Height of widget, -1 sets it to default (might be adjusted automatically by menu) + * @param text: Text displayed on the check box + * @retval Struct identifying the widget and it's linked window + */ +struct twr_window_widget twr_window_menu_add_check_box_widget(const struct twr_window_widget* menu, long height, const char* text); + + +__attribute__((import_name("twrWindowMenuAddWidget"))) int twrWindowMenuAddWidget(int jsid, int menu_id, const struct twr_widget_constructor* widget); +struct twr_window_widget twr_window_menu_add_widget(const struct twr_window_widget* menu, const struct twr_widget_constructor* widget); + +__attribute__((import_name("twrWindowMenuWidgetAddCallback"))) void twrWindowMenuWidgetAddCallback(int jsid, int widget_id, int event_id, void* extraPtr); +void twr_window_menu_widget_add_callback(const struct twr_window_widget* widget, int event_id, void* extraPtr); + +__attribute__((import_name("twrWindowMenuDeleteWidget"))) void twrWindowMenuDeleteWidget(int jsid, int widget_id); +void twr_window_menu_delete_widget(const struct twr_window_widget* widget); + +__attribute__((import_name("twrWindowMenuRadioItemMerge"))) void twrWindowMenuRadioItemMerge(int jsid, int widget_id1, int widget_id2); +void twr_window_menu_radio_item_merge(const struct twr_window_widget* widget1, const struct twr_window_widget* widget2); + + +enum WindowWidgetPropVal { + WINDOW_WIDGET_PROP_STRING = 1, //0b0001 + WINDOW_WIDGET_PROP_BOOLEAN = 2,//0b0010 + WINDOW_WIDGET_PROP_NUMBER = 4, //0b0100 + WINDOW_WIDGET_PROP_UNDEFINED = 8, //0b1000 + // WINDOW_WIDGET_PROP_STRING_OR_UNDEFINED = 9, //0b1001 + // WINDOW_WIDGET_PROP_BOOLEAN_OR_UNDEFINED = 10,//0b1010 + // WINDOW_WIDGET_PROP_NUMBER_OR_UNDEFINED = 12, //0b1100 +}; +struct twr_widget_prop_value { + union { + const char* string; + double number; + int boolean; + }; + enum WindowWidgetPropVal type; +}; +enum WindowWidgetPropAccess { + WINDOW_WIDGET_PROP_GET = 1, + WINDOW_WIDGET_PROP_SET = 2, + WINDOW_WIDGET_PROP_GETANDSET = 3, +}; +struct twr_widget_prop_details { + const char* name; + enum WindowWidgetPropVal type; + enum WindowWidgetPropAccess access; +}; +__attribute__((import_name("twrWindowMenuWidgetSetProp"))) void twrWindowMenuWidgetSetProp(int jsid, int widget_id, const char* prop_name, const struct twr_widget_prop_value* data); +void twr_window_menu_widget_set_string(const struct twr_window_widget* widget, const char* prop_name, const char* val); +void twr_window_menu_widget_set_bool(const struct twr_window_widget* widget, const char* prop_name, int val); +void twr_window_menu_widget_set_number(const struct twr_window_widget* widget, const char* prop_name, double val); +void twr_window_menu_widget_set_undefined(const struct twr_window_widget* widget, const char* prop_name); + +__attribute__((import_name("twrWindowMenuWidgetGetProp"))) struct twr_widget_prop_value* twrWindowMenuWidgetGetProp(int jsid, int widget_id, const char* prop_name); + +struct twr_widget_prop_value* twr_window_menu_widget_get_prop(const struct twr_window_widget* widget, const char* prop_name); +/// @brief if type is string, returns 1, otherwise returns 0 and sets ret_str to null ptr +int twr_window_menu_widget_get_prop_string(const struct twr_window_widget* widget, const char* prop_name, char** ret_str); +/// @brief if type is not string, returns null ptr +char* twr_window_menu_widget_get_prop_string_or_null(const struct twr_window_widget* widget, const char* prop_name); +/// @brief if type is not boolean, returns 0 +int twr_window_menu_widget_get_prop_boolean(const struct twr_window_widget* widget, const char* prop_name, int* ret_bool); +/// @brief if type is not boolean, returns default value +int twr_window_menu_widget_get_prop_boolean_or_default(const struct twr_window_widget* widget, const char* prop_name, int default_val); +/// @brief if type is not number, returns 1 +int twr_window_menu_widget_get_prop_number(const struct twr_window_widget* widget, const char* prop_name, double* ret_number); +/// @brief if type is not number, returns default value +double twr_window_menu_widget_get_prop_number_or_default(const struct twr_window_widget* widget, const char* prop_name, double default_val); + +__attribute__((import_name("twrWindowMenuWidgetGetPropDetails"))) void twrWindowMenuWidgetGetPropDetails(int jsid, int widget_id, struct twr_widget_prop_details* details); +/** + * Given a details struct with the name filled out, + * this function will fill in the type and access fields of the struct + */ +void twr_window_menu_widget_fill_in_details(const struct twr_window_widget* widget, struct twr_widget_prop_details* details); +__attribute__((import_name("twrWindowMenuWidgetListProps"))) struct twr_widget_prop_details* twrWindowMenuWidgetListProps(int jsid, int widget_id, long* length); +/** + * Returns an array of twr_widget_prop_details representing the name, access, and types of each property + * The array of structs and the strings representing their names are made in one large allocation + */ +struct twr_widget_prop_details* twr_window_menu_widget_list_props(const struct twr_window_widget* widget, long* length); + + +struct twr_menu_prop_details { + const char* name; + enum WindowWidgetPropVal type; +}; +__attribute__((import_name("twrWindowMenuListProps"))) struct twr_menu_prop_details* twrWindowMenuListProps(int jsid, long* length); +/** + * returns an array of twr_menu_prop_details representing the name and type of each property + * The array of structs and the strings representing their names are made in one large allocation + */ +struct twr_menu_prop_details* twr_window_menu_list_props(twr_ioconsole_t* window, long* length); + +__attribute__((import_name("twrWindowMenuGetProp"))) struct twr_widget_prop_value* twrWindowMenuGetProp(int jsid, const char* prop_name); +struct twr_widget_prop_value* twr_window_menu_get_prop(twr_ioconsole_t* window, const char* prop_name); +int twr_window_menu_get_prop_boolean(twr_ioconsole_t* window, const char* prop_name, int* ret_bool); +int twr_window_menu_get_prop_boolean_or_default(twr_ioconsole_t* window, const char* prop_name, int def); +int twr_window_menu_get_prop_number(twr_ioconsole_t* window, const char* prop_name, double* ret_number); +double twr_window_menu_get_prop_number_or_default(twr_ioconsole_t* window, const char* prop_name, double def); +int twr_window_menu_get_prop_string(twr_ioconsole_t* window, const char* prop_name, char** ret_str); +char* twr_window_menu_get_prop_string_or_default(twr_ioconsole_t* window, const char* prop_name); + +__attribute__((import_name("twrWindowMenuSetProp"))) void twrWindowMenuSetProp(int jsid, const char* prop_name, struct twr_widget_prop_value* val); +void twr_window_menu_set_prop(twr_ioconsole_t* window, const char* prop_name, struct twr_widget_prop_value* val); +void twr_window_menu_set_prop_boolean(twr_ioconsole_t* window, const char* prop_name, int val); +void twr_window_menu_set_prop_number(twr_ioconsole_t* window, const char* prop_name, double val); +void twr_window_menu_set_prop_string(twr_ioconsole_t* window, const char* prop_name, const char* val); + + +enum TwrWindowEvents { + TWR_WINDOW_RESIZE_EVENT +}; +void twr_window_register_event(twr_ioconsole_t* window, enum TwrWindowEvents event, int event_id); +void twr_window_unregister_event(twr_ioconsole_t* window, enum TwrWindowEvents event, int event_id); +void twr_window_unregiser_all_events(twr_ioconsole_t* window); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/source/twr-ts/index.ts b/source/twr-ts/index.ts index 69714d34..95eecc12 100644 --- a/source/twr-ts/index.ts +++ b/source/twr-ts/index.ts @@ -8,6 +8,7 @@ export * from "./twrcon.js" export * from "./twrcondebug.js" export * from "./twrconcanvas.js" export * from "./twrlibrary.js" +export * from "./twrconwindow.js" diff --git a/source/twr-ts/twrcanvasevents.ts b/source/twr-ts/twrcanvasevents.ts new file mode 100644 index 00000000..c285dc3e --- /dev/null +++ b/source/twr-ts/twrcanvasevents.ts @@ -0,0 +1,85 @@ +import { keyEventToCodePoint } from "./twrcon.js"; + +export enum CanvasEventTypes { + KEY_DOWN, + KEY_UP, + + MOUSE_DOWN, + MOUSE_UP, + MOUSE_CLICK, + MOUSE_DBLCLICK, + MOUSE_MOVE, + + WHEEL, + + ANIMATION_FRAME +} +export const NUM_CANVAS_EVENTS = Object.values(CanvasEventTypes).length; + +export const CANVAS_EVENTS = [ + "keydown", + "keyup", + + "mousedown", + "mouseup", + "click", + "dblclick", + "mousemove", + + "wheel", + + "ANIMATION_FRAME" +]; + +export interface ICanvasEvents { + /// Get canvas key events, return True to intercept event and stop it from being passed along + handleCanvasKeyEvent: (event: CanvasEventTypes, key: number) => boolean; + /// Get canvas mouse events, return True to intercept event and stop it from being passed along + /// Button is taken directly from event as specified here: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button + handleCanvasMouseEvent: (event: CanvasEventTypes, x: number, y: number, button: number) => boolean; + /// Get canvas wheel events, return True to intercept event and stop it from being passed along + handleCanvasWheelEvent: (event: CanvasEventTypes, deltaX: number, deltaY: number, deltaZ: number, deltaMode: number) => boolean; + /// Get canvas animation frame events + handleCanvasAnimationFrameEvent: (event: CanvasEventTypes, delta: number) => void; +} + +function registerSimilarEvents(canvas: HTMLCanvasElement, start: CanvasEventTypes, end: CanvasEventTypes, handler: (eventType: CanvasEventTypes) => (event: any) => void) { + for (let i = start; i <= end; i++) { + canvas.addEventListener(CANVAS_EVENTS[i], handler(i)); + } +} +export function bindCanvasEvents(handler: ICanvasEvents, canvas: HTMLCanvasElement) { + registerSimilarEvents(canvas, CanvasEventTypes.KEY_DOWN, CanvasEventTypes.KEY_UP, + (type) => (e: KeyboardEvent) => { + console.log(e); + const r=keyEventToCodePoint(e); // twr-wasm utility function + if (r) { + handler.handleCanvasKeyEvent(type, r); + } + } + ); + + const bounding = canvas.getBoundingClientRect(); + const top = bounding.top + window.scrollY; + const left = bounding.left + window.scrollX; + registerSimilarEvents(canvas, CanvasEventTypes.MOUSE_DOWN, CanvasEventTypes.MOUSE_MOVE, + (type) => (e: MouseEvent) => { + handler.handleCanvasMouseEvent( + type, + e.pageX - left, + e.pageY - top, + e.button + ); + } + ); + + canvas.addEventListener("wheel", (e: WheelEvent) => { + handler.handleCanvasWheelEvent(CanvasEventTypes.WHEEL, e.deltaX, e.deltaY, e.deltaZ, e.deltaMode); + }); + + const animation_loop = (delta: number) => { + handler.handleCanvasAnimationFrameEvent(CanvasEventTypes.ANIMATION_FRAME, delta); + requestAnimationFrame(animation_loop); + }; + requestAnimationFrame(animation_loop); +} \ No newline at end of file diff --git a/source/twr-ts/twrcon.ts b/source/twr-ts/twrcon.ts index a55d1d0d..942a0e01 100644 --- a/source/twr-ts/twrcon.ts +++ b/source/twr-ts/twrcon.ts @@ -96,12 +96,36 @@ export interface IConsoleDrawable { twrConLoadImage_async: (mod:IWasmModuleAsync, urlPtr: number, id: number)=>Promise, } +export interface IConsoleEvents { + twrRegisterEvent: (callingMod:IWasmModuleAsync|IWasmModule, eventType: number, eventID: number) => void, + twrUnregisterEvent: (callingMod: IWasmModuleAsync|IWasmModule, eventType: number, eventID: number) => void, + twrUnregisterAllEvents: (callingMod: IWasmModuleAsync|IWasmModule) => void, +} + + export interface IConsoleTerminal extends IConsoleBase, IConsoleStreamOut, IConsoleStreamIn, IConsoleAddressable {} export interface IConsoleDiv extends IConsoleBase, IConsoleStreamOut, IConsoleStreamIn {} export interface IConsoleDebug extends IConsoleBase, IConsoleStreamOut {} -export interface IConsoleCanvas extends IConsoleBase, IConsoleDrawable {} +export interface IConsoleCanvas extends IConsoleBase, IConsoleDrawable, IConsoleEvents {} +export interface IConsoleWindow extends IConsoleBase, IConsoleEvents { + twrGetDrawCanvasJSID: (callingMod: IWasmModuleAsync|IWasmModule) => number, + twrWindowAddMenu: (callingMod: IWasmModuleAsync|IWasmModule, textPtr: number) => number, + twrWindowMenuAddWidget: (mod: IWasmModuleAsync | IWasmModule, menuID: number, consPtr: number) => number, + twrWindowMenuWidgetAddCallback: (mod: IWasmModuleAsync | IWasmModule, widgetID: number, eventID: number, extraPtr: number) => void, + twrWindowMenuDeleteWidget: (mod: IWasmModuleAsync | IWasmModule, widgetID: number) => void, + twrWindowMenuRadioItemMerge: (mod: IWasmModuleAsync | IWasmModule, widgetID1: number, widgetID2: number) => void, + twrWindowMenuWidgetSetProp: (mod: IWasmModuleAsync | IWasmModule, widgetID: number, propNamePtr: number, dataPtr: number) => void, + twrWindowMenuWidgetGetProp: (mod: IWasmModule, widgetID: number, propNamePtr: number) => number, + twrWindowMenuWidgetListProps: (mod: IWasmModule, widgetID: number, lengthPtr: number) => number, + twrWindowMenuWidgetGetPropDetails: (mod: IWasmModule, widgetID: number, detailsStructPtr: number) => void, + + twrWindowMenuListProps: (mod: IWasmModule, lengthPtr: number) => number; + + twrWindowMenuSetProp: (mod: IWasmModuleAsync | IWasmModule, propNamePtr: number, dataPtr: number) => void, + twrWindowMenuGetProp: (mod: IWasmModule, propNamePtr: number) => number, +} -export interface IConsole extends IConsoleBase, Partial, Partial, Partial, Partial {} +export interface IConsole extends IConsoleBase, Partial, Partial, Partial, Partial, Partial {} // must match IO_TYPEs in twr_io.h diff --git a/source/twr-ts/twrconcanvas.ts b/source/twr-ts/twrconcanvas.ts index e08c8326..ab33d4cf 100644 --- a/source/twr-ts/twrconcanvas.ts +++ b/source/twr-ts/twrconcanvas.ts @@ -2,6 +2,7 @@ import {IConsoleCanvas, ICanvasProps, IOTypes} from "./twrcon.js"; import {IWasmModuleAsync} from "./twrmodasync.js" import {IWasmModule} from "./twrmod.js"; import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; +import { bindCanvasEvents, CanvasEventTypes, ICanvasEvents, NUM_CANVAS_EVENTS } from "./twrcanvasevents.js"; enum D2DType { D2D_FILLRECT=1, @@ -69,8 +70,16 @@ function calculateID(mod:IWasmModule|IWasmModuleAsync, id: number) { //should be equivalent to (mod.id << 32) | id return (mod.id & (2**20 - 1)) * 2**32 + id; } - -export class twrConsoleCanvas extends twrLibrary implements IConsoleCanvas { +///modID -> [module, eventID -> numRegistrations] +type EventHandlerMap = Map< + number, + [ + WeakRef, + Set + ] +>; + +export class twrConsoleCanvas extends twrLibrary implements IConsoleCanvas, ICanvasEvents { id:number; ctx:CanvasRenderingContext2D; element:HTMLCanvasElement @@ -83,16 +92,21 @@ export class twrConsoleCanvas extends twrLibrary implements IConsoleCanvas { HTMLImageElement }; + registeredEvents: { [eventType in CanvasEventTypes]: EventHandlerMap }; + imports:TLibImports = { twrConGetProp:{}, twrConDrawSeq:{}, twrConLoadImage:{isModuleAsyncOnly:true, isAsyncFunction:true}, + twrRegisterEvent:{}, + twrUnregisterEvent:{}, + twrUnregisterAllEvents:{}, }; libSourcePath = new URL(import.meta.url).pathname; interfaceName = "twrConsole"; - constructor(element:HTMLCanvasElement) { + constructor(element:HTMLCanvasElement, ctxOptions?: CanvasRenderingContext2DSettings, selfRegisterEvents: boolean = true) { // all library constructors should start with these two lines super(); this.id=twrLibraryInstanceRegistry.register(this); @@ -113,6 +127,104 @@ export class twrConsoleCanvas extends twrLibrary implements IConsoleCanvas { c.textBaseline="top"; this.props = {canvasHeight: element.height, canvasWidth: element.width, type: IOTypes.CANVAS2D}; + + if (selfRegisterEvents) { + bindCanvasEvents(this, element); + } + + this.registeredEvents = Object.values(CanvasEventTypes) + .filter(value => typeof value == "number") + .reduce((acc, eventType) => { + acc[eventType as CanvasEventTypes] = new Map(); + return acc; + }, {} as { [eventType in CanvasEventTypes]: EventHandlerMap}); + } + + resizeCanvas(width: number, height: number) { + const imageData = this.ctx.getImageData(0, 0, this.element.width, this.element.height); + this.element.width = width; + this.element.height = height; + this.props.canvasWidth = width; + this.props.canvasHeight = height; + + this.ctx.putImageData(imageData, 0, 0); + // this.ctx = this.element.getContext("2d")!; + } + + internalSendEvent(event: CanvasEventTypes, ...args: number[]) { + const eventHandlers = this.registeredEvents[event]; + let toDelete: number[] = []; + for (const [modID, [mod, eventIDs]] of eventHandlers) { + const derefedMod = mod.deref(); + if (derefedMod) { + for (const eventID of eventIDs.keys()) { + derefedMod.postEvent(eventID, ...args); + } + } else { + toDelete.push(modID); + } + } + for (const modID of toDelete) { + eventHandlers.delete(modID); + } + } + handleCanvasKeyEvent(event: CanvasEventTypes, key: number) { + // console.log(`${event}, ${key}`); + this.internalSendEvent(event, key); + return false; //for now, modules can't intercept events, only receive them + } + handleCanvasMouseEvent(event: CanvasEventTypes, x: number, y: number, button: number) { + this.internalSendEvent(event, x, y, button); + return false; //for now, modules can't intercept events, only receive them + } + handleCanvasWheelEvent(event: CanvasEventTypes, deltaX: number, deltaY: number, deltaZ: number, deltaMode: number) { + this.internalSendEvent(event, deltaX, deltaY, deltaZ, deltaMode); + return false; //for now, modules can't intercept events, only receive them + } + handleCanvasAnimationFrameEvent(event: CanvasEventTypes, delta: number) { + this.internalSendEvent(event, delta); + } + + twrRegisterEvent(mod: IWasmModule|IWasmModuleAsync, eventType: number, eventID: number) { + if (eventType < 0 || eventType >= NUM_CANVAS_EVENTS) throw new Error(`twrRegisterEvent was given an out of bound eventType. 0 <= ${eventType} < ${NUM_CANVAS_EVENTS}`); + + const event = eventType as CanvasEventTypes; + + const eventHandlers = this.registeredEvents[event]; + + if (!eventHandlers.has(mod.id)) + eventHandlers.set(mod.id, [new WeakRef(mod), new Set()]); + + const individualHandlers = eventHandlers.get(mod.id)![1]; + + if (eventID in individualHandlers) + throw new Error(`Error: twrRegisterEvent was given an eventID (${eventID}) that was already registered to this event (${event.toString()})!`); + + individualHandlers.add(eventID); + }; + + twrUnregisterEvent(mod: IWasmModule|IWasmModuleAsync, eventType: number, eventID: number) { + if (eventType < 0 || eventType >= NUM_CANVAS_EVENTS) throw new Error(`twrUnregisterEvent was given an out of bounds eventType. 0 <= ${eventType} < ${NUM_CANVAS_EVENTS}`); + + const event = eventType as CanvasEventTypes; + + const eventHandlers = this.registeredEvents[event]; + + if (!eventHandlers.has(mod.id)) + throw new Error(`twrUnregisterEvent: tried to unregister an event when this module has never registered an event!`); + + const individualHandlers = eventHandlers.get(mod.id)![1]; + + if (!(eventID in individualHandlers)) { + throw new Error(`twrUnregisterEvent: Tried to unregister an eventID (${eventID}) that hasn't been registered!`); + } else { + individualHandlers.delete(eventID); + } + } + twrUnregisterAllEvents(mod: IWasmModuleAsync | IWasmModule) { + for (const handlers of Object.values(this.registeredEvents)) { + handlers.delete(mod.id); + } } diff --git a/source/twr-ts/twrcondummy.ts b/source/twr-ts/twrcondummy.ts index b166540c..33eedb54 100644 --- a/source/twr-ts/twrcondummy.ts +++ b/source/twr-ts/twrcondummy.ts @@ -1,4 +1,4 @@ -import {IConsoleStreamOut, IConsoleStreamIn, IConsoleCanvas, IConsoleAddressable, ICanvasProps } from "./twrcon.js" +import {IConsoleStreamOut, IConsoleStreamIn, IConsoleCanvas, IConsoleAddressable, ICanvasProps, IConsoleWindow } from "./twrcon.js" import {IWasmModuleAsync} from "./twrmodasync.js"; import {IWasmModule} from "./twrmod.js" import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; @@ -8,7 +8,7 @@ import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary. // These functions should never be called, because twrLibrary routes a call (like io_cls(id)) to the correct console instance based on id // see TODO comments in twrLibrary.ts for possible better fixes -export default class twrConsoleDummy extends twrLibrary implements IConsoleStreamIn, IConsoleStreamOut, IConsoleAddressable, IConsoleCanvas { +export default class twrConsoleDummy extends twrLibrary implements IConsoleStreamIn, IConsoleStreamOut, IConsoleAddressable, IConsoleCanvas, IConsoleWindow { id:number; imports:TLibImports = { @@ -27,6 +27,22 @@ export default class twrConsoleDummy extends twrLibrary implements IConsoleStrea twrConSetColors:{noBlock:true}, twrConDrawSeq:{}, twrConLoadImage:{isModuleAsyncOnly:true, isAsyncFunction:true}, + twrRegisterEvent:{}, + twrUnregisterEvent:{}, + twrUnregisterAllEvents:{}, + twrGetDrawCanvasJSID:{}, + twrWindowAddMenu:{}, + twrWindowMenuAddWidget: {}, + twrWindowMenuWidgetAddCallback: {}, + twrWindowMenuDeleteWidget: {}, + twrWindowMenuRadioItemMerge: {}, + twrWindowMenuWidgetSetProp: {}, + twrWindowMenuWidgetGetProp: {isAsyncFunction: true}, + twrWindowMenuWidgetListProps: {isAsyncFunction: true}, + twrWindowMenuWidgetGetPropDetails: {}, + twrWindowMenuListProps: {isAsyncFunction: true}, + twrWindowMenuSetProp: {isAsyncFunction: true}, + twrWindowMenuGetProp: {}, }; libSourcePath = new URL(import.meta.url).pathname; @@ -38,6 +54,9 @@ export default class twrConsoleDummy extends twrLibrary implements IConsoleStrea this.id=twrLibraryInstanceRegistry.register(this); } + element?: HTMLElement | undefined; + + twrConGetProp(callingMod:IWasmModule|IWasmModuleAsync, pn:number):number { throw new Error("internal error"); } @@ -118,4 +137,54 @@ export default class twrConsoleDummy extends twrLibrary implements IConsoleStrea throw new Error("internal error"); } + twrRegisterEvent(callingMod: IWasmModuleAsync | IWasmModule, eventType: number, eventID: number) { + throw new Error("internal error"); + } + twrUnregisterEvent(callingMod: IWasmModuleAsync | IWasmModule, eventType: number, eventID: number) { + throw new Error("internal error"); + } + twrUnregisterAllEvents(callingMod: IWasmModuleAsync | IWasmModule) { + throw new Error("internal error"); + } + + twrGetDrawCanvasJSID(callingMod: IWasmModuleAsync | IWasmModule) : number { + throw new Error("internal error"); + } + twrWindowAddMenu(callingMod: IWasmModuleAsync | IWasmModule, textPtr: number): number { + throw new Error("internal error"); + } + twrWindowMenuAddWidget(mod: IWasmModuleAsync | IWasmModule, menuID: number, consPtr: number): number { + throw new Error("internal error"); + } + twrWindowMenuWidgetAddCallback(mod: IWasmModuleAsync | IWasmModule, widgetID: number, eventID: number, extraPtr: number) { + throw new Error("internal Error!"); + } + twrWindowMenuDeleteWidget(mod: IWasmModuleAsync | IWasmModule, widgetID: number) { + throw new Error("internal error"); + } + twrWindowMenuRadioItemMerge(mod: IWasmModuleAsync | IWasmModule, widgetID1: number, widgetID2: number) { + throw new Error("internal error"); + } + twrWindowMenuWidgetSetProp(mod: IWasmModuleAsync | IWasmModule, widgetID: number, propNamePtr: number, dataPtr: number) { + throw new Error("internal error"); + } + twrWindowMenuWidgetGetProp(mod: IWasmModule, widgetID: number, propNamePtr: number): number { + throw new Error("internal error"); + } + twrWindowMenuWidgetListProps(mod: IWasmModule, widgetID: number, lengthPtr: number): number { + throw new Error("internal error"); + } + twrWindowMenuWidgetGetPropDetails(mod: IWasmModule, widgetID: number, detailsStructPtr: number) { + throw new Error("internal error"); + } + twrWindowMenuListProps(mod: IWasmModule, lengthPtr: number): number { + throw new Error("internal error"); + } + twrWindowMenuSetProp(mod: IWasmModuleAsync | IWasmModule, propNamePtr: number, dataPtr: number): void { + throw new Error("internal Error"); + } + twrWindowMenuGetProp(mod: IWasmModule, propNamePtr: number): number { + throw new Error("internal Error"); + } + } diff --git a/source/twr-ts/twrconwindow.ts b/source/twr-ts/twrconwindow.ts new file mode 100644 index 00000000..47102d4e --- /dev/null +++ b/source/twr-ts/twrconwindow.ts @@ -0,0 +1,2628 @@ +import { bindCanvasEvents, CanvasEventTypes, ICanvasEvents } from "./twrcanvasevents.js"; +import { IConsole, IConsoleBaseProps, IConsoleEvents, IConsoleWindow } from "./twrcon.js"; +import twrConsoleCanvas from "./twrconcanvas.js"; +import { TLibImports, twrLibrary, twrLibraryInstanceRegistry } from "./twrlibrary.js"; +import { IWasmModule } from "./twrmod"; +import { IWasmModuleAsync } from "./twrmodasync"; + + +type FullID = number; + +function calculateID(mod:IWasmModule|IWasmModuleAsync, id: number): FullID { + if (mod.id >= (2**20)) throw new Error("twrlibaudio was given a module ID greater than 20 bits long!"); + if (id >= (2**32)) throw new Error("twrlibaudio was given an object ID greater than 32 bits!"); + //should be equivalent to (mod.id << 32) | id + //can't use shift operations without being limited to 32-bit signed integers or using bignumber + return ((mod.id & (2**20 - 1)) * 2**32 + id) as FullID; +} + +enum twrWindowEvents { + CLOSE, + +} +// interface AsyncLikeSyncInterface { +// isAsync: boolean, +// then: (func: (...val: T) => U) => U | Promise; +// } +class FakePromise { + private val: T; + constructor(val: T) { + if (val instanceof Promise) { + throw new Error("FakePromise can't accept promises!!"); + } + this.val = val; + } + then(onfulfilled?: ((value: T) => TResult1 | FakePromise) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null | undefined): FakePromise { + if (onfulfilled != undefined && onfulfilled != null) { + const val = onfulfilled(this.extractVal()); + if (val instanceof FakePromise) { + return new FakePromise(val.extractVal()); + } else { + return new FakePromise(val); + } + } else { + throw new Error("must have a fullfilled case?"); + } + } + extractVal(): T { + if (this.val instanceof FakePromise) { + return this.val.extractVal(); + } else { + return this.val; + } + } +} +/// meant to fake the promise notation so that they can be written together +/// main downside is that it requires chains of callbacks since await isn't +/// really compatible with FakePromise +function wrapPossibleSync(val: Promise|T): Promise|FakePromise { + if (val instanceof Promise) { + return val; + } else { + return new FakePromise(val); + } +} + +/// simply unwraps the fake promise. If it's a promise, it leaves it be +function unwrapPossibleSync(val: Promise|FakePromise): Promise|T { + if (val instanceof FakePromise) { + return val.extractVal(); + } else { + return val; + } +} + +/// takes in a list of FakePromise or a list of Promise and +/// either returns a promise that returns the internal list, +/// or, it returns a FakePromise that does the same +function waitForAll( + vals: (FakePromise | Promise)[] +): Promise | FakePromise { + if (vals[0] instanceof Promise) { + return Promise.all(vals) as Promise; + } else if (vals[0] instanceof FakePromise) { + return new FakePromise((vals as FakePromise[]).map((val) => val.extractVal())) as FakePromise; + } + throw new Error('internal error!'); +} + +enum MenuItemEvents { + HOVERING, + UNHOVERED, + CLICKED, + MOUSE_MOVE, + CLICKED_OFF, +} +type MenuItemHoverEvent = { + type: MenuItemEvents.HOVERING +}; +type MenuItemUnhoverEvent = { + type: MenuItemEvents.UNHOVERED +}; +type MenuItemClickedEvent = { + type: MenuItemEvents.CLICKED, + x: number, + y: number, + button: number +}; +type MenuItemMouseMoveEvent = { + type: MenuItemEvents.MOUSE_MOVE, + x: number, + y: number +}; +type MenuItemClickedOffEvent = { + type: MenuItemEvents.CLICKED_OFF +}; +type MenuItemEventData = MenuItemHoverEvent + | MenuItemUnhoverEvent + | MenuItemClickedEvent + | MenuItemMouseMoveEvent + | MenuItemClickedOffEvent; + +// Globals used in widgets for a given window to specify things like: +// font, colors, spacing, etc. +// can be updated from the program and values should propogate +interface GlobalWidgetProperties { + borderColor: string, + selectedColor: string, + disabledColor: string, + disabledTextColor: string, + widgetTextFont: string, + menuOpenOffset: number, + subMenuOpenOffset: number, + textColor: string, + reservedPrefixLen: number, + xPadding: number, + yPadding: number, + menuBorderWidth: number, + menuBorderColor: string, + emptyMenuHeight: number, + emptyMenuWidth: number, + checkBoxCheckedPrefix: string; + checkBoxUncheckedPrefix: string; + radioMenuCheckedPrefix: string; + radioMenuUncheckedPrefix: string; +}; +enum PropBaseType { + String = 1, //0b0001 + Boolean = 2,//0b0010 + Number = 4, //0b0100 + Undefined = 8, //0b1000 + StringOrUndefined = 9, //0b1001 + BooleanOrUndefined = 10,//0b1010 + NumberOrUndefined = 12 //0b1100 +}; +enum PropPerms { + ReadOnly = 1, //0b01 + SetOnly = 2, //0b10 + ReadAndSet = 3, //0b11 -- Just addition of above two flags +} +interface ManagerAndWidgetCombined { + fullUpdate: (ctx: CanvasRenderingContext2D) => void; +} +interface WidgetManager extends ManagerAndWidgetCombined { + getCtx: () => CanvasRenderingContext2D; + childUpdated: (ctx: CanvasRenderingContext2D, widget?: Widget, sendToRoot?: boolean) => void; + + openPopup: (ctx: CanvasRenderingContext2D, widget: Widget, x: number, y: number, relativeChild?: Widget) => void; + closePopup: (ctx: CanvasRenderingContext2D, widget: Widget) => void; + handleDelete: (ctx: CanvasRenderingContext2D, widget: Widget) => void; + + resendLastMove: () => void; +} +type PublicPropertiesType = { [propName: string]: [[undefined|(() => string|number|undefined|boolean), undefined|((val: any) => void)], PropBaseType]; }; + +interface Widget extends ManagerAndWidgetCombined { + readonly globalProps: GlobalWidgetProperties; + readonly parent: WidgetManager; + readonly id: number; + readonly handledEvents: MenuItemEvents[]; + // readonly publicProperties: { [propName: string]: [PropPerms, PropType] }; + getPublicProperties: () => PublicPropertiesType; + setIsVisible: (val: boolean) => void; + getIsVisible: () => boolean; + + setWidth: (val: number|undefined) => void; + getWidth: () => number|undefined; + setHeight: (val: number|undefined) => void; + getHeight: () => number|undefined; + getUsedWidth: () => number; + getUsedHeight: () => number; + getMinWidth: () => number; + getMinHeight: () => number; + + setIsDisabled: (val: boolean) => void; + getIsDisabled: () => boolean; + + render: (ctx: CanvasRenderingContext2D, offsetX?: number, offsetY?: number) => void; + handleMenuEvent: (ctx: CanvasRenderingContext2D, event: MenuItemEventData) => void; + delete: (ctx: CanvasRenderingContext2D) => Widget[]; +} + +let NEXT_WIDGET_ID: number = 0; +abstract class WidgetImpl implements Widget { + readonly globalProps: GlobalWidgetProperties; + readonly parent: WidgetManager; + readonly id: number; + abstract readonly handledEvents: MenuItemEvents[]; + getPublicProperties(): PublicPropertiesType { + return { + "width": [[this.getWidth.bind(this), this.setWidth.bind(this)], PropBaseType.NumberOrUndefined], + "height": [[this.getHeight.bind(this), this.setHeight.bind(this)], PropBaseType.NumberOrUndefined], + "isVisible": [[this.getIsVisible.bind(this), this.setIsVisible.bind(this)], PropBaseType.Boolean], + "isDisabled": [[this.getIsDisabled.bind(this), this.setIsDisabled.bind(this)], PropBaseType.Boolean], + "minWidth": [[this.getMinWidth.bind(this), undefined], PropBaseType.Number], + "usedWidth": [[this.getUsedWidth.bind(this), undefined], PropBaseType.Number], + "minHeight": [[this.getMinHeight.bind(this), undefined], PropBaseType.Number], + "usedHeight": [[this.getUsedHeight.bind(this), undefined], PropBaseType.Number] + } + } + + protected abstract _width?: number; + protected abstract _height?: number; + protected _isVisible: boolean = true; + protected _isDisabled: boolean = false; + + constructor(globalProps: GlobalWidgetProperties, parent: WidgetManager, cons: WidgetConstructor) { + this.id = ++NEXT_WIDGET_ID; + this.parent = parent; + this.globalProps = globalProps; + + } + abstract render(ctx: CanvasRenderingContext2D, offsetX?: number, offsetY?: number): void; + abstract handleMenuEvent(ctx: CanvasRenderingContext2D, event: MenuItemEventData): void; + abstract delete(ctx: CanvasRenderingContext2D): Widget[]; + abstract fullUpdate(ctx: CanvasRenderingContext2D): void; + protected abstract propagateUpdate(ctx?: CanvasRenderingContext2D): void; + + setIsVisible(val: boolean) { + if (this._isVisible != val) { + this._isVisible = val; + this.propagateUpdate(); + } + } + getIsVisible() {return this._isVisible}; + + protected abstract disableStateChange(): void; + setIsDisabled(val: boolean) { + if (this._isDisabled != val) { + this._isDisabled = val; + this.disableStateChange(); + } + } + getIsDisabled() {return this._isDisabled}; + + setWidth(val: number|undefined) { + if (this._width != val) { + this._width = val; + this.propagateUpdate(); + } + } + getWidth() {return this._width}; + abstract getMinWidth(): number; + getUsedWidth(): number { + return this._width ?? this.getMinWidth(); + } + + setHeight(val: number|undefined) { + if (this._height != val) { + this._height = val; + this.propagateUpdate(); + } + } + getHeight() {return this._height}; + abstract getMinHeight(): number; + getUsedHeight(): number { + return this._height ?? this.getMinHeight(); + } +} + +//constructor fields in interfaces are ... weird +//They aren't implemented on classes themselves, instead, +// they are sort of auto implemented on any class that match their requirements +// So this is "automatically" implemented on any class that implements the given constructor fields +interface WidgetCreation { + new (ctx: CanvasRenderingContext2D, parent: WidgetManager, cons: U, globalProps: GlobalWidgetProperties): T +} + +interface WidgetConstructor { + width?: number; + height?: number; +} +enum ButtonWidgetVerticalCenteringMethod { + //don't center the widget vertically + None, + //center it based on the font so all strings of the same font will be centered the same + FontCentering, + //center it based on each individual string's minimum and maximum heights + StringCentering +} +interface ButtonWidgetConstructor extends WidgetConstructor { + text: string; + prefixText?: string; + suffixText?: string; + centeredHorizontally?: boolean; + verticalCentering?: ButtonWidgetVerticalCenteringMethod; + + reservePrefixSpace?: boolean; +} + +abstract class ButtonBase extends WidgetImpl { + readonly handledEvents: MenuItemEvents[] = [ + MenuItemEvents.CLICKED, + MenuItemEvents.HOVERING, + MenuItemEvents.UNHOVERED + ]; + + protected _width?: number; + protected _height?: number; + + private _text: string; + private _prefixText?: string; + private _suffixText?: string; + private centeredHorizontally: boolean; + private reservePrefixSpace: boolean; + private _verticalCentering: ButtonWidgetVerticalCenteringMethod; + + setText(val: string) {this._text = val; this.propagateUpdate(undefined, true)}; + getText(): string {return this._text}; + protected setPrefixText(val: string|undefined) {this._prefixText = val; this.propagateUpdate(undefined, true)}; + protected getPrefixText(): string|undefined {return this._prefixText}; + protected setSuffixText(val: string|undefined) {this._suffixText = val; this.propagateUpdate(undefined, true)}; + protected getSuffixText(): string|undefined {return this._suffixText}; + + + getPublicProperties(): PublicPropertiesType { + return { + ...super.getPublicProperties(), + "text": [[this.getText.bind(this), this.setText.bind(this)], PropBaseType.String] + } + }; + + private mousedOver: boolean = false; + + private calculatedFields = { + width: 0, + height: 0, + textOffsets: { + mainTextX: 0, + suffixX: 0, + y: 0, + } + }; + + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, cons: ButtonWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(globalProps, parent, cons); + + this._text = cons.text; + this._prefixText = cons.prefixText; + this._suffixText = cons.suffixText; + this.centeredHorizontally = cons.centeredHorizontally ?? false; + + this.reservePrefixSpace = cons.reservePrefixSpace ?? true; + + this._width = cons.width; + this._height = cons.height; + + this._verticalCentering = cons.verticalCentering ?? ButtonWidgetVerticalCenteringMethod.StringCentering; + + this.updateCalculatedFields(ctx); + } + + protected propagateUpdate(ctx?: CanvasRenderingContext2D, text_based: boolean = false): void { + const n_ctx = ctx ?? this.parent.getCtx(); + + const prev = { + x: this.calculatedFields.width, + y: this.calculatedFields.height + }; + this.updateCalculatedFields(n_ctx); + if (!text_based || prev.x != this.calculatedFields.width || prev.y != this.calculatedFields.height) + this.parent.childUpdated(n_ctx); + } + + private updateCalculatedFields(ctx: CanvasRenderingContext2D) { + const [minWidth, minHeight, minPrefix, minSuffix] = this.getFullMinSize(ctx); + + const calcFields = this.calculatedFields; + calcFields.width = minWidth; + calcFields.height = minHeight*1.1; + + const textOffsets = calcFields.textOffsets; + + + function getHeight(ctx: CanvasRenderingContext2D, font: string): number { + ctx.save();minHeight + ctx.font = font; + const measure = ctx.measureText("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.!?*&^%$#@)(0123456789-=_+`~[]{}\\|;:'\""); + ctx.restore(); + + return measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent; + } + + textOffsets.mainTextX = this.centeredHorizontally + ? (this.getUsedWidth() - minWidth)/2.0 + : minPrefix; + if (this.centeredHorizontally) { + console.log(`centered horizontally! ${this._text}`); + } + switch (this._verticalCentering) { + case ButtonWidgetVerticalCenteringMethod.None: + ctx.save(); + ctx.font = this.getFont(); + const below = ctx.measureText(this._text).actualBoundingBoxDescent; + ctx.restore(); + textOffsets.y = this.getUsedHeight() - minHeight + below; + break; + case ButtonWidgetVerticalCenteringMethod.FontCentering: + textOffsets.y = (this.getUsedHeight() + getHeight(ctx, this.getFont()))/2.0; + break; + case ButtonWidgetVerticalCenteringMethod.StringCentering: + textOffsets.y = (this.getUsedHeight() - minHeight)/2.0; + break; + } + textOffsets.suffixX = minSuffix; + } + + fullUpdate(ctx: CanvasRenderingContext2D) { + this.updateCalculatedFields(ctx); + } + delete(ctx: CanvasRenderingContext2D): Widget[] { + this.parent.handleDelete(ctx, this); + return [this]; + } + + private getFont() { + return this.globalProps.widgetTextFont; + } + + private getFullMinSize(ctx: CanvasRenderingContext2D): [number, number, number, number] { + ctx.save(); + ctx.font = this.getFont(); + const spaceMeasure = ctx.measureText(" "); + const spaceWidth = spaceMeasure.width; + + // ctx.textBaseline = "top"; + let minWidth = 0; + let minHeight = 0; + const parts = [this._prefixText, this._suffixText, this._text]; + let partWidths = []; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const minPartWidth = i == 0 && this.reservePrefixSpace + ? this.globalProps.reservedPrefixLen + : 0; + + if (part != undefined) { + const measure = ctx.measureText(part); + const width = Math.max(minPartWidth, measure.width) + minWidth += width + spaceWidth; + minHeight = Math.max(minHeight, measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent); + partWidths[i] = width; + } else { + minWidth += minPartWidth + ((i != parts.length-1) ? spaceWidth : 0 ); + partWidths[i] = minPartWidth; + } + } + ctx.restore(); + minWidth -= spaceWidth; + + const [minPrefix, minSuffix, ] = partWidths; + + + return [minWidth, minHeight, minPrefix, minSuffix]; + } + + getMinWidth() { + return this.calculatedFields.width; + } + + getMinHeight() { + return this.calculatedFields.height; + } + + render(ctx: CanvasRenderingContext2D, offsetX: number = 0, offsetY: number = 0) { + if (!this._isVisible) + return; + + ctx.save(); + + if (this._verticalCentering == ButtonWidgetVerticalCenteringMethod.StringCentering) + ctx.textBaseline = "top"; + + const textOffsets = this.calculatedFields.textOffsets; + + const button_color = this._isDisabled + ? this.globalProps.disabledColor + : (this.mousedOver + ? this.globalProps.selectedColor + : this.globalProps.borderColor); + + ctx.fillStyle = button_color; + ctx.fillRect(offsetX, offsetY, this.getUsedWidth(), this.getUsedHeight()); + + ctx.font = this.getFont(); + ctx.fillStyle = this._isDisabled + ? this.globalProps.disabledTextColor + : this.globalProps.textColor; + + if (this._prefixText != undefined) { + ctx.fillText( + this._prefixText, + offsetX, + textOffsets.y + offsetY + ); + } + ctx.fillText( + this._text, + textOffsets.mainTextX + offsetX, + textOffsets.y + offsetY + ); + if (this._suffixText != undefined) { + ctx.fillText( + " " + this._suffixText, + offsetX + (this.getUsedWidth() - textOffsets.suffixX), + textOffsets.y + offsetY + ); + } + + ctx.restore(); + } + handleMenuEvent(ctx: CanvasRenderingContext2D, event: MenuItemEventData) { + if (!this._isVisible || this._isDisabled) + return; + + switch (event.type) { + case MenuItemEvents.CLICKED: + { + this.buttonPressed(ctx); + } + break; + + case MenuItemEvents.HOVERING: + { + this.mousedOver = true; + } + break; + + case MenuItemEvents.UNHOVERED: + { + this.mousedOver = false; + } + break; + + default: + throw new Error(`Button handleMenuEvent was given an unrecognized event ${MenuItemEvents[event.type] ?? event.type}!`); + } + } + protected disableStateChange(): void { + this.mousedOver = false; + } + + abstract buttonPressed(ctx: CanvasRenderingContext2D): void; +} + +class Button extends ButtonBase implements WidgetEvents { + private events: Set<((ctx: CanvasRenderingContext2D) => void)> = new Set(); + + getSuffixText(): string|undefined {return super.getSuffixText()}; + setSuffixText(val: string|undefined) {super.setSuffixText(val)}; + getPrefixText(): string|undefined {return super.getPrefixText()}; + setPrefixText(val: string|undefined) {super.setPrefixText(val)}; + + getPublicProperties(): PublicPropertiesType { + return { + ...super.getPublicProperties(), + "suffixText": [[this.getSuffixText.bind(this), this.setSuffixText.bind(this)], PropBaseType.StringOrUndefined], + "prefixText": [[this.getPrefixText.bind(this), this.setPrefixText.bind(this)], PropBaseType.StringOrUndefined], + } + } + + buttonPressed(ctx: CanvasRenderingContext2D): void { + for (const callback of this.events.keys()) { + callback(ctx); + } + } + + addEvent(callback: (ctx: CanvasRenderingContext2D) => void) { + this.events.add(callback); + } + removeEvent(callback: (ctx: CanvasRenderingContext2D) => void) { + return this.events.delete(callback); + } +} + +interface ContainedWidget { + widget: Widget, + x: number, + y: number +} +interface Vec2 { + x: number, + y: number +} +abstract class WidgetContainer extends WidgetImpl implements WidgetManager { + + readonly handledEvents: MenuItemEvents[] = [ + MenuItemEvents.CLICKED, + MenuItemEvents.UNHOVERED, + MenuItemEvents.MOUSE_MOVE, + MenuItemEvents.CLICKED_OFF + ]; + + protected _width?: number; + protected _height?: number; + + protected children: ContainedWidget[] = []; + + private unhoverHandlers: (() => void)[] = []; + private clickedOffHandlers: (() => void)[] = []; + + protected _calculatedDimensions: Vec2; + protected getCalculatedDimensions() {return this._calculatedDimensions}; + protected setCalculatedDimensions(val: Vec2) { + this._calculatedDimensions = val; + if (this._width == undefined || this._height == undefined) { + this.parent.childUpdated(this.parent.getCtx()); + } + } + + private selectedItem?: ContainedWidget; + + private _drawOutline: boolean; + getDrawOutline() {return this._drawOutline}; + + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, props: MenuWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(globalProps, parent, {}); + + this._width = props.width; + this._height = props.height; + this._calculatedDimensions = { + x: globalProps.emptyMenuHeight, + y: globalProps.emptyMenuHeight, + }; + + this._drawOutline = props.drawOutline ?? false; + } + resendLastMove() { + this.parent.resendLastMove(); + } + getCtx() { + return this.parent.getCtx(); + } + fullUpdate(ctx: CanvasRenderingContext2D) { + for (const {widget} of this.children) { + widget.fullUpdate(ctx); + } + this.updateChildSize(); + this.selectedItem = undefined; + this.parent.resendLastMove(); + } + + protected propagateUpdate(ctx?: CanvasRenderingContext2D, click_off: boolean = false): void { + const n_ctx = ctx ?? this.parent.getCtx(); + if (!this._isVisible) return; + if (click_off) + this.handleMenuEvent(n_ctx, {type: MenuItemEvents.CLICKED_OFF}); + else + this.handleMenuEvent(n_ctx, {type: MenuItemEvents.UNHOVERED}); + + const prev_dims = { + x: this.getUsedWidth(), + y: this.getUsedHeight(), + }; + this.updateChildSize(); + if (prev_dims.x != this.getUsedWidth() || prev_dims.y != this.getUsedHeight()) { + this.parent.childUpdated(this.parent.getCtx()); + } + + this.parent.resendLastMove(); + + } + + pauseDeleteHandle: boolean = false; + handleDelete(ctx: CanvasRenderingContext2D, widget: Widget) { + if (this.pauseDeleteHandle) + return; + + for (let i = 0; i < this.children.length; i++) { + if (this.children[i].widget == widget) { + if (i >= 0) { + this.children.splice(i, 1); + this.childUpdated(ctx); + return; + } + } + } + } + + delete(ctx: CanvasRenderingContext2D) { + this.parent.handleDelete(ctx, this); + + this.pauseDeleteHandle = true; + let deleted: Widget[] = [this]; + for (const {widget} of this.children) { + deleted = deleted.concat(widget.delete(ctx)); + } + this.children = []; + this.updateChildSize(); + this.pauseDeleteHandle = false; + return deleted; + + } + abstract openPopup(ctx: CanvasRenderingContext2D, widget: Widget, x: number, y: number, relativeChild?: Widget): void; + + closePopup(ctx: CanvasRenderingContext2D, widget: Widget) { + this.parent.closePopup(ctx, widget); + } + + protected supressChildUpdates: boolean = false; + protected abstract updateChildSize(forceRun?: boolean): void; + + childUpdated(ctx: CanvasRenderingContext2D, widget?: Widget, sendToRoot: boolean = false) { + if (sendToRoot) { + this.parent.childUpdated(ctx, widget, true); + } else if (!this.supressChildUpdates) { + this.propagateUpdate(ctx); + } + } + protected addChild< + T extends Widget, + U extends WidgetConstructor, + >(ctx: CanvasRenderingContext2D, cons: WidgetCreation, props: U, x: number, y: number): T { + const widget: T = new cons(ctx, this, props, this.globalProps); + + this.children.push({ + widget: widget, + x: x, + y: y + }); + this.updateChildSize(true); + this.parent.childUpdated(ctx, this); + + return widget; + } + + getMinWidth() { + return this._calculatedDimensions.x; + } + getMinHeight() { + return this._calculatedDimensions.y; + } + render(ctx: CanvasRenderingContext2D, offsetX: number = 0, offsetY: number = 0): void { + if (!this._isVisible) + return; + + ctx.save(); + + const width = this._width ?? this._calculatedDimensions.x; + const height = this._height ?? this._calculatedDimensions.y; + + ctx.fillStyle = this.globalProps.borderColor; + ctx.fillRect( + offsetX, + offsetY, + width, + height, + ); + + const menuBorderWidth = this.globalProps.menuBorderWidth; + if (menuBorderWidth > 0 && this._drawOutline) { + ctx.strokeStyle = this.globalProps.menuBorderColor; + ctx.lineWidth = menuBorderWidth; + ctx.beginPath(); + ctx.rect( + offsetX + menuBorderWidth/2.0, + offsetY + menuBorderWidth/2.0, + width - menuBorderWidth, + height - menuBorderWidth + ); + ctx.stroke(); + ctx.closePath(); + } + + for (const {widget, x: wX, y: wY} of this.children) { + widget.render(ctx, offsetX + wX + menuBorderWidth, offsetY + wY + menuBorderWidth); + } + + ctx.restore(); + } + + private dispatchEvent(ctx: CanvasRenderingContext2D, widget: Widget, event: MenuItemEventData) { + if (widget.handledEvents.includes(event.type) && widget.getIsVisible()) { + widget.handleMenuEvent(ctx, event); + } + } + + private mouseInWidgetBounds(widgetContainer: ContainedWidget, x: number, y: number) { + const {widget, x: widgetX, y: widgetY} = widgetContainer; + return widgetX <= x && x <= widgetX + widget.getUsedWidth() + && widgetY <= y && y <= widgetY + widget.getUsedHeight(); + } + + //updates the currently selected object using the given coords + //also handles HOVERING and UNHOVERED events + private updateSelectedObject(ctx: CanvasRenderingContext2D,x: number, y: number) { + //check if hovering over selected item + if (this.selectedItem) { + if (this.mouseInWidgetBounds(this.selectedItem, x, y) && this.selectedItem.widget.getIsVisible()) { + return; //the selected item is in bounds + } else { + //selected item is out of bounds + this.dispatchEvent(ctx, this.selectedItem.widget, {type:MenuItemEvents.UNHOVERED}); + this.selectedItem = undefined; + } + } + //otherwise, find what object (if any) are hovered over + for (const child of this.children) { + //found child it's hovering over + if (this.mouseInWidgetBounds(child, x, y) && child.widget.getIsVisible()) { + this.selectedItem = child; + this.dispatchEvent(ctx, child.widget, {type:MenuItemEvents.HOVERING}); + //break early + return; + } + } + } + handleMenuEvent(ctx: CanvasRenderingContext2D, event: MenuItemEventData): void { + if (!this._isVisible) + return; + switch (event.type) { + case MenuItemEvents.MOUSE_MOVE: + { + const {x: eX, y: eY} = event; + this.updateSelectedObject(ctx, eX, eY); + if (this.selectedItem) { + const {widget, x: wX, y: wY} = this.selectedItem; + const [x, y] = [eX - wX, eY - wY]; + this.dispatchEvent(ctx, widget, {type: MenuItemEvents.MOUSE_MOVE, x: x, y: y}); + } + } + break; + case MenuItemEvents.CLICKED: + { + const {x: eX, y: eY, button} = event; + this.updateSelectedObject(ctx, eX, eY); + const selected = this.selectedItem; + for (const {widget} of this.children) { + if (selected == undefined || widget != selected.widget) + this.dispatchEvent(ctx, widget, {type: MenuItemEvents.CLICKED_OFF}); + } + if (selected) { + const [x, y] = [eX - selected.x, eY - selected.y]; + this.dispatchEvent(ctx, selected.widget, {type: MenuItemEvents.CLICKED, x: x, y: y, button: button}); + } + + } + break; + case MenuItemEvents.UNHOVERED: + { + if (this.selectedItem) { + this.dispatchEvent(ctx, this.selectedItem.widget, {type: MenuItemEvents.UNHOVERED}); + this.selectedItem = undefined; + } + for (const handler of this.unhoverHandlers) { + handler(); + } + } + break; + case MenuItemEvents.CLICKED_OFF: + { + for (const handler of this.clickedOffHandlers) { + handler(); + } + for (const {widget} of this.children) + this.dispatchEvent(ctx, widget, {type: MenuItemEvents.CLICKED_OFF}); + } + break; + } + } + + bindUnhoverEvent(callback: () => void) { + this.unhoverHandlers.push(callback); + } + unbindUnhoverEvent(callback: () => void) { + const index = this.unhoverHandlers.indexOf(callback); + if (index >= 0) + this.unhoverHandlers.splice(index, 1); + } + + bindClickedOffEvent(callback: () => void) { + this.clickedOffHandlers.push(callback); + } + unbindClickedOffEvent(callback: () => void) { + const index = this.clickedOffHandlers.indexOf(callback); + if (index >= 0) { + this.clickedOffHandlers.splice(index, 1); + } + } + + protected disableStateChange(): void { + //TODO@ figure out what to do here? + //should menus be capable of being disabled? + // in fact, since RadioMenu was converted to linked Radio Items + // is there any point in menus being considered Widgets at all? + // you can link them to widgets just fine with things like MenuButtons + } + +} + +interface MenuWidgetConstructor extends WidgetConstructor { + drawOutline?: boolean, +} + +class Menu extends WidgetContainer { + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, props: MenuWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(ctx, parent, props, globalProps); + } + getCtx() { + return this.parent.getCtx(); + } + openPopup(ctx: CanvasRenderingContext2D, widget: Widget, x: number, y: number, relativeChild?: Widget) { + let [nX, nY] = [x, y]; + let nextRelative = relativeChild; + if (relativeChild != undefined) { + for (const {widget, y: wY} of this.children) { + if (widget == relativeChild) { + nY += wY; + if (nX > 0) + nX += this.globalProps.menuBorderWidth * 2; + else + nX -= this.globalProps.menuBorderWidth; + nextRelative = this; + break; + } + } + } + this.parent.openPopup(ctx, widget, nX, nY, nextRelative); + } + + lastWidth: number = 0; + lastHeight: number = 0; + supressChildUpdates: boolean = false; + protected updateChildSize(forceRun: boolean = false) { + let childWidth = 0; + let childHeight = 0; + for (const {widget} of this.children) { + if (!widget.getIsVisible()) + continue; + const width = widget.getMinWidth(); + childWidth = Math.max(childWidth, width); + childHeight += widget.getUsedHeight() + this.globalProps.yPadding; + } + childWidth += this.globalProps.menuBorderWidth * 4; + childHeight += this.globalProps.menuBorderWidth * 2; + + + childWidth = Math.max(childWidth, this.globalProps.emptyMenuWidth); + childHeight = Math.max(childHeight, this.globalProps.emptyMenuHeight); + + const newWidth = super.getWidth() ?? childWidth; + const newHeight = super.getHeight() ?? childHeight; + this._calculatedDimensions = { + x: newWidth, + y: newHeight + }; + + if (this.lastWidth != newWidth || this.lastHeight != newHeight || forceRun) { + this.lastWidth = newWidth; + this.lastHeight = newHeight; + + let curHeight = this.globalProps.menuBorderWidth; + this.supressChildUpdates = true; + for (const child of this.children) { + if (!child.widget.getIsVisible()) + continue; + child.widget.setWidth(newWidth - this.globalProps.menuBorderWidth * 4); + child.y = curHeight; + curHeight += child.widget.getUsedHeight() + this.globalProps.yPadding; + } + this.supressChildUpdates = false; + + } + } + addChild< + T extends Widget, + U extends WidgetConstructor, + >(ctx: CanvasRenderingContext2D, cons: WidgetCreation, props: U): T { + return super.addChild(ctx, cons, props, this.globalProps.menuBorderWidth, 0); + } +} + +class MenuBar extends WidgetContainer { + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, props: MenuWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(ctx, parent, props, globalProps); + } + + protected updateChildSize(forceRun?: boolean) { + let minHeight = this.globalProps.emptyMenuWidth; + let width = 0; + for (const {widget} of this.children) { + if (!widget.getIsVisible()) + continue; + + const [widgetWidth, widgetHeight] = [widget.getUsedWidth(), widget.getUsedHeight()]; + const [widgetMinWidth, widgetMinHeight] = [widget.getMinWidth(), widget.getMinHeight()]; + + minHeight = Math.max(minHeight, widgetHeight, widgetMinHeight); + width += Math.max(this.globalProps.emptyMenuWidth, widgetWidth, widgetMinWidth) + this.globalProps.xPadding; + } + + this._calculatedDimensions = { + x: width, + y: minHeight + }; + + const tmpHeight = super.getHeight() ?? minHeight; + const tmpWidth = super.getWidth() ?? width; + + let pos = 0; + this.supressChildUpdates = true; + for (const child of this.children) { + if (!child.widget.getIsVisible()) + continue; + + const widgetWidth = child.widget.getUsedWidth(); + const minWidgetWidth = child.widget.getMinWidth(); + const nWidth = Math.max(this.globalProps.emptyMenuWidth, widgetWidth, minWidgetWidth); + child.widget.setWidth(nWidth); + child.widget.setHeight(tmpHeight); + + child.x = pos; + pos += nWidth + this.globalProps.xPadding; + } + this.supressChildUpdates = false; + } + addChild< + T extends Widget, + U extends WidgetConstructor, + >(ctx: CanvasRenderingContext2D, cons: WidgetCreation, props: U): T { + return super.addChild( + ctx, cons, props, + 0, super.getDrawOutline() ? this.globalProps.menuBorderWidth/2 : 0 + ); + } + + openPopup(ctx: CanvasRenderingContext2D, widget: Widget, x: number, y: number, relativeChild?: Widget) { + let [nX, nY] = [x, y]; + let nextRelative = relativeChild; + if (relativeChild != undefined) { + for (const child of this.children) { + if (child.widget == relativeChild) { + nX += child.x; + nextRelative = this; + break; + } + } + } + this.parent.openPopup(ctx, widget, nX, nY += this.globalProps.menuBorderWidth, nextRelative); + } +} + +class RootWidgetManager implements WidgetManager { + private boundWidgets: [Widget, number, number][] = []; + private popupWidgets: Map = new Map(); + + private selectedWidget?: [Widget, number, number]; + private ctx: CanvasRenderingContext2D; + constructor(ctx: CanvasRenderingContext2D) { + this.ctx = ctx; + } + getCtx() { + return this.ctx; + } + handleDelete(ctx: CanvasRenderingContext2D, widget: Widget) { + for (let i = 0; i < this.boundWidgets.length; i++) { + if (this.boundWidgets[i][0] == widget) { + this.boundWidgets.splice(i, 1); + this.childUpdated(ctx); + return; + } + } + } + fullUpdate(ctx?: CanvasRenderingContext2D) { + for (let i = 0; i < this.boundWidgets.length; i++) { + this.boundWidgets[i][0].fullUpdate(this.ctx); + } + for (const [widget, ] of this.popupWidgets) { + widget.fullUpdate(this.ctx); + } + } + + addChild< + T extends Widget, + U extends WidgetConstructor, + >(ctx: CanvasRenderingContext2D, cons: WidgetCreation, props: U, globalProps: GlobalWidgetProperties, x: number, y: number): T { + const widget: T = new cons(ctx, this, props, globalProps); + + this.boundWidgets.push([widget, x, y]); + + return widget; + } + childUpdated(ctx: CanvasRenderingContext2D, widget?: Widget) { + this.selectedWidget = undefined; + } + + openPopup(ctx: CanvasRenderingContext2D, widget: Widget, x: number, y: number, relativeChild?: Widget) { + let [nX, nY] = [x, y]; + if (relativeChild != undefined) { + let fromPopups = this.popupWidgets.get(relativeChild); + if (fromPopups) { + nX += fromPopups[0]; + nY += fromPopups[1]; + } else { + for (const child of this.boundWidgets) { + nX += child[1]; + nY += child[2]; + } + } + } + this.popupWidgets.set(widget, [nX, nY]); + this.childUpdated(ctx, widget); + } + closePopup(ctx: CanvasRenderingContext2D, widget: Widget) { + this.popupWidgets.delete(widget); + this.childUpdated(ctx); + } + + handleCanvasKeyEvent(ctx: CanvasRenderingContext2D, event: CanvasEventTypes, key: number): boolean { + return false; + } + + private widgetInBounds(widget: Widget, widgetX: number, widgetY: number, x: number, y: number): boolean { + const [wW, wH] = [widget.getUsedWidth(), widget.getUsedHeight()]; + return widgetX <= x && x <= widgetX + wW + && widgetY <= y && y <= widgetY + wH; + } + private dispatchEvent(ctx: CanvasRenderingContext2D, widget: Widget, event: MenuItemEventData) { + if (widget.handledEvents.includes(event.type)) { + widget.handleMenuEvent(ctx, event); + } + } + private updateSelected(ctx: CanvasRenderingContext2D, x: number, y: number) { + if (this.selectedWidget) { + if (this.widgetInBounds(this.selectedWidget[0], this.selectedWidget[1], this.selectedWidget[2], x, y) && this.selectedWidget[0].getIsVisible()) { + return; + } else { + this.dispatchEvent(ctx, this.selectedWidget[0], {type: MenuItemEvents.UNHOVERED}); + this.selectedWidget = undefined; + } + } + + //most recently added is on top in order of popups -> bound + + const widgetList = Array.from(this.popupWidgets.entries()); + for (let i = widgetList.length-1; i >= 0; i--) { + const [widget, [widgetX, widgetY]] = widgetList[i]; + if (this.widgetInBounds(widget, widgetX, widgetY, x, y)) { + this.selectedWidget = [widget, widgetX, widgetY]; + this.dispatchEvent(ctx, widget, {type: MenuItemEvents.HOVERING}); + return; + } + } + + for (let i = this.boundWidgets.length-1; i >= 0; i--) { + const [widget, widgetX, widgetY] = this.boundWidgets[i]; + + if (this.widgetInBounds(widget, widgetX, widgetY, x, y) && widget.getIsVisible()) { + this.selectedWidget = [widget, widgetX, widgetY]; + this.dispatchEvent(ctx, widget, {type: MenuItemEvents.HOVERING}); + return; + } + } + } + + private lastMouseMove: [number, number]|undefined = undefined; + resendLastMove() { + if (this.lastMouseMove != undefined) { + const [x, y] = this.lastMouseMove; + this.handleCanvasMouseEvent(this.ctx, CanvasEventTypes.MOUSE_MOVE, x, y, 0); + } + } + handleCanvasMouseEvent(ctx: CanvasRenderingContext2D, event: CanvasEventTypes, x: number, y: number, button: number): boolean { + this.lastMouseMove = [x, y]; + this.updateSelected(ctx, x, y); + if (!this.selectedWidget) { + if (event == CanvasEventTypes.MOUSE_CLICK) { + for (const [popup, ] of this.popupWidgets) { + this.dispatchEvent(ctx, popup, {type: MenuItemEvents.CLICKED_OFF}); + } + } + return false; + } + + switch (event) { + case CanvasEventTypes.MOUSE_MOVE: + { + const [nX, nY] = [x - this.selectedWidget[1], y - this.selectedWidget[2]]; + this.dispatchEvent(ctx, this.selectedWidget[0], {type: MenuItemEvents.MOUSE_MOVE, x: nX, y: nY}); + } + break; + + case CanvasEventTypes.MOUSE_CLICK: + { + const selected = this.selectedWidget; + if (!this.popupWidgets.has(selected[0])) { + for (const popup of this.popupWidgets) { + if (selected[0] != popup[0]) + this.dispatchEvent(ctx, popup[0], {type: MenuItemEvents.CLICKED_OFF}); + } + } + const [nX, nY] = [x - selected[1], y - selected[2]]; + this.dispatchEvent(ctx, selected[0], {type: MenuItemEvents.CLICKED, x: nX, y: nY, button: button}); + } + break; + } + + return true; + } + + handleCanvasWheelEvent(ctx: CanvasRenderingContext2D, event: CanvasEventTypes, deltaX: number, deltaY: number, deltaZ: number, deltaMode: number) { + return false; + } + + handleCanvasAnimationFrameEvent(ctx: CanvasRenderingContext2D, event: CanvasEventTypes, delta: number) { + //render in reverse order of the way events are handled + //that way top most items are drawn last so they appear on top + for (const [widget, [x, y]] of this.popupWidgets) + widget.render(ctx, x, y); + for (const [widget, x, y] of this.boundWidgets) + widget.render(ctx, x, y); + } +} + +interface MenuButtonWidgetConstructor extends ButtonWidgetConstructor { + menuWidth?: number; + menuHeight?: number; + openToRight?: boolean, + offset?: number, +} +class MenuButton extends ButtonBase implements WidgetManager { + readonly handledEvents: MenuItemEvents[] = [ + MenuItemEvents.CLICKED, + MenuItemEvents.HOVERING, + MenuItemEvents.UNHOVERED, + MenuItemEvents.CLICKED_OFF + ]; + + readonly menu: Menu; + + private openToRight: boolean; + private offset: number; + + private menuOpened: boolean = false; + + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, props: MenuButtonWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(ctx, parent, props, globalProps); + + this.openToRight = props.openToRight ?? false; + this.offset = props.offset ?? 0; + this.menu = new Menu(ctx, this, { + width: props.menuWidth, + height: props.menuHeight, + drawOutline: true, + }, globalProps); + + this.menu.bindClickedOffEvent((() => { + this.parent.closePopup(ctx, this.menu); + this.menuOpened = false; + }).bind(this)); + } + resendLastMove() { + this.parent.resendLastMove(); + } + buttonPressed(ctx: CanvasRenderingContext2D): void { + const [width, height] = [super.getUsedWidth(), super.getUsedHeight()]; + let x = this.openToRight ? width + this.offset : 0; + let y = this.openToRight ? 0 : height + this.offset; + this.parent.openPopup(ctx, this.menu, x, y, this); + this.menuOpened = true; + } + getCtx() { + return this.parent.getCtx(); + } + fullUpdate(ctx: CanvasRenderingContext2D) { + super.fullUpdate(ctx); + this.menu.fullUpdate(ctx); + } + + private supressHandleDelete: boolean = false; + handleDelete(ctx: CanvasRenderingContext2D, widget: Widget) { + if (this.supressHandleDelete) + return; + else if (widget == this.menu) + throw new Error("MenuButton's internal menu shouldn't be externally accesible"); + else + throw new Error("MenuButton shouldn't be handling any deletions?"); + } + delete(ctx: CanvasRenderingContext2D): Widget[] { + this.supressHandleDelete = true; + this.parent.handleDelete(ctx, this); + let deleted: Widget[] = this.menu.delete(ctx); + //delete menu from list since it's not exposed externally + deleted.splice(deleted.indexOf(this.menu), 1); + deleted.push(this); //push self + this.supressHandleDelete = false; + return deleted; + } + private closeSubMenu(ctx: CanvasRenderingContext2D) { + if (this.menuOpened) { + this.menu.handleMenuEvent(ctx, {type: MenuItemEvents.CLICKED_OFF}); + this.parent.closePopup(ctx, this.menu); + } + } + handleMenuEvent(ctx: CanvasRenderingContext2D, event: MenuItemEventData) { + if (event.type == MenuItemEvents.CLICKED_OFF) { + this.closeSubMenu(ctx); + } else { + super.handleMenuEvent(ctx, event); + } + } + + childUpdated(ctx: CanvasRenderingContext2D, widget?: Widget, sendToRoot?: boolean) { + this.parent.childUpdated(ctx, widget, sendToRoot); + } + addChild< + T extends Widget, + U extends WidgetConstructor, + >(ctx: CanvasRenderingContext2D, cons: WidgetCreation, props: U): T { + return this.menu.addChild(ctx, cons, props); + } + openPopup(ctx: CanvasRenderingContext2D, widget: Widget, x: number, y: number, relativeChild?: Widget) { + this.parent.openPopup(ctx, widget, x, y, relativeChild); + } + closePopup(ctx: CanvasRenderingContext2D, widget: Widget) { + this.parent.closePopup(ctx, widget); + } + protected disableStateChange(): void { + this.closeSubMenu(this.parent.getCtx()); + } +} + +interface SeperatorWidgetConstructor extends WidgetConstructor { + seperatorText: string, +} +class Seperator extends WidgetImpl { + readonly handledEvents: MenuItemEvents[] = []; + + private seperatorText: string; + + protected _width?: number; + protected _height?: number; + + private _minHeight: number = 0; + + private _text: string = ""; + private textXOffset: number = 0; + private textYOffset: number = 0; + + setText(val: string) {this._text = val; this.fullUpdate(this.parent.getCtx())}; + getText(): string {return this._text}; + + getPublicProperties(): PublicPropertiesType { + return { + ...super.getPublicProperties(), + "text": [[this.getText.bind(this), this.setText.bind(this)], PropBaseType.String] + } + } + + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, props: SeperatorWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(globalProps, parent, {}); + + this.seperatorText = props.seperatorText; + + this._width = props.width; + this._height = props.height; + + this.updateText(ctx); + } + fullUpdate(ctx: CanvasRenderingContext2D) { + this.updateText(ctx); + } + + delete(ctx: CanvasRenderingContext2D) { + this.parent.handleDelete(ctx, this); + return [this]; + } + private updateText(ctx: CanvasRenderingContext2D) { + ctx.save(); + + ctx.font = this.globalProps.widgetTextFont; + const metrics = ctx.measureText(this.seperatorText); + + const repeats = Math.floor(this.getUsedWidth()/metrics.width); + const width = repeats*metrics.width; + const height = metrics.actualBoundingBoxAscent; + this._minHeight = height; + + this._text = this.seperatorText.repeat(repeats); + + this.textXOffset = (this.getUsedWidth() - width)/2.0; + this.textYOffset = (this.getUsedHeight() + height)/2.0; + + + ctx.restore(); + } + + + getMinWidth() { + return 0; + } + + getMinHeight() { + if (this._width == 0) { + return 0; + } else { + return this._minHeight; + } + } + + render(ctx: CanvasRenderingContext2D, offsetX: number = 0, offsetY: number = 0) { + if (!this._isVisible) + return; + + ctx.save(); + + ctx.font = this.globalProps.widgetTextFont; + ctx.fillStyle = this.globalProps.textColor; + + ctx.fillText( + this._text, + offsetX + this.textXOffset, + offsetY + this.textYOffset + ); + + ctx.restore(); + } + handleMenuEvent(ctx: CanvasRenderingContext2D, event: MenuItemEventData) { + throw new Error(`Seperator widget doesn't accept events!!`); + } + protected propagateUpdate(ctx?: CanvasRenderingContext2D): void { + this.updateText(ctx ?? this.parent.getCtx()); + } + protected disableStateChange(): void { + //Do nothing, seperator can't really be disabled... + } +} + + +interface CheckBoxWidgetConstructor extends WidgetConstructor { + text: string; +} +class CheckBox extends ButtonBase implements WidgetEvents { + private callbacks: Set<(ctx: CanvasRenderingContext2D, selected: boolean) => void> = new Set(); + + private _selected = false; + + setSelected(val: boolean, supressEventPassdown: boolean = false, ctx?: CanvasRenderingContext2D) { + if (val == this._selected) return; + this._selected = val; + //prefixText uses builtin getter/setter to auto update properties + super.setPrefixText(this.globalProps[this._selected ? "checkBoxCheckedPrefix" : "checkBoxUncheckedPrefix"]); + + if (!supressEventPassdown) { + const n_ctx = ctx ?? this.parent.getCtx(); + for (const callback of this.callbacks) { + callback(n_ctx, this._selected); + } + } + + } + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, cons: ButtonWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(ctx, parent, cons, globalProps); + + super.setPrefixText(globalProps.checkBoxUncheckedPrefix); + } + + buttonPressed(ctx: CanvasRenderingContext2D): void { + this.setSelected(!this._selected, false, ctx); + } + + + addEvent(callback: (ctx: CanvasRenderingContext2D, selected: boolean) => void) { + this.callbacks.add(callback); + } + removeEvent(callback: (ctx: CanvasRenderingContext2D, selected: boolean) => void) { + return this.callbacks.delete(callback); + } +} + +class RadioItemGroup { + private items: Mapvoid, + setRadioGroup: (group: RadioItemGroup)=>void + }> = new Map(); + + private selected: RadioItem; + constructor(item: RadioItem, deselect: (ctx: CanvasRenderingContext2D)=>void, setRadioGroup: (group: RadioItemGroup)=>void) { + this.items.set(item, { + deselect: deselect, + setRadioGroup: setRadioGroup + }); + this.selected = item; + } + + optionSeleted(item: RadioItem, ctx: CanvasRenderingContext2D) { + if (this.selected != item) { + this.items.get(this.selected)!.deselect(ctx); + this.selected = item; + } + } + + mergeGroups(oth: RadioItemGroup, ctx: CanvasRenderingContext2D) { + if (oth == this) return; + for (const [item, funcs] of oth.items) { + if (this.items.has(item)) + throw new Error(`Somehow merged item twice??`); + this.items.set(item, funcs); + funcs.deselect(ctx); + funcs.setRadioGroup(this); + } + } +} +class RadioItem extends ButtonBase implements WidgetEvents { + private callbacks: Set<(ctx: CanvasRenderingContext2D, selected: boolean) => void> = new Set(); + + private _selected = true; + private radioGroup: RadioItemGroup; + + constructor(ctx: CanvasRenderingContext2D, parent: WidgetManager, cons: ButtonWidgetConstructor, globalProps: GlobalWidgetProperties) { + super(ctx, parent, cons, globalProps); + + this.radioGroup = new RadioItemGroup(this, this.deselect.bind(this), this.setRadioGroup.bind(this)); + + super.setPrefixText(globalProps.radioMenuCheckedPrefix); + } + + buttonPressed(ctx: CanvasRenderingContext2D): void { + if (!this._selected) { + this.makeSelected(true, ctx); + } + } + + private deselect(ctx: CanvasRenderingContext2D) { + this._selected = false; + this.setPrefixText(this.globalProps.radioMenuUncheckedPrefix); + for (const callback of this.callbacks) { + callback(ctx, false); + } + } + private setRadioGroup(group: RadioItemGroup) { + this.radioGroup = group; + } + + makeSelected(sendEvent: boolean = true, ctx?: CanvasRenderingContext2D) { + const n_ctx = ctx ?? this.parent.getCtx(); + this.radioGroup.optionSeleted(this, this.parent.getCtx()); + this.setPrefixText(this.globalProps.radioMenuCheckedPrefix); + this._selected = true; + if (sendEvent) { + for (const callback of this.callbacks) { + callback(n_ctx, true); + } + } + } + + mergeGroups(oth: RadioItem) { + this.radioGroup.mergeGroups(oth.radioGroup, this.parent.getCtx()); + } + + addEvent(callback: (ctx: CanvasRenderingContext2D, selected: boolean) => void) { + this.callbacks.add(callback); + } + removeEvent(callback: (ctx: CanvasRenderingContext2D, selected: boolean) => void) { + return this.callbacks.delete(callback); + } +} + + +interface WidgetEvents { + addEvent: (callback: () => void) => void; + removeEvent: (callback: () => void) => void; +} + +const MENU_PADDING_X = 5; +const MENU_PADDING_Y = 4; +const TOP_BAR_SIZE: number = 30; +const BORDER_SIZE: number = 5; +const MENU_START_X = BORDER_SIZE; + +enum WidgetType { + Button, + Seperator, + RadioItem, + SubMenu, + CheckBox, +} +enum WindowEventTypes { + WindowResize, +} + +export class twrConsoleWindow extends twrLibrary implements ICanvasEvents, IConsoleWindow { + id: number; + props: IConsoleBaseProps; + + element: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + + drawCanvasWidth: number; + drawCanvasHeight: number; + readonly drawCanvas: twrConsoleCanvas; + + private windowEventHandlers: Map, number][]> = new Map(); + widgets: Map< + number, + [WidgetType.Button, Button] + | [WidgetType.Seperator, Seperator] + | [WidgetType.RadioItem, RadioItem] + | [WidgetType.SubMenu, MenuButton] + | [WidgetType.CheckBox, CheckBox] + > = new Map(); + + //list of events to run before drawing + // only example right now is resizing the window + private runBeforeDrawEvents: (() => void)[] = []; + + readonly manager: RootWidgetManager; + readonly menu: MenuBar; + + widgetSettings: GlobalWidgetProperties = { + borderColor: "#B0B0B0", + selectedColor: "#D0D0D0", + disabledColor: "#808080", + disabledTextColor: "#D0D0D0", + widgetTextFont: "12px Seriph", + menuOpenOffset: 5, + subMenuOpenOffset: 1, + textColor: "black", + reservedPrefixLen: 15, + yPadding: 5, + menuBorderWidth: 1.0, + menuBorderColor: "Black", + + xPadding: 5, + emptyMenuHeight: 20, + emptyMenuWidth: 20, + radioMenuCheckedPrefix: "*", + radioMenuUncheckedPrefix: "", + checkBoxCheckedPrefix: "[*]", + checkBoxUncheckedPrefix: "[ ]" + }; + + + imports: TLibImports = { + twrGetDrawCanvasJSID: {}, + twrWindowAddMenu: {}, + twrWindowMenuAddWidget: {}, + twrWindowMenuWidgetAddCallback: {}, + twrWindowMenuDeleteWidget: {}, + twrWindowMenuRadioItemMerge: {}, + twrWindowMenuWidgetSetProp: {}, + twrWindowMenuWidgetGetProp: {isAsyncFunction: true}, + twrWindowMenuWidgetListProps: {isAsyncFunction: true}, + twrWindowMenuWidgetGetPropDetails: {}, + twrWindowMenuListProps: {isAsyncFunction: true}, + twrWindowMenuSetProp: {}, + twrWindowMenuGetProp: {isAsyncFunction: true}, + twrRegisterEvent: {}, + twrUnregisterEvent: {}, + twrUnregisterAllEvents: {}, + }; + + // every library should have this line + libSourcePath = new URL(import.meta.url).pathname; + + calculatedrawCanvasDims(): [number, number] { + return [ + this.element.width - BORDER_SIZE*2.0, + this.element.height - BORDER_SIZE - TOP_BAR_SIZE, + ]; + } + + constructor(canvas: HTMLCanvasElement, selfRegisterEvents: boolean = true) { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + + this.element = canvas; + this.ctx = canvas.getContext("2d")!; + this.manager = new RootWidgetManager(this.ctx); + + // this.drawCanvasHeight = Math.floor(canvas.height - BORDER_SIZE - TOP_BAR_SIZE); + // this.drawCanvasWidth = Math.floor(canvas.width - BORDER_SIZE*2.0); + [this.drawCanvasWidth, this.drawCanvasHeight] = this.calculatedrawCanvasDims(); + + const drawCanvas = document.createElement("canvas"); + drawCanvas.height = this.drawCanvasHeight; + drawCanvas.width = this.drawCanvasWidth; + + this.drawCanvas = new twrConsoleCanvas(drawCanvas, undefined, false); + + if (selfRegisterEvents) + bindCanvasEvents(this, this.element); + + this.props = { + //TODO: Figure out what type to add/use here + type: 0 + }; + + const menuBarCons: MenuWidgetConstructor = { + height: TOP_BAR_SIZE - 2, + }; + this.menu = this.manager.addChild(this.ctx, MenuBar, menuBarCons, this.widgetSettings, BORDER_SIZE + MENU_PADDING_X, 0); + } + + getProp(propName: string) { + return this.props[propName]; + }; + + twrConGetProp(callingMod: IWasmModule | IWasmModuleAsync, pn: number) { + const propName=callingMod.wasmMem.getString(pn); + return this.getProp(propName); + }; + + private internalSendEvent(eventType: WindowEventTypes, ...extraArgs: number[]) { + const handlers = this.windowEventHandlers.get(eventType); + if (handlers == undefined) return; + + // for (const [mod, eventID] of handlers) { + // mod.postEvent(eventID, ...extraArgs); + // } + for (let i = handlers.length-1; i >= 0; i--) { + const [mod, eventID] = handlers[i]; + const derefedMod = mod.deref(); + if (derefedMod) { + derefedMod.postEvent(eventID, ...extraArgs); + } else { + handlers.splice(i, 1); + } + } + } + + twrRegisterEvent(callingMod: IWasmModuleAsync | IWasmModule, eventType: number, eventID: number) { + if (WindowEventTypes[eventType] == undefined) throw new Error(`twrRegisterEvent: Invalid event type (${eventType}) for twrConsoleWindow!`); + const event: WindowEventTypes = eventType as WindowEventTypes; + + const eventHandlersTmp = this.windowEventHandlers.get(event); + const eventHandlers = eventHandlersTmp ?? []; + if (eventHandlersTmp == undefined) { + this.windowEventHandlers.set(event, eventHandlers); + } + + for (let i = 0; i < eventHandlers.length; i++) { + if (eventHandlers[i][0].deref() == callingMod && eventHandlers[i][1] == eventID) + throw new Error(`twrRegisterEvent: eventID ${eventID} is already registered for module ${callingMod.id}!`); + } + + eventHandlers.push([new WeakRef(callingMod), eventID]); + } + twrUnregisterEvent(callingMod: IWasmModuleAsync | IWasmModule, eventType: number, eventID: number) { + if (WindowEventTypes[eventType] == undefined) throw new Error(`twrUnregisterEvent: Invalid event type ${eventType} for twrConsoleWindow!`); + const event: WindowEventTypes = eventType as WindowEventTypes; + + const eventHandlers = this.windowEventHandlers.get(event); + if (eventHandlers == undefined || eventHandlers.length == 0) + throw new Error(`twrUnregisterEvent: There are no events registered for event type ${WindowEventTypes[eventType]}!`); + + for (let i = eventHandlers.length-1; i >= 0; i--) { + if (eventHandlers[i][0].deref() == callingMod && eventHandlers[i][1] == eventID) { + eventHandlers.splice(i, 1); + return; + } + } + throw new Error(`twrUnregisterEvent: eventID ${eventID} isn't registered for event type ${WindowEventTypes[eventType]} with module ${callingMod.id}!`); + } + twrUnregisterAllEvents(callingMod: IWasmModuleAsync | IWasmModule) { + for (const [, handlers] of this.windowEventHandlers) { + for (let i = handlers.length-1; i >= 0; i--) { + if (handlers[i][0].deref() == callingMod) { + handlers.splice(i, 1); + } + } + } + } + + handleCanvasKeyEvent(event: CanvasEventTypes, key: number) { + this.drawCanvas.handleCanvasKeyEvent(event, key); + return true; + } + + mouseWasOnTopBar: boolean = false; + mouseWasOnMenu: boolean = false; + handleCanvasMouseEvent(event: CanvasEventTypes, x: number, y: number, button: number) { + const n_x = x - BORDER_SIZE; + const n_y = y - TOP_BAR_SIZE; + + if (this.manager.handleCanvasMouseEvent(this.ctx, event, x, y, button)) { + + } else if ( + n_x >= 0 && n_y >= 0 + && n_x <= this.drawCanvasWidth + && n_y <= this.drawCanvasHeight + ) { + this.drawCanvas.handleCanvasMouseEvent(event, n_x, n_y, button); + } + + return true; + } + handleCanvasWheelEvent(event: CanvasEventTypes, deltaX: number, deltaY: number, deltaZ: number, deltaMode: number) { + this.drawCanvas.handleCanvasWheelEvent(event, deltaX, deltaY, deltaZ, deltaMode); + return true; + } + + handleCanvasAnimationFrameEvent(event: CanvasEventTypes, delta: number) { + const newEvents = this.runBeforeDrawEvents; + this.runBeforeDrawEvents = []; + for (const event of newEvents) { + event(); + } + this.drawCanvas.handleCanvasAnimationFrameEvent(event, delta); + + this.ctx.reset(); + //draw "app" canvas + this.ctx.drawImage(this.drawCanvas.element, BORDER_SIZE, TOP_BAR_SIZE); + + const SELECT_GREY = "#D0D0D0"; + //draw border around it + this.ctx.fillStyle = "#B0B0B0"; + this.ctx.fillRect(0, 0, BORDER_SIZE, this.element.height); + this.ctx.fillRect(this.element.width - BORDER_SIZE, 0, this.element.width, this.element.height); + this.ctx.fillRect(0, this.element.height - BORDER_SIZE, this.element.width, this.element.height); + + this.ctx.fillRect(0, 0, this.element.width, TOP_BAR_SIZE); + + this.ctx.fillStyle = "grey"; + this.ctx.lineWidth = 1.0; + + this.ctx.beginPath(); + this.ctx.moveTo(0, TOP_BAR_SIZE - this.ctx.lineWidth/2.0); + this.ctx.lineTo(this.element.width, TOP_BAR_SIZE - this.ctx.lineWidth/2.0); + this.ctx.stroke(); + this.ctx.closePath(); + + + this.manager.handleCanvasAnimationFrameEvent(this.ctx, event, delta); + } + + twrGetDrawCanvasJSID(mod:IWasmModule|IWasmModuleAsync) { + return this.drawCanvas.id; + } + + twrWindowAddMenu(mod: IWasmModuleAsync | IWasmModule, textPtr: number) { + const text = mod.getString(textPtr); + + + const menuOptions: MenuWidgetConstructor = { + }; + + const button: MenuButton = this.menu.addChild(this.ctx, MenuButton, { + height: TOP_BAR_SIZE - 0.5, + text: text, + textColor: "black", + buttonColor: "#B0B0B0", + selectedButtonColor: "#D0D0D0", + menuOptions: menuOptions, + offset: 0.5, + reservePrefixSpace: false, + verticalCentering: ButtonWidgetVerticalCenteringMethod.FontCentering, + centeredHorizontally: true, + }); + + this.widgets.set(button.id, [WidgetType.SubMenu, button]); + + + return button.id; + } + + twrWindowMenuAddWidget(mod: IWasmModuleAsync | IWasmModule, menuID: number, consPtr: number): number { + if (!this.widgets.has(menuID)) throw new Error(`twrWindowMenuAddWidget was given an invalid menu ID (${menuID})!`); + const widgetBase = this.widgets.get(menuID)!; + if (widgetBase[0] != WidgetType.SubMenu) + throw new Error(`twrWindowMenuAddWidget was given a non-menu (${WidgetType[widgetBase[0]]}) widget as a menu!`); + + const menu = widgetBase[1]; + + function getLongOrDef(ptr: number, def: T): number|T { + const ptrx32=Math.floor(ptr/4); + if (ptrx32*4!=ptr) throw new Error("getLongOrDef passed non long aligned address") + if (ptrx32<0 || ptrx32 >= mod.wasmMem.mem32.length) throw new Error("invalid index passed to getLongOrDef: "+ptr+", this.mem32.length: "+mod.wasmMem.mem32.length); + const val: number = mod.wasmMem.mem32[ptrx32]; + + if (val < 0) { + return def; + } else { + return val; + } + } + function getStringOrDef(ptr: number, def: string): string { + const strPtr = mod.getLong(ptr); + if (strPtr == 0) { + return def; + } else { + return mod.getString(strPtr); + } + } + + const type = mod.getLong(consPtr + 0) as WidgetType; + + // const width = getLongOrDef(consPtr + 4, undefined); + const height = getLongOrDef(consPtr + 4, undefined); + + const extraPtr = consPtr + 8; + + switch (type) { + case WidgetType.Button: + { + // struct twr_widget_button_constructor { + // /// @brief Base widget constructor + // struct twr_widget_constructor base; + // /// @brief defaults to "Lorem Ipsum" + // const char* text; + // }; + let button: ButtonWidgetConstructor = { + // width: width, + height: height, + text: getStringOrDef(extraPtr + 0, "Lorem Ipsum"), + }; + const widget: Button = menu.addChild(this.ctx, Button, button); + this.widgets.set(widget.id, [WidgetType.Button, widget]); + return widget.id; + } + break; + + case WidgetType.Seperator: + { + // struct twr_widget_seperator_constructor { + // /// @brief Base widget constructor + // struct twr_widget_constructor base; + // /// @brief defaults to "-" + // const char* seperator_text; + // /// @brief defaults to 16px Seriph + // const char* seperator_font; + // }; + let seperator: SeperatorWidgetConstructor = { + // width: width, + height: height, + seperatorText: getStringOrDef(extraPtr + 0, "-"), + }; + const widget: Seperator = menu.addChild(this.ctx, Seperator, seperator); + this.widgets.set(widget.id, [WidgetType.Seperator, widget]); + return widget.id; + } + break; + + case WidgetType.RadioItem: + { + // struct twr_widget_button_constructor { + // /// @brief Base widget constructor + // struct twr_widget_constructor base; + // /// @brief defaults to "Lorem Ipsum" + // const char* text; + // }; + + const cons: ButtonWidgetConstructor = { + // width: width, + height: height, + text: getStringOrDef(extraPtr + 0, "Lorem Ipsum"), + }; + const radioItem: RadioItem = menu.addChild(this.ctx, RadioItem, cons); + this.widgets.set(radioItem.id, [WidgetType.RadioItem, radioItem]); + return radioItem.id; + } + break; + + case WidgetType.SubMenu: + { + // struct twr_widget_sub_menu_constructor { + // /// @brief Base widget constructor + // struct twr_widget_constructor base; + // /** + // * Text for the button that opens this menu + // * defaults to Lorem Ipsum + // */ + // const char* button_text; + + // /// @brief defaults to 10 + // long minimum_menu_width; + // /// @brief defaults to 10 + // long minimum_menu_height; + // }; + + const cons: MenuButtonWidgetConstructor = { + // width: width, + height: height, + text: getStringOrDef(extraPtr + 0, "Lorem Ipsum"), + menuWidth: getLongOrDef(extraPtr + 4, undefined), + menuHeight: getLongOrDef(extraPtr + 8, undefined), + + offset: this.widgetSettings.subMenuOpenOffset, + openToRight: true, + suffixText: "▶", + }; + const subMenu: MenuButton = menu.addChild(this.ctx, MenuButton, cons); + this.widgets.set(subMenu.id, [WidgetType.SubMenu, subMenu]); + return subMenu.id; + } + break; + + case WidgetType.CheckBox: + { + // struct twr_widget_check_box_constructor { + // /// @brief Base widget constructor + // struct twr_widget_constructor base; + // /** + // * Text for the check box + // * defaults to Lorem Ipsum + // */ + // const char* text; + // }; + + const props: CheckBoxWidgetConstructor = { + text: getStringOrDef(extraPtr + 0, "Lorem Ipsum"), + }; + const widget: CheckBox = menu.addChild(this.ctx, CheckBox, props); + this.widgets.set(widget.id, [WidgetType.CheckBox, widget]); + return widget.id; + } + break; + + default: + { + throw new Error(`twrWindowMenuAddWidget: Error! Was given an unknown type (${type})!`); + } + break; + } + + // return id; + } + + twrWindowMenuWidgetAddCallback(mod: IWasmModuleAsync | IWasmModule, widgetID: number, eventID: number, extraPtr: number) { + if (!this.widgets.has(widgetID)) throw new Error(`twrWindowMenuButtonAddCallback: Error! was given an invalid widgetID (${widgetID})`); + const widget = this.widgets.get(widgetID)!; + + const weakMod = new WeakRef(mod); + switch (widget[0]) { + case WidgetType.Button: + { + const button: Button = widget[1]; + const callbackFunc = () => { + const strongMod = weakMod.deref(); + if (strongMod) { + strongMod.postEvent(eventID, extraPtr); + } else { + button.removeEvent(callbackFunc); + } + } + button.addEvent(callbackFunc); + } + break; + + case WidgetType.RadioItem: + { + const radioItem: RadioItem = widget[1]; + const callbackFunc = (ctx: CanvasRenderingContext2D, selected: boolean) => { + const strongMod = weakMod.deref(); + if (strongMod) { + strongMod.postEvent(eventID, extraPtr, selected ? 1 : 0); + } else { + radioItem.removeEvent(callbackFunc); + } + }; + radioItem.addEvent(callbackFunc); + } + break; + + case WidgetType.CheckBox: + { + const checkBox: CheckBox = widget[1]; + const callbackFunc = (ctx: CanvasRenderingContext2D, selected: boolean) => { + const strongMod = weakMod.deref(); + if (strongMod) { + strongMod.postEvent(eventID, extraPtr, selected ? 1 : 0); + } else { + checkBox.removeEvent(callbackFunc); + } + }; + checkBox.addEvent(callbackFunc); + } + break; + + default: + { + throw new Error(`twrWindowMenuWidgetAddCallback: Error! was given an invalid widget type! Expected button, checkBox, or radioMenu, got ${WidgetType[widget[0]]}`); + } + break; + } + } + + twrWindowMenuDeleteWidget(mod: IWasmModuleAsync | IWasmModule, widgetID: number) { + if (!this.widgets.has(widgetID)) throw new Error(`twrWindowMenuDeleteWidget: Error! was given an invalid widgetID (${widgetID})`); + const widget = this.widgets.get(widgetID)!; + + const deleted = widget[1].delete(this.ctx); + for (const del of deleted) { + this.widgets.delete(del.id); + } + } + + twrWindowMenuRadioItemMerge(mod: IWasmModuleAsync | IWasmModule, widgetID1: number, widgetID2: number) { + const widget1 = this.widgets.get(widgetID1); + const widget2 = this.widgets.get(widgetID2); + if (widget1 == undefined) throw new Error(`twrWindowMenuRadioItemMerge: Error! was given an invalid widgetID1 (${widgetID1})`); + if (widget2 == undefined) throw new Error(`twrWindowMenuRadioItemMerge: Error! was given an invalid widgetID2 (${widgetID2})`); + + if (widget1[0] != WidgetType.RadioItem) throw new Error(`twrWindowMenuRadioItemMerge: Error! was given an invalid widget1 type! Got ${WidgetType[widget1[0]]}, expected RadioItem!`); + if (widget2[0] != WidgetType.RadioItem) throw new Error(`twrWindowMenuRadioItemMerge: Error! was given an invalid widget2 type! Got ${WidgetType[widget2[0]]}, expected RadioItem!`); + + widget1[1].mergeGroups(widget2[1]); + } + + + private checkValiditity(propType: PropBaseType, propName: string, typ: PropBaseType, widget: [WidgetType, Widget]) { + if ((propType & typ) != 0) + return; + // else if (propType[0] == PropBaseType.Combination && propType.includes(typ)) { + // return; + /*}*/ else { + throw new Error(`twrWindowMenuWidgetSetProp: Property "${propName}" does not accept type ${PropBaseType[typ] ?? typ} in widget ${widget[1].id} of type ${WidgetType[widget[0]] ?? widget[0]}`); + } + } + + twrWindowMenuWidgetSetProp(mod: IWasmModuleAsync | IWasmModule, widgetID: number, propNamePtr: number, dataPtr: number) { + /* struct twr_widget_prop_value { + union { + const char* string; + double number; + int boolean; + }; + enum WindowWidgetPropVal type; + };*/ + const valPtr = dataPtr + 0; + const typPtr = dataPtr + 8; + const widget = this.widgets.get(widgetID); + if (widget == undefined) throw new Error(`twrWindowMenuWidgetSetProp: Error! was given an invalid widgetID (${widgetID})`); + + const propName = mod.getString(propNamePtr); + const propField = widget[1].getPublicProperties()[propName]; + if (propField == undefined) throw new Error(`twrWindowMenuWidgetSetProp: Couldn't find prop name "${propName}" in widget ${widgetID} of type ${WidgetType[widget[0]] ?? widget[0]}`); + const propSetter = propField[0][1]; + if (propSetter == undefined) throw new Error(`twrWindowMenuWidgetSetProp: Property "${propName}" is read only in widget ${widgetID} of type ${WidgetType[widget[0]] ?? widget[0]}`); + + const typ = mod.getLong(typPtr) as PropBaseType; + + console.log(`Attempting to modify ${propName} in widget ${widgetID}`); + switch (typ) { + case PropBaseType.String: + { + this.checkValiditity(propField[1], propName, typ, widget); + + let str; + const strPtr = mod.getLong(valPtr); + if (strPtr == 0) { + console.log("twrwindowMenuWidgetSetProp: Warning! Given a null ptr for string, default to empty string."); + str = ""; + } else { + str = mod.getString(strPtr); + } + + // (widget[1] as any)[propName] = str; + propSetter(str); + } + break; + case PropBaseType.Boolean: + { + this.checkValiditity(propField[1], propName, typ, widget); + + let val = mod.getLong(valPtr) != 0; + // (widget[1] as any)[propName] = val; + propSetter(val); + } + break; + case PropBaseType.Number: + { + this.checkValiditity(propField[1], propName, typ, widget); + + // (widget[1] as any)[propName] = mod.getDouble(valPtr); + propSetter(mod.getDouble(valPtr)); + } + break; + case PropBaseType.Undefined: + { + this.checkValiditity(propField[1], propName, typ, widget); + // (widget[1] as any)[propName] = undefined; + propSetter(undefined); + } + break; + default: + { + throw new Error(`twrWindowMenuWidgetSetProp: Was given an invalid prop type. Expected a String (${PropBaseType.String}), Number (${PropBaseType.Number}), Boolean (${PropBaseType.Boolean}) or Undefined (${PropBaseType.Undefined})`); + } + break; + } + } + + private twrWindowMenuWidgetGetPropHelper(mod: IWasmModule | IWasmModuleAsync, widgetID: number, propNamePtr: number) { + /* struct JSPropVal { + enum JSPropValType type; + const void* val; + }*/ + const widget = this.widgets.get(widgetID); + if (widget == undefined) throw new Error(`twrWindowMenuWidgetGetProp: Error! was given an invalid widgetID (${widgetID})`); + + const propName = mod.getString(propNamePtr); + const propField = widget[1].getPublicProperties()[propName]; + if (propField == undefined) throw new Error(`twrWindowMenuWidgetGetProp: Couldn't find prop name "${propName}" in widget ${widgetID} of type ${WidgetType[widget[0]] ?? widget[0]}`); + const propGetter = propField[0][0]; + if (propGetter == undefined) throw new Error(`twrWindowMenuWidgetGetProp: Property "${propName}" is set only in widget ${widgetID} of type ${WidgetType[widget[0]] ?? widget[0]}`); + + // const val = (widget[1] as any)[propName]; + const val = propGetter(); + let propType: [PropBaseType.String, Uint8Array] + | [PropBaseType.Number, number] + | [PropBaseType.Boolean, boolean] + | [PropBaseType.Undefined]; + //in struct, max unioned type is a double (8 bytes) + //and the type enum is a long (4 bytes) + const baseStructSize = 8 + 4; + let structSize = baseStructSize; + switch (typeof val) { + case "string": + const u8Str = mod.wasmMem.stringToU8(val); + //string + null character will be appended to end of struct allocation + structSize += u8Str.length + 1; + propType = [PropBaseType.String, u8Str]; + break; + case "number": + propType = [PropBaseType.Number, val]; + break; + case "boolean": + propType = [PropBaseType.Boolean, val]; + break; + case "undefined": + propType = [PropBaseType.Undefined]; + break; + default: + throw new Error(`twrWindowMenuWidgetGetProp: Internal Error! Got unexpected type from property "${propName}" in Widget ${widgetID} of type ${WidgetType[widget[0]] ?? widget[0]}`); + } + + return unwrapPossibleSync(wrapPossibleSync(mod.wasmMem.malloc(structSize)).then((alloc) => { + const valPtr = alloc + 0; + const typePtr = alloc + 8; + switch (propType[0]) { + case PropBaseType.String: + { + mod.wasmMem.mem8u.set(propType[1], alloc + baseStructSize); + mod.wasmMem.mem8u[alloc + baseStructSize + propType[1].length] = 0; + mod.setLong(valPtr, alloc + baseStructSize); + } + break; + case PropBaseType.Number: + { + mod.setDouble(valPtr, propType[1]); + } + break; + case PropBaseType.Undefined: + break; + case PropBaseType.Boolean: + { + mod.setLong(valPtr, propType[1] ? 1 : 0); + } + break; + default: + throw new Error(`internal error, shouldn't be possible`); + } + mod.setLong(typePtr, propType[0]); + return alloc; + })); + } + twrWindowMenuWidgetGetProp(mod: IWasmModule, widgetID: number, propNamePtr: number) { + const ret = this.twrWindowMenuWidgetGetPropHelper(mod, widgetID, propNamePtr); + if (!(ret instanceof Promise)) { + return ret; + } else { + throw new Error(`internal error!`); + } + } + async twrWindowMenuWidgetGetProp_async(mod: IWasmModuleAsync, widgetID: number, propNamePtr: number) { + const ret = this.twrWindowMenuWidgetGetPropHelper(mod, widgetID, propNamePtr); + if (ret instanceof Promise) { + return await ret; + } else { + throw new Error(`internal error!`); + } + } + + //takes in a a pointer to a twr_widget_prop_details struct with the name already filled out + twrWindowMenuWidgetGetPropDetails(mod: IWasmModule, widgetID: number, detailsStructPtr: number) { + const namePtr = 0 + detailsStructPtr; + const typePtr = 4 + detailsStructPtr; + const accessPtr = 8 + detailsStructPtr; + + const widget = this.widgets.get(widgetID); + if (widget == undefined) throw new Error(`twrWindowMenuWidgetGetPropDetails: Error! was given an invalid widgetID (${widgetID})`); + + const propName = mod.wasmMem.getString(namePtr); + const propField = widget[1].getPublicProperties()[propName]; + if (propField == undefined) throw new Error(`twrWindowMenuWidgetGetPropDetails: Couldn't find prop name "${propName}" in widget ${widgetID} of type ${WidgetType[widget[0]] ?? widget[0]}`); + + // let typ = 0; + // if (propField[1][0] == PropBaseType.Combination) { + // for (let i = 1; i < propField[1].length; i++) { + // typ |= propField[1][i]; + // } + // } else { + // typ = propField[1][0]; + // } + mod.wasmMem.setLong(typePtr, propField[1]); + + // mod.wasmMem.setLong(accessPtr, propField[0]); + const canGet = propField[0][0] != undefined; + const canSet = propField[0][1] != undefined; + let perm: PropPerms; + if (canGet && canSet) { + perm = PropPerms.ReadAndSet; + } else if (canGet) { + perm = PropPerms.ReadOnly; + } else if (canSet) { + perm = PropPerms.SetOnly; + } else { + throw new Error(`shouldn't be valid?`); + } + mod.wasmMem.setLong(accessPtr, perm); + + } + + private twrWindowMenuWidgetListPropsHelper(mod: IWasmModuleAsync | IWasmModule, widgetID: number, lengthPtr: number) { + const widget = this.widgets.get(widgetID); + if (widget == undefined) throw new Error(`twrWindowMenuWidgetListProps: Error! was given an invalid widgetID (${widgetID})`); + + // struct twr_widget_prop_details { + // const char* name; + // enum WindowWidgetPropVal type; + // enum WindowWidgetPropAccess access; + // }; + const baseStructSize = 4*3; + const nameOffset = 0; + const typeOffset = 4; + const accessOffset = 8; + + const props: [Uint8Array, PropPerms, PropBaseType][] = []; + let totalSize = 0; + for (const name in widget[1].getPublicProperties()) { + const ru8=mod.wasmMem.stringToU8(name); + totalSize += baseStructSize + ru8.length + 1; + + const [propFuncs, propType] = widget[1].getPublicProperties()[name]; + const canGet = propFuncs[0] != undefined; + const canSet = propFuncs[1] != undefined; + let perm: PropPerms; + if (canGet && canSet) { + perm = PropPerms.ReadAndSet; + } else if (canGet) { + perm = PropPerms.ReadOnly; + } else if (canSet) { + perm = PropPerms.SetOnly; + } else { + throw new Error(`shouldn't be valid?`); + } + props.push([ru8, perm, propType]); + } + mod.wasmMem.setLong(lengthPtr, props.length); + + + return unwrapPossibleSync(wrapPossibleSync(mod.malloc(totalSize)).then((alloc) => { + let offset = baseStructSize * props.length + alloc; + for (let i = 0; i < props.length; i++) { + const [propName, propAccess, propType] = props[i]; + const structPtr = baseStructSize * i + alloc; + //set name, strings are put after the list of structs in the allocation + mod.wasmMem.setLong(structPtr + nameOffset, offset); + mod.wasmMem.mem8u.set(propName, offset); + mod.wasmMem.mem8u[offset+propName.length] = 0; //null ptr at end + offset += propName.length + 1; + + //set access + mod.wasmMem.setLong(structPtr + accessOffset, propAccess); + + //set type + // let typ = 0; + // if (propType[0] != PropBaseType.Combination) { + // typ = propType[0]; + // } else { + // for (let i = 1; i < propType.length; i++) { + // typ |= propType[i]; + // } + // } + mod.wasmMem.setLong(structPtr + typeOffset, propType); + } + return alloc; + })); + } + twrWindowMenuWidgetListProps(mod: IWasmModule, widgetID: number, lengthPtr: number) { + const ret = this.twrWindowMenuWidgetListPropsHelper(mod, widgetID, lengthPtr); + if (!(ret instanceof Promise)) { + return ret; + } else { + throw new Error(`internal error`); + } + } + async twrWindowMenuWidgetListProps_async(mod: IWasmModuleAsync, widgetID: number, lengthPtr: number) { + const ret = this.twrWindowMenuWidgetListPropsHelper(mod, widgetID, lengthPtr); + if (ret instanceof Promise) { + return await ret; + } else { + throw new Error(`internal error`); + } + } + + twrWindowMenuListPropsHelper(mod: IWasmModuleAsync | IWasmModule, lengthPtr: number) { + /* + struct window_menu_prop { + const char* name; + enum WindowWidgetPropVal type; + } + */ + const baseStructSize = 4 + 4; + const nameOffset = 0; + const typeOffset = 4; + + let returnArraySize = 0; + const props: [Uint8Array, PropBaseType][] = []; + for (const [propName, val] of Object.entries(this.widgetSettings)) { + let typ: PropBaseType; + switch (typeof val) { + case "string": + typ = PropBaseType.String; + break; + case "number": + typ = PropBaseType.Number; + break; + case "boolean": + typ = PropBaseType.Boolean; + break; + default: + throw new Error(`twrWindowMenuListPropHelper found invalid widget setting type: ${typeof val}!`); + } + const u8StringName = mod.wasmMem.stringToU8(propName); + props.push([u8StringName, typ]); + + returnArraySize += baseStructSize + u8StringName.length + 1; + } + + mod.wasmMem.setLong(lengthPtr, props.length); + + return unwrapPossibleSync(wrapPossibleSync(mod.wasmMem.malloc(returnArraySize)).then((alloc) => { + let nameStringOffset = baseStructSize * props.length + alloc; + for (let i = 0; i < props.length; i++) { + const [propName, propTyp] = props[i]; + + const structBase = baseStructSize * i + alloc; + mod.wasmMem.setLong(structBase + nameOffset, nameStringOffset); + mod.wasmMem.setLong(structBase + typeOffset, propTyp); + + mod.wasmMem.mem8u.set(propName, nameStringOffset); + mod.wasmMem.mem8u[nameStringOffset + propName.length] = 0; //null character + nameStringOffset += propName.length + 1; + } + + return alloc; + })); + } + + + twrWindowMenuListProps(mod: IWasmModule, lengthPtr: number) { + const ret = this.twrWindowMenuListPropsHelper(mod, lengthPtr); + if (ret instanceof Promise) { + throw new Error(`internal error!!!`); + } else { + return ret; + } + } + async twrWindowMenuListProps_async(mod: IWasmModuleAsync, lengthPtr: number) { + const ret = this.twrWindowMenuListPropsHelper(mod, lengthPtr); + if (ret instanceof Promise) { + return await ret; + } else { + throw new Error(`internal error!!!`); + } + } + + twrWindowMenuSetProp(mod: IWasmModuleAsync | IWasmModule, propNamePtr: number, dataPtr: number) { + // struct twr_widget_prop_value { + // union { + // const char* string; + // double number; + // int boolean; + // }; + // enum WindowWidgetPropVal type; + // }; + const valPos = dataPtr + 0; + const typPos = dataPtr + 8; + + const typ = mod.getLong(typPos) as PropBaseType; + + const propName = mod.getString(propNamePtr); + + if (!(propName in this.widgetSettings)) throw new Error(`twrWindowMenuSetProp: property ${propName} isn't a valid property!`); + + const assertType = (typ: "string" | "boolean" | "number") => { + const expectedTyp = typeof (this.widgetSettings as any)[propName]; + if (expectedTyp != typ) + throw new Error(`twrWindowMenuSetProp was given an invalid type. ${propName} is of type ${expectedTyp} but was given ${typ}!`); + } + switch (typ) { + case PropBaseType.Boolean: + { + assertType("boolean"); + (this.widgetSettings as any)[propName] = mod.wasmMem.getLong(valPos) ? 1 : 0; + } + break; + + case PropBaseType.Number: + { + assertType("number"); + (this.widgetSettings as any)[propName] = mod.wasmMem.getDouble(valPos); + } + break; + + case PropBaseType.String: + { + assertType("string"); + (this.widgetSettings as any)[propName] = mod.wasmMem.getString(mod.wasmMem.getLong(valPos)); + } + break; + + default: + throw new Error(`twrWindowMenuSetProp was given an invalid type (${PropBaseType[typ] ?? typ})!`); + } + this.manager.fullUpdate(); + } + + twrWindowMenuGetPropHelper(mod: IWasmModule | IWasmModuleAsync, propNamePtr: number) { + const propName = mod.wasmMem.getString(propNamePtr); + if (!(propName in this.widgetSettings)) throw new Error(`twrWindowMenuGetPropHelper: Error! property ${propName} isn't a valid property name!`); + const propVal = (this.widgetSettings as any)[propName]; + /*struct twr_widget_prop_value { + union { + const char* string; + double number; + int boolean; + }; + enum WindowWidgetPropVal type; + };*/ + const valOffset = 0; + const typOffset = 8; + const baseStructSize = 12; + + let retSize = baseStructSize; + let retVal: [PropBaseType.String, Uint8Array] + | [PropBaseType.Number, number] + | [PropBaseType.Boolean, boolean]; + + switch (typeof propVal) { + case "number": + retVal = [PropBaseType.Number, propVal]; + break; + + case "string": + retVal = [PropBaseType.String, mod.wasmMem.stringToU8(propVal)]; + retSize += retVal[1].length + 1; + break; + + case "boolean": + retVal = [PropBaseType.Boolean, propVal]; + break; + + default: + throw new Error(`twrWindowMenuGetProp: internal error!`); + } + + return unwrapPossibleSync(wrapPossibleSync(mod.wasmMem.malloc(retSize)).then((retPtr) => { + switch (retVal[0]) { + case PropBaseType.Number: + mod.wasmMem.setDouble(retPtr + valOffset, retVal[1]); + break; + case PropBaseType.String: + mod.wasmMem.setLong(retPtr + valOffset, baseStructSize + retPtr); + mod.wasmMem.mem8u.set(retVal[1], retPtr + baseStructSize); //load string at end of struct alloc + mod.wasmMem.mem8u[retVal[1].length + retPtr + baseStructSize] = 0; //insert null character + break; + case PropBaseType.Boolean: + mod.wasmMem.setLong(retPtr + valOffset, retVal[1] ? 1 : 0); + break; + } + mod.wasmMem.setLong(retPtr + typOffset, retVal[0]); + + return retPtr; + })); + } + twrWindowMenuGetProp(mod: IWasmModule, propNamePtr: number): number { + const ret = this.twrWindowMenuGetPropHelper(mod, propNamePtr); + if (ret instanceof Promise) { + throw new Error(`internal error!`); + } else { + return ret; + } + } + + async twrWindowMenuGetProp_async(mod: IWasmModule, propNamePtr: number) { + const ret = this.twrWindowMenuGetPropHelper(mod, propNamePtr); + if (ret instanceof Promise) { + return await ret; + } else { + throw new Error(`internal error!`); + } + } + + resizeWindow(width: number, height: number) { + this.runBeforeDrawEvents.push((() => { + this.element.width = width; + this.element.height = height; + [this.drawCanvasWidth, this.drawCanvasHeight] = this.calculatedrawCanvasDims(); + + this.drawCanvas.resizeCanvas(this.drawCanvasWidth, this.drawCanvasHeight); + + this.internalSendEvent(WindowEventTypes.WindowResize, width, height); + }).bind(this)); + } +} \ No newline at end of file diff --git a/source/twr-ts/twrlibscreen.ts b/source/twr-ts/twrlibscreen.ts new file mode 100644 index 00000000..91229d04 --- /dev/null +++ b/source/twr-ts/twrlibscreen.ts @@ -0,0 +1,160 @@ + +import { keyEventToCodePoint } from "./twrcon.js"; +import { TLibImports, twrLibrary, twrLibraryInstanceRegistry } from "./twrlibrary.js"; +import { IWasmModule } from "./twrmod"; +import { IWasmModuleAsync } from "./twrmodasync"; + + + +//nominal type - Allows FullID to be considered a unique type compared to number +//can easily remove { readonly '': unique symbol } to make it a simple type alias +// type FullID = number & { readonly '': unique symbol }; + +type FullID = number; + +enum EventTypes { + KEY_DOWN, + KEY_UP, + + MOUSE_DOWN, + MOUSE_UP, + MOUSE_CLICK, + MOUSE_DBLCLICK, + MOUSE_MOVE, + + WHEEL +} +const NUM_EVENTS = EventTypes.WHEEL - EventTypes.KEY_DOWN + 1; + +const EVENTS = [ + "keydown", + "keyup", + + "mousedown", + "mouseup", + "click", + "dblclick", + "mousemove", + + "wheel" +] + + +function calculateID(mod:IWasmModule|IWasmModuleAsync, id: number): FullID { + if (mod.id >= (2**20)) throw new Error("twrlibaudio was given a module ID greater than 20 bits long!"); + if (id >= (2**32)) throw new Error("twrlibaudio was given an object ID greater than 32 bits!"); + //should be equivalent to (mod.id << 32) | id + //can't use shift operations without being limited to 32-bit signed integers or using bignumber + return ((mod.id & (2**20 - 1)) * 2**32 + id) as FullID; +} + +export default class twrLibAudio extends twrLibrary { + id: number; + + readonly canvas: HTMLCanvasElement; + + imports: TLibImports = { + + }; + + tabs: { [id: FullID]: [IWasmModule|IWasmModuleAsync, ...any] } = {}; + registeredEvents: Map>> = new Map(); + selectedTab: number = -1; + + // every library should have this line + libSourcePath = new URL(import.meta.url).pathname; + + constructor(canvas: HTMLCanvasElement) { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + + this.canvas = canvas; + + + const register_similar_events = (rangeStart: number, rangeEnd: number, handler: (e: any) => number[] | void) => { + for (let i = rangeStart; i <= rangeEnd; i++) { + canvas.addEventListener(EVENTS[i], (e) => { + const res = handler(e); + if (res != undefined) + this.internalSendEvent(i, ...res); + }); + }; + } + + register_similar_events(EventTypes.KEY_DOWN, EventTypes.KEY_UP, (e: KeyboardEvent) => { + const r=keyEventToCodePoint(e); // twr-wasm utility function + if (r) { + // postEvent can only post numbers -- no translation of arguments is performed prior to making the C event callback + // See ex_append_two_strings below for an example using strings. + return [r]; + } + }); + + register_similar_events(EventTypes.MOUSE_DOWN, EventTypes.MOUSE_MOVE, (e: MouseEvent) => { + return [e.pageX + window.scrollX, e.pageY + window.scrollY]; + }); + + register_similar_events(EventTypes.WHEEL, EventTypes.WHEEL, (e: WheelEvent) => { + return [e.deltaX, e.deltaY, e.deltaZ, e.deltaMode]; + }); + + } + + internalSendEvent(event_type: EventTypes, ...args: number[]) { + const event_types = this.registeredEvents.get(this.selectedTab); + if (event_types == undefined) return; + + const event_handlers = event_types[event_type]; + + for (const [i, _] of event_handlers) { + this.tabs[this.selectedTab][0].postEvent( + i, + ...args + ); + } + } + + twrRegisterEvent(mod: IWasmModule|IWasmModuleAsync, tabID: number, eventType: EventTypes, eventID: number) { + if (0 < eventType || eventType >= NUM_EVENTS) throw new Error(`twrRegisterEvent was given an out of bounds event type (${eventType})!`); + + const fullTabID = calculateID(mod, tabID); + + const tabEvents = this.registeredEvents.get(tabID); + if (tabEvents == undefined) throw new Error(`twrRegisterEvent was given an unregistered tab (${tabID})!`); + + const eventHandlers = tabEvents[eventType]; + + const prevValTmp = eventHandlers.get(eventID); + const prevVal = prevValTmp == undefined ? 0 : prevValTmp; + if (prevValTmp != undefined) console.log("warning! twrRegisterEvent was given an already registered eventID!"); + + eventHandlers.set(eventID, prevVal+1); + } + + twrUnregisterEvent(mod: IWasmModule|IWasmModuleAsync, tabID: number, eventType: EventTypes, eventID: number) { + if (0 < eventType || eventType >= NUM_EVENTS) throw new Error(`twrUnregisterEvent was given an out of bounds event type (${eventType})!`); + + const fullTabID = calculateID(mod, tabID); + + const tabEvents = this.registeredEvents.get(tabID); + if (tabEvents == undefined) throw new Error(`twrUnregisterEvent was given an unregistered tab (${tabID})!`); + + const eventHandlers = tabEvents[eventType]; + + const prevVal = eventHandlers.get(eventID); + + if (prevVal == undefined) { + throw new Error(`twrUnregisterEvent was given an eventID that isn't registered (${eventID})!`); + } else if (prevVal == 0) { + throw new Error(`twrUnregisterEvent was given an eventID that isn't registered (${eventID}) and it wasn't properly removed!`); + } else if (prevVal == 1) { + eventHandlers.delete(prevVal); + } else { + console.log(`warning: twrUnregisterEvent didn't unregister eventID ${eventID} since it still has ${prevVal-1} registration(s) left!`); + eventHandlers.set(eventID, prevVal-1); + } + } + + +} \ No newline at end of file