diff --git a/CMakeLists.txt b/CMakeLists.txt index bc40f7d..f77776c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,7 @@ add_executable(${PROJECT_NAME} src/main.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}-lib) # Prevent the creation of a console window on Windows and create an application bundle on macOS -# If "WIN32_EXECUTABLE" is set to ON, the "sfml-main" library must be linked to the target, which is done in "cmake/External.cmake" +# If "WIN32_EXECUTABLE" is set to ON, the "SFML::Main" library must be linked to the target, which is done in "cmake/External.cmake" set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE ON MACOSX_BUNDLE ON @@ -123,8 +123,8 @@ if(APPLE) # Set macOS-specific properties set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_BINARY_DIR}/Info.plist - INSTALL_RPATH "@executable_path/../Frameworks" - BUILD_WITH_INSTALL_RPATH TRUE + #INSTALL_RPATH "@executable_path/../Frameworks" + #BUILD_WITH_INSTALL_RPATH TRUE ) # Copy the icon into the app bundle @@ -135,19 +135,19 @@ if(APPLE) COMMENT "Copying icon to the app bundle" ) - # Clean up the Frameworks directory before copying - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove_directory $/Contents/Frameworks - COMMAND ${CMAKE_COMMAND} -E make_directory $/Contents/Frameworks - COMMENT "Cleaning Frameworks directory" - ) - - # Copy only the SFML freetype framework into the app bundle - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND rsync -a ${SFML_SOURCE_DIR}/extlibs/libs-osx/Frameworks/freetype.framework - $/Contents/Frameworks/ - COMMENT "Copying SFML freetype framework into the app bundle" - ) + # # Clean up the Frameworks directory before copying + # add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + # COMMAND ${CMAKE_COMMAND} -E remove_directory $/Contents/Frameworks + # COMMAND ${CMAKE_COMMAND} -E make_directory $/Contents/Frameworks + # COMMENT "Cleaning Frameworks directory" + # ) + + # # Copy only the SFML freetype framework into the app bundle + # add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + # COMMAND rsync -a ${SFML_SOURCE_DIR}/extlibs/libs-osx/Frameworks/freetype.framework + # $/Contents/Frameworks/ + # COMMENT "Copying SFML freetype framework into the app bundle" + # ) elseif(WIN32) # If on Windows, add an icon to the executable # Specify the path to the .ico file diff --git a/README.md b/README.md index 530940e..030c6a3 100644 --- a/README.md +++ b/README.md @@ -130,16 +130,6 @@ On macOS, this will install the program to `/Applications`. You can then run `ae To start the program, simply run the `aegyo` executable (`aegyo.app` on macOS, `open /Applications/aegyo.app` to run from the terminal). -On first run on macOS, the app will request access to keystrokes from any application: - -``` -"aegyo.app" would like to receive keystrokes from any application. - -Grant access to this application in Privacy & Security settings, located in System Settings. -``` - -This is caused by the underlying SFML library, which reads raw keyboard input. You should **deny** this request, as the app does not expect any input while it's not in focus. I'd rather not have such a request appear in the first place, but I have no control over it. - ### Controls diff --git a/cmake/External.cmake b/cmake/External.cmake index 8d2e73d..978be9b 100644 --- a/cmake/External.cmake +++ b/cmake/External.cmake @@ -23,7 +23,7 @@ function(fetch_and_link_external_dependencies target) set(SFML_BUILD_NETWORK OFF) FetchContent_Declare( sfml - URL https://github.com/SFML/SFML/archive/refs/tags/2.6.1.zip + URL https://github.com/SFML/SFML/archive/refs/tags/3.0.0.zip DOWNLOAD_EXTRACT_TIMESTAMP TRUE EXCLUDE_FROM_ALL SYSTEM @@ -33,12 +33,12 @@ function(fetch_and_link_external_dependencies target) FetchContent_MakeAvailable(fmt sfml) # Link dependencies to the target - target_link_libraries(${target} PUBLIC fmt::fmt sfml-graphics) + target_link_libraries(${target} PUBLIC fmt::fmt SFML::Graphics) - # Link sfml-main for WIN32 targets to manage the WinMain entry point + # Link SFML::Main for WIN32 targets to manage the WinMain entry point # This makes Windows use main() instead of WinMain(), so we can use the same entry point for all platforms if(WIN32) - target_link_libraries(${target} PUBLIC sfml-main) + target_link_libraries(${target} PUBLIC SFML::Main) endif() message(STATUS "[INFO] Linked dependencies 'fmt' and 'sfml' to target '${target}'.") diff --git a/src/app.cpp b/src/app.cpp index 6738c1a..70160fd 100755 --- a/src/app.cpp +++ b/src/app.cpp @@ -5,6 +5,7 @@ #include // for std::array #include // for std::size_t #include // for std::numeric_limits +#include // for std::optional #include // for std::string #include // for std::unordered_map @@ -40,7 +41,7 @@ namespace { [[nodiscard]] sf::ContextSettings get_improved_context_settings(const unsigned int antialiasing = 8) { sf::ContextSettings settings; - settings.antialiasingLevel = antialiasing; + settings.antiAliasingLevel = antialiasing; return settings; } @@ -58,11 +59,11 @@ class UI final { * @brief Construct a new UI object. */ explicit UI() - : window_(sf::VideoMode(800, 600), - fmt::format("aegyo ({})", PROJECT_VERSION), - sf::Style::Titlebar | sf::Style::Close, - // Overwrite the default context settings with improved settings - get_improved_context_settings()), + : window_( + sf::VideoMode({800, 600}), + fmt::format("aegyo ({})", PROJECT_VERSION), + sf::State::Windowed, + get_improved_context_settings()), font_(core::assets::load_font()), vocabulary_(), toggle_labels_({"Vow", "Con", "DCon", "CompV"}), @@ -74,8 +75,23 @@ class UI final { {modules::vocabulary::Category::BasicConsonant, true}, {modules::vocabulary::Category::DoubleConsonant, true}, {modules::vocabulary::Category::CompoundVowel, true}}), + question_circle_(), + question_text_(this->font_, "", 48), + memo_text_(this->font_, "", 16), + percentage_text_(this->font_, "", 18), // Smaller font size, top-left corner button_shapes_(), - answer_buttons_() + answer_buttons_({sf::Text(this->font_, "", 28), + sf::Text(this->font_, "", 28), + sf::Text(this->font_, "", 28), + sf::Text(this->font_, "", 28)}), + toggle_buttons_({sf::RectangleShape(), + sf::RectangleShape(), + sf::RectangleShape(), + sf::RectangleShape()}), + toggle_texts_({sf::Text(this->font_), + sf::Text(this->font_), + sf::Text(this->font_), + sf::Text(this->font_)}) { // Enable V-Sync to limit the frame rate to the refresh rate of the monitor this->window_.setVerticalSyncEnabled(true); @@ -98,81 +114,68 @@ class UI final { // Initialize question circle this->question_circle_.setRadius(80.f); this->question_circle_.setPointCount(100); + this->question_circle_.setOrigin({80.f, 80.f}); + this->question_circle_.setPosition({400.f, 150.f}); this->question_circle_.setFillColor(this->colors_question_circle); - this->question_circle_.setOrigin(80.f, 80.f); - this->question_circle_.setPosition(400.f, 150.f); // Initialize question text - this->question_text_.setFont(this->font_); - this->question_text_.setCharacterSize(48); this->question_text_.setFillColor(this->colors_text); this->question_text_.setPosition(this->question_circle_.getPosition()); // Initialize memo text - this->memo_text_.setFont(this->font_); - this->memo_text_.setCharacterSize(16); this->memo_text_.setFillColor(this->colors_text); - this->memo_text_.setPosition(400.f, 270.f); // Position below the question circle + this->memo_text_.setPosition({400.f, 270.f}); // Position below the question circle // Initialize answer buttons constexpr float button_radius = 60.f; for (std::size_t idx = 0; idx < 4; ++idx) { this->button_shapes_[idx].setRadius(button_radius); this->button_shapes_[idx].setPointCount(100); + this->button_shapes_[idx].setOrigin({button_radius, button_radius}); this->button_shapes_[idx].setFillColor(this->colors_default_button); - this->button_shapes_[idx].setOrigin(button_radius, button_radius); - this->answer_buttons_[idx].setFont(this->font_); - this->answer_buttons_[idx].setCharacterSize(28); - this->answer_buttons_[idx].setFillColor(this->colors_text); } - this->button_shapes_[0].setPosition(250.f, 350.f); - this->button_shapes_[1].setPosition(550.f, 350.f); - this->button_shapes_[2].setPosition(250.f, 500.f); - this->button_shapes_[3].setPosition(550.f, 500.f); + this->button_shapes_[0].setPosition({250.f, 350.f}); + this->button_shapes_[1].setPosition({550.f, 350.f}); + this->button_shapes_[2].setPosition({250.f, 500.f}); + this->button_shapes_[3].setPosition({550.f, 500.f}); // Set positions of answer button texts for (std::size_t idx = 0; idx < 4; ++idx) { + this->answer_buttons_[idx].setFillColor(this->colors_text); this->answer_buttons_[idx].setPosition(this->button_shapes_[idx].getPosition()); } // Initialize percentage text - this->percentage_text_.setFont(this->font_); - this->percentage_text_.setCharacterSize(18); // Smaller font size this->percentage_text_.setFillColor(this->colors_text); - this->percentage_text_.setPosition(10.f, 10.f); // Top-left corner + this->percentage_text_.setPosition({10.f, 10.f}); // Position below the question circle // Initialize toggle buttons - constexpr float total_toggle_width = 4.f * 60.f; // 4 buttons * 60 width - const float start_x = static_cast(this->window_.getSize().x) - total_toggle_width - 10.f; // 10.f padding from the right - + constexpr float total_toggle_width = 4.f * 60.f; // 4 buttons * 60 width + const float start_x = + static_cast(this->window_.getSize().x) - total_toggle_width - 10.f; // 10.f padding from the right for (std::size_t idx = 0; idx < 4; ++idx) { - sf::RectangleShape button; - button.setSize({50.f, 35.f}); + this->toggle_buttons_[idx].setSize({50.f, 35.f}); // Set fill color based on initial state if (this->toggle_states_.at(this->toggle_categories_[idx])) { - button.setFillColor(this->colors_enabled_toggle); // Enabled state color + this->toggle_buttons_[idx].setFillColor(this->colors_enabled_toggle); // Enabled state color } else { - button.setFillColor(this->colors_disabled_toggle); // Disabled state color + this->toggle_buttons_[idx].setFillColor(this->colors_disabled_toggle); // Disabled state color } - button.setOutlineColor(this->colors_text); - button.setOutlineThickness(1.f); - button.setPosition(start_x + static_cast(idx) * 60.f, 10.f); // Positioned in the top-right corner + this->toggle_buttons_[idx].setOutlineColor(this->colors_text); + this->toggle_buttons_[idx].setOutlineThickness(1.f); + this->toggle_buttons_[idx].setPosition({start_x + static_cast(idx) * 60.f, 10.f}); // Positioned in the top-right corner - this->toggle_buttons_[idx] = button; - - sf::Text text; - text.setFont(this->font_); - text.setCharacterSize(14); - text.setFillColor(this->colors_text); - text.setString(this->toggle_labels_[idx]); + this->toggle_texts_[idx].setString(this->toggle_labels_[idx]); + this->toggle_texts_[idx].setCharacterSize(14); + this->toggle_texts_[idx].setFillColor(this->colors_text); // Center text in the button - const sf::FloatRect text_bounds = text.getLocalBounds(); - text.setOrigin(text_bounds.left + text_bounds.width / 2.0f, - text_bounds.top + text_bounds.height / 2.0f); - text.setPosition(button.getPosition() + sf::Vector2f(25.f, 17.5f)); - - this->toggle_texts_[idx] = text; + const auto tbounds = this->toggle_texts_[idx].getLocalBounds(); + const auto ox = tbounds.position.x + tbounds.size.x / 2.f; + const auto oy = tbounds.position.y + tbounds.size.y / 2.f; + this->toggle_texts_[idx].setOrigin({ox, oy}); + this->toggle_texts_[idx].setPosition( + this->toggle_buttons_[idx].getPosition() + sf::Vector2f(25.f, 17.5f)); } } @@ -214,9 +217,9 @@ class UI final { this->question_text_.setCharacterSize(72); // Increase font size for the 'X' // Center text in the question circle const sf::FloatRect text_bounds = this->question_text_.getLocalBounds(); - this->question_text_.setOrigin(text_bounds.left + text_bounds.width / 2.0f, - text_bounds.top + text_bounds.height / 2.0f); + this->question_text_.setOrigin({text_bounds.position.x + text_bounds.size.x * 0.5f, + text_bounds.position.y + text_bounds.size.y * 0.5f}); game_state = GameState::NoEntriesEnabled; // Disable answer buttons visually for (auto &button_shape : this->button_shapes_) { @@ -246,9 +249,9 @@ class UI final { this->question_text_.setCharacterSize(48); // Reset to default size this->question_text_.setString(core::string::to_sfml_string(is_hangul ? correct_entry.hangul : correct_entry.latin)); // Center text in the question circle - const sf::FloatRect text_bounds = this->question_text_.getLocalBounds(); - this->question_text_.setOrigin(text_bounds.left + text_bounds.width / 2.0f, - text_bounds.top + text_bounds.height / 2.0f); + const auto question_bounds = this->question_text_.getLocalBounds(); + this->question_text_.setOrigin({question_bounds.position.x + question_bounds.size.x * 0.5f, + question_bounds.position.y + question_bounds.size.y * 0.5f}); // Clear memo text this->memo_text_.setString(""); @@ -259,10 +262,12 @@ class UI final { this->answer_buttons_[idx].setString(core::string::to_sfml_string(is_hangul ? options[idx].latin : options[idx].hangul)); // Center text in the answer buttons - const sf::FloatRect ans_text_bounds = this->answer_buttons_[idx].getLocalBounds(); - this->answer_buttons_[idx].setOrigin(ans_text_bounds.left + ans_text_bounds.width / 2.0f, - ans_text_bounds.top + ans_text_bounds.height / 2.0f); + const auto answer_bounds = this->answer_buttons_[idx].getLocalBounds(); + const auto ox = answer_bounds.position.x + answer_bounds.size.x / 2.f; + const auto oy = answer_bounds.position.y + answer_bounds.size.y / 2.f; + // Set position to center the text within the button + this->answer_buttons_[idx].setOrigin({ox, oy}); this->answer_buttons_[idx].setPosition(this->button_shapes_[idx].getPosition()); } @@ -275,56 +280,90 @@ class UI final { // Main loop while (this->window_.isOpen()) { // Variables for event handling - const sf::Vector2i mouse_pos = sf::Mouse::getPosition(this->window_); - - sf::Event event; - while (this->window_.pollEvent(event)) { - if (event.type == sf::Event::Closed) { + while (const std::optional event = this->window_.pollEvent()) { + if (event->is()) { this->window_.close(); } // Handle toggle button clicks - if (event.type == sf::Event::MouseButtonReleased) { - for (std::size_t idx = 0; idx < this->toggle_buttons_.size(); ++idx) { - if (this->toggle_buttons_[idx].getGlobalBounds().contains(static_cast(mouse_pos.x), static_cast(mouse_pos.y))) { - // Toggle the category - const bool current_state = this->toggle_states_[this->toggle_categories_[idx]]; - this->toggle_states_[this->toggle_categories_[idx]] = !current_state; - // Update button appearance - if (this->toggle_states_[this->toggle_categories_[idx]]) { - this->toggle_buttons_[idx].setFillColor(this->colors_enabled_toggle); // Enabled state color + else if (const auto mb = event->getIf()) { + if (mb->button == sf::Mouse::Button::Left) { + const sf::Vector2f mouse_position( + static_cast(sf::Mouse::getPosition(this->window_).x), + static_cast(sf::Mouse::getPosition(this->window_).y)); + for (std::size_t idx = 0; idx < this->toggle_buttons_.size(); ++idx) { + if (this->toggle_buttons_[idx].getGlobalBounds().contains(mouse_position)) { + // Toggle the category + const bool current_state = this->toggle_states_[this->toggle_categories_[idx]]; + this->toggle_states_[this->toggle_categories_[idx]] = !current_state; + // Update button appearance + if (this->toggle_states_[this->toggle_categories_[idx]]) { + this->toggle_buttons_[idx].setFillColor(this->colors_enabled_toggle); // Enabled state color + } + else { + this->toggle_buttons_[idx].setFillColor(this->colors_disabled_toggle); // Disabled state color + } + // Reset the game + total_questions = 0; + correct_answers = 0; + update_percentage_text(); + setup_new_question(); + break; } - else { - this->toggle_buttons_[idx].setFillColor(this->colors_disabled_toggle); // Disabled state color + } + // Handle hover effect for toggle buttons + if (game_state == GameState::WaitingForAnswer) { + for (std::size_t i = 0; i < 4; ++i) { + if (this->button_shapes_[i].getGlobalBounds().contains(mouse_position)) { + ++total_questions; + if (i == correct_index) { + ++correct_answers; + this->button_shapes_[i].setFillColor(this->colors_correct_answer); + } + else { + this->button_shapes_[i].setFillColor(this->colors_selected_wrong_answer); + this->button_shapes_[correct_index].setFillColor(this->colors_correct_answer); + } + for (std::size_t j = 0; j < 4; ++j) { + if (j != i && j != correct_index) { + this->button_shapes_[j].setFillColor(this->colors_incorrect_answer); + } + } + update_percentage_text(); + this->memo_text_.setString( + core::string::to_sfml_string(correct_entry.memo)); + const auto mbounds = this->memo_text_.getLocalBounds(); + this->memo_text_.setOrigin({mbounds.position.x + mbounds.size.x * 0.5f, + mbounds.position.y + mbounds.size.y * 0.5f}); + game_state = GameState::ShowResult; + break; + } } - // Reset the game - total_questions = 0; - correct_answers = 0; - update_percentage_text(); + } + else if (game_state == GameState::ShowResult) { + this->memo_text_.setString(""); setup_new_question(); - break; } } } - - // Handle hover effect for toggle buttons - if (event.type == sf::Event::MouseMoved) { + else if (const auto mm = event->getIf()) { + const sf::Vector2f mousePos( + static_cast(mm->position.x), + static_cast(mm->position.y)); for (std::size_t idx = 0; idx < this->toggle_buttons_.size(); ++idx) { - if (this->toggle_buttons_[idx].getGlobalBounds().contains(static_cast(mouse_pos.x), static_cast(mouse_pos.y))) { + if (this->toggle_buttons_[idx].getGlobalBounds().contains(mousePos)) { this->toggle_buttons_[idx].setOutlineThickness(2.f); } else { this->toggle_buttons_[idx].setOutlineThickness(1.f); } } - } - // Game logic - if (game_state == GameState::WaitingForAnswer) { - if (event.type == sf::Event::MouseMoved) { + // Game logic + if (game_state == GameState::WaitingForAnswer) { // Handle hover effect for answer buttons for (std::size_t idx = 0; idx < 4; ++idx) { - if (this->button_shapes_[idx].getGlobalBounds().contains(static_cast(mouse_pos.x), static_cast(mouse_pos.y))) { + if (this->button_shapes_[idx].getGlobalBounds().contains(mousePos)) { this->button_shapes_[idx].setFillColor(this->colors_hover_button); } else { @@ -332,51 +371,22 @@ class UI final { } } } - else if (event.type == sf::Event::MouseButtonReleased) { - // Handle answer button clicks - for (std::size_t idx = 0; idx < 4; ++idx) { - if (this->button_shapes_[idx].getGlobalBounds().contains(static_cast(mouse_pos.x), static_cast(mouse_pos.y))) { - ++total_questions; - if (idx == correct_index) { - ++correct_answers; - this->button_shapes_[idx].setFillColor(this->colors_correct_answer); - } - else { - this->button_shapes_[idx].setFillColor(this->colors_selected_wrong_answer); - this->button_shapes_[correct_index].setFillColor(this->colors_correct_answer); - } - for (std::size_t jdx = 0; jdx < 4; ++jdx) { - if (jdx != idx && jdx != correct_index) { - this->button_shapes_[jdx].setFillColor(this->colors_incorrect_answer); - } - } - update_percentage_text(); - // Display memo text - this->memo_text_.setString(core::string::to_sfml_string(correct_entry.memo)); - // Center memo text - const sf::FloatRect memo_bounds = this->memo_text_.getLocalBounds(); - this->memo_text_.setOrigin(memo_bounds.left + memo_bounds.width / 2.0f, - memo_bounds.top + memo_bounds.height / 2.0f); - game_state = GameState::ShowResult; - break; - } - } - } - else if (event.type == sf::Event::KeyPressed) { - // Handle keyboard input - const auto key_code = event.key.code; + } + else if (const auto kp = event->getIf()) { + // Handle keyboard input + if (game_state == GameState::WaitingForAnswer) { std::size_t selected_index = std::numeric_limits::max(); - switch (key_code) { - case sf::Keyboard::Num1: + switch (kp->scancode) { + case sf::Keyboard::Scancode::Num1: selected_index = 0; break; - case sf::Keyboard::Num2: + case sf::Keyboard::Scancode::Num2: selected_index = 1; break; - case sf::Keyboard::Num3: + case sf::Keyboard::Scancode::Num3: selected_index = 2; break; - case sf::Keyboard::Num4: + case sf::Keyboard::Scancode::Num4: selected_index = 3; break; default: @@ -392,33 +402,28 @@ class UI final { this->button_shapes_[selected_index].setFillColor(this->colors_selected_wrong_answer); this->button_shapes_[correct_index].setFillColor(this->colors_correct_answer); } - for (std::size_t jdx = 0; jdx < 4; ++jdx) { - if (jdx != selected_index && jdx != correct_index) { - this->button_shapes_[jdx].setFillColor(this->colors_incorrect_answer); + for (std::size_t j = 0; j < 4; ++j) { + if (j != selected_index && j != correct_index) { + this->button_shapes_[j].setFillColor(this->colors_incorrect_answer); } } update_percentage_text(); // Display memo text this->memo_text_.setString(core::string::to_sfml_string(correct_entry.memo)); // Center memo text - const sf::FloatRect memo_bounds = this->memo_text_.getLocalBounds(); - this->memo_text_.setOrigin(memo_bounds.left + memo_bounds.width / 2.0f, - memo_bounds.top + memo_bounds.height / 2.0f); + const auto mbounds = this->memo_text_.getLocalBounds(); + this->memo_text_.setOrigin({mbounds.position.x + mbounds.size.x * 0.5f, + mbounds.position.y + mbounds.size.y * 0.5f}); game_state = GameState::ShowResult; } } - } - else if (game_state == GameState::ShowResult) { - if (event.type == sf::Event::MouseButtonReleased || event.type == sf::Event::KeyPressed) { + else if (game_state == GameState::ShowResult) { // Proceed to the next question // Hide memo text this->memo_text_.setString(""); setup_new_question(); } } - else if (game_state == GameState::NoEntriesEnabled) { - // Do nothing; wait for user to toggle categories - } } // Render @@ -474,7 +479,6 @@ class UI final { // Button colors inline static const sf::Color colors_enabled_toggle = sf::Color(100, 200, 100); // Green for enabled inline static const sf::Color colors_disabled_toggle = sf::Color(200, 100, 100); // Red for disabled - inline static const sf::Color colors_hover_toggle = sf::Color(150, 150, 150); // Gray for hover // Answer button colors inline static const sf::Color colors_default_button = sf::Color(80, 80, 80); diff --git a/src/core/assets.cpp b/src/core/assets.cpp index a49caa1..5df97d2 100644 --- a/src/core/assets.cpp +++ b/src/core/assets.cpp @@ -34,7 +34,7 @@ const sf::Font &load_font() if (!font_opt.has_value()) { sf::Font font; - if (!font.loadFromMemory(font_data, sizeof(font_data))) { + if (!font.openFromMemory(font_data, sizeof(font_data))) { throw std::runtime_error("Failed to load embedded font"); } // font.setSmooth(false);