diff --git a/cmake/yup_modules.cmake b/cmake/yup_modules.cmake index ff6215057..4ed77ec5f 100644 --- a/cmake/yup_modules.cmake +++ b/cmake/yup_modules.cmake @@ -168,6 +168,7 @@ function (_yup_module_setup_target module_name module_defines module_sources module_libs + module_link_options module_frameworks module_dependencies module_arc_enabled) @@ -208,18 +209,16 @@ function (_yup_module_setup_target module_name target_link_libraries (${module_name} INTERFACE ${module_libs} - ${module_frameworks}) - - target_link_libraries (${module_name} INTERFACE + ${module_frameworks} ${module_dependencies}) + target_link_options (${module_name} INTERFACE + ${module_link_options}) + # Add coverage support if enabled if (YUP_ENABLE_COVERAGE) _yup_setup_coverage_flags (${module_name}) endif() - - #add_library("yup::${module_name}" ALIAS ${module_name}) - endfunction() #============================================================================== @@ -257,6 +256,7 @@ function (_yup_module_setup_plugin_client target_name plugin_client_target folde get_target_property (module_defines ${plugin_client_target} YUP_MODULE_DEFINES) get_target_property (module_options ${plugin_client_target} YUP_MODULE_OPTIONS) get_target_property (module_libs ${plugin_client_target} YUP_MODULE_LIBS) + get_target_property (module_link_options ${plugin_client_target} YUP_MODULE_LINK_OPTIONS) get_target_property (module_frameworks ${plugin_client_target} YUP_MODULE_FRAMEWORK) get_target_property (module_dependencies ${plugin_client_target} YUP_MODULE_DEPENDENCIES) get_target_property (module_arc_enabled ${plugin_client_target} YUP_MODULE_ARC_ENABLED) @@ -295,6 +295,7 @@ function (_yup_module_setup_plugin_client target_name plugin_client_target folde "${module_defines}" "${module_sources}" "${module_libs}" + "${module_link_options}" "${module_frameworks}" "${module_dependencies}" "${module_arc_enabled}") @@ -333,7 +334,7 @@ function (yup_add_module module_path module_group) # ==== Assign Configurations Dynamically set (global_properties "dependencies|defines|options|searchpaths") - set (platform_properties "^(.*)Deps$|^(.*)Defines$|^(.*)Libs$|^(.*)Frameworks$|^(.*)WeakFrameworks$|^(.*)Options$|^(.*)Packages$|^(.*)Searchpaths$|^(.*)CppStandard$") + set (platform_properties "^(.*)Deps$|^(.*)Defines$|^(.*)Libs$|^(.*)Frameworks$|^(.*)WeakFrameworks$|^(.*)Options$|^(.*)LinkOptions$|^(.*)Packages$|^(.*)Searchpaths$|^(.*)CppStandard$") set (parsed_config "") foreach (module_config ${module_configs}) @@ -370,6 +371,7 @@ function (yup_add_module module_path module_group) list (APPEND module_defines ${module_iosSimDefines}) list (APPEND module_options ${module_iosSimOptions}) list (APPEND module_libs ${module_iosSimLibs}) + list (APPEND module_link_options ${module_iosSimLinkOptions}) _yup_resolve_variable_paths ("${module_iosSimSearchpaths}" module_iosSimSearchpaths) list (APPEND module_searchpaths ${module_iosSimSearchpaths}) _yup_module_prepare_frameworks ("${module_iosSimFrameworks}" "${module_iosSimWeakFrameworks}" module_iosSimframeworks) @@ -382,6 +384,7 @@ function (yup_add_module module_path module_group) list (APPEND module_defines ${module_iosDefines}) list (APPEND module_options ${module_iosOptions}) list (APPEND module_libs ${module_iosLibs}) + list (APPEND module_link_options ${module_iosLinkOptions}) _yup_resolve_variable_paths ("${module_iosSearchpaths}" module_iosSearchpaths) list (APPEND module_searchpaths ${module_iosSearchpaths}) _yup_module_prepare_frameworks ("${module_iosFrameworks}" "${module_iosWeakFrameworks}" module_iosFrameworks) @@ -395,6 +398,7 @@ function (yup_add_module module_path module_group) list (APPEND module_defines ${module_appleDefines}) list (APPEND module_options ${module_appleOptions}) list (APPEND module_libs ${module_appleLibs}) + list (APPEND module_link_options ${module_appleLinkOptions}) _yup_resolve_variable_paths ("${module_appleSearchpaths}" module_appleSearchpaths) list (APPEND module_searchpaths ${module_appleSearchpaths}) _yup_module_prepare_frameworks ("${module_appleFrameworks}" "${module_appleWeakFrameworks}" module_appleFrameworks) @@ -411,7 +415,9 @@ function (yup_add_module module_path module_group) list (APPEND module_options ${module_osxOptions}) list (APPEND module_options ${module_appleOptions}) list (APPEND module_libs ${module_osxLibs}) + list (APPEND module_link_options ${module_osxLinkOptions}) list (APPEND module_libs ${module_appleLibs}) + list (APPEND module_link_options ${module_appleLinkOptions}) _yup_resolve_variable_paths ("${module_osxSearchpaths}" module_osxSearchpaths) list (APPEND module_searchpaths ${module_osxSearchpaths}) _yup_resolve_variable_paths ("${module_appleSearchpaths}" module_appleSearchpaths) @@ -432,6 +438,7 @@ function (yup_add_module module_path module_group) list (APPEND module_defines ${module_linuxDefines}) list (APPEND module_options ${module_linuxOptions}) list (APPEND module_libs ${module_linuxLibs}) + list (APPEND module_link_options ${module_linuxLinkOptions}) _yup_resolve_variable_paths ("${module_linuxSearchpaths}" module_linuxSearchpaths) list (APPEND module_searchpaths ${module_linuxSearchpaths}) foreach (package ${module_linuxPackages}) @@ -447,6 +454,7 @@ function (yup_add_module module_path module_group) list (APPEND module_defines ${module_wasmDefines}) list (APPEND module_options ${module_wasmOptions}) list (APPEND module_libs ${module_wasmLibs}) + list (APPEND module_link_options ${module_wasmLinkOptions}) _yup_resolve_variable_paths ("${module_wasmSearchpaths}" module_wasmSearchpaths) list (APPEND module_searchpaths ${module_wasmSearchpaths}) @@ -458,6 +466,7 @@ function (yup_add_module module_path module_group) list (APPEND module_defines ${module_androidDefines}) list (APPEND module_options ${module_androidOptions}) list (APPEND module_libs ${module_androidLibs}) + list (APPEND module_link_options ${module_androidLinkOptions}) _yup_resolve_variable_paths ("${module_androidSearchpaths}" module_androidSearchpaths) list (APPEND module_searchpaths ${module_androidSearchpaths}) @@ -465,19 +474,33 @@ function (yup_add_module module_path module_group) if (module_msftCppStandard) set (module_cpp_standard "${module_msftCppStandard}") endif() - list (APPEND module_dependencies ${module_windowsDeps}) - list (APPEND module_defines ${module_windowsDefines}) - list (APPEND module_options ${module_windowsOptions}) - _yup_resolve_variable_paths ("${module_windowsSearchpaths}" module_windowsSearchpaths) - list (APPEND module_searchpaths ${module_windowsSearchpaths}) + list (APPEND module_libs ${module_msftLibs}) + list (APPEND module_dependencies ${module_msftDeps}) + list (APPEND module_defines ${module_msftDefines}) + list (APPEND module_options ${module_msftOptions}) + list (APPEND module_link_options ${module_msftLinkOptions}) + _yup_resolve_variable_paths ("${module_msftSearchpaths}" module_msftSearchpaths) + list (APPEND module_searchpaths ${module_msftSearchpaths}) if (MINGW) list (APPEND module_libs ${module_mingwLibs}) + list (APPEND module_dependencies ${module_mingwDeps}) + list (APPEND module_defines ${module_mingwDefines}) + list (APPEND module_options ${module_mingwOptions}) + list (APPEND module_link_options ${module_mingwLinkOptions}) + _yup_resolve_variable_paths ("${module_mingwSearchpaths}" module_mingwSearchpaths) + list (APPEND module_searchpaths ${module_mingwSearchpaths}) if (module_mingwCppStandard) set (module_cpp_standard "${module_mingwCppStandard}") endif() else() list (APPEND module_libs ${module_windowsLibs}) + list (APPEND module_dependencies ${module_windowsDeps}) + list (APPEND module_defines ${module_windowsDefines}) + list (APPEND module_options ${module_windowsOptions}) + list (APPEND module_link_options ${module_windowsLinkOptions}) + _yup_resolve_variable_paths ("${module_windowsSearchpaths}" module_windowsSearchpaths) + list (APPEND module_searchpaths ${module_windowsSearchpaths}) if (module_windowsCppStandard) set (module_cpp_standard "${module_windowsCppStandard}") endif() @@ -500,13 +523,14 @@ function (yup_add_module module_path module_group) _yup_module_collect_sources ("${module_path}" module_sources) # ==== Setup module sources and properties - _yup_module_setup_target (${module_name} + _yup_module_setup_target ("${module_name}" "${module_cpp_standard}" "${module_include_paths}" "${module_options}" "${module_defines}" "${module_sources}" "${module_libs}" + "${module_link_options}" "${module_frameworks}" "${module_dependencies}" "${module_arc_enabled}") @@ -531,6 +555,7 @@ function (yup_add_module module_path module_group) YUP_MODULE_DEFINES "${module_defines}" YUP_MODULE_SOURCES "${module_sources}" YUP_MODULE_LIBS "${module_libs}" + YUP_MODULE_LINK_OPTIONS "${module_link_options}" YUP_MODULE_FRAMEWORK "${module_frameworks}" YUP_MODULE_DEPENDENCIES "${module_dependencies}" YUP_MODULE_ARC_ENABLED "${module_arc_enabled}") @@ -562,11 +587,26 @@ function (_yup_add_default_modules modules_path) # Yup modules set (modules_group "Modules") yup_add_module (${modules_path}/modules/yup_core ${modules_group}) + add_library (yup::yup_core ALIAS yup_core) + yup_add_module (${modules_path}/modules/yup_events ${modules_group}) + add_library (yup::yup_events ALIAS yup_events) + yup_add_module (${modules_path}/modules/yup_audio_basics ${modules_group}) + add_library (yup::yup_audio_basics ALIAS yup_audio_basics) + yup_add_module (${modules_path}/modules/yup_audio_devices ${modules_group}) + add_library (yup::yup_audio_devices ALIAS yup_audio_devices) + yup_add_module (${modules_path}/modules/yup_audio_processors ${modules_group}) + add_library (yup::yup_audio_processors ALIAS yup_audio_processors) + yup_add_module (${modules_path}/modules/yup_audio_plugin_client ${modules_group}) + add_library (yup::yup_audio_plugin_client ALIAS yup_audio_plugin_client) + yup_add_module (${modules_path}/modules/yup_graphics ${modules_group}) + add_library (yup::yup_graphics ALIAS yup_graphics) + yup_add_module (${modules_path}/modules/yup_gui ${modules_group}) + add_library (yup::yup_gui ALIAS yup_gui) endfunction() diff --git a/cmake/yup_standalone.cmake b/cmake/yup_standalone.cmake index e7fe3d679..9eb19649e 100644 --- a/cmake/yup_standalone.cmake +++ b/cmake/yup_standalone.cmake @@ -145,9 +145,6 @@ function (yup_standalone_app) elseif (YUP_PLATFORM_EMSCRIPTEN) if (NOT "${target_console}") set_target_properties (${target_name} PROPERTIES SUFFIX ".html") - - list (APPEND additional_options -sUSE_SDL=2) - list (APPEND additional_link_options -sUSE_SDL=2 -sMAX_WEBGL_VERSION=2) endif() _yup_set_default (YUP_ARG_CUSTOM_SHELL "${CMAKE_SOURCE_DIR}/cmake/platforms/${YUP_PLATFORM}/shell.html") diff --git a/cmake/yup_utilities.cmake b/cmake/yup_utilities.cmake index 43480578e..1e0fac825 100644 --- a/cmake/yup_utilities.cmake +++ b/cmake/yup_utilities.cmake @@ -291,7 +291,7 @@ endfunction() #============================================================================== -function (_yup_setup_coverage_targets modules_list) +function (_yup_setup_coverage_targets test_target_name modules_list) if (YUP_ENABLE_COVERAGE AND (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")) find_program (LCOV_PATH lcov) find_program (GENHTML_PATH genhtml) @@ -316,16 +316,26 @@ function (_yup_setup_coverage_targets modules_list) add_custom_target (coverage_${module_name} COMMAND ${CMAKE_COMMAND} -E echo "Generating coverage for ${module_name}" - COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --capture --output-file ${module_coverage_dir}/coverage.info --ignore-errors mismatch,gcov,source,negative - COMMAND ${LCOV_PATH} --extract ${module_coverage_dir}/coverage.info "*/modules/${module_name}/*" --output-file ${module_coverage_dir}/coverage_filtered.info --ignore-errors mismatch,gcov,source,negative - COMMAND ${LCOV_PATH} --remove ${module_coverage_dir}/coverage_filtered.info "*/thirdparty/*" "*/build/*" "*/tests/*" --output-file ${module_coverage_dir}/coverage_final.info --ignore-errors mismatch,gcov,source,negative + COMMAND ${LCOV_PATH} + --directory ${CMAKE_BINARY_DIR} --capture + --output-file ${module_coverage_dir}/coverage.info + --ignore-errors mismatch,gcov,source,negative + COMMAND ${LCOV_PATH} + --extract ${module_coverage_dir}/coverage.info "*/modules/${module_name}/*" + --output-file ${module_coverage_dir}/coverage_filtered.info + --ignore-errors mismatch,gcov,source,negative + COMMAND ${LCOV_PATH} + --remove ${module_coverage_dir}/coverage_filtered.info "*/thirdparty/*" "*/build/*" "*/tests/*" "*/examples/*" + --output-file ${module_coverage_dir}/coverage_final.info + --ignore-errors mismatch,gcov,source,negative WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - DEPENDS yup_tests + DEPENDS ${test_target_name} COMMENT "Processing coverage data for ${module_name}") if (GENHTML_PATH) add_custom_target (coverage_html_${module_name} - COMMAND ${GENHTML_PATH} ${module_coverage_dir}/coverage_final.info --output-directory ${module_coverage_dir}/html + COMMAND ${GENHTML_PATH} + ${module_coverage_dir}/coverage_final.info --output-directory ${module_coverage_dir}/html DEPENDS coverage_${module_name} COMMENT "Generating HTML coverage report for ${module_name}") endif() @@ -334,15 +344,22 @@ function (_yup_setup_coverage_targets modules_list) # Combined coverage target add_custom_target (coverage_all COMMAND ${CMAKE_COMMAND} -E echo "Generating combined coverage report" - COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --capture --output-file ${CMAKE_BINARY_DIR}/coverage/coverage.info --ignore-errors mismatch,gcov,source,negative - COMMAND ${LCOV_PATH} --remove ${CMAKE_BINARY_DIR}/coverage/coverage.info "*/thirdparty/*" "*/build/*" "*/tests/*" "*/examples/*" --output-file ${CMAKE_BINARY_DIR}/coverage/coverage_final.info --ignore-errors mismatch,gcov,source,negative + COMMAND ${LCOV_PATH} + --directory ${CMAKE_BINARY_DIR} --capture + --output-file ${CMAKE_BINARY_DIR}/coverage/coverage.info + --ignore-errors mismatch,gcov,source,negative + COMMAND ${LCOV_PATH} + --remove ${CMAKE_BINARY_DIR}/coverage/coverage.info "*/thirdparty/*" "*/build/*" "*/tests/*" "*/examples/*" + --output-file ${CMAKE_BINARY_DIR}/coverage/coverage_final.info + --ignore-errors mismatch,gcov,source,negative WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - DEPENDS yup_tests + DEPENDS ${test_target_name} COMMENT "Processing combined coverage data") if (GENHTML_PATH) add_custom_target (coverage_html_all - COMMAND ${GENHTML_PATH} ${CMAKE_BINARY_DIR}/coverage/coverage_final.info --output-directory ${CMAKE_BINARY_DIR}/coverage/html + COMMAND ${GENHTML_PATH} + ${CMAKE_BINARY_DIR}/coverage/coverage_final.info --output-directory ${CMAKE_BINARY_DIR}/coverage/html DEPENDS coverage_all COMMENT "Generating combined HTML coverage report") endif() diff --git a/docs/Building Plugins.md b/docs/Building Plugins.md index 1ce1804ac..3c7e81c4f 100644 --- a/docs/Building Plugins.md +++ b/docs/Building Plugins.md @@ -137,7 +137,9 @@ yup_audio_plugin ( PLUGIN_CREATE_CLAP ON PLUGIN_CREATE_VST3 ON PLUGIN_CREATE_STANDALONE ON - MODULES yup_gui yup_audio_processors) + MODULES + yup::yup_gui + yup::yup_audio_processors) # Add source files file (GLOB sources "${CMAKE_CURRENT_LIST_DIR}/*.cpp") diff --git a/docs/YUP Module Format.md b/docs/YUP Module Format.md index 60955ae06..df94685d9 100644 --- a/docs/YUP Module Format.md +++ b/docs/YUP Module Format.md @@ -45,13 +45,13 @@ The names of these source files must begin with the name of the module, but they In order to specify that a source file should only be compiled for a specific platform, then the filename can be suffixed with one of the following (case insensitive) strings: - _apple <- compiled for Apple platforms only - _mac or _osx <- compiled for macOS only + _apple <- compiled for Apple platforms + _mac <- compiled for macOS only _ios <- compiled for iOS only _msft <- compiled for Microsoft platforms only - _win32 <- compiled for Windows Win32 Desktop only _uwp <- compiled for Universal Windows Platform only - _windows <- compiled for Windows platforms only + _windows <- compiled for Windows desktop only (MSVC) + _mingw <- compiled for Windows desktop only (MinGW) _linux <- compiled for Linux and FreeBSD only _android <- compiled for Android only _posix <- compiled for Posix platforms only @@ -62,7 +62,7 @@ e.g. yup_mymodule/yup_mymodule_1.cpp <- compiled for all platforms yup_mymodule/yup_mymodule_2.cpp <- compiled for all platforms yup_mymodule/yup_mymodule_mac.cpp <- compiled for macOS platforms only - yup_mymodule/yup_mymodule_win32.cpp <- compiled for Windows platforms only + yup_mymodule/yup_mymodule_windows.cpp <- compiled for Windows platforms only Often this isn't necessary, as in most cases you can easily add checks inside the files to do different things depending on the platform, but this may be handy just to avoid clutter in user projects where files aren't needed. @@ -130,7 +130,7 @@ Possible values: - (Optional) A description of the type of software license that applies. - minimumCppStandard - - (Optional) A number indicating the minimum C++ language standard that is required for this module This must be just the standard number with no prefix e.g. 20 for C++20. + - (Optional) A number indicating the minimum C++ language standard that is required for this module. This must be just the standard number with no prefix e.g. 20 for C++20. - defines - (Optional) A list (space or comma-separated) of macro defines needed by this module. @@ -138,6 +138,9 @@ Possible values: - searchpaths - (Optional) A space-separated list of internal include paths, relative to the module's parent folder, which need to be added to a project's header search path. +- [android|apple|ios|linux|mingw|mobile|msft|osx|wasm|win32|windows]CppStandard + - (Optional) A number indicating the minimum C++ language standard that is required for this module and this platform exclusively. This must be just the standard number with no prefix e.g. 20 for C++20. + - [android|apple|ios|linux|mingw|mobile|msft|osx|wasm|win32|windows]Deps - (Optional) A list (space or comma-separated) of other modules that are required by this one. @@ -150,6 +153,9 @@ Possible values: - [android|apple|ios|linux|mingw|mobile|msft|osx|wasm|win32|windows]Options - (Optional) A list (space or comma-separated) of compile options needed by this module in a build. +- [android|apple|ios|linux|mingw|mobile|msft|osx|wasm|win32|windows]LinkOptions + - (Optional) A list (space or comma-separated) of link options needed by this module in a build. + - [android|apple|ios|linux|mingw|mobile|msft|osx|wasm|win32|windows]Searchpaths - (Optional) A space-separated list of internal include paths, relative to the module's parent folder, which need to be added to a project's header search path. diff --git a/examples/app/CMakeLists.txt b/examples/app/CMakeLists.txt index 4cfa8feb9..75a7c4823 100644 --- a/examples/app/CMakeLists.txt +++ b/examples/app/CMakeLists.txt @@ -40,13 +40,13 @@ yup_standalone_app ( TARGET_APP_ID "org.yup.${target_name}" TARGET_APP_NAMESPACE "org.yup" MODULES - yup_core - yup_audio_basics - yup_audio_devices - yup_events - yup_graphics - yup_gui - yup_audio_processors) + yup::yup_core + yup::yup_audio_basics + yup::yup_audio_devices + yup::yup_events + yup::yup_graphics + yup::yup_gui + yup::yup_audio_processors) # ==== Prepare sources if (NOT YUP_TARGET_ANDROID) diff --git a/examples/console/CMakeLists.txt b/examples/console/CMakeLists.txt index 75717a42e..9a32b1cf3 100644 --- a/examples/console/CMakeLists.txt +++ b/examples/console/CMakeLists.txt @@ -41,10 +41,10 @@ yup_standalone_app ( TARGET_APP_NAMESPACE "org.yup" TARGET_CONSOLE ON MODULES - yup_core - yup_audio_basics - yup_audio_devices - yup_events) + yup::yup_core + yup::yup_audio_basics + yup::yup_audio_devices + yup::yup_events) # ==== Prepare sources if (NOT YUP_TARGET_ANDROID) diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt index 09a1b17b2..d2abf8a77 100644 --- a/examples/graphics/CMakeLists.txt +++ b/examples/graphics/CMakeLists.txt @@ -54,9 +54,9 @@ yup_standalone_app ( PRELOAD_FILES "${CMAKE_CURRENT_LIST_DIR}/data/RobotoFlex-VariableFont.ttf@data/RobotoFlex-VariableFont.ttf" MODULES - yup_audio_devices - yup_gui - yup_audio_processors + yup::yup_audio_devices + yup::yup_gui + yup::yup_audio_processors libpng ${link_libraries}) diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index 99d26486f..afca4e9bc 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -104,28 +104,56 @@ class Oscilloscope : public yup::Component { auto bounds = getLocalBounds(); - g.setFillColor (0xff101010); + auto backgroundColor = yup::Color (0xff101010); + g.setFillColor (backgroundColor); g.fillAll(); - g.setStrokeColor (0xff4b4bff); - g.setStrokeWidth (1.0f); - g.strokeRect (bounds); - + auto lineColor = yup::Color (0xff4b4bff); if (renderData.empty()) return; float xSize = getWidth() / float (renderData.size()); + float centerY = getHeight() * 0.5f; + // Build the main waveform path path.clear(); path.reserveSpace ((int) renderData.size()); - path.moveTo (0.0f, (renderData[0] + 1.0f) * 0.5f * getHeight()); for (std::size_t i = 1; i < renderData.size(); ++i) path.lineTo (i * xSize, (renderData[i] + 1.0f) * 0.5f * getHeight()); + // Outermost glow layer + g.setStrokeColor (lineColor.withAlpha (0.1f)); + g.setStrokeWidth (12.0f); + g.setStrokeCap (yup::StrokeCap::Round); + g.setStrokeJoin (yup::StrokeJoin::Round); + g.strokePath (path); + + // Second glow layer + g.setStrokeColor (lineColor.withAlpha (0.2f)); + g.setStrokeWidth (8.0f); + g.strokePath (path); + + // Third glow layer + g.setStrokeColor (lineColor.withAlpha (0.4f)); + g.setStrokeWidth (5.0f); + g.strokePath (path); + + // Main stroke + g.setStrokeColor (lineColor.withAlpha (0.8f)); + g.setStrokeWidth (2.5f); + g.strokePath (path); + + // Bright center line + g.setStrokeColor (lineColor.brighter (0.3f)); g.setStrokeWidth (1.0f); g.strokePath (path); + + // Ultra-bright core + g.setStrokeColor (yup::Colors::white.withAlpha (0.9f)); + g.setStrokeWidth (0.3f); + g.strokePath (path); } private: diff --git a/examples/graphics/source/examples/LayoutFonts.h b/examples/graphics/source/examples/LayoutFonts.h index a5dcab25d..7c070c59e 100644 --- a/examples/graphics/source/examples/LayoutFonts.h +++ b/examples/graphics/source/examples/LayoutFonts.h @@ -53,14 +53,15 @@ class LayoutFontsExample : public yup::Component const int numTexts = yup::numElementsInArray (text); for (int i = 0; i < numTexts; ++i) { - auto labelBounds = text[i].bounds; + const auto& textInstance = text[i]; + auto labelBounds = textInstance.bounds; g.setFillColor (0xffffffff); g.setFeather (10.0f); - g.fillFittedText (text[i].styledText, labelBounds); + g.fillFittedText (textInstance.styledText, labelBounds); g.setFeather (0.0f); - g.fillFittedText (text[i].styledText, labelBounds); + g.fillFittedText (textInstance.styledText, labelBounds); /* g.setStrokeColor (yup::Colors::green); @@ -73,10 +74,12 @@ class LayoutFontsExample : public yup::Component g.strokeLine (labelBounds.getX(), labelBounds.getBottom(), labelBounds.getRight(), labelBounds.getBottom()); */ - auto offset = text[i].styledText.getOffset (labelBounds); - g.setStrokeColor (yup::Colors::magenta); - const auto& lines = text[i].styledText.getOrderedLines(); + + const auto textHeight = textInstance.styledText.getComputedTextBounds().getHeight(); + + const auto offset = textInstance.styledText.getOffset (labelBounds); + const auto& lines = textInstance.styledText.getOrderedLines(); for (const auto& line : lines) { for (auto [glyph, _] : line) @@ -87,9 +90,7 @@ class LayoutFontsExample : public yup::Component labelBounds.getX() + offset.getX() + xPos, labelBounds.getTop() + offset.getY(), labelBounds.getX() + offset.getX() + xPos, - labelBounds.getTop() + offset.getY() + text[i].styledText.getComputedTextBounds().getHeight()); - - //accum += advances; // / g.getContextScale(); + labelBounds.getTop() + offset.getY() + textHeight); } } } @@ -113,16 +114,16 @@ class LayoutFontsExample : public yup::Component { bounds = newBounds; - styledText.setMaxSize (newBounds.getSize()); - styledText.setHorizontalAlign (hAlign); - styledText.setVerticalAlign (vAlign); - styledText.setParagraphSpacing (0.0f); - styledText.setOverflow (overflow); - styledText.setWrap (wrap); + auto modifier = styledText.startUpdate(); + modifier.setMaxSize (newBounds.getSize()); + modifier.setHorizontalAlign (hAlign); + modifier.setVerticalAlign (vAlign); + modifier.setParagraphSpacing (0.0f); + modifier.setOverflow (overflow); + modifier.setWrap (wrap); - styledText.clear(); - styledText.appendText (text, nullptr, font.getFont(), fontSize); - styledText.update(); + modifier.clear(); + modifier.appendText (text, nullptr, font.getFont(), fontSize); } }; diff --git a/examples/graphics/source/examples/TextEditor.h b/examples/graphics/source/examples/TextEditor.h new file mode 100644 index 000000000..4a1343feb --- /dev/null +++ b/examples/graphics/source/examples/TextEditor.h @@ -0,0 +1,185 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +class TextEditorDemo : public yup::Component +{ +public: + TextEditorDemo() + : Component ("TextEditorDemo") + { + // Create the editors + singleLineEditor = std::make_unique ("singleLineEditor"); + multiLineEditor = std::make_unique ("multiLineEditor"); + readOnlyEditor = std::make_unique ("readOnlyEditor"); + focused = std::make_unique ("focused"); + + // Configure the editors + singleLineEditor->setText ("Single line editor"); + singleLineEditor->setMultiLine (false); + + multiLineEditor->setText ("Multi-line editor\nSupports multiple lines\nTry typing here!"); + multiLineEditor->setMultiLine (true); + + readOnlyEditor->setText ("This is read-only text that cannot be edited"); + readOnlyEditor->setReadOnly (true); + + // Create buttons with componentID as text + selectAllButton = std::make_unique ("Select All"); + selectAllButton->onClick = [this]() + { + if (auto* activeEditor = getActiveEditor()) + activeEditor->selectAll(); + }; + + copyButton = std::make_unique ("Copy"); + copyButton->onClick = [this]() + { + if (auto* activeEditor = getActiveEditor()) + activeEditor->copy(); + }; + + pasteButton = std::make_unique ("Paste"); + pasteButton->onClick = [this]() + { + if (auto* activeEditor = getActiveEditor()) + activeEditor->paste(); + }; + + clearButton = std::make_unique ("Clear"); + clearButton->onClick = [this]() + { + if (auto* activeEditor = getActiveEditor()) + activeEditor->setText (""); + }; + + focused->setText (""); + + // Create labels + titleLabel = std::make_unique ("titleLabel"); + singleLineLabel = std::make_unique ("singleLineLabel"); + multiLineLabel = std::make_unique ("multiLineLabel"); + readOnlyLabel = std::make_unique ("readOnlyLabel"); + + titleLabel->setText ("TextEditor Widget Example"); + singleLineLabel->setText ("Single Line Editor:"); + multiLineLabel->setText ("Multi Line Editor:"); + readOnlyLabel->setText ("Read Only Editor:"); + + // Add all components + addAndMakeVisible (*titleLabel); + addAndMakeVisible (*singleLineLabel); + addAndMakeVisible (*singleLineEditor); + addAndMakeVisible (*multiLineLabel); + addAndMakeVisible (*multiLineEditor); + addAndMakeVisible (*readOnlyLabel); + addAndMakeVisible (*readOnlyEditor); + addAndMakeVisible (*selectAllButton); + addAndMakeVisible (*copyButton); + addAndMakeVisible (*pasteButton); + addAndMakeVisible (*clearButton); + addAndMakeVisible (*focused); + + setSize ({ 800, 600 }); + } + + void paint (yup::Graphics& g) override + { + // Header separator + g.setStrokeColor (yup::Colors::darkgray); + g.setStrokeWidth (2.0f); + g.strokeLine (10.0f, 60.0f, getWidth() - 10.0f, 60.0f); + } + + void resized() override + { + auto area = getLocalBounds().reduced (20); + + // Title + titleLabel->setBounds (area.removeFromTop (40)); + + area.removeFromTop (10); // Spacer + + // Single line editor + singleLineLabel->setBounds (area.removeFromTop (25)); + singleLineEditor->setBounds (area.removeFromTop (26)); + + area.removeFromTop (15); // Spacer + + // Multi line editor + multiLineLabel->setBounds (area.removeFromTop (25)); + multiLineEditor->setBounds (area.removeFromTop (26 * 5)); + + area.removeFromTop (15); // Spacer + + // Read only editor + readOnlyLabel->setBounds (area.removeFromTop (25)); + readOnlyEditor->setBounds (area.removeFromTop (26)); + + area.removeFromTop (20); // Spacer + + // Buttons + auto buttonArea = area.removeFromTop (40); + auto buttonWidth = buttonArea.getWidth() / 4 - 10; + + selectAllButton->setBounds (buttonArea.removeFromLeft (buttonWidth)); + buttonArea.removeFromLeft (10); + copyButton->setBounds (buttonArea.removeFromLeft (buttonWidth)); + buttonArea.removeFromLeft (10); + pasteButton->setBounds (buttonArea.removeFromLeft (buttonWidth)); + buttonArea.removeFromLeft (10); + clearButton->setBounds (buttonArea); + + area.removeFromTop (10); // Spacer + + // Hidden focused editor for testing + focused->setBounds (area.removeFromTop (26)); + } + +private: + yup::TextEditor* getActiveEditor() + { + if (singleLineEditor->hasKeyboardFocus()) + return singleLineEditor.get(); + + if (multiLineEditor->hasKeyboardFocus()) + return multiLineEditor.get(); + + if (readOnlyEditor->hasKeyboardFocus()) + return readOnlyEditor.get(); + + return nullptr; + } + + std::unique_ptr singleLineEditor; + std::unique_ptr multiLineEditor; + std::unique_ptr readOnlyEditor; + std::unique_ptr focused; + + std::unique_ptr selectAllButton; + std::unique_ptr copyButton; + std::unique_ptr pasteButton; + std::unique_ptr clearButton; + + std::unique_ptr titleLabel; + std::unique_ptr singleLineLabel; + std::unique_ptr multiLineLabel; + std::unique_ptr readOnlyLabel; +}; diff --git a/examples/graphics/source/examples/VariableFonts.h b/examples/graphics/source/examples/VariableFonts.h index 0ccaebbcd..d54ce3f02 100644 --- a/examples/graphics/source/examples/VariableFonts.h +++ b/examples/graphics/source/examples/VariableFonts.h @@ -84,14 +84,16 @@ class VariableFontsExample : public yup::Component textBounds = bounds.removeFromBottom (proportionOfHeight (0.5f)).reduced (10); - styledText.setMaxSize (textBounds.getSize()); - styledText.setHorizontalAlign (yup::StyledText::justified); - styledText.setVerticalAlign (yup::StyledText::middle); - styledText.setOverflow (yup::StyledText::visible); - styledText.setWrap (yup::StyledText::wrap); - styledText.clear(); - styledText.appendText (text, nullptr, font.getFont(), fontSize); - styledText.update(); + { + auto modifier = styledText.startUpdate(); + modifier.setMaxSize (textBounds.getSize()); + modifier.setHorizontalAlign (yup::StyledText::justified); + modifier.setVerticalAlign (yup::StyledText::middle); + modifier.setOverflow (yup::StyledText::visible); + modifier.setWrap (yup::StyledText::wrap); + modifier.clear(); + modifier.appendText (text, font.getFont(), fontSize); + } bounds = bounds.reduced (10); diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 9df0f87e4..8ed2a00ab 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -31,6 +31,7 @@ #include "examples/Audio.h" #include "examples/LayoutFonts.h" #include "examples/VariableFonts.h" +#include "examples/TextEditor.h" #include "examples/Paths.h" //============================================================================== @@ -73,33 +74,73 @@ class CustomWindow } */ - // Add the demos - int demo = 1; - - if (demo == 0) { + auto button = std::make_unique ("Audio"); + button->onClick = [this] + { + selectComponent (0); + }; + addAndMakeVisible (button.get()); + buttons.add (std::move (button)); + components.add (std::make_unique (font)); - addAndMakeVisible (components.getLast()); + addChildComponent (components.getLast()); } - if (demo == 1) { + auto button = std::make_unique ("Layout Fonts"); + button->onClick = [this] + { + selectComponent (1); + }; + addAndMakeVisible (button.get()); + buttons.add (std::move (button)); + components.add (std::make_unique (font)); - addAndMakeVisible (components.getLast()); + addChildComponent (components.getLast()); } - if (demo == 2) { + auto button = std::make_unique ("Variable Fonts"); + button->onClick = [this] + { + selectComponent (2); + }; + addAndMakeVisible (button.get()); + buttons.add (std::move (button)); + components.add (std::make_unique (font)); - addAndMakeVisible (components.getLast()); + addChildComponent (components.getLast()); } - if (demo == 3) { + auto button = std::make_unique ("Paths"); + button->onClick = [this] + { + selectComponent (3); + }; + addAndMakeVisible (button.get()); + buttons.add (std::move (button)); + components.add (std::make_unique()); - addAndMakeVisible (components.getLast()); + addChildComponent (components.getLast()); } + { + auto button = std::make_unique ("Text Editor"); + button->onClick = [this] + { + selectComponent (4); + }; + addAndMakeVisible (button.get()); + buttons.add (std::move (button)); + + components.add (std::make_unique()); + addChildComponent (components.getLast()); + } + + selectComponent (0); + // Timer startTimerHz (10); } @@ -110,8 +151,22 @@ class CustomWindow void resized() override { + constexpr auto margin = 5; + + auto bounds = getLocalBounds().reduced (margin); + auto buttonBounds = bounds.removeFromTop (30); + + const auto totalMargin = margin * (buttons.size() - 1); + const auto buttonWidth = (buttonBounds.getWidth() - totalMargin) / buttons.size(); + for (auto& button : buttons) + { + button->setBounds (buttonBounds.removeFromLeft (buttonWidth)); + buttonBounds.removeFromLeft (margin); + } + + bounds.removeFromTop (margin); for (auto& component : components) - component->setBounds (getLocalBounds()); + component->setBounds (bounds); } void paint (yup::Graphics& g) override @@ -169,6 +224,14 @@ class CustomWindow yup::YUPApplication::getInstance()->systemRequestedQuit(); } + void selectComponent (int index) + { + for (auto& component : components) + component->setVisible (false); + + components[index]->setVisible (true); + } + private: void updateWindowTitle() { @@ -187,6 +250,7 @@ class CustomWindow setTitle (title); } + yup::OwnedArray buttons; yup::OwnedArray components; yup::Font font; diff --git a/examples/plugin/CMakeLists.txt b/examples/plugin/CMakeLists.txt index 8ad2c955c..27f8da425 100644 --- a/examples/plugin/CMakeLists.txt +++ b/examples/plugin/CMakeLists.txt @@ -45,8 +45,8 @@ yup_audio_plugin ( PLUGIN_CREATE_VST3 ON PLUGIN_CREATE_STANDALONE ON MODULES - yup_gui - yup_audio_processors) + yup::yup_gui + yup::yup_audio_processors) # ==== Prepare sources file (GLOB_RECURSE sources @@ -54,4 +54,3 @@ file (GLOB_RECURSE sources "${CMAKE_CURRENT_LIST_DIR}/source/*.h") source_group (TREE ${CMAKE_CURRENT_LIST_DIR}/ FILES ${sources}) target_sources (${target_name}_shared PUBLIC ${sources}) - diff --git a/examples/render/CMakeLists.txt b/examples/render/CMakeLists.txt index 0ec087ba4..0a0232c57 100644 --- a/examples/render/CMakeLists.txt +++ b/examples/render/CMakeLists.txt @@ -58,13 +58,13 @@ yup_standalone_app ( PRELOAD_FILES "${CMAKE_CURRENT_LIST_DIR}/${rive_file}@data/artboard.riv" MODULES - yup_core - yup_audio_basics - yup_audio_devices - yup_events - yup_graphics - yup_gui - yup_audio_processors + yup::yup_core + yup::yup_audio_basics + yup::yup_audio_devices + yup::yup_events + yup::yup_graphics + yup::yup_gui + yup::yup_audio_processors libpng libwebp ${link_libraries}) diff --git a/guidelines.md b/guidelines.md index 828c54476..2922e1471 100644 --- a/guidelines.md +++ b/guidelines.md @@ -144,7 +144,7 @@ while (condition) using namespace yup; // Prefer limited scope usage -TEST(MyClassTests, someFunction) +TEST (MyClassTests, someFunction) { using namespace std::chrono; // use chrono types without std::chrono:: prefix @@ -189,11 +189,11 @@ public: ~ClassName(); // Copy/move constructors if needed - ClassName(const ClassName& other) = delete; - ClassName& operator=(const ClassName& other) = delete; + ClassName (const ClassName& other) = delete; + ClassName& operator= (const ClassName& other) = delete; // Public interface - void doSomething(); + void doSomething (int arg); int getValue() const; bool isValid() const; @@ -220,7 +220,7 @@ public: private: int memberVar; - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(YupStyleClass) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (YupStyleClass) }; ``` @@ -263,16 +263,16 @@ protected: ClassName instance; }; -TEST_F(ClassNameTests, ConstructorInitializesCorrectly) +TEST_F (ClassNameTests, ConstructorInitializesCorrectly) { - EXPECT_TRUE(instance.isValid()); - EXPECT_EQ(0, instance.getValue()); + EXPECT_TRUE (instance.isValid()); + EXPECT_EQ (0, instance.getValue()); } -TEST(ClassNameTests, StaticMethodBehavesCorrectly) +TEST (ClassNameTests, StaticMethodBehavesCorrectly) { auto result = ClassName::staticMethod(); - EXPECT_NE(nullptr, result.get()); + EXPECT_NE (nullptr, result.get()); } ``` @@ -295,6 +295,7 @@ TEST(ClassNameTests, StaticMethodBehavesCorrectly) 3. **Use descriptive test names** (e.g., `ReturnsNullForInvalidInput`) 4. **Group related tests** in test fixtures 5. **Keep tests independent** and deterministic +6. **Never Use C or C++ macros (like M_PI)** use yup alternatives ### When suggesting refactoring: 1. **Maintain existing API contracts** @@ -327,7 +328,7 @@ TEST(ClassNameTests, StaticMethodBehavesCorrectly) yup::Result performOperation() { if (preconditionFailed) - return yup::Result::fail("Precondition not met"); + return yup::Result::fail ("Precondition not met"); return yup::Result::ok(); } @@ -335,16 +336,15 @@ yup::Result performOperation() yup::ResultValue maybeGetInteger() { if (preconditionFailed) - return yup::ResultValue::fail("Precondition not met"); + return yup::ResultValue::fail ("Precondition not met"); return 1; } // Use assertions for programming errors -void publicMethod(int value) +void publicMethod (int value) { - jassert(value >= 0); // Debug builds only - + jassert (value >= 0); // Debug builds only if (value < 0) return; // Graceful handling in release } @@ -381,13 +381,13 @@ private: ```cpp // Use yup::var for dynamic types // Use std::optional for optional values -std::optional findValue(const String& key); +std::optional findValue (const String& key); ``` ### String Handling ```cpp // Use yup::String for most string operations -void processText(const yup::String& text); +void processText (const yup::String& text); // Use std::string only when interfacing with non-YUP code ``` diff --git a/modules/yup_audio_basics/yup_audio_basics.h b/modules/yup_audio_basics/yup_audio_basics.h index 953518458..5e9f74de0 100644 --- a/modules/yup_audio_basics/yup_audio_basics.h +++ b/modules/yup_audio_basics/yup_audio_basics.h @@ -49,7 +49,6 @@ description: Classes for audio buffer manipulation, midi message handling, synthesis, etc. website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_core appleFrameworks: Accelerate diff --git a/modules/yup_audio_devices/yup_audio_devices.h b/modules/yup_audio_devices/yup_audio_devices.h index ec5b24bf5..a7bec6aaf 100644 --- a/modules/yup_audio_devices/yup_audio_devices.h +++ b/modules/yup_audio_devices/yup_audio_devices.h @@ -49,7 +49,6 @@ description: Classes to play and record from audio and MIDI I/O devices website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_audio_basics yup_events appleFrameworks: CoreAudio CoreMIDI AudioToolbox diff --git a/modules/yup_audio_plugin_client/yup_audio_plugin_client.h b/modules/yup_audio_plugin_client/yup_audio_plugin_client.h index 1ff36290c..dad8d5db3 100644 --- a/modules/yup_audio_plugin_client/yup_audio_plugin_client.h +++ b/modules/yup_audio_plugin_client/yup_audio_plugin_client.h @@ -31,7 +31,6 @@ description: The essential set of basic YUP audio plugin clients. website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_audio_processors yup_gui enableARC: 1 diff --git a/modules/yup_audio_processors/yup_audio_processors.h b/modules/yup_audio_processors/yup_audio_processors.h index ad3d1e0cd..b8d776390 100644 --- a/modules/yup_audio_processors/yup_audio_processors.h +++ b/modules/yup_audio_processors/yup_audio_processors.h @@ -31,7 +31,6 @@ description: The essential set of basic YUP audio processing classes. website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_audio_basics yup_gui enableARC: 1 diff --git a/modules/yup_core/misc/yup_Functional.h b/modules/yup_core/misc/yup_Functional.h index 871eac807..b656d0ab2 100644 --- a/modules/yup_core/misc/yup_Functional.h +++ b/modules/yup_core/misc/yup_Functional.h @@ -123,4 +123,34 @@ static constexpr auto toFnPtr (Functor functor) return detail::toFnPtr (functor, &Functor::operator()); } +template +static auto tuplePrepend (T&& t, Args&&... args) +{ + return std::tuple_cat (std::forward (t), std::forward_as_tuple (args...)); +} + +template +static auto tupleAppend (T&& t, Args&&... args) +{ + return std::tuple_cat (std::forward_as_tuple (args...), std::forward (t)); +} + +template +static decltype (auto) bindFront (F&& f, FrontArgs&&... frontArgs) +{ + return [f = std::forward (f), frontArgs = std::make_tuple (std::forward (frontArgs)...)] (auto&&... backArgs) + { + return std::apply (f, tuplePrepend (frontArgs, std::forward (backArgs)...)); + }; +} + +template +static decltype (auto) bindBack (F&& f, BackArgs&&... backArgs) +{ + return [f = std::forward (f), backArgs = std::make_tuple (std::forward (backArgs)...)] (auto&&... frontArgs) + { + return std::apply (f, tupleAppend (backArgs, std::forward (frontArgs)...)); + }; +} + } // namespace yup diff --git a/modules/yup_core/yup_core.h b/modules/yup_core/yup_core.h index 11e27aa6d..840b5792e 100644 --- a/modules/yup_core/yup_core.h +++ b/modules/yup_core/yup_core.h @@ -49,7 +49,6 @@ description: The essential set of basic YUP classes, as required by all the other YUP modules. website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: zlib osxFrameworks: Cocoa Foundation IOKit Security diff --git a/modules/yup_events/yup_events.h b/modules/yup_events/yup_events.h index 57870307a..8fd4a08bc 100644 --- a/modules/yup_events/yup_events.h +++ b/modules/yup_events/yup_events.h @@ -49,7 +49,6 @@ description: Classes for running an application's main event loop and sending/receiving messages, timers, etc. website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_core diff --git a/modules/yup_graphics/context/yup_GraphicsContext.h b/modules/yup_graphics/context/yup_GraphicsContext.h index b2d7df2b8..0710f5749 100644 --- a/modules/yup_graphics/context/yup_GraphicsContext.h +++ b/modules/yup_graphics/context/yup_GraphicsContext.h @@ -22,8 +22,6 @@ namespace yup { -class LowLevelRenderContext; - //============================================================================== /** Encapsulates a graphics context that abstracts rendering operations across various APIs. @@ -42,6 +40,7 @@ class YUP_API GraphicsContext /** Enumerates supported graphics APIs. */ enum Api { + Headless, ///< Specifies the use of a headless context for rendering. OpenGL, ///< Specifies the use of OpenGL for rendering. Direct3D, ///< Specifies the use of Direct3D for rendering. Metal, ///< Specifies the use of Metal for rendering. diff --git a/modules/yup_graphics/fonts/yup_StyledText.cpp b/modules/yup_graphics/fonts/yup_StyledText.cpp index 2cde5a6e9..b12f00908 100644 --- a/modules/yup_graphics/fonts/yup_StyledText.cpp +++ b/modules/yup_graphics/fonts/yup_StyledText.cpp @@ -24,6 +24,9 @@ namespace yup //============================================================================== +namespace +{ + rive::TextAlign toTextAlign (StyledText::HorizontalAlign align) noexcept { if (align == StyledText::left || align == StyledText::justified) @@ -49,6 +52,74 @@ rive::TextWrap toTextWrap (StyledText::TextWrap wrap) noexcept return rive::TextWrap::noWrap; } +} // namespace + +//============================================================================== + +StyledText::TextModifier::TextModifier (StyledText& styledText) + : styledText (styledText) +{ +} + +StyledText::TextModifier::~TextModifier() +{ + styledText.update(); +} + +void StyledText::TextModifier::clear() +{ + styledText.clear(); +} + +void StyledText::TextModifier::appendText (StringRef text, + const Font& font, + float fontSize, + float lineHeight, + float letterSpacing) +{ + styledText.appendText (text, nullptr, font, fontSize, lineHeight, letterSpacing); +} + +void StyledText::TextModifier::appendText (StringRef text, + rive::rcp paint, + const Font& font, + float fontSize, + float lineHeight, + float letterSpacing) +{ + styledText.appendText (text, paint, font, fontSize, lineHeight, letterSpacing); +} + +void StyledText::TextModifier::setOverflow (StyledText::TextOverflow value) +{ + styledText.setOverflow (value); +} + +void StyledText::TextModifier::setHorizontalAlign (StyledText::HorizontalAlign value) +{ + styledText.setHorizontalAlign (value); +} + +void StyledText::TextModifier::setVerticalAlign (StyledText::VerticalAlign value) +{ + styledText.setVerticalAlign (value); +} + +void StyledText::TextModifier::setMaxSize (const Size& value) +{ + styledText.setMaxSize (value); +} + +void StyledText::TextModifier::setParagraphSpacing (float value) +{ + styledText.setParagraphSpacing (value); +} + +void StyledText::TextModifier::setWrap (StyledText::TextWrap value) +{ + styledText.setWrap (value); +} + //============================================================================== StyledText::StyledText() @@ -62,6 +133,18 @@ bool StyledText::isEmpty() const return styledTexts.empty(); } +bool StyledText::needsUpdate() const +{ + return isDirty; +} + +//============================================================================== + +StyledText::TextModifier StyledText::startUpdate() +{ + return { *this }; +} + //============================================================================== void StyledText::clear() @@ -69,7 +152,7 @@ void StyledText::clear() styledTexts.clear(); styles.clear(); - isDirty = true; + update(); } //============================================================================== @@ -224,6 +307,9 @@ void StyledText::update() ellipsisRun = {}; const auto& runs = styledTexts.runs(); + if (runs[0].font == nullptr) + return; + shape = runs[0].font->shapeText (styledTexts.unichars(), runs); lines = rive::Text::BreakLines (shape, maxSize.getWidth(), // -1.0f @@ -236,6 +322,9 @@ void StyledText::update() return; } + // Compute glyph lookup for text positioning + glyphLookup.compute (styledTexts.unichars(), shape); + // Build up ordered runs as we go. int paragraphIndex = 0; float y = 0.0f; @@ -316,7 +405,7 @@ void StyledText::update() float renderX = x; int numGlyphs = 0; - for (auto [run, glyphIndex] : orderedLines[lineIndex]) + for (const auto& [run, glyphIndex] : orderedLines[lineIndex]) { const rive::Vec2D& offset = run->offsets[glyphIndex]; renderX += run->advances[glyphIndex] + offset.x; @@ -328,7 +417,7 @@ void StyledText::update() adjustX = (measuredWidth - renderX) / numGlyphs; } - for (auto [run, glyphIndex] : orderedLines[lineIndex]) + for (const auto& [run, glyphIndex] : orderedLines[lineIndex]) { const rive::Font* font = run->font.get(); const rive::Vec2D& offset = run->offsets[glyphIndex]; @@ -375,24 +464,330 @@ void StyledText::update() //============================================================================== -Rectangle StyledText::getGlyphPosition (int index) const +int StyledText::getGlyphIndexAtPosition (const Point& position) const { - return {}; // TODO + jassert (! isDirty); + if (isDirty || orderedLines.empty()) + return 0; + + float clickX = position.getX(); + float clickY = position.getY(); + + // Use the same approach as getSelectionRectangles to find the line + int targetLineIndex = -1; + + for (size_t lineIdx = 0; lineIdx < orderedLines.size(); ++lineIdx) + { + const rive::OrderedLine& line = orderedLines[lineIdx]; + const rive::GlyphLine& glyphLine = line.glyphLine(); + + float lineY = line.y(); + float lineTop = lineY + glyphLine.top; + float lineBottom = lineY + glyphLine.bottom; + + // Check if click is within this line's vertical bounds (same as getSelectionRectangles) + if (clickY >= lineTop && clickY <= lineBottom) + { + targetLineIndex = static_cast (lineIdx); + break; + } + // If click is above the first line, use the first line + else if (lineIdx == 0 && clickY < lineTop) + { + targetLineIndex = 0; + break; + } + // If click is below all lines, use the last line + else if (lineIdx == orderedLines.size() - 1 && clickY > lineBottom) + { + targetLineIndex = static_cast (lineIdx); + break; + } + } + + if (targetLineIndex == -1) + return static_cast (styledTexts.unichars().size()); + + const rive::OrderedLine& targetLine = orderedLines[targetLineIndex]; + const rive::GlyphLine& glyphLine = targetLine.glyphLine(); + + // Find the closest character using the same xpos logic as getSelectionRectangles + int bestCharIndex = 0; + float minDistance = std::numeric_limits::max(); + bool foundAnyGlyph = false; + + // If click is before the line start, return the first character in the line + if (clickX <= glyphLine.startX) + { + for (const auto& [glyphRun, glyphIndex] : targetLine) + { + if (glyphIndex < glyphRun->textIndices.size()) + return static_cast (glyphRun->textIndices[glyphIndex]); + } + + return 0; + } + + for (const auto& [glyphRun, glyphIndex] : targetLine) + { + // Check if this glyph run has valid data (same check as getSelectionRectangles) + if (glyphIndex >= glyphRun->textIndices.size() || glyphIndex >= glyphRun->xpos.size()) + continue; + + uint32_t textIndex = glyphRun->textIndices[glyphIndex]; + int charIndex = static_cast (textIndex); + + // Use the same X positioning logic as getSelectionRectangles + float glyphX = glyphRun->xpos[glyphIndex]; + float nextGlyphX = (glyphIndex + 1 < glyphRun->xpos.size()) ? glyphRun->xpos[glyphIndex + 1] : glyphX + (glyphIndex < glyphRun->advances.size() ? glyphRun->advances[glyphIndex] : 0); + + // Check if click is within this character's bounds + if (clickX >= glyphX && clickX <= nextGlyphX) + { + // Return the closest boundary + float midPoint = (glyphX + nextGlyphX) * 0.5f; + return (clickX <= midPoint) ? charIndex : charIndex + 1; + } + + // Calculate distances to start and end of this character + float distanceToStart = std::abs (clickX - glyphX); + float distanceToEnd = std::abs (clickX - nextGlyphX); + + // Check if click is closer to the start of this character + if (distanceToStart < minDistance) + { + minDistance = distanceToStart; + bestCharIndex = charIndex; + foundAnyGlyph = true; + } + + // Check if click is closer to the end of this character + if (distanceToEnd < minDistance) + { + minDistance = distanceToEnd; + bestCharIndex = charIndex + 1; + foundAnyGlyph = true; + } + } + + // If no glyph was found, return the start of this line + if (! foundAnyGlyph) + { + // Find the first character in this line + for (const auto& [glyphRun, glyphIndex] : targetLine) + { + if (glyphIndex < glyphRun->textIndices.size()) + return static_cast (glyphRun->textIndices[glyphIndex]); + } + + return 0; + } + + // Ensure the result is within valid bounds + return jlimit (0, static_cast (styledTexts.unichars().size()), bestCharIndex); } //============================================================================== -Rectangle StyledText::getComputedTextBounds() +Rectangle StyledText::getCaretBounds (int characterIndex) const { - update(); + jassert (! isDirty); + if (isDirty || orderedLines.empty()) + return {}; + + // Handle bounds checking + if (characterIndex < 0) + characterIndex = 0; + + // Use the same approach as getSelectionRectangles + for (size_t lineIdx = 0; lineIdx < orderedLines.size(); ++lineIdx) + { + const rive::OrderedLine& line = orderedLines[lineIdx]; + const rive::GlyphLine& glyphLine = line.glyphLine(); + + float lineY = line.y(); + float lineHeight = glyphLine.bottom - glyphLine.top; + + for (const auto& [glyphRun, glyphIndex] : line) + { + // Check if this glyph run has valid data + if (glyphIndex >= glyphRun->textIndices.size() || glyphIndex >= glyphRun->xpos.size()) + continue; + + uint32_t textIndex = glyphRun->textIndices[glyphIndex]; + int charIndex = static_cast (textIndex); + + // Check if this is our target character + if (charIndex == characterIndex) + { + float caretX = glyphRun->xpos[glyphIndex]; + const float caretWidth = 1.0f; + + return Rectangle ( + caretX, + lineY + glyphLine.top, + caretWidth, + lineHeight); + } + // Check if we've passed our target character (for end-of-line positioning) + else if (charIndex > characterIndex) + { + float caretX = glyphRun->xpos[glyphIndex]; + const float caretWidth = 1.0f; + + return Rectangle ( + caretX, + lineY + glyphLine.top, + caretWidth, + lineHeight); + } + } + + // If we've checked all glyphs in this line and character index is beyond them, + // position at the end of this line + if (characterIndex <= static_cast (line.lastCodePointIndex (glyphLookup))) + { + // Find the rightmost position in this line + float endX = glyphLine.startX; + for (auto [glyphRun, glyphIndex] : line) + { + if (glyphIndex < glyphRun->xpos.size()) + { + if (glyphIndex + 1 < glyphRun->xpos.size()) + endX = glyphRun->xpos[glyphIndex + 1]; + else + endX = glyphRun->xpos[glyphIndex] + (glyphIndex < glyphRun->advances.size() ? glyphRun->advances[glyphIndex] : 0); + } + } + + const float caretWidth = 1.0f; + return Rectangle ( + endX, + lineY + glyphLine.top, + caretWidth, + lineHeight); + } + } + + // If character index is beyond all text, position at the end of the last line + if (! orderedLines.empty()) + { + const rive::OrderedLine& lastLine = orderedLines.back(); + const rive::GlyphLine& glyphLine = lastLine.glyphLine(); + + float lineY = lastLine.y(); + float lineHeight = glyphLine.bottom - glyphLine.top; + + // Find the rightmost position in the last line + float endX = glyphLine.startX; + for (const auto& [glyphRun, glyphIndex] : lastLine) + { + if (glyphIndex < glyphRun->xpos.size()) + { + if (glyphIndex + 1 < glyphRun->xpos.size()) + endX = glyphRun->xpos[glyphIndex + 1]; + else + endX = glyphRun->xpos[glyphIndex] + (glyphIndex < glyphRun->advances.size() ? glyphRun->advances[glyphIndex] : 0); + } + } + + const float caretWidth = 1.0f; + return Rectangle ( + endX, + lineY + glyphLine.top, + caretWidth, + lineHeight); + } + + return {}; +} + +//============================================================================== + +std::vector> StyledText::getSelectionRectangles (int startIndex, int endIndex) const +{ + std::vector> rectangles; + + jassert (! isDirty); + if (isDirty || orderedLines.empty() || startIndex < 0 || endIndex < 0 || startIndex >= endIndex) + return rectangles; + + rectangles.reserve (orderedLines.size()); + + // Use the orderedLines to find selection rectangles + for (size_t lineIdx = 0; lineIdx < orderedLines.size(); ++lineIdx) + { + const rive::OrderedLine& line = orderedLines[lineIdx]; + const rive::GlyphLine& glyphLine = line.glyphLine(); + + // Track selection bounds for this line + float selectionStartX = -1.0f; + float selectionEndX = -1.0f; + float lineY = line.y(); + float lineHeight = glyphLine.bottom - glyphLine.top; + + bool hasSelectionInLine = false; + + for (auto [glyphRun, glyphIndex] : line) + { + // Check if this glyph run has valid data + if (glyphIndex >= glyphRun->textIndices.size() || glyphIndex >= glyphRun->xpos.size()) + continue; + + uint32_t textIndex = glyphRun->textIndices[glyphIndex]; + int charIndex = static_cast (textIndex); + + // Check if this character is within the selection + if (charIndex >= startIndex && charIndex < endIndex) + { + float glyphX = glyphRun->xpos[glyphIndex]; + float nextGlyphX = (glyphIndex + 1 < glyphRun->xpos.size()) ? glyphRun->xpos[glyphIndex + 1] : glyphX + (glyphIndex < glyphRun->advances.size() ? glyphRun->advances[glyphIndex] : 0); + + if (! hasSelectionInLine) + { + selectionStartX = glyphX; + selectionEndX = nextGlyphX; + hasSelectionInLine = true; + } + else + { + selectionStartX = std::min (selectionStartX, glyphX); + selectionEndX = std::max (selectionEndX, nextGlyphX); + } + } + } + + // If this line has selection, add a rectangle for it + if (hasSelectionInLine && selectionStartX >= 0.0f && selectionEndX > selectionStartX) + { + rectangles.push_back (Rectangle ( + selectionStartX, + lineY + glyphLine.top, + selectionEndX - selectionStartX, + lineHeight)); + } + } + + return rectangles; +} + +//============================================================================== + +Rectangle StyledText::getComputedTextBounds() const +{ + jassert (! isDirty); return bounds; } -Point StyledText::getOffset (const Rectangle& area) +//============================================================================== + +Point StyledText::getOffset (const Rectangle& area) const { - update(); + jassert (! isDirty); + if (isDirty) + return {}; - Point result { 0.0f, 0.0f }; + auto result = Point { 0.0f, 0.0f }; if (getHorizontalAlign() == StyledText::center) result.setX ((area.getWidth() - bounds.getWidth()) * 0.5f); @@ -409,16 +804,30 @@ Point StyledText::getOffset (const Rectangle& area) //============================================================================== -const std::vector& StyledText::getOrderedLines() +Span StyledText::getOrderedLines() const { - update(); + jassert (! isDirty); return orderedLines; } -const std::vector& StyledText::getRenderStyles() +Span StyledText::getRenderStyles() const { - update(); + jassert (! isDirty); return renderStyles; } +//============================================================================== + +bool StyledText::isValidCharacterIndex (int characterIndex) const +{ + jassert (! isDirty); + if (isDirty || characterIndex < 0) + return false; + + if (glyphLookup.size() == 0) + return characterIndex == 0; + + return characterIndex <= (int) styledTexts.unichars().size(); +} + } // namespace yup diff --git a/modules/yup_graphics/fonts/yup_StyledText.h b/modules/yup_graphics/fonts/yup_StyledText.h index a6bc920bc..eefd932b6 100644 --- a/modules/yup_graphics/fonts/yup_StyledText.h +++ b/modules/yup_graphics/fonts/yup_StyledText.h @@ -70,52 +70,71 @@ class YUP_API StyledText bool isEmpty() const; + bool needsUpdate() const; + //============================================================================== - void clear(); + struct TextModifier + { + TextModifier (StyledText& styledText); + ~TextModifier(); - //============================================================================== + void clear(); - void appendText (StringRef text, - rive::rcp paint, - const Font& font, - float fontSize = 16.0f, - float lineHeight = -1.0f, - float letterSpacing = 0.0f); + void appendText (StringRef text, + const Font& font, + float fontSize = 16.0f, + float lineHeight = -1.0f, + float letterSpacing = 0.0f); - //============================================================================== + void appendText (StringRef text, + rive::rcp paint, + const Font& font, + float fontSize = 16.0f, + float lineHeight = -1.0f, + float letterSpacing = 0.0f); - void update(); + void setOverflow (TextOverflow value); + + void setHorizontalAlign (HorizontalAlign value); + + void setVerticalAlign (VerticalAlign value); + + void setMaxSize (const Size& value); + + void setParagraphSpacing (float value); + + void setWrap (TextWrap value); + + private: + StyledText& styledText; + }; + + TextModifier startUpdate(); //============================================================================== TextOverflow getOverflow() const; - void setOverflow (TextOverflow value); HorizontalAlign getHorizontalAlign() const; - void setHorizontalAlign (HorizontalAlign value); VerticalAlign getVerticalAlign() const; - void setVerticalAlign (VerticalAlign value); Size getMaxSize() const; - void setMaxSize (const Size& value); float getParagraphSpacing() const; - void setParagraphSpacing (float value); TextWrap getWrap() const; - void setWrap (TextWrap value); //============================================================================== - Rectangle getComputedTextBounds(); + Rectangle getComputedTextBounds() const; - Point getOffset (const Rectangle& area); + Point getOffset (const Rectangle& area) const; //============================================================================== - const std::vector& getOrderedLines(); + Span getOrderedLines() const; //============================================================================== @@ -136,13 +155,62 @@ class YUP_API StyledText bool isEmpty; }; - const std::vector& getRenderStyles(); + Span getRenderStyles() const; //============================================================================== - Rectangle getGlyphPosition (int index) const; + /** Find the glyph index at a given position in the text area. + + @param position The position to check + + @returns The glyph index at the position, or -1 if not found + */ + int getGlyphIndexAtPosition (const Point& position) const; + + /** Get the bounds of the caret at a given character position. + + @param characterIndex The character index to get bounds for + + @returns Rectangle representing the caret bounds + */ + Rectangle getCaretBounds (int characterIndex) const; + + /** Returns all selection rectangles for multiline selections. + + @param startIndex The start character index + @param endIndex The end character index + @returns A vector of rectangles representing the selection + */ + std::vector> getSelectionRectangles (int startIndex, int endIndex) const; + + /** Validates if a character index is within valid bounds. + + @param characterIndex The character index to validate + @returns True if the index is valid + */ + bool isValidCharacterIndex (int characterIndex) const; private: + friend class TextModifier; + + void clear(); + + void appendText (StringRef text, + rive::rcp paint, + const Font& font, + float fontSize, + float lineHeight, + float letterSpacing); + + void setOverflow (TextOverflow value); + void setHorizontalAlign (HorizontalAlign value); + void setVerticalAlign (VerticalAlign value); + void setMaxSize (const Size& value); + void setParagraphSpacing (float value); + void setWrap (TextWrap value); + + void update(); + rive::SimpleArray shape; rive::SimpleArray> lines; std::vector orderedLines; @@ -150,6 +218,7 @@ class YUP_API StyledText rive::StyledText styledTexts; std::vector styles; std::vector renderStyles; + rive::GlyphLookup glyphLookup; TextOrigin origin = TextOrigin::topOrigin; TextOverflow overflow = TextOverflow::visible; diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index f5290dd75..047accfac 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -604,10 +604,10 @@ void Graphics::drawImageAt (const Image& image, const Point& pos) } //============================================================================== -void Graphics::fillFittedText (StyledText& text, const Rectangle& rect) +void Graphics::fillFittedText (const StyledText& text, const Rectangle& rect) { - text.update(); - if (text.isEmpty()) + jassert (! text.needsUpdate()); + if (text.needsUpdate() || text.isEmpty()) return; const auto& options = currentRenderOptions(); @@ -625,10 +625,10 @@ void Graphics::fillFittedText (StyledText& text, const Rectangle& rect) renderFittedText (text, rect, std::addressof (paint)); } -void Graphics::strokeFittedText (StyledText& text, const Rectangle& rect) +void Graphics::strokeFittedText (const StyledText& text, const Rectangle& rect) { - text.update(); - if (text.isEmpty()) + jassert (! text.needsUpdate()); + if (text.needsUpdate() || text.isEmpty()) return; const auto& options = currentRenderOptions(); @@ -648,25 +648,25 @@ void Graphics::strokeFittedText (StyledText& text, const Rectangle& rect) renderFittedText (text, rect, std::addressof (paint)); } -void Graphics::renderFittedText (StyledText& text, const Rectangle& rect, rive::RiveRenderPaint* paint) +void Graphics::renderFittedText (const StyledText& text, const Rectangle& rect, rive::RiveRenderPaint* paint) { - jassert (! text.isEmpty()); + jassert (! text.needsUpdate()); + if (text.needsUpdate() || text.isEmpty()) + return; const auto& options = currentRenderOptions(); - auto offset = text.getOffset (rect); - if (text.getMaxSize().getWidth() > 0.0f) // Non negative max size in text layout will adjust X axis alignment - offset = offset.withX (0.0f); + auto offset = text.getOffset (rect); // We will just use vertical offset renderer.save(); rive::RawPath path; - path.addRect ({ rect.getLeft(), rect.getTop(), rect.getRight(), rect.getBottom() }); + path.addRect (rect.toAABB()); path.transformInPlace (options.getFixedTransform().toMat2D()); auto renderPath = rive::make_rcp (rive::FillRule::clockwise, path); renderer.clipPath (renderPath.get()); - auto transform = options.getTransform (rect.getX() + offset.getX(), rect.getY() + offset.getY()); + auto transform = options.getTransform (rect.getX(), rect.getY() + offset.getY()); renderer.transform (transform.toMat2D()); for (auto style : text.getRenderStyles()) diff --git a/modules/yup_graphics/graphics/yup_Graphics.h b/modules/yup_graphics/graphics/yup_Graphics.h index 447d209c6..1981a1b60 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.h +++ b/modules/yup_graphics/graphics/yup_Graphics.h @@ -410,8 +410,8 @@ class YUP_API Graphics //============================================================================== /** Draws an attributed text. */ - void fillFittedText (StyledText& text, const Rectangle& rect); - void strokeFittedText (StyledText& text, const Rectangle& rect); + void fillFittedText (const StyledText& text, const Rectangle& rect); + void strokeFittedText (const StyledText& text, const Rectangle& rect); //============================================================================== /** Clips the drawing area to the specified rectangle. @@ -558,7 +558,7 @@ class YUP_API Graphics void renderStrokePath (const Path& path, const RenderOptions& options, const AffineTransform& transform); void renderFillPath (const Path& path, const RenderOptions& options, const AffineTransform& transform); - void renderFittedText (StyledText& text, const Rectangle& rect, rive::RiveRenderPaint* paint); + void renderFittedText (const StyledText& text, const Rectangle& rect, rive::RiveRenderPaint* paint); GraphicsContext& context; diff --git a/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp b/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp new file mode 100644 index 000000000..5a5f5bf1c --- /dev/null +++ b/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp @@ -0,0 +1,246 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +class NoOpRenderBuffer : public rive::RenderBuffer +{ +public: + NoOpRenderBuffer() + : rive::RenderBuffer (rive::RenderBufferType::index, rive::RenderBufferFlags::none, 0) + { + } + + void* onMap() override { return nullptr; } + + void onUnmap() override {} +}; + +//============================================================================== + +class NoOpRenderShader : public rive::RenderShader +{ +public: +}; + +//============================================================================== + +class NoOpRenderImage : public rive::RenderImage +{ +public: +}; + +//============================================================================== + +class NoOpRenderPaint : public rive::RenderPaint +{ +public: + void color (unsigned int) override {} + + void style (rive::RenderPaintStyle) override {} + + void thickness (float) override {} + + void join (rive::StrokeJoin) override {} + + void cap (rive::StrokeCap) override {} + + void blendMode (rive::BlendMode) override {} + + void shader (rive::rcp) override {} + + void invalidateStroke() override {} + + void feather (float) override {} +}; + +//============================================================================== + +class NoOpRenderPath : public rive::RenderPath +{ +public: + void rewind() override {} + + void fillRule (rive::FillRule) override {} + + void addPath (rive::CommandPath*, const rive::Mat2D&) override {} + + void addRenderPath (rive::RenderPath*, const rive::Mat2D&) override {} + + void moveTo (float, float) override {} + + void lineTo (float, float) override {} + + void cubicTo (float, float, float, float, float, float) override {} + + void close() override {} + + void addRawPath (const rive::RawPath&) override {} +}; + +//============================================================================== + +class NoOpFactory : public rive::Factory +{ +public: + NoOpFactory() = default; + + rive::rcp makeRenderBuffer ( + rive::RenderBufferType, + rive::RenderBufferFlags, + size_t) override + { + return rive::make_rcp(); + } + + rive::rcp makeLinearGradient ( + float sx, + float sy, + float ex, + float ey, + const rive::ColorInt colors[], + const float stops[], + size_t count) override + { + return rive::make_rcp(); + } + + rive::rcp makeRadialGradient ( + float cx, + float cy, + float radius, + const rive::ColorInt colors[], + const float stops[], + size_t count) override + { + return rive::make_rcp(); + } + + rive::rcp makeRenderPath (rive::RawPath&, rive::FillRule) override + { + return rive::make_rcp(); + } + + rive::rcp makeEmptyRenderPath() override + { + return rive::make_rcp(); + } + + rive::rcp makeRenderPaint() override + { + return rive::make_rcp(); + } + + rive::rcp decodeImage (rive::Span) override + { + return rive::make_rcp(); + } +}; + +//============================================================================== + +class NoOpRenderer : public rive::Renderer +{ +public: + NoOpRenderer() = default; + + void save() override {} + + void restore() override {} + + void transform (const rive::Mat2D&) override {} + + void drawPath (rive::RenderPath*, rive::RenderPaint*) override {} + + void clipPath (rive::RenderPath*) override {} + + void drawImage (const rive::RenderImage*, rive::ImageSampler, rive::BlendMode, float) override {} + + void drawImageMesh (const rive::RenderImage*, + rive::ImageSampler, + rive::rcp, + rive::rcp, + rive::rcp, + uint32_t vertexCount, + uint32_t indexCount, + rive::BlendMode, + float) override {} +}; + +//============================================================================== + +class NoOpGraphicsContext : public GraphicsContext +{ +public: + NoOpGraphicsContext() = default; + + float dpiScale (void*) const override + { + return 1.0f; + } + + rive::Factory* factory() override + { + return std::addressof (noOpFactory); + } + + rive::gpu::RenderContext* renderContext() override + { + return nullptr; + } + + rive::gpu::RenderTarget* renderTarget() override + { + return nullptr; + } + + std::unique_ptr makeRenderer (int, int) override + { + return std::make_unique(); + } + + void onSizeChanged (void*, int, int, uint32_t) override + { + } + + void begin (const rive::gpu::RenderContext::FrameDescriptor&) override + { + } + + void end (void*) override + { + } + +private: + NoOpFactory noOpFactory; +}; + +//============================================================================== + +std::unique_ptr yup_constructHeadlessGraphicsContext (GraphicsContext::Options fiddleOptions) +{ + return std::make_unique(); +} + +} // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_impl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_impl.cpp index a0fec0797..8716cef9e 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_impl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_impl.cpp @@ -26,6 +26,9 @@ std::unique_ptr GraphicsContext::createContext (Api graphicsApi { switch (graphicsApi) { + case Api::Headless: + return yup_constructHeadlessGraphicsContext (options); + #if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) case Api::Metal: return yup_constructMetalGraphicsContext (options); diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 6a3555b6c..bc47cc588 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -187,23 +187,39 @@ Path& Path::addRectangle (const Rectangle& rect) Path& Path::addRoundedRectangle (float x, float y, float width, float height, float radiusTopLeft, float radiusTopRight, float radiusBottomLeft, float radiusBottomRight) { - reserveSpace (size() + 10); + reserveSpace (size() + 9); - radiusTopLeft = jmin (radiusTopLeft, jmin (width / 2.0f, height / 2.0f)); - radiusTopRight = jmin (radiusTopRight, jmin (width / 2.0f, height / 2.0f)); - radiusBottomLeft = jmin (radiusBottomLeft, jmin (width / 2.0f, height / 2.0f)); - radiusBottomRight = jmin (radiusBottomRight, jmin (width / 2.0f, height / 2.0f)); + const float centerWidth = width * 0.5f; + const float centerHeight = height * 0.5f; + radiusTopLeft = jmin (radiusTopLeft, centerWidth, centerHeight); + radiusTopRight = jmin (radiusTopRight, centerWidth, centerHeight); + radiusBottomLeft = jmin (radiusBottomLeft, centerWidth, centerHeight); + radiusBottomRight = jmin (radiusBottomRight, centerWidth, centerHeight); + + // Use the mathematically correct constant for circular arc approximation with cubic Bezier curves + // This is 4/3 * tan(pi/8) ≈ 0.5522847498f + constexpr float kappa = 0.5522847498f; moveTo (x + radiusTopLeft, y); lineTo (x + width - radiusTopRight, y); - cubicTo (x + width - radiusTopRight * 0.55f, y, x + width, y + radiusTopRight * 0.45f, x + width, y + radiusTopRight); + + // Top-right corner + cubicTo (x + width - radiusTopRight + radiusTopRight * kappa, y, x + width, y + radiusTopRight - radiusTopRight * kappa, x + width, y + radiusTopRight); + lineTo (x + width, y + height - radiusBottomRight); - cubicTo (x + width, y + height - radiusBottomRight * 0.55f, x + width - radiusBottomRight * 0.55f, y + height, x + width - radiusBottomRight, y + height); + + // Bottom-right corner + cubicTo (x + width, y + height - radiusBottomRight + radiusBottomRight * kappa, x + width - radiusBottomRight + radiusBottomRight * kappa, y + height, x + width - radiusBottomRight, y + height); + lineTo (x + radiusBottomLeft, y + height); - cubicTo (x + radiusBottomLeft * 0.55f, y + height, x, y + height - radiusBottomLeft * 0.55f, x, y + height - radiusBottomLeft); + + // Bottom-left corner + cubicTo (x + radiusBottomLeft - radiusBottomLeft * kappa, y + height, x, y + height - radiusBottomLeft + radiusBottomLeft * kappa, x, y + height - radiusBottomLeft); + lineTo (x, y + radiusTopLeft); - cubicTo (x, y + radiusTopLeft * 0.55f, x + radiusTopLeft * 0.55f, y, x + radiusTopLeft, y); - lineTo (x + radiusTopLeft, y); + + // Top-left corner + cubicTo (x, y + radiusTopLeft - radiusTopLeft * kappa, x + radiusTopLeft - radiusTopLeft * kappa, y, x + radiusTopLeft, y); return *this; } diff --git a/modules/yup_graphics/yup_graphics.cpp b/modules/yup_graphics/yup_graphics.cpp index 68322f43d..ee415311c 100644 --- a/modules/yup_graphics/yup_graphics.cpp +++ b/modules/yup_graphics/yup_graphics.cpp @@ -94,6 +94,10 @@ //============================================================================== +#include "native/yup_GraphicsContext_headless.cpp" + +//============================================================================== + #include "native/yup_GraphicsContext_impl.cpp" //============================================================================== diff --git a/modules/yup_graphics/yup_graphics.h b/modules/yup_graphics/yup_graphics.h index e1bfd2665..62d21cc38 100644 --- a/modules/yup_graphics/yup_graphics.h +++ b/modules/yup_graphics/yup_graphics.h @@ -31,7 +31,6 @@ description: The essential set of basic YUP graphics classes. website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_core rive rive_renderer appleFrameworks: Metal @@ -55,6 +54,7 @@ YUP_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wattributes", "-Wdeprecated-declarations") #include +#include #include #include YUP_END_IGNORE_WARNINGS_GCC_LIKE diff --git a/modules/yup_gui/clipboard/yup_SystemClipboard.cpp b/modules/yup_gui/clipboard/yup_SystemClipboard.cpp new file mode 100644 index 000000000..2b4750cb4 --- /dev/null +++ b/modules/yup_gui/clipboard/yup_SystemClipboard.cpp @@ -0,0 +1,42 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +void SystemClipboard::copyTextToClipboard (const String& text) +{ + SDL_SetClipboardText (text.toRawUTF8()); +} + +String SystemClipboard::getTextFromClipboard() +{ + if (char* clipboardText = SDL_GetClipboardText()) + { + String textInClipboard = String::fromUTF8 (clipboardText); + SDL_free (clipboardText); + return textInClipboard; + } + + return {}; +} + +} // namespace yup diff --git a/modules/yup_gui/clipboard/yup_SystemClipboard.h b/modules/yup_gui/clipboard/yup_SystemClipboard.h new file mode 100644 index 000000000..ccf9b21b8 --- /dev/null +++ b/modules/yup_gui/clipboard/yup_SystemClipboard.h @@ -0,0 +1,35 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +class YUP_API SystemClipboard +{ +public: + /** Copies the given text to the system clipboard. */ + static void copyTextToClipboard (const String& text); + + /** Retrieves the text from the system clipboard. */ + static String getTextFromClipboard(); +}; + +} // namespace yup diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index ac56351d1..a263bd2a5 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -473,6 +473,8 @@ bool Component::isRenderingUnclipped() const void Component::repaint() { + jassert (! options.isRepainting); // You are likely repainting from paint ! + if (getBounds().isEmpty()) return; @@ -482,6 +484,8 @@ void Component::repaint() void Component::repaint (const Rectangle& rect) { + jassert (! options.isRepainting); // You are likely repainting from paint ! + if (rect.isEmpty()) return; @@ -989,8 +993,11 @@ void Component::internalPaint (Graphics& g, const Rectangle& repaintArea, auto bounds = getBoundsRelativeToTopLevelComponent(); - auto dirtyBounds = repaintArea; - auto boundsToRedraw = bounds.intersection (dirtyBounds); + auto boundsToRedraw = bounds + .intersection (repaintArea) + .roundToInt() + .to(); + if (! renderContinuous && boundsToRedraw.isEmpty()) return; @@ -998,6 +1005,8 @@ void Component::internalPaint (Graphics& g, const Rectangle& repaintArea, if (opacity <= 0.0f) return; + options.isRepainting = true; + { const auto globalState = g.saveState(); @@ -1020,6 +1029,8 @@ void Component::internalPaint (Graphics& g, const Rectangle& repaintArea, paintOverChildren (g); } + options.isRepainting = false; + #if YUP_ENABLE_COMPONENT_REPAINT_DEBUGGING g.setFillColor (debugColor); g.setOpacity (0.2f); diff --git a/modules/yup_gui/component/yup_Component.h b/modules/yup_gui/component/yup_Component.h index e74fc6893..e0e49ba69 100644 --- a/modules/yup_gui/component/yup_Component.h +++ b/modules/yup_gui/component/yup_Component.h @@ -924,6 +924,7 @@ class YUP_API Component bool isFullScreen : 1; bool unclippedRendering : 1; bool wantsKeyboardFocus : 1; + bool isRepainting : 1; }; union diff --git a/modules/yup_gui/keyboard/yup_KeyModifiers.h b/modules/yup_gui/keyboard/yup_KeyModifiers.h index dfcb1a897..bf91fd563 100644 --- a/modules/yup_gui/keyboard/yup_KeyModifiers.h +++ b/modules/yup_gui/keyboard/yup_KeyModifiers.h @@ -69,6 +69,15 @@ class YUP_API KeyModifiers return modifiers & controlMask; } + /** Checks if the Command key is down. + + @return True if Command is active, false otherwise. + */ + constexpr bool isCommandDown() const noexcept + { + return modifiers & commandMask; + } + /** Checks if the Alt key is down. @return True if Alt is active, false otherwise. @@ -176,10 +185,11 @@ class YUP_API KeyModifiers //============================================================================== static constexpr int shiftMask = 0x0001; static constexpr int controlMask = 0x0002; - static constexpr int altMask = 0x0004; - static constexpr int superMask = 0x0008; - static constexpr int capsLockMask = 0x0010; - static constexpr int numLockMask = 0x0020; + static constexpr int commandMask = 0x0004; + static constexpr int altMask = 0x0008; + static constexpr int superMask = 0x0010; + static constexpr int capsLockMask = 0x0020; + static constexpr int numLockMask = 0x0040; private: int32_t modifiers = 0; diff --git a/modules/yup_gui/mouse/yup_MouseEvent.cpp b/modules/yup_gui/mouse/yup_MouseEvent.cpp index 9c875c757..7e5db45ec 100644 --- a/modules/yup_gui/mouse/yup_MouseEvent.cpp +++ b/modules/yup_gui/mouse/yup_MouseEvent.cpp @@ -117,6 +117,37 @@ MouseEvent MouseEvent::withTranslatedPosition (const Point& translation) return { buttons, modifiers, position.translated (translation), lastMouseDownPosition, lastMouseDownTime, sourceComponent }; } +MouseEvent MouseEvent::withRelativePositionTo (Component* targetComponent) const noexcept +{ + if (targetComponent == nullptr) + return *this; + + // Calculate the position relative to the target component + auto relativePos = position; + + // Walk up the component hierarchy to find the offset from the top-level component + auto currentComponent = targetComponent; + while (currentComponent != nullptr && currentComponent->getParentComponent() != nullptr) + { + relativePos = relativePos - currentComponent->getBounds().getPosition(); + currentComponent = currentComponent->getParentComponent(); + } + + // Also translate the last mouse down position if it exists + auto relativeLastPos = lastMouseDownPosition; + if (lastMouseDownPosition != Point() && targetComponent != nullptr) + { + currentComponent = targetComponent; + while (currentComponent != nullptr && currentComponent->getParentComponent() != nullptr) + { + relativeLastPos = relativeLastPos - currentComponent->getBounds().getPosition(); + currentComponent = currentComponent->getParentComponent(); + } + } + + return { buttons, modifiers, relativePos, relativeLastPos, lastMouseDownTime, targetComponent }; +} + //============================================================================== Point MouseEvent::getLastMouseDownPosition() const noexcept diff --git a/modules/yup_gui/mouse/yup_MouseEvent.h b/modules/yup_gui/mouse/yup_MouseEvent.h index bee588a37..08e673097 100644 --- a/modules/yup_gui/mouse/yup_MouseEvent.h +++ b/modules/yup_gui/mouse/yup_MouseEvent.h @@ -65,7 +65,7 @@ class YUP_API MouseEvent /** Creates a MouseEvent object. - @param newButtons The buttons that are currently held down + @param newButtons The buttons that are currently held down @param newModifiers The key modifiers that are currently active @param newPosition The mouse position, relative to the component that receives the event */ @@ -73,7 +73,7 @@ class YUP_API MouseEvent /** Creates a MouseEvent object. - @param newButtons The buttons that are currently held down + @param newButtons The buttons that are currently held down @param newModifiers The key modifiers that are currently active @param newPosition The mouse position, relative to the component that receives the event @param sourceComponent The component that the mouse event applies to @@ -96,31 +96,31 @@ class YUP_API MouseEvent //============================================================================== /** Returns true if the left mouse button is currently held down. - @returns true if the left button is down + @returns true if the left button is down */ bool isLeftButtoDown() const noexcept; /** Returns true if the middle mouse button is currently held down. - @returns true if the middle button is down + @returns true if the middle button is down */ bool isMiddleButtonDown() const noexcept; /** Returns true if the right mouse button is currently held down. - @returns true if the right button is down + @returns true if the right button is down */ bool isRightButtonDown() const noexcept; /** Returns true if any mouse button is currently held down. - @returns true if any button is down + @returns true if any button is down */ bool isAnyButtonDown() const noexcept; /** Returns the current mouse button state. - @returns a bitmask of the buttons that are currently held down + @returns a bitmask of the buttons that are currently held down @see Buttons */ @@ -128,17 +128,17 @@ class YUP_API MouseEvent /** Creates a copy of this event with the specified buttons added. - @param buttonsToAdd the buttons to add to the new event + @param buttonsToAdd the buttons to add to the new event - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withButtons (Buttons buttonsToAdd) const noexcept; /** Creates a copy of this event with the specified buttons removed. - @param buttonsToRemove the buttons to remove from the new event + @param buttonsToRemove the buttons to remove from the new event - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withoutButtons (Buttons buttonsToRemove) const noexcept; @@ -151,9 +151,9 @@ class YUP_API MouseEvent /** Creates a copy of this event with different modifiers. - @param newModifiers the new modifier flags to use + @param newModifiers the new modifier flags to use - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withModifiers (KeyModifiers newModifiers) const noexcept; @@ -166,20 +166,31 @@ class YUP_API MouseEvent /** Creates a copy of this event with a different position. - @param newPosition the new position to use + @param newPosition the new position to use - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withPosition (const Point& newPosition) const noexcept; /** Creates a copy of this event with its position offset by the specified amount. - @param translation the offset to apply to the position + @param translation the offset to apply to the position - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withTranslatedPosition (const Point& translation) const noexcept; + /** Creates a copy of this event with its position relative to the specified component. + + This is used internally by the component system to ensure that mouse events + are delivered with coordinates relative to the receiving component. + + @param targetComponent the component to make the position relative to + + @returns a new MouseEvent object with position relative to the target component + */ + MouseEvent withRelativePositionTo (Component* targetComponent) const noexcept; + //============================================================================== /** Returns the position at which the last mouse-down event occurred. @@ -189,9 +200,9 @@ class YUP_API MouseEvent /** Creates a copy of this event with a different last mouse-down position. - @param newPosition the new last mouse-down position to use + @param newPosition the new last mouse-down position to use - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withLastMouseDownPosition (const Point& newPosition) const noexcept; @@ -203,41 +214,41 @@ class YUP_API MouseEvent /** Creates a copy of this event with a different last mouse-down time. - @param newTime the new time to use + @param newTime the new time to use - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withLastMouseDownTime (yup::Time newTime) const noexcept; //============================================================================== /** Returns the component that this event applies to. - @returns the component that the event occurred on + @returns the component that the event occurred on */ Component* getSourceComponent() const noexcept; /** Creates a copy of this event with a different source component. - @param newComponent the new component to use as the source + @param newComponent the new component to use as the source - @returns a new MouseEvent object + @returns a new MouseEvent object */ MouseEvent withSourceComponent (Component* newComponent) const noexcept; //============================================================================== /** Compares two MouseEvent objects. - @param other the other event to compare with + @param other the other event to compare with - @returns true if the events are identical + @returns true if the events are identical */ bool operator== (const MouseEvent& other) const noexcept; /** Compares two MouseEvent objects. - @param other the other event to compare with + @param other the other event to compare with - @returns true if the events are different + @returns true if the events are different */ bool operator!= (const MouseEvent& other) const noexcept; diff --git a/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp b/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp index fb165f65b..8309e3db7 100644 --- a/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp +++ b/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp @@ -58,7 +58,7 @@ KeyModifiers toKeyModifiers (Uint16 sdlMod) noexcept modifiers |= KeyModifiers::altMask; if (sdlMod & KMOD_GUI) - modifiers |= KeyModifiers::superMask; + modifiers |= KeyModifiers::commandMask; return modifiers; } diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index c88cc4433..0ba53d97a 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -353,12 +353,18 @@ float SDL2ComponentNative::getOpacity() const void SDL2ComponentNative::setFocusedComponent (Component* comp) { if (lastComponentFocused != nullptr) + { lastComponentFocused->focusLost(); + lastComponentFocused->repaint(); + } lastComponentFocused = comp; if (lastComponentFocused) + { lastComponentFocused->focusGained(); + lastComponentFocused->repaint(); + } if (window != nullptr) { @@ -712,14 +718,14 @@ void SDL2ComponentNative::handleMouseMoveOrDrag (const Point& position) { event = event.withSourceComponent (lastComponentClicked); - lastComponentClicked->internalMouseDrag (event); + lastComponentClicked->internalMouseDrag (event.withRelativePositionTo (lastComponentClicked)); } else { updateComponentUnderMouse (event); if (lastComponentUnderMouse != nullptr) - lastComponentUnderMouse->internalMouseMove (event); + lastComponentUnderMouse->internalMouseMove (event.withRelativePositionTo (lastComponentUnderMouse)); } lastMouseMovePosition = position; @@ -755,11 +761,11 @@ void SDL2ComponentNative::handleMouseDown (const Point& position, MouseEv event = event.withLastMouseDownPosition (*lastMouseDownPosition); event = event.withLastMouseDownTime (*lastMouseDownTime); - lastComponentClicked->internalMouseDoubleClick (event); + lastComponentClicked->internalMouseDoubleClick (event.withRelativePositionTo (lastComponentClicked)); } else { - lastComponentClicked->internalMouseDown (event); + lastComponentClicked->internalMouseDown (event.withRelativePositionTo (lastComponentClicked)); } lastMouseDownPosition = position; @@ -789,7 +795,7 @@ void SDL2ComponentNative::handleMouseUp (const Point& position, MouseEven { event = event.withSourceComponent (lastComponentClicked); - lastComponentClicked->internalMouseUp (event); + lastComponentClicked->internalMouseUp (event.withRelativePositionTo (lastComponentClicked)); } if (currentMouseButtons == MouseEvent::noButtons) @@ -800,8 +806,6 @@ void SDL2ComponentNative::handleMouseUp (const Point& position, MouseEven } lastMouseMovePosition = position; - lastMouseDownPosition.reset(); - lastMouseDownTime.reset(); if (isMouseOutsideWindow (window)) handleFocusChanged (false); @@ -828,15 +832,15 @@ void SDL2ComponentNative::handleMouseWheel (const Point& position, const { event = event.withSourceComponent (lastComponentClicked); - lastComponentClicked->internalMouseWheel (event, wheelData); + lastComponentClicked->internalMouseWheel (event.withRelativePositionTo (lastComponentClicked), wheelData); } else if (lastComponentFocused != nullptr) { - lastComponentFocused->internalMouseWheel (event, wheelData); + lastComponentFocused->internalMouseWheel (event.withRelativePositionTo (lastComponentFocused), wheelData); } else if (lastComponentUnderMouse != nullptr) { - lastComponentUnderMouse->internalMouseWheel (event, wheelData); + lastComponentUnderMouse->internalMouseWheel (event.withRelativePositionTo (lastComponentUnderMouse), wheelData); } } @@ -852,7 +856,11 @@ void SDL2ComponentNative::handleMouseEnter (const Point& position) updateComponentUnderMouse (event); if (lastComponentUnderMouse != nullptr) - lastComponentUnderMouse->mouseEnter (event); + { + event = event.withSourceComponent (lastComponentUnderMouse); + + lastComponentUnderMouse->mouseEnter (event.withRelativePositionTo (lastComponentUnderMouse)); + } } void SDL2ComponentNative::handleMouseLeave (const Point& position) @@ -863,7 +871,11 @@ void SDL2ComponentNative::handleMouseLeave (const Point& position) .withPosition (position); if (lastComponentUnderMouse != nullptr) - lastComponentUnderMouse->mouseExit (event); + { + event = event.withSourceComponent (lastComponentUnderMouse); + + lastComponentUnderMouse->mouseExit (event.withRelativePositionTo (lastComponentUnderMouse)); + } updateComponentUnderMouse (event); } @@ -876,7 +888,7 @@ void SDL2ComponentNative::handleKeyDown (const KeyPress& keys, const PointinternalKeyDown (keys, position); + lastComponentFocused->internalKeyDown (keys, position); // TODO: remove position else component.internalKeyDown (keys, position); } @@ -887,7 +899,7 @@ void SDL2ComponentNative::handleKeyUp (const KeyPress& keys, const Point& keyState.set (keys.getKey(), 0); if (lastComponentFocused != nullptr) - lastComponentFocused->internalKeyUp (keys, position); + lastComponentFocused->internalKeyUp (keys, position); // TODO: remove position else component.internalKeyUp (keys, position); } @@ -1016,18 +1028,18 @@ void SDL2ComponentNative::updateComponentUnderMouse (const MouseEvent& event) { if (lastComponentUnderMouse == nullptr) { - child->internalMouseEnter (event); + child->internalMouseEnter (event.withRelativePositionTo (child)); } else if (lastComponentUnderMouse != child) { - lastComponentUnderMouse->internalMouseExit (event); - child->internalMouseEnter (event); + lastComponentUnderMouse->internalMouseExit (event.withRelativePositionTo (lastComponentUnderMouse)); + child->internalMouseEnter (event.withRelativePositionTo (child)); } } else { if (lastComponentUnderMouse != nullptr) - lastComponentUnderMouse->internalMouseExit (event); + lastComponentUnderMouse->internalMouseExit (event.withRelativePositionTo (lastComponentUnderMouse)); } lastComponentUnderMouse = child; diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index 70a19f482..37d6cb93d 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -91,8 +91,8 @@ void paintSlider (Graphics& g, const ApplicationTheme& theme, const Slider& s) g.setStrokeWidth (s.proportionOfWidth (0.03f)); g.strokePath (foregroundLine); - const auto& font = theme.getDefaultFont(); /* + const auto& font = theme.getDefaultFont(); StyledText text; text.appendText (font, s.proportionOfHeight (0.1f), s.proportionOfHeight (0.1f), String (s.getValue(), 3).toRawUTF8()); text.layout (s.getLocalBounds().reduced (5).removeFromBottom (s.proportionOfWidth (0.1f)), StyledText::center); @@ -109,27 +109,107 @@ void paintSlider (Graphics& g, const ApplicationTheme& theme, const Slider& s) } } +void paintTextEditor (Graphics& g, const ApplicationTheme& theme, const TextEditor& t) +{ + auto bounds = t.getLocalBounds(); + auto textBounds = t.getTextBounds(); + auto scrollOffset = t.getScrollOffset(); + constexpr auto cornerRadius = 6.0f; + + // Draw background + auto backgroundColor = t.findColor (TextEditor::Colors::backgroundColorId).value_or (Colors::white); + g.setFillColor (backgroundColor); + g.fillRoundedRect (bounds.reduced (1.0f), cornerRadius); + + // Draw outline + auto outlineColor = t.hasKeyboardFocus() + ? t.findColor (TextEditor::Colors::focusedOutlineColorId).value_or (Colors::cornflowerblue) + : t.findColor (TextEditor::Colors::outlineColorId).value_or (Colors::gray); + g.setStrokeColor (outlineColor); + + float strokeWidth = t.hasKeyboardFocus() ? 2.0f : 1.0f; + g.setStrokeWidth (strokeWidth); + + g.strokeRoundedRect (bounds.reduced (1.0f), cornerRadius); + + // Draw selection background + if (t.hasSelection()) + { + auto selectionColor = t.findColor (TextEditor::Colors::selectionColorId).value_or (Colors::cornflowerblue.withAlpha (0.5f)); + g.setFillColor (selectionColor); + + // Get all selection rectangles for proper multiline selection rendering + auto selectionRects = t.getSelectedTextAreas(); + for (const auto& rect : selectionRects) + { + // Adjust each rectangle for scroll offset and text bounds + auto adjustedRect = rect.translated (textBounds.getTopLeft() - scrollOffset); + g.fillRect (adjustedRect); + } + } + + // Draw text with scroll offset + auto textColor = t.findColor (TextEditor::Colors::textColorId).value_or (Colors::gray); + g.setFillColor (textColor); + + auto scrolledTextBounds = textBounds.translated (-scrollOffset.getX(), -scrollOffset.getY()); + g.fillFittedText (t.getStyledText(), scrolledTextBounds); + + // Draw caret + if (t.hasKeyboardFocus() && t.isCaretVisible()) + { + auto caretColor = t.findColor (TextEditor::Colors::caretColorId).value_or (yup::Colors::black); + g.setFillColor (caretColor); + + auto caretBounds = t.getCaretBounds(); + g.fillRect (caretBounds); + } +} + //============================================================================== void paintTextButton (Graphics& g, const ApplicationTheme& theme, const TextButton& b) { - const auto& font = ApplicationTheme::getGlobalTheme()->getDefaultFont(); - auto bounds = b.getLocalBounds().reduced (b.proportionOfWidth (0.01f)); - const auto center = bounds.getCenter(); + auto bounds = b.getLocalBounds(); + constexpr auto cornerRadius = 6.0f; - Path backgroundPath; - backgroundPath.addRoundedRectangle (bounds.reduced (b.proportionOfWidth (0.045f)), 10.0f, 10.0f, 10.0f, 10.0f); - g.setFillColor (b.isButtonDown() ? Color (0xff000000) : Color (0xffffffff)); - g.fillPath (backgroundPath); + Color backgroundColor, textColor; - /* - StyledText text; - text.appendText (font, bounds.getHeight() * 0.5f, bounds.getHeight() * 0.5f, b.getComponentID().toRawUTF8()); - text.layout (bounds.reduced (0.0f, 10.0f), yup::StyledText::center); + if (b.isButtonDown()) + { + backgroundColor = b.findColor (TextButton::Colors::backgroundPressedColorId).value_or (Colors::gray); + textColor = b.findColor (TextButton::Colors::textPressedColorId).value_or (Colors::dimgray); + } + else + { + backgroundColor = b.findColor (TextButton::Colors::backgroundColorId).value_or (Colors::gray); + textColor = b.findColor (TextButton::Colors::textColorId).value_or (Colors::white); + } - g.setStrokeColor (isButtonDown ? Color (0xffffffff) : Color (0xff000000)); - g.strokeFittedText (text, {}); - */ + if (b.isButtonOver()) + { + backgroundColor = backgroundColor.brighter (0.2f); + textColor = textColor.brighter (0.2f); + } + + // Draw background with flat color (no gradient for modern flat design) + g.setFillColor (backgroundColor); + g.fillRoundedRect (bounds.reduced (1.0f), cornerRadius); + + // Draw modern outline + Color outlineColor = b.hasKeyboardFocus() + ? b.findColor (TextButton::Colors::outlineFocusedColorId).value_or (Colors::cornflowerblue) + : b.findColor (TextButton::Colors::outlineColorId).value_or (Colors::dimgray); + g.setStrokeColor (outlineColor); + + float strokeWidth = b.hasKeyboardFocus() ? 2.0f : 1.0f; + g.setStrokeWidth (strokeWidth); + + g.strokeRoundedRect (bounds.reduced (1.0f), cornerRadius); + + // Draw text + g.setFillColor (textColor); + g.fillFittedText (b.getStyledText(), b.getTextBounds()); } //============================================================================== @@ -167,6 +247,7 @@ ApplicationTheme::Ptr createThemeVersion1() theme->setComponentStyle (ComponentStyle::createStyle (paintSlider)); theme->setComponentStyle (ComponentStyle::createStyle (paintTextButton)); + theme->setComponentStyle (ComponentStyle::createStyle (paintTextEditor)); theme->setComponentStyle