diff --git a/include/select_menu.h b/include/select_menu.h index 7d6e2c3..5445aa1 100644 --- a/include/select_menu.h +++ b/include/select_menu.h @@ -1,33 +1,29 @@ #ifndef SELECT_MENU_H #define SELECT_MENU_H -#include -#include "libstd_replacements.h" -#include "text_engine.h" +#include "vertical_menu.h" #define LANG_MENU 1 #define CART_MENU 2 -class Select_Menu +class Select_Menu : public i_vertical_menu_state_changed_handler, public i_run_cycle_handler { public: - Select_Menu(bool enable_cancel, u8 nMenu_type, int nStartX, int nStartY); + Select_Menu(bool enable_cancel, u8 nMenu_type, unsigned nStartX, unsigned nStartY); int select_menu_main(); - void hide_menu(); - void show_menu(); - void clear_options(); void add_option(const u8 option, u8 return_value); + void clear_options(); void set_lang(u8 nLang); + void on_show() override; + void on_hide() override; + void on_selection_changed(unsigned new_index, unsigned x, unsigned y) override; + + void on_run_cycle() override; private: - ptgb::vector menu_options; - ptgb::vector return_values; - u16 curr_selection; - bool cancel_enabled; + vertical_menu menu_widget_; u8 menu_type; u8 lang; - int startTileX; - int startTileY; }; #endif \ No newline at end of file diff --git a/include/sound.h b/include/sound.h new file mode 100644 index 0000000..cda4a93 --- /dev/null +++ b/include/sound.h @@ -0,0 +1,94 @@ +#ifndef _SOUND_H +#define _SOUND_H + +#include + +#include "soundbank.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned short PTGBSFXHandle; + +/** + * @brief The API's defined here are a thin abstraction layer over the sound engine functions. + * This allows the underlying sound engine to be swapped out without affecting the rest of the codebase. + */ + +/** + * @brief This function initializes the sound engine and sets up a VBLANK handler + * to process audio every frame. It should be called during the initialization phase of the program. + */ +bool sound_init(void); + +/** + * @brief This function starts playing the song at the given index. + * If loop is true, the song will loop indefinitely until stopped or another song is played. + * + * @param song_index The index of the song to play. + * @param loop Whether the song should loop indefinitely. + */ +void play_song(u32 song_index, bool loop); + +/** + * @brief This function checks if a song is currently playing. + * + * @return true if a song is playing, false otherwise. + */ +bool is_song_playing(void); + +/** + * @brief This function stops the currently playing song. + */ +void stop_song(void); + +/** + * @brief This function plays the sound effect at the given index. + * Sound effects are typically short audio clips that play in response to specific events in the game, + * such as button presses or character actions. + * These are usually raw PCM samples (e.g., WAV files) + * + * To play a MOD- S3M- XM- or IT format based sound effect, + * use the play_jingle function instead. + */ +PTGBSFXHandle play_sound_effect(u32 sound_effect_index); + +/** + * @brief This function stops the given sound effect from playing and invalidates the handle + */ +void stop_sound_effect(PTGBSFXHandle handle); + +/** + * @brief This function stops all currently playing sound effects and resets + */ +void stop_all_sound_effects(void); + +/** + * @brief This function marks the sound effect as unimportant, + * allowing the sound engine to stop it if it needs to free up channels for new sound effects. + * + * It also invalidates the handle, so it should not be used after calling this function. + */ +void release_sound_effect(PTGBSFXHandle handle); + +/** + * @brief This function plays a jingle at the given index. + * A jingle is a sound effect that is based on a tracker format like MOD, S3M, XM, or IT. + * They can be played simultaneously with songs. + * If you want to play PCM-based sound effects (e.g., WAV files), use the play_sound_effect function instead. + * + * Note that jingles must be limited to 4 channels only. (https://blocksds.skylyrac.net/maxmod/group__gba__jingle__playback.html) + */ +void play_jingle(u32 jingle_index); + +/** + * @brief Checks if a jingle is actively playing. + */ +bool is_jingle_playing(void); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/include/vertical_menu.h b/include/vertical_menu.h new file mode 100644 index 0000000..77c6bc7 --- /dev/null +++ b/include/vertical_menu.h @@ -0,0 +1,245 @@ +#ifndef _VERTICAL_MENU_H +#define _VERTICAL_MENU_H + +#include "typeDefs.h" +#include "libstd_replacements.h" +#include "text_data_table.h" + +enum class MenuInputHandleState +{ + /** + * @brief This value means no input was handled + */ + NOT_HANDLED, + /** + * @brief This value means that the input was handled. + */ + HANDLED, + /** + * @brief This value means that a choice was made. + * This means that if the vertical_menu is running with ::run(), + * it should exit and return that choice. + */ + CHOICE_MADE, + /** + * @brief The menu was cancelled. + */ + CANCELLED +}; + +/** + * @brief This interface represents an item widget in the vertical menu. + * It's responsible for rendering itself and handling its input if + * the widget is focused. + */ +class i_item_widget +{ +public: + virtual ~i_item_widget(); + virtual void render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) = 0; + virtual MenuInputHandleState handle_input() = 0; +protected: +private: +}; + +/** + * @brief This interface allows you to register a handler + * to deal with selection changes. + * This is useful for managing external components, such as a cursor. + */ +class i_vertical_menu_state_changed_handler +{ +public: + virtual ~i_vertical_menu_state_changed_handler(); + + virtual void on_show() = 0; + virtual void on_hide() = 0; + virtual void on_selection_changed(unsigned new_index, unsigned x, unsigned y) = 0; +protected: +private: +}; + +/** + * @brief If you are using the vertical_menu::run() function, + * you may want to manage external components on very run cycle. + * This interface allows you to do that by implementing the i_run_cycle_handler + */ +class i_run_cycle_handler +{ +public: + virtual ~i_run_cycle_handler(); + virtual void on_run_cycle() = 0; +protected: +private: +}; + +typedef struct vertical_menu_settings +{ + unsigned x; + unsigned y; + unsigned width; + unsigned height; + unsigned margin_top; + unsigned margin_bottom; + unsigned initial_focus_index; + unsigned item_height; + int text_table_index; + + /** + * @brief This boolean indicates whether the menu + * can be "cancelled with the B button." + */ + bool allow_cancel; + + /** + * @brief This boolean indicates whether the menu should take + * ownership of its item widgets. + * That means that if this boolean is true, + * the destructor of vertical_menu will delete/free all of its + * item widgets. + */ + bool should_delete_item_widgets_on_destruct; + + /** + * @brief This boolean indicates whether the state changed handler (cursor?) + * should be hidden when the menu is not focused. + */ + bool should_hide_state_changed_handler_on_not_focused; +} vertical_menu_settings; + +class vertical_menu +{ +public: + vertical_menu(const vertical_menu_settings &settings); + ~vertical_menu(); + + void show(); + void hide(); + + /** + * @brief Get the current settings of this widget + */ + const vertical_menu_settings& get_settings() const; + + /** + * @brief Replace the settings of this widget. + */ + void set_settings(const vertical_menu_settings &settings); + + /** + * @brief This function is a way to bulk add item widgets to the menu. + * The main difference between calling this function and add_item_widget() + * multiple times is that this function will reserve() enough space in the + * vector first, to avoid multiple resizes. + */ + void add_item_widgets(i_item_widget **item_widgets, unsigned num_widgets); + + /** + * @brief This function adds an item widget to the menu. + * The menu will take ownership of the pointer, if + * settings_.should_delete_item_widgets_on_destruct is true. + * Otherwise, the caller is responsible for managing the memory of the item widgets. + */ + void add_item_widget(i_item_widget *item_widget); + + /** + * @brief This function clears all item widgets from the menu. + * If settings_.should_delete_item_widgets_on_destruct is true, + * it will also delete the item widgets. + */ + void clear_item_widgets(); + + /** + * @brief This function gets the item widget + * at the given index. + */ + i_item_widget* get_item_widget_at(unsigned index) const; + + /** + * @brief Sets whether the menu is focused or not. + * A focused menu will handle input. + */ + void set_focused(bool is_focused); + + /** + * @brief This function handles button input for the menu. + */ + MenuInputHandleState handle_input(); + + /** + * @brief This function updates the viewport by + * re-rendering the visible items. + */ + void update_viewport(); + + void clear_viewport(); + + /** + * @brief This function can be used to manage a blocking call + * to vertical_menu, in which you want vertical_menu + * to handle everything. + * + * This can be useful during one of those script_array commands. + * Alternatively, you can just call handle_input() and manage the loop + * manually. + * + * @return Will return the index of the chosen item if a choice was made, + * or UINT32_MAX if no choice was made and the menu was exited in some other way + * (ex: B button). + */ + unsigned run(); + + /** + * @brief Sets the optional state changed handler. + * This is useful for managing external components, such as a cursor. + */ + void set_state_changed_handler(i_vertical_menu_state_changed_handler *handler); + + /** + * @brief This function sets a handler that will run on every + * cycle in the run() function. + * + * It's only relevant if you use ::run() though. + */ + void set_run_cycle_handler(i_run_cycle_handler *handler); +protected: +private: + void handle_selection_change(unsigned new_index, unsigned x, unsigned y); + + vertical_menu_settings settings_; + i_vertical_menu_state_changed_handler *state_changed_handler_; + i_run_cycle_handler *run_cycle_handler_; + unsigned focused_index_; + unsigned viewport_start_index_; + ptgb::vector items_; + bool is_focused_; +}; + +typedef struct simple_item_widget_data +{ + struct + { + unsigned text_table_index; + unsigned margin_left; + unsigned margin_top; + } text; + unsigned value; + void (*on_execute_callback)(void *context); +} simple_item_widget_data; + +class simple_item_renderer : public i_item_widget +{ +public: + simple_item_renderer(const simple_item_widget_data &data); + virtual ~simple_item_renderer(); + + const simple_item_widget_data& get_data() const; + + void render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) override; + MenuInputHandleState handle_input() override; +protected: +private: + simple_item_widget_data data_; +}; + +#endif \ No newline at end of file diff --git a/source/script_array.cpp b/source/script_array.cpp index f351a47..aad1827 100644 --- a/source/script_array.cpp +++ b/source/script_array.cpp @@ -682,6 +682,8 @@ const script_obj_params event_script_params[SCRIPT_SIZE] = { void populate_lang_menu() { + langs.clear_options(); + langs.add_option(GENERAL_option_english, ENG_ID); langs.add_option(GENERAL_option_japanese, JPN_ID); langs.add_option(GENERAL_option_spanish, SPA_ID); @@ -696,6 +698,8 @@ void populate_lang_menu() void populate_game_menu(int lang) { + games.clear_options(); + switch (lang) { case (JPN_ID): @@ -849,6 +853,9 @@ bool run_conditional(int index) case CMD_LANG_MENU: populate_lang_menu(); lang = langs.select_menu_main(); + // We have our choice, we should release + // the memory of the options since we won't need them anymore + langs.clear_options(); if (lang == BUTTON_CANCEL) { return false; @@ -856,17 +863,18 @@ bool run_conditional(int index) games.set_lang(static_cast(lang)); party_data.set_lang(static_cast(lang)); return true; - case CMD_GAME_MENU: populate_game_menu(party_data.get_lang()); game = games.select_menu_main(); + // We have our choice, we should release + // the memory of the options since we won't need them anymore + games.clear_options(); if (game == BUTTON_CANCEL) { return false; } party_data.set_game(game); return true; - case CMD_SLIDE_PROF_LEFT: for (int i = 0; i <= (8 * 7); i += 2) { diff --git a/source/select_menu.cpp b/source/select_menu.cpp index 2f51ffe..27421e3 100644 --- a/source/select_menu.cpp +++ b/source/select_menu.cpp @@ -9,145 +9,133 @@ #define TILE_HEIGHT 8 #define TILE_WIDTH 8 -Select_Menu::Select_Menu(bool enable_cancel, u8 nMenu_type, int nStartTileX, int nStartTileY) +Select_Menu::Select_Menu(bool enable_cancel, u8 nMenu_type, unsigned nStartTileX, unsigned nStartTileY) + : menu_widget_(vertical_menu_settings{ + .x = static_cast(nStartTileX * TILE_WIDTH), + .y = static_cast(nStartTileY * TILE_HEIGHT), + .width = 10 * TEXT_WIDTH, + .height = TILE_HEIGHT * 2, // to account for the margins + .margin_top = TILE_HEIGHT, + .margin_bottom = TILE_HEIGHT, + .initial_focus_index = 0, + .item_height = TEXT_HEIGHT, + .text_table_index = GENERAL_INDEX, + .allow_cancel = enable_cancel, + .should_delete_item_widgets_on_destruct = true, + .should_hide_state_changed_handler_on_not_focused = false + }) + , menu_type(nMenu_type) + , lang(0) { - cancel_enabled = enable_cancel; - menu_type = nMenu_type; - startTileX = nStartTileX; - startTileY = nStartTileY; + menu_widget_.set_state_changed_handler(this); + menu_widget_.set_run_cycle_handler(this); } void Select_Menu::add_option(const u8 option, u8 return_value) { - menu_options.push_back(option); - return_values.push_back(return_value); -} + const simple_item_widget_data item_data = { + .text = { + .text_table_index = option, + .margin_left = 2 * TILE_WIDTH, + .margin_top = 0 + }, + .value = return_value, + .on_execute_callback = nullptr + }; + menu_widget_.add_item_widget(new simple_item_renderer(item_data)); -int Select_Menu::select_menu_main() -{ - show_menu(); - curr_selection = 0; - - key_poll(); // Reset the buttons - - bool update; - bool first = true; - while (true) - { - update = false; - if (key_hit(KEY_DOWN)) - { - curr_selection = ((curr_selection + 1) % menu_options.size()); - update = true; - } - else if (key_hit(KEY_UP)) - { - curr_selection = ((curr_selection + (menu_options.size() - 1)) % menu_options.size()); - update = true; - } - else if (key_hit(KEY_A)) - { - hide_menu(); - return return_values[curr_selection]; - } - else if (cancel_enabled && key_hit(KEY_B)) - { - hide_menu(); - return -1; - } - else if (first) - { - update = true; - first = false; - } - update_y_offset(); - obj_set_pos( - point_arrow, - (startTileX + 1) * TEXT_WIDTH, - (startTileY + 1) * TILE_HEIGHT + (curr_selection * TEXT_HEIGHT) + 2); - global_next_frame(); - - if (update) - { - if (return_values[curr_selection] == UINT8_MAX) - { - switch (menu_type) - { - case CART_MENU: - obj_hide(cart_shell); - obj_hide(cart_label); - break; - case LANG_MENU: - obj_hide(flag); - break; - } - } - else - { - switch (menu_type) - { - case CART_MENU: - load_select_sprites(return_values[curr_selection], lang); - obj_unhide(cart_shell, 0); - obj_unhide(cart_label, 0); - break; - case LANG_MENU: - load_select_sprites(0, return_values[curr_selection]); - obj_unhide(flag, 0); - break; - } - } - } - } - return 0; -} - -void Select_Menu::show_menu() -{ - u8 decompression_buffer[2048]; - text_data_table text_data(decompression_buffer); - text_data.decompress(get_compressed_text_table(GENERAL_INDEX)); - - add_menu_box(menu_options.size(), startTileX, startTileY); - for (unsigned int i = 0; i < menu_options.size(); i++) - { - tte_set_pos((startTileX + 2) * TEXT_WIDTH, (startTileY + 1) * TILE_HEIGHT + (i * TEXT_HEIGHT)); - ptgb_write(text_data.get_text_entry(menu_options[i]), true); - } - obj_unhide(point_arrow, 0); - // obj_set_pos(point_arrow, startTileX + (2 * TEXT_WIDTH), (1 + i) * TEXT_HEIGHT); -} - -void Select_Menu::hide_menu() -{ - obj_hide(point_arrow); - tte_erase_rect( - startTileX * TILE_WIDTH, - startTileY * TILE_HEIGHT, - (startTileX + 10 + 1) * TEXT_WIDTH, - ((startTileY + 2) * TILE_HEIGHT) + (menu_options.size() * TEXT_HEIGHT)); - reload_textbox_background(); - clear_options(); - obj_hide(point_arrow); - switch (menu_type) - { - case CART_MENU: - obj_hide(cart_shell); - obj_hide(cart_label); - break; - case LANG_MENU: - obj_hide(flag); - break; - } + vertical_menu_settings settings = menu_widget_.get_settings(); + settings.height += TEXT_HEIGHT + (item_data.text.margin_top * 2); + menu_widget_.set_settings(settings); } void Select_Menu::clear_options() { - menu_options.clear(); - return_values.clear(); + menu_widget_.clear_item_widgets(); + + vertical_menu_settings settings = menu_widget_.get_settings(); + settings.height = TILE_HEIGHT * 2; // reset to just the margins + menu_widget_.set_settings(settings); +} + +int Select_Menu::select_menu_main() +{ + unsigned choice_index; + unsigned item_value; + simple_item_renderer* widget = nullptr; + + menu_widget_.show(); + choice_index = menu_widget_.run(); + + if(choice_index != UINT32_MAX) + { + widget = static_cast(menu_widget_.get_item_widget_at(choice_index)); + } + item_value = (widget) ? widget->get_data().value : UINT8_MAX; + + menu_widget_.hide(); + + global_next_frame(); + return item_value; } void Select_Menu::set_lang(u8 nLang) { lang = nLang; +} + +void Select_Menu::on_show() +{ + obj_unhide(point_arrow, 0); +} + +void Select_Menu::on_hide() +{ + obj_hide(point_arrow); + obj_hide(cart_shell); + obj_hide(cart_label); + obj_hide(flag); +} + +void Select_Menu::on_selection_changed(unsigned new_index, unsigned x, unsigned y) +{ + // set the cursor accordingly + obj_set_pos(point_arrow, x + TEXT_WIDTH, y + 3); + + simple_item_renderer* widget = static_cast(menu_widget_.get_item_widget_at(new_index)); + const unsigned item_value = (widget) ? widget->get_data().value : UINT8_MAX; + + if (item_value == UINT8_MAX) + { + switch (menu_type) + { + case CART_MENU: + obj_hide(cart_shell); + obj_hide(cart_label); + break; + case LANG_MENU: + obj_hide(flag); + break; + } + } + else + { + switch (menu_type) + { + case CART_MENU: + load_select_sprites(item_value, lang); + obj_unhide(cart_shell, 0); + obj_unhide(cart_label, 0); + break; + case LANG_MENU: + load_select_sprites(0, item_value); + obj_unhide(flag, 0); + break; + } + } +} + +void Select_Menu::on_run_cycle() +{ + update_y_offset(); } \ No newline at end of file diff --git a/source/vertical_menu.cpp b/source/vertical_menu.cpp new file mode 100644 index 0000000..8d6baa9 --- /dev/null +++ b/source/vertical_menu.cpp @@ -0,0 +1,312 @@ +#include "vertical_menu.h" +#include "sprite_data.h" +#include "global_frame_controller.h" +#include "text_engine.h" +#include "translated_text.h" + +#define TILE_HEIGHT 8 +#define TILE_WIDTH 8 + +i_item_widget::~i_item_widget() +{ +} + +i_vertical_menu_state_changed_handler::~i_vertical_menu_state_changed_handler() +{ +} + +i_run_cycle_handler::~i_run_cycle_handler() +{ +} + +static unsigned get_num_visible_items(unsigned vertical_menu_height, unsigned margin_top, unsigned margin_bottom, unsigned item_height) +{ + return ((vertical_menu_height - margin_top - margin_bottom) / item_height); +} + +static unsigned get_viewport_end_index(unsigned viewport_start_index, unsigned num_visible_items, unsigned total_items) +{ + unsigned end_index = viewport_start_index + num_visible_items; + // bounds check to make sure we don't go past the total number of items + end_index = std::min(end_index, total_items); + + return end_index; +} + +vertical_menu::vertical_menu(const vertical_menu_settings &settings) + : settings_(settings) + , state_changed_handler_(nullptr) + , run_cycle_handler_(nullptr) + , focused_index_(settings.initial_focus_index) + , viewport_start_index_(0) + , items_() + , is_focused_(true) +{ +} + +vertical_menu::~vertical_menu() +{ + clear_item_widgets(); +} + +void vertical_menu::show() +{ + add_menu_box(settings_.x / TILE_WIDTH, settings_.y / TILE_HEIGHT, settings_.width, settings_.height); + + update_viewport(); + + if(state_changed_handler_) + { + state_changed_handler_->on_show(); + } + + // initial selection change notification + const unsigned render_index = focused_index_ - viewport_start_index_; + handle_selection_change(focused_index_, settings_.x, settings_.y + settings_.margin_top + (render_index * settings_.item_height)); +} + +i_item_widget* vertical_menu::get_item_widget_at(unsigned index) const +{ + if(index >= items_.size()) + { + return nullptr; + } + return items_[index]; +} + +void vertical_menu::hide() +{ + if(state_changed_handler_) + { + state_changed_handler_->on_hide(); + } + + clear_viewport(); + + reload_textbox_background(); +} + +const vertical_menu_settings& vertical_menu::get_settings() const +{ + return settings_; +} + +void vertical_menu::set_settings(const vertical_menu_settings &settings) +{ + settings_ = settings; +} + +void vertical_menu::add_item_widgets(i_item_widget **item_widgets, unsigned num_widgets) +{ + items_.reserve(items_.size() + num_widgets); + for (unsigned i = 0; i < num_widgets; ++i) + { + add_item_widget(item_widgets[i]); + } +} + +void vertical_menu::add_item_widget(i_item_widget *item_widget) +{ + items_.push_back(item_widget); +} + +void vertical_menu::clear_item_widgets() +{ + if (settings_.should_delete_item_widgets_on_destruct) + { + for (unsigned i = 0; i < items_.size(); ++i) + { + delete items_[i]; + } + } + items_.clear(); + focused_index_ = settings_.initial_focus_index; + viewport_start_index_ = 0; +} + +void vertical_menu::set_focused(bool is_focused) +{ + is_focused_ = is_focused; + if(state_changed_handler_ && settings_.should_hide_state_changed_handler_on_not_focused) + { + if(is_focused_) + { + state_changed_handler_->on_show(); + } + else + { + state_changed_handler_->on_hide(); + } + } +} + +MenuInputHandleState vertical_menu::handle_input() +{ + MenuInputHandleState result; + bool did_navigate = false; + + // If no items or not focused, there's nothing to handle. + if(items_.size() <= 1 || !is_focused_) + { + return MenuInputHandleState::NOT_HANDLED; + } + + // the focused item widget gets the first chance to handle input, + // since it might have some special behavior for certain keys. + result = items_[focused_index_]->handle_input(); + if(result != MenuInputHandleState::NOT_HANDLED) + { + return result; + } + + // if the user presses B, and the current settings allow it, + // we will return the CANCELLED state, which the caller can use to know that they should exit the menu. + if(settings_.allow_cancel && key_hit(KEY_B)) + { + return MenuInputHandleState::CANCELLED; + } + + if(key_hit(KEY_DOWN) && focused_index_ < (items_.size() - 1u)) + { + const unsigned current_viewport_end_index = get_viewport_end_index(viewport_start_index_, get_num_visible_items(settings_.height, settings_.margin_top, settings_.margin_bottom, settings_.item_height), items_.size()); + ++focused_index_; + if(focused_index_ >= current_viewport_end_index) + { + ++viewport_start_index_; + } + did_navigate = true; + } + else if(key_hit(KEY_UP) && focused_index_ > 0) + { + --focused_index_; + if(focused_index_ < viewport_start_index_) + { + --viewport_start_index_; + } + did_navigate = true; + } + + // if we did navigate, we need to update the viewport and state changed handler. + if(did_navigate) + { + const unsigned render_index = focused_index_ - viewport_start_index_; + update_viewport(); + handle_selection_change(focused_index_, settings_.x, settings_.y + settings_.margin_top + (render_index * settings_.item_height)); + return MenuInputHandleState::HANDLED; + } + + return MenuInputHandleState::NOT_HANDLED; +} + +void vertical_menu::update_viewport() +{ + uint8_t decompression_buffer[2048]; + text_data_table text_table(decompression_buffer); + + clear_viewport(); + + text_table.decompress(get_compressed_text_table(settings_.text_table_index)); + + const unsigned num_visible_items = get_num_visible_items(settings_.height, settings_.margin_top, settings_.margin_bottom, settings_.item_height); + const unsigned viewport_end_index = get_viewport_end_index(viewport_start_index_, num_visible_items, items_.size()); + unsigned cur_y = settings_.y + settings_.margin_top; + + for (unsigned i = viewport_start_index_; i < viewport_end_index; ++i) + { + items_[i]->render_item(text_table, settings_.x, cur_y, (i == focused_index_)); + cur_y += settings_.item_height; + } +} + +void vertical_menu::clear_viewport() +{ + // tte_erase_rect expects (left, top, right, bottom), not (x, y, width, height) + tte_erase_rect(settings_.x, settings_.y, settings_.x + settings_.width, settings_.y + settings_.height); +} + +unsigned vertical_menu::run() +{ + MenuInputHandleState input_result; + + while(true) + { + key_poll(); // Reset the buttons + + input_result = handle_input(); + + switch(input_result) + { + case MenuInputHandleState::CHOICE_MADE: + return focused_index_; + case MenuInputHandleState::CANCELLED: + return UINT32_MAX; + default: + break; + } + + // if any global elements need to be updated + // on every cycle (animations, for example) + // the run_cycle_handler can be used for that. + if(run_cycle_handler_) + { + run_cycle_handler_->on_run_cycle(); + } + + global_next_frame(); + } + // should never happen + return UINT32_MAX; +} + +void vertical_menu::set_state_changed_handler(i_vertical_menu_state_changed_handler *handler) +{ + state_changed_handler_ = handler; +} + +void vertical_menu::set_run_cycle_handler(i_run_cycle_handler *handler) +{ + run_cycle_handler_ = handler; +} + +void vertical_menu::handle_selection_change(unsigned new_index, unsigned x, unsigned y) +{ + if(state_changed_handler_) + { + state_changed_handler_->on_selection_changed(new_index, x, y); + } +} + +simple_item_renderer::simple_item_renderer(const simple_item_widget_data &data) + : data_(data) +{ +} + +simple_item_renderer::~simple_item_renderer() +{ +} + +const simple_item_widget_data& simple_item_renderer::get_data() const +{ + return data_; +} + +void simple_item_renderer::render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) +{ + tte_set_pos(x + data_.text.margin_left, y + data_.text.margin_top); + ptgb_write(text_table.get_text_entry(data_.text.text_table_index), true); +} + +MenuInputHandleState simple_item_renderer::handle_input() +{ + if(!key_hit(KEY_A)) + { + return MenuInputHandleState::NOT_HANDLED; + } + + if(data_.on_execute_callback) + { + data_.on_execute_callback(this); + return MenuInputHandleState::HANDLED; + } + return MenuInputHandleState::CHOICE_MADE; +} \ No newline at end of file