diff options
| -rw-r--r-- | app/mac/scene.mm | 103 | ||||
| -rw-r--r-- | lib/nes/app/application.cc | 17 | ||||
| -rw-r--r-- | lib/nes/app/application.hh | 4 | ||||
| -rw-r--r-- | lib/nes/app/graphics/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | lib/nes/app/graphics/renderer.cc | 95 | ||||
| -rw-r--r-- | lib/nes/app/graphics/renderer.hh | 35 | ||||
| -rw-r--r-- | lib/nes/app/graphics/text-attributes.hh | 39 | ||||
| -rw-r--r-- | lib/nes/app/input/input-buffer.hh | 1 | ||||
| -rw-r--r-- | lib/nes/app/input/input-event.hh | 1 | ||||
| -rw-r--r-- | lib/nes/app/ui/CMakeLists.txt | 5 | ||||
| -rw-r--r-- | lib/nes/app/ui/action.hh | 6 | ||||
| -rw-r--r-- | lib/nes/app/ui/screen-browser.cc | 71 | ||||
| -rw-r--r-- | lib/nes/app/ui/screen-browser.hh | 32 | ||||
| -rw-r--r-- | lib/nes/app/ui/screen-title.cc | 61 | ||||
| -rw-r--r-- | lib/nes/app/ui/screen-title.hh | 19 | ||||
| -rw-r--r-- | lib/nes/app/ui/selection.hh | 125 | ||||
| -rw-r--r-- | lib/nes/common/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | lib/nes/common/string-builder.cc | 255 | ||||
| -rw-r--r-- | lib/nes/common/string-builder.hh | 98 |
19 files changed, 915 insertions, 57 deletions
diff --git a/app/mac/scene.mm b/app/mac/scene.mm index bf1a61b..031413f 100644 --- a/app/mac/scene.mm +++ b/app/mac/scene.mm @@ -6,6 +6,10 @@ #import "nes/common/display.hh" #import <memory> #import <optional> +#import <fcntl.h> +#import <unistd.h> +#import <termios.h> +#import <sys/ioctl.h> namespace { @@ -252,12 +256,108 @@ namespace return res; } }; + + class input_device_controller_serial : public nes::app::input_device_controller + { + char const* path_; + int fd_{ 0 }; + nes::sys::button_mask current_{}; + index index_{ index::unused }; + + public: + explicit input_device_controller_serial(char const* path) + : path_{ path } + { + fd_ = open(path, O_RDONLY | O_NOCTTY | O_NDELAY | O_NONBLOCK); + if (fd_ == -1) + { + perror("open controller"); + return; + } + + if (!setup()) { close(); } + } + + ~input_device_controller_serial() override + { + close(); + } + + auto read_buttons() -> nes::sys::button_mask override + { + if (fd_ == -1) { return nes::sys::button_mask{}; } + + // Poll for any updates until we can't read anymore. + while (true) + { + auto buf = nes::u8{}; + auto res = read(fd_, &buf, 1); + if (res == -1) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } + + perror("read from controller"); + close(); + break; + } + + current_ = nes::sys::button_mask::from_raw_value(buf); + } + + return current_; + } + + auto is_reliable() const -> bool override { return fd_ != -1; } + auto get_name() const -> std::string_view override { return path_; } + auto set_index(index const index) -> void override { index_ = index; } + auto get_index() const -> index override { return index_; } + + private: + auto close() -> void + { + if (fd_ != -1) + { + ::close(fd_); + fd_ = -1; + } + } + + auto setup() -> bool + { + if (ioctl(fd_, TIOCEXCL) == -1) + { + perror("ioctl for controller"); + return false; + } + + auto attr = termios{}; + if (tcgetattr(fd_, &attr) == -1) + { + perror("tcgetattr for controller"); + return false; + } + + cfsetispeed(&attr, B57600); + cfsetospeed(&attr, B57600); + attr.c_cflag = attr.c_cflag | CLOCAL | CREAD; + attr.c_cflag = (attr.c_cflag & static_cast<tcflag_t>(~CSIZE)) | CS8; + + if (tcsetattr(fd_, TCSANOW, &attr) == -1) + { + perror("tcsetattr for controller"); + return false; + } + + return true; + } + }; } // namespace @implementation Scene { std::unique_ptr<display_impl> _display; std::unique_ptr<input_device_keyboard_impl> _keyboard; + std::unique_ptr<input_device_controller_serial> _serialController; std::vector<std::unique_ptr<input_device_controller_impl>> _controllers; std::unique_ptr<nes::app::application> _app; std::optional<NSTimeInterval> _lastTimestamp; @@ -277,8 +377,11 @@ namespace _display = std::make_unique<display_impl>(node); _keyboard = std::make_unique<input_device_keyboard_impl>([[GCKeyboard coalescedKeyboard] keyboardInput]); + _serialController = std::make_unique<input_device_controller_serial>("/dev/cu.usbserial-FTB6SPL3"); _app = std::make_unique<nes::app::application>(*_display, *_keyboard); + _app->add_controller(*_serialController); + for (GCController* controller in [GCController controllers]) { auto gamepad = [controller extendedGamepad]; diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc index b18e6d6..5f73e01 100644 --- a/lib/nes/app/application.cc +++ b/lib/nes/app/application.cc @@ -28,7 +28,8 @@ namespace nes::app application::application(display& display, input_device_keyboard& keyboard) : display_{ display } , input_manager_{ keyboard } - , screen_title_{ input_manager_ } + , screen_title_{ keyboard } + , screen_browser_{ keyboard } { display_.screen = &screen_title_; } @@ -72,7 +73,19 @@ namespace nes::app { break; } - case action::load_game: + case action::go_to_title: + { + console_.clear(); + display_.screen = &screen_title_; + break; + } + case action::go_to_browser: + { + console_.clear(); + display_.screen = &screen_browser_; + break; + } + case action::launch_game: { auto stream = std::ifstream{ file, std::ios::binary }; if (!stream.good()) diff --git a/lib/nes/app/application.hh b/lib/nes/app/application.hh index 91ab35f..44b99bb 100644 --- a/lib/nes/app/application.hh +++ b/lib/nes/app/application.hh @@ -2,6 +2,7 @@ #include "nes/app/input/input-manager.hh" #include "nes/app/ui/screen-title.hh" +#include "nes/app/ui/screen-browser.hh" #include "nes/sys/nes.hh" #include "nes/common/box.hh" #include "nes/common/display.hh" @@ -40,8 +41,9 @@ namespace nes::app display_proxy display_; input_manager input_manager_; - box<sys::nes> console_; + box<sys::nes> console_{}; screen_title screen_title_; + screen_browser screen_browser_; public: explicit application(display&, input_device_keyboard&); diff --git a/lib/nes/app/graphics/CMakeLists.txt b/lib/nes/app/graphics/CMakeLists.txt index 0f869b7..2415f4f 100644 --- a/lib/nes/app/graphics/CMakeLists.txt +++ b/lib/nes/app/graphics/CMakeLists.txt @@ -5,6 +5,7 @@ target_sources( image-view.hh mask-tile.hh image-tile.hh + text-attributes.hh renderer.hh renderer.cc) diff --git a/lib/nes/app/graphics/renderer.cc b/lib/nes/app/graphics/renderer.cc index 6a45b6c..6429a52 100644 --- a/lib/nes/app/graphics/renderer.cc +++ b/lib/nes/app/graphics/renderer.cc @@ -3,6 +3,7 @@ #include "nes/app/graphics/mask-tile.hh" #include "nes/app/graphics/image-tile.hh" #include "nes/app/graphics/image-view.hh" +#include "nes/app/graphics/text-attributes.hh" #include "nes/app/graphics/tiles/characters.hh" #include "nes/common/display.hh" @@ -13,9 +14,9 @@ namespace nes::app { } - auto renderer::render_image_tile(u32 const x, u32 const y, image_tile const& tile) -> void + auto renderer::render_image_tile(i32 const x, i32 const y, image_tile const& tile) -> void { - if (x >= width || y >= height) { return; } + if (x < 0 || y < 0 || x >= static_cast<i32>(width) || y >= static_cast<i32>(height)) { return; } for (auto y_px = u32{ 0 }; y_px < 8; ++y_px) { @@ -24,16 +25,16 @@ namespace nes::app auto const c = tile.data[y_px * 8 + x_px]; if (c != color::transparent) { - display_.set(8 * x + x_px, 8 * y + y_px, resolve_color(c)); + display_.set(8 * static_cast<u32>(x) + x_px, 8 * static_cast<u32>(y) + y_px, resolve_color(c)); } } } } - auto renderer::render_mask_tile(u32 const x, u32 const y, mask_tile const tile, color const c) -> void + auto renderer::render_mask_tile(i32 const x, i32 const y, mask_tile const tile, color const c) -> void { if (c == color::transparent) { return; } - if (x >= width || y >= height) { return; } + if (x < 0 || y < 0 || x >= static_cast<i32>(width) || y >= static_cast<i32>(height)) { return; } auto const resolved = resolve_color(c); for (auto y_px = u32{ 0 }; y_px < 8; ++y_px) @@ -43,47 +44,103 @@ namespace nes::app { if (row & 0b10000000) { - display_.set(8 * x + x_px, 8 * y + y_px, resolved); + display_.set(8 * static_cast<u32>(x) + x_px, 8 * static_cast<u32>(y) + y_px, resolved); } row <<= 1; } } } - auto renderer::render_image(u32 const x, u32 const y, image_view const image) -> void + auto renderer::render_image(i32 const x, i32 const y, image_view const image) -> void { for (auto y_img = u32{ 0 }; y_img < image.get_height(); ++y_img) { for (auto x_img = u32{ 0 }; x_img < image.get_width(); ++x_img) { - render_image_tile(x + x_img, y + y_img, image.get(x_img, y_img)); + render_image_tile(x + static_cast<i32>(x_img), y + static_cast<i32>(y_img), image.get(x_img, y_img)); } } } - auto renderer::render_text(u32 const x, u32 const y, std::string_view const str, color const c) -> void + auto renderer::render_text( + i32 x, i32 const y, std::string_view const str, color const c, text_attributes const attributes) -> void { - for (auto i = u32{ 0 }; i < str.size(); ++i) + // Enforce max length + char buffer[width]; + auto const length = std::min(static_cast<u32>(str.length()), std::min(width, attributes.get_max_width())); + auto const ellipsize_dots = std::min(length, u32{ 3 }); + + // Enforce ellipsize mode + if (str.length() > length) + { + switch (attributes.get_ellipsize_mode()) + { + case ellipsize_mode::clip: + { + std::copy_n(str.begin(), length, buffer); + break; + } + case ellipsize_mode::end: + { + std::copy_n(str.begin(), length, buffer); + for (auto i = u32{ 0 }; i < ellipsize_dots; ++i) { buffer[length - 1 - i] = '.'; } + break; + } + case ellipsize_mode::start: + { + std::copy_n(str.begin() + str.length() - length, length, buffer); + for (auto i = u32{ 0 }; i < ellipsize_dots; ++i) { buffer[i] = '.'; } + break; + } + case ellipsize_mode::middle: + { + auto const visible = length - ellipsize_dots; + auto const start = visible / 2; + auto const end = visible - start; + std::copy_n(str.begin(), start, buffer); + for (auto i = u32{ 0 }; i < ellipsize_dots; ++i) { buffer[start + i] = '.'; } + std::copy_n(str.begin() + str.length() - end, end, buffer + start + ellipsize_dots); + break; + } + } + } + else + { + std::copy_n(str.begin(), length, buffer); + } + + // Enforce alignment + switch (attributes.get_alignment()) + { + case text_alignment::left: break; + case text_alignment::center: x -= length / 2; break; + case text_alignment::right: x -= length; break; + } + + // Draw the text + for (auto i = u32{ 0 }; i < length; ++i) { - auto const character = str[i]; - render_mask_tile(x + i, y, resolve_character(str[i]), c); + auto const character = static_cast<u32>(buffer[i]); + render_mask_tile(x + static_cast<i32>(i), y, resolve_character(character), c); } } auto renderer::render_rect( - u32 const x, u32 const y, u32 const width, u32 const height, color const c) -> void + i32 const x, i32 const y, u32 const width, u32 const height, color const c) -> void { if (c == color::transparent) { return; } - if (x >= width || y >= height) { return; } + if (x >= static_cast<i32>(width) || y >= static_cast<i32>(height)) { return; } auto const resolved = resolve_color(c); - auto const x_stop = std::min(x + width, renderer::width); - auto const y_stop = std::min(y + height, renderer::height); - for (auto y_px = u32{ y * 8 }; y_px < y_stop * 8; ++y_px) + auto const x_stop = std::min(x + static_cast<i32>(width), static_cast<i32>(renderer::width)); + auto const y_stop = std::min(y + static_cast<i32>(height), static_cast<i32>(renderer::height)); + auto const x_start = std::max(x, 0); + auto const y_start = std::max(y, 0); + for (auto y_px = i32{ y_start * 8 }; y_px < y_stop * 8; ++y_px) { - for (auto x_px = u32{ x * 8 }; x_px < x_stop * 8; ++x_px) + for (auto x_px = i32{ x_start * 8 }; x_px < x_stop * 8; ++x_px) { - display_.set(x_px, y_px, resolved); + display_.set(static_cast<u32>(x_px), static_cast<u32>(y_px), resolved); } } } diff --git a/lib/nes/app/graphics/renderer.hh b/lib/nes/app/graphics/renderer.hh index 08e4225..cc0b9af 100644 --- a/lib/nes/app/graphics/renderer.hh +++ b/lib/nes/app/graphics/renderer.hh @@ -1,6 +1,9 @@ #pragma once #include "nes/common/types.hh" +#include "nes/app/graphics/color.hh" +#include "nes/app/graphics/text-attributes.hh" +#include "nes/common/string-builder.hh" #include <string_view> namespace nes @@ -14,7 +17,6 @@ namespace nes::app struct image_tile; struct mask_tile; class image_view; - enum class color : u8; /// Renders tiles and other primitive shapes onto a display. /// @@ -41,18 +43,33 @@ namespace nes::app auto set_border(u32 px, color) -> void; /// Render a colored tile at the given tile coordinates. - auto render_image_tile(u32 x, u32 y, image_tile const&) -> void; + auto render_image_tile(i32 x, i32 y, image_tile const&) -> void; /// Render an alpha-only tile at the given tile coordinates with the given color. - auto render_mask_tile(u32 x, u32 y, mask_tile, color) -> void; + auto render_mask_tile(i32 x, i32 y, mask_tile, color) -> void; /// Render an image at the given tile coordinates. - auto render_image(u32 x, u32 y, image_view) -> void; + auto render_image(i32 x, i32 y, image_view) -> void; /// Render a string of text at the given tile coordinates (one tile per character). - auto render_text(u32 x, u32 y, std::string_view, color) -> void; - /// Render a string of text at the given tile coordinates, limiting its length to the given width and - /// ellipsizing afterward. - auto render_text(u32 x, u32 y, std::string_view, u32 width, color) -> void; + auto render_text(i32 x, i32 y, std::string_view, color, text_attributes = text_attributes{}) -> void; + + template<typename... Args> + auto render_text_format( + i32 const x, i32 const y, color const c, text_attributes const attrs, std::string_view const fmt, + Args... args) -> void + { + char buffer[width]; + auto const str = string_builder{ buffer, sizeof(buffer) }.append_format(fmt, args...).get_result(); + render_text(x, y, str, c, attrs); + } + + template<typename... Args> + auto render_text_format( + i32 const x, i32 const y, color const c, std::string_view const fmt, Args... args) -> void + { + render_text_format(x, y, c, text_attributes{}, fmt, args...); + } + /// Render a rectangle at the given tile coordinates with a given size (measured in tiles). - auto render_rect(u32 x, u32 y, u32 width, u32 height, color) -> void; + auto render_rect(i32 x, i32 y, u32 width, u32 height, color) -> void; /// Completely fill the screen. auto render_fill(color) -> void; diff --git a/lib/nes/app/graphics/text-attributes.hh b/lib/nes/app/graphics/text-attributes.hh new file mode 100644 index 0000000..6c82311 --- /dev/null +++ b/lib/nes/app/graphics/text-attributes.hh @@ -0,0 +1,39 @@ +#pragma once + +#include "nes/common/types.hh" + +namespace nes::app +{ + enum class ellipsize_mode + { + clip, + start, + middle, + end, + }; + + enum class text_alignment + { + left, + center, + right, + }; + + class text_attributes + { + u32 max_width_{ 32 }; + ellipsize_mode ellipsize_mode_{ ellipsize_mode::clip }; + text_alignment alignment_{ text_alignment::left }; + + public: + explicit text_attributes() = default; + + auto get_max_width() const -> u32 { return max_width_; } + auto get_ellipsize_mode() const -> ellipsize_mode { return ellipsize_mode_; } + auto get_alignment() const -> text_alignment { return alignment_; } + + auto set_max_width(u32 const v) -> text_attributes& { max_width_ = v; return *this; } + auto set_ellipsize_mode(ellipsize_mode const v) -> text_attributes& { ellipsize_mode_ = v; return *this; } + auto set_alignment(text_alignment const v) -> text_attributes& { alignment_ = v; return *this; } + }; +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/input/input-buffer.hh b/lib/nes/app/input/input-buffer.hh index 68fca6b..319cfa9 100644 --- a/lib/nes/app/input/input-buffer.hh +++ b/lib/nes/app/input/input-buffer.hh @@ -7,6 +7,7 @@ namespace nes::app { class input_event; + /// A buffer which stores the "pressed" state for each key and a queue of key-down and key-up events. class input_buffer { struct event final : list<event>::node diff --git a/lib/nes/app/input/input-event.hh b/lib/nes/app/input/input-event.hh index 90e67b0..26d6448 100644 --- a/lib/nes/app/input/input-event.hh +++ b/lib/nes/app/input/input-event.hh @@ -4,6 +4,7 @@ namespace nes::app { + /// A keyboard input event which can be polled. class input_event { public: diff --git a/lib/nes/app/ui/CMakeLists.txt b/lib/nes/app/ui/CMakeLists.txt index 2faed29..0980e97 100644 --- a/lib/nes/app/ui/CMakeLists.txt +++ b/lib/nes/app/ui/CMakeLists.txt @@ -2,6 +2,9 @@ target_sources( nes PRIVATE action.hh + selection.hh screen.hh screen-title.hh - screen-title.cc)
\ No newline at end of file + screen-title.cc + screen-browser.hh + screen-browser.cc)
\ No newline at end of file diff --git a/lib/nes/app/ui/action.hh b/lib/nes/app/ui/action.hh index 2994f37..52e5ba6 100644 --- a/lib/nes/app/ui/action.hh +++ b/lib/nes/app/ui/action.hh @@ -6,6 +6,10 @@ namespace nes::app enum class action { none, - load_game, + go_to_title, + go_to_browser, + go_to_settings, + go_to_help, + launch_game, }; } // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/ui/screen-browser.cc b/lib/nes/app/ui/screen-browser.cc new file mode 100644 index 0000000..da2d82d --- /dev/null +++ b/lib/nes/app/ui/screen-browser.cc @@ -0,0 +1,71 @@ +#include "nes/app/ui/screen-browser.hh" +#include "nes/app/graphics/renderer.hh" +#include "nes/app/graphics/color.hh" +#include "nes/app/input/input-device-keyboard.hh" +#include "nes/common/string-builder.hh" + +namespace nes::app +{ + auto screen_browser::selection_impl::render_item( + renderer& renderer, i32 const& item, i32 const x, i32 const y, u32 const width, color const color) const -> void + { + renderer.render_text_format(x, y, color, text_attributes{}.set_max_width(width), "Item {}", item); + } + + auto screen_browser::selection_impl::load_page(int (&items)[5], u32 const page) -> u32 + { + for (auto i = u32{ 0 }; i < 5; ++i) + { + items[i] = static_cast<i32>(page * 5 + i); + } + return 5; + } + + screen_browser::screen_browser(input_device_keyboard& keyboard) + : keyboard_{ keyboard } + { + selection_.set_page_count(2); + } + + auto screen_browser::render(renderer& renderer) -> void + { + renderer.render_fill(color::background_primary); + renderer.render_rect(0, 0, 32, 3, color::background_secondary); + + renderer.render_text(2, 1, "Load Game", color::foreground_primary); + renderer.render_text_format( + 30, 1, color::foreground_secondary, text_attributes{}.set_alignment(text_alignment::right), + "Page {}/{}", selection_.get_current_page() + 1, selection_.get_page_count()); + + selection_.render(renderer); + } + + auto screen_browser::process_events() -> action + { + while (auto const event = keyboard_.poll_event()) + { + if (event == input_event::key_down(key::space)) + { + selection_.go_next(); + } + else if (event == input_event::key_down(key::arrow_up)) + { + selection_.go_up(); + } + else if (event == input_event::key_down(key::arrow_down)) + { + selection_.go_down(); + } + else if (event == input_event::key_down(key::backspace)) + { + return action::go_to_title; + } + else if (event == input_event::key_down(key::enter)) + { + return action::launch_game; + } + } + + return action::none; + } +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/ui/screen-browser.hh b/lib/nes/app/ui/screen-browser.hh new file mode 100644 index 0000000..eb66266 --- /dev/null +++ b/lib/nes/app/ui/screen-browser.hh @@ -0,0 +1,32 @@ +#pragma once + +#include "nes/app/ui/screen.hh" +#include "nes/app/ui/selection.hh" + +namespace nes::app +{ + class input_device_keyboard; + + /// An interface which allows the user to browse the filesystem for a game. + class screen_browser final : public screen + { + class selection_impl final : public selection<i32, 5> + { + public: + using selection::selection; + + protected: + auto render_item(renderer&, i32 const&, i32 x, i32 y, u32 width, color) const -> void override; + auto load_page(i32(&)[5], u32 page) -> u32 override; + }; + + input_device_keyboard& keyboard_; + selection_impl selection_{ 2, 5, 28 }; + + public: + explicit screen_browser(input_device_keyboard&); + + auto render(renderer&) -> void override; + auto process_events() -> action override; + }; +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/ui/screen-title.cc b/lib/nes/app/ui/screen-title.cc index 5a2f757..32a6ea6 100644 --- a/lib/nes/app/ui/screen-title.cc +++ b/lib/nes/app/ui/screen-title.cc @@ -2,13 +2,35 @@ #include "nes/app/graphics/renderer.hh" #include "nes/app/graphics/color.hh" #include "nes/app/graphics/tiles/banner.hh" -#include "nes/app/input/input-manager.hh" +#include "nes/app/input/input-device-keyboard.hh" namespace nes::app { - screen_title::screen_title(input_manager& input_manager) - : input_manager_{ input_manager } + auto screen_title::selection_impl::render_item( + renderer& renderer, item const& item, i32 const x, i32 const y, u32 const width, color const color) const -> void { + auto const attrs = text_attributes{} + .set_max_width(width); + switch (item) + { + case item::load_game: renderer.render_text(x, y, "Load Game", color, attrs); break; + case item::settings: renderer.render_text(x, y, "Settings", color, attrs); break; + case item::help: renderer.render_text(x, y, "Help", color, attrs); break; + } + } + + auto screen_title::selection_impl::load_page(item (&items)[3], u32) -> u32 + { + items[0] = item::load_game; + items[1] = item::settings; + items[2] = item::help; + return 3; + } + + screen_title::screen_title(input_device_keyboard& keyboard) + : keyboard_{ keyboard } + { + selection_.set_page_count(1); } auto screen_title::render(renderer& renderer) -> void @@ -16,32 +38,33 @@ namespace nes::app renderer.render_fill(color::background_primary); renderer.render_image(2, 4, tiles::banner); - if (selected_item_ == item::load_game) { renderer.render_text(10, 16, ">", color::accent_primary); } - renderer.render_text(12, 16, "Load Game", selected_item_ == item::load_game ? color::accent_primary : color::foreground_primary); - - if (selected_item_ == item::settings) { renderer.render_text(10, 19, ">", color::accent_primary); } - renderer.render_text(12, 19, "Settings", selected_item_ == item::settings ? color::accent_primary : color::foreground_primary); - - if (selected_item_ == item::help) { renderer.render_text(10, 22, ">", color::accent_primary); } - renderer.render_text(12, 22, "Help", selected_item_ == item::help ? color::accent_primary : color::foreground_primary); + selection_.render(renderer); } auto screen_title::process_events() -> action { - while (auto event = input_manager_.get_keyboard().poll_event()) + while (auto const event = keyboard_.poll_event()) { if (event == input_event::key_down(key::space)) { - switch (selected_item_) - { - case item::load_game: selected_item_ = item::settings; break; - case item::settings: selected_item_ = item::help; break; - case item::help: selected_item_ = item::load_game; break; - } + selection_.go_next(); + } + else if (event == input_event::key_down(key::arrow_up)) + { + selection_.go_up(); + } + else if (event == input_event::key_down(key::arrow_down)) + { + selection_.go_down(); } else if (event == input_event::key_down(key::enter)) { - return action::load_game; + switch (*selection_.get_selected()) + { + case item::load_game: return action::go_to_browser; + case item::settings: return action::go_to_settings; + case item::help: return action::go_to_help; + } } } diff --git a/lib/nes/app/ui/screen-title.hh b/lib/nes/app/ui/screen-title.hh index 2ebc78a..6384c3e 100644 --- a/lib/nes/app/ui/screen-title.hh +++ b/lib/nes/app/ui/screen-title.hh @@ -1,10 +1,11 @@ #pragma once #include "nes/app/ui/screen.hh" +#include "nes/app/ui/selection.hh" namespace nes::app { - class input_manager; + class input_device_keyboard; /// The main title screen for the emulator, used as an entrypoint for the UI. /// @@ -18,11 +19,21 @@ namespace nes::app help, }; - input_manager& input_manager_; - item selected_item_{ item::load_game }; + class selection_impl final : public selection<item, 3> + { + public: + using selection::selection; + + protected: + auto render_item(renderer&, item const&, i32 x, i32 y, u32 width, color) const -> void override; + auto load_page(item(&)[3], u32 page) -> u32 override; + }; + + input_device_keyboard& keyboard_; + selection_impl selection_{ 10, 16, 11 }; public: - explicit screen_title(input_manager&); + explicit screen_title(input_device_keyboard&); auto render(renderer&) -> void override; auto process_events() -> action override; diff --git a/lib/nes/app/ui/selection.hh b/lib/nes/app/ui/selection.hh new file mode 100644 index 0000000..fafa4a6 --- /dev/null +++ b/lib/nes/app/ui/selection.hh @@ -0,0 +1,125 @@ +#pragma once + +#include "nes/common/types.hh" +#include "nes/app/graphics/renderer.hh" +#include "nes/app/graphics/color.hh" + +namespace nes::app +{ + /// A selection menu managing a fixed number of items and pagination. + template<typename Item, u32 PageSize> + class selection + { + Item items_[PageSize]; + u32 count_{ 0 }; + u32 page_count_{ 0 }; + i32 x_{}; + i32 y_{}; + u32 width_{}; + i32 selected_{ -1 }; + i32 current_page_{ -1 }; + + public: + explicit selection(i32 const x, i32 const y, u32 const width) + : x_{ x } + , y_{ y } + , width_{ width } + { + } + + virtual ~selection() = default; + + selection(selection const&) = delete; + selection(selection&&) = delete; + auto operator=(selection const&) -> selection& = delete; + auto operator=(selection&&) -> selection& = delete; + + auto set_page_count(u32 const page_count) -> void + { + page_count_ = page_count; + set_page(page_count_ > 0 ? 0 : -1); + } + + auto get_page_count() const -> u32 { return page_count_; } + auto get_current_page() const -> i32 { return current_page_; } + auto get_selected() const -> Item const* { return selected_ >= 0 ? &items_[selected_] : nullptr; } + + auto render(renderer& renderer) const -> void + { + for (auto i = u32{ 0 }; i < count_; ++i) + { + auto const y = y_ + static_cast<i32>(3 * i); + auto const is_selected = static_cast<i32>(i) == selected_; + auto const color = is_selected ? color::accent_primary : color::foreground_primary; + if (is_selected) + { + renderer.render_text(x_, y, ">", color); + } + + render_item(renderer, items_[i], x_ + 2, y, width_ - 2, color); + } + } + + auto go_next() -> void + { + if (selected_ < 0) { return; } + + selected_ += 1; + if (selected_ == static_cast<i32>(count_)) { selected_ = 0; } + } + + auto go_up() -> void + { + if (selected_ <= 0) + { + if (current_page_ <= 0) { return; } + + set_page(current_page_ - 1, true); + return; + } + selected_ -= 1; + } + + auto go_down() -> void + { + if (selected_ < 0 || selected_ + 1 == static_cast<i32>(count_)) + { + if (current_page_ < 0 || current_page_ + 1 == static_cast<i32>(page_count_)) { return; } + + set_page(current_page_ + 1, false); + return; + } + selected_ += 1; + } + + protected: + /// Render an item's caption at the given position and width. + virtual auto render_item(renderer&, Item const&, i32 x, i32 y, u32 width, color) const -> void = 0; + + /// Load the items for a page (max. PageSize items) and return the number of loaded items. + virtual auto load_page(Item(&)[PageSize], u32 page) -> u32 = 0; + + private: + auto set_page(i32 const page, bool const select_last = false) -> void + { + if (page < 0) + { + current_page_ = -1; + count_ = 0; + selected_ = -1; + return; + } + + current_page_ = page; + count_ = load_page(items_, static_cast<u32>(page)); + if (count_ > 0) + { + selected_ = select_last ? static_cast<i32>(count_) - 1 : 0; + } + else + { + selected_ = -1; + } + } + }; +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/common/CMakeLists.txt b/lib/nes/common/CMakeLists.txt index bd83a57..11143d7 100644 --- a/lib/nes/common/CMakeLists.txt +++ b/lib/nes/common/CMakeLists.txt @@ -6,4 +6,6 @@ target_sources( rgb.hh list.hh box.hh - display.hh)
\ No newline at end of file + display.hh + string-builder.hh + string-builder.cc)
\ No newline at end of file diff --git a/lib/nes/common/string-builder.cc b/lib/nes/common/string-builder.cc new file mode 100644 index 0000000..efc1326 --- /dev/null +++ b/lib/nes/common/string-builder.cc @@ -0,0 +1,255 @@ +#include "nes/common/string-builder.hh" + +namespace nes +{ + string_builder::string_builder(char* buffer, u32 buffer_length) + : buffer_{ buffer } + , buffer_length_{ buffer_length } + { + } + + auto string_builder::append_char(char const value) -> string_builder& + { + if (actual_length_ == buffer_length_) + { + is_good_ = false; + return *this; + } + + buffer_[actual_length_] = value; + actual_length_ += 1; + return *this; + } + + auto string_builder::append_string(std::string_view const value) -> string_builder& + { + for (auto const c : value) + { + append_char(c); + } + + return *this; + } + + auto string_builder::append_int(i64 value, number_format const format) -> string_builder& + { + if (value < 0) + { + append_char('-'); + value += 1; // to prevent lossy conversion to unsigned + auto abs_value = static_cast<u64>(-value); + abs_value += 1; // to correct the previous offset + append_int(abs_value, format); + } + else + { + append_int(static_cast<u64>(value), format); + } + + return *this; + } + + auto string_builder::append_int(u64 value, number_format const format) -> string_builder& + { + constexpr auto max_string_length = u32{ 64 }; // worst case is a 64-bit integer in binary representation + constexpr auto digits = "0123456789abcdef"; + + auto base = u64{ 10 }; + switch (format) + { + case number_format::binary: + append_string("0b"); + base = 2; + break; + case number_format::octal: + append_string("0"); + base = 8; + break; + case number_format::decimal: + break; + case number_format::hexadecimal: + append_string("0x"); + base = 16; + break; + } + + char buf[max_string_length + 1] = {}; // null-terminated + auto pos = max_string_length; + // Go through each digit in reverse order by continuously dividing the integer by the base. + while (value) + { + pos--; + + buf[pos] = digits[value % base]; // Convert digit to char representation + value /= base; + } + if (pos == max_string_length) + { + // Special case 0 would not print any character otherwise. + pos--; + buf[pos] = '0'; + } + + append_string(&buf[pos]); + return *this; + } + + auto string_builder::append_bool(bool const value) -> string_builder& + { + append_string(value ? "true" : "false"); + return *this; + } + + auto string_builder::append_format( + std::string_view const fmt, format_arg const* args, u32 const arg_count) -> string_builder& + { + // Go through the format string, inserting the arguments for the templates as necessary. + auto current_arg = u32{ 0 }; + for (auto i = u32{ 0 }; i < fmt.length(); ++i) + { + if (fmt[i] == '{') + { + i += 1; + + if (fmt[i] == '{') { + // escaped '{' + append_char('{'); + } + else + { + // start of template + auto const params = &fmt[i]; // remember start position + auto params_length = u32{ 0 }; + // skip to the end of the template + while (fmt[i] && fmt[i] != '}') + { + params_length++; + i++; + } + if (fmt[i] != '}') + { + // unmatched '{' + is_good_ = false; + break; + } + + // end of template + auto const p = std::string_view{ params, params_length }; + if (current_arg < arg_count) + { + append_format_arg(args[current_arg], p); + current_arg += 1; + } + else + { + // missing argument in parameter pack + is_good_ = false; + } + } + } + else if (fmt[i] == '}') + { + i += 1; + + if (fmt[i] == '}') + { + // escaped '}' + append_char('}'); + } + else + { + // unmatched '}' + is_good_ = false; + break; + } + } + else + { + // normal character in the template string + append_char(fmt[i]); + } + } + + if (current_arg < arg_count) + { + // not all supplied arguments were used + is_good_ = false; + } + + return *this; + } + + auto string_builder::append_format_arg(format_arg const arg, std::string_view const params) -> string_builder& + { + auto const parse_params = [&] + { + if (!params.empty()) + { + is_good_ = false; + } + }; + + auto const parse_params_integer = [&](number_format& out_format) + { + if (params.empty() || params == "d") + { + out_format = number_format::decimal; + } + else if (params == "x") + { + out_format = number_format::hexadecimal; + } + else if (params == "o") + { + out_format = number_format::octal; + } + else if (params == "b") + { + out_format = number_format::binary; + } + else + { + out_format = number_format::decimal; + is_good_ = false; + } + }; + + switch (arg.type) + { + case format_arg_type::string: + { + parse_params(); + append_string(arg.string); + break; + } + case format_arg_type::character: + { + parse_params(); + append_char(arg.character); + break; + } + case format_arg_type::signed_integer: + { + auto format = number_format{}; + parse_params_integer(format); + append_int(arg.signed_integer, format); + break; + } + case format_arg_type::unsigned_integer: + { + auto format = number_format{}; + parse_params_integer(format); + append_int(arg.unsigned_integer, format); + break; + } + case format_arg_type::boolean: + { + parse_params(); + append_bool(arg.character); + break; + } + } + + return *this; + } +} // namespace nes
\ No newline at end of file diff --git a/lib/nes/common/string-builder.hh b/lib/nes/common/string-builder.hh new file mode 100644 index 0000000..fda85a4 --- /dev/null +++ b/lib/nes/common/string-builder.hh @@ -0,0 +1,98 @@ +#pragma once + +#include "nes/common/types.hh" +#include <string_view> + +namespace nes +{ + enum class number_format + { + decimal, + hexadecimal, + octal, + binary, + }; + + /// Builds a string into a fixed-length buffer. + class string_builder + { + char* buffer_{ nullptr }; + u32 buffer_length_{ 0 }; + u32 actual_length_{ 0 }; + bool is_good_{ true }; + + enum class format_arg_type + { + string, + character, + signed_integer, + unsigned_integer, + boolean, + }; + + struct format_arg + { + format_arg_type type; + union + { + std::string_view string; + char character; + i64 signed_integer; + u64 unsigned_integer; + bool boolean; + }; + + format_arg() = delete; + + explicit format_arg(char const* v) : type{ format_arg_type::string } { string = v; } + explicit format_arg(std::string_view const v) : type{ format_arg_type::string } { string = v; } + explicit format_arg(char const v) : type{ format_arg_type::character } { character = v; } + explicit format_arg(unsigned char const v) : format_arg{ static_cast<char>(v) } {} + explicit format_arg(long long const v) : type{ format_arg_type::signed_integer } { signed_integer = v; } + explicit format_arg(unsigned long long const v) : type{ format_arg_type::unsigned_integer } { unsigned_integer = v; } + explicit format_arg(short const v) : format_arg{ static_cast<signed long long>(v) } {} + explicit format_arg(unsigned short const v) : format_arg{ static_cast<unsigned long long>(v) } {} + explicit format_arg(int const v) : format_arg{ static_cast<signed long long>(v) } {} + explicit format_arg(unsigned int const v) : format_arg{ static_cast<unsigned long long>(v) } {} + explicit format_arg(long const v) : format_arg{ static_cast<signed long long>(v) } {} + explicit format_arg(unsigned long const v) : format_arg{ static_cast<unsigned long long>(v) } {} + explicit format_arg(bool const v) : type{ format_arg_type::boolean } { boolean = v; } + }; + + public: + explicit string_builder(char* buffer, u32 buffer_length); + + /// Get the resulting string. + auto get_result() const -> std::string_view { return std::string_view{ buffer_, actual_length_ }; } + + /// Returns false after an invalid format string or buffer overflow. + auto is_good() const -> bool { return is_good_; } + + auto append_char(char) -> string_builder&; + auto append_string(std::string_view) -> string_builder&; + auto append_int(i64, number_format = number_format::decimal) -> string_builder&; + auto append_int(u64, number_format = number_format::decimal) -> string_builder&; + auto append_bool(bool) -> string_builder&; + + /// Print a templated string. + /// + /// Placeholders are signified by `{<params>}` where `params` contains optional formatting parameters: + /// - for integers either "d" (the default), "x", "o", or "b" + /// - for any other type, no parameters are supported. + /// + /// Curly braces can be escaped by repeating them twice like {{this}}. + template<typename... Args> + auto append_format(std::string_view const fmt, Args... args) -> string_builder& + { + constexpr u32 arg_count = sizeof...(Args); + // Add another argument at the end so we don't have an empty array expression. + const format_arg arg_wrappers[arg_count + 1] = { format_arg{ args }..., format_arg(0) }; + + return append_format(fmt, arg_wrappers, arg_count); + } + + private: + auto append_format(std::string_view fmt, format_arg const* args, u32 arg_count) -> string_builder&; + auto append_format_arg(format_arg, std::string_view params) -> string_builder&; + }; +} // namespace nes
\ No newline at end of file |
