diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e9f460..ee4823c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,17 +30,22 @@ else () endif () add_definitions(-DMIRAGE_HDR_FORMAT=${MIRAGE_HDR_FORMAT} -DMIRAGE_PIXEL_FORMAT=${MIRAGE_PIXEL_FORMAT}) -# 配置输出目录 -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(MIRAGE_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) - include(cmake/retrieve_files.cmake) include(cmake/detect_os.cmake) include(cmake/config_macos.cmake) include(cmake/compile_shaders.cmake) include(cmake/mingw_dll.cmake) +include(cmake/mirage_utils.cmake) + +# 配置输出目录 +configure_project_defaults() + +# --- 设置项目根目录变量 --- +# **定义项目源代码根目录变量**: +# CMAKE_CURRENT_SOURCE_DIR 在根 CMakeLists.txt 中即为项目源代码的根目录 +# 使用 PARENT_SCOPE 使该变量在调用此函数的 CMakeLists.txt 文件中也可用 +set(MIRAGE_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +message(STATUS "mirage 项目根源目录 (MIRAGE_ROOT_DIR) 设置为: ${MIRAGE_ROOT_DIR}") # 如果是Debug模式, 添加宏定义 if (CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/cmake/mirage_utils.cmake b/cmake/mirage_utils.cmake new file mode 100644 index 0000000..4a4b4bb --- /dev/null +++ b/cmake/mirage_utils.cmake @@ -0,0 +1,44 @@ + +# 定义一个函数来配置项目的默认设置 +# 这包括设置输出目录和项目根目录变量 +function(configure_project_defaults) + # 检查是否在顶层 CMakeLists.txt 中调用 (可选但推荐) + # 确保 CMAKE_SOURCE_DIR 和 CMAKE_CURRENT_SOURCE_DIR 相同 + if(NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + message(WARNING "configure_project_defaults() 应该在项目的根 CMakeLists.txt 中调用。") + # 如果您确实需要在子目录中设置不同的根目录,请调整此逻辑 + endif() + + # --- 配置输出目录 --- + # 使用 CMAKE_BINARY_DIR 作为基础构建目录 + # ${CMAKE_BINARY_DIR} 指向您配置 CMake 时指定的构建目录 + # 例如,在 CLion 中通常是 cmake-build-debug 或 cmake-build-release + # 如果手动运行 cmake ..,它就是您运行 cmake 命令的目录 + + # **设置可执行文件输出路径**: + # 对于单配置生成器 (如 Makefiles, Ninja), 可执行文件将位于 /bin/ + # 对于多配置生成器 (如 Visual Studio, Xcode), CMake 通常会自动在此路径下附加配置名称 + # (例如 /bin/Debug/, /bin/Release/) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin CACHE PATH "Directory for runtime executables") + message(STATUS "运行时输出目录设置为: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") + + # **设置库文件输出路径 (共享库和静态库)**: + # 规则同上,库文件将位于 /lib/ 或 /lib// + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib CACHE PATH "Directory for shared libraries") + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib CACHE PATH "Directory for static libraries") + message(STATUS "库输出目录设置为: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") + message(STATUS "存档输出目录设置为: ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}") + + # --- 提示 --- + # 这种全局设置输出目录的方法对于中小型项目是常见的。 + # 对于更复杂的项目或需要更细粒度控制的情况,可以考虑为每个目标(target)单独设置输出目录属性: + # 例如: + # add_executable(my_app main.cpp) + # set_target_properties(my_app PROPERTIES + # RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/executables" + # ) + # add_library(my_lib STATIC my_lib.cpp) + # set_target_properties(my_lib PROPERTIES + # ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/static_libs" + # ) +endfunction() \ No newline at end of file diff --git a/cmake/retrieve_files.cmake b/cmake/retrieve_files.cmake index 4f780a9..b8ed860 100644 --- a/cmake/retrieve_files.cmake +++ b/cmake/retrieve_files.cmake @@ -155,4 +155,85 @@ function(retrieve_files path out_files) # 合并结果到输出变量 set(${out_files} ${${out_files}} ${temp_files} PARENT_SCOPE) +endfunction() + +#[=======================================================================[ + 用于添加资源文件并在编译后复制到目标文件 (可执行文件或库) 所在目录 + 参数: + TARGET_NAME: 目标 (可执行文件或库) 的名称 + RESOURCE_FILE_PATHS - 一个或多个要复制的资源文件的路径 (可以是相对或绝对路径) +#]=======================================================================] +function(add_resources TARGET_NAME) + # 获取 TARGET_NAME 之后的所有参数作为资源文件列表 + set(RESOURCE_FILES ${ARGN}) + + # 检查目标是否存在 (可选,但推荐) + if(NOT TARGET ${TARGET_NAME}) + message(FATAL_ERROR "目标 '${TARGET_NAME}' 不存在,无法添加资源。") + return() + endif() + + # **获取目标可执行文件的输出目录** (只需执行一次) + # 对于多配置生成器 (如 Visual Studio, Xcode),使用 $ + # 对于单配置生成器 (如 Makefiles, Ninja),使用 CMAKE_RUNTIME_OUTPUT_DIRECTORY 或 $ + if(CMAKE_CONFIGURATION_TYPES) + set(DESTINATION_DIR "$") + else() + if(CMAKE_RUNTIME_OUTPUT_DIRECTORY) + set(DESTINATION_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") + else() + # 使用 $ 通常更健壮 + set(DESTINATION_DIR "$") + endif() + endif() + + # **遍历所有传入的资源文件** + foreach(RESOURCE_FILE ${RESOURCE_FILES}) + set(ABS_RESOURCE_FILE "") # 重置/初始化 + + # **处理相对路径和绝对路径,并检查文件是否存在** + if(IS_ABSOLUTE "${RESOURCE_FILE}") + # 如果是绝对路径,直接检查是否存在 + if(EXISTS "${RESOURCE_FILE}") + set(ABS_RESOURCE_FILE "${RESOURCE_FILE}") + else() + message(WARNING "资源文件未找到 (绝对路径,将跳过): ${RESOURCE_FILE}") + continue() # 处理下一个文件 + endif() + else() + # 如果是相对路径,相对于当前 CMakeLists.txt 文件所在目录解析 + set(TEMP_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_FILE}") + if(EXISTS "${TEMP_PATH}") + set(ABS_RESOURCE_FILE "${TEMP_PATH}") + else() + message(WARNING "资源文件未找到 (相对路径,将跳过): ${RESOURCE_FILE} (相对于: ${CMAKE_CURRENT_SOURCE_DIR})") + continue() # 处理下一个文件 + endif() + endif() + + # **检查是否是文件而非目录** (copy_if_different 用于文件) + if(IS_DIRECTORY "${ABS_RESOURCE_FILE}") + message(WARNING "提供的资源是一个目录,而非文件 (将跳过): ${ABS_RESOURCE_FILE}. 请使用其他方法复制目录。") + continue() + endif() + + # **获取资源文件的文件名部分** + get_filename_component(RESOURCE_FILENAME "${ABS_RESOURCE_FILE}" NAME) + + # **为每个文件添加自定义构建后命令** + add_custom_command( + TARGET ${TARGET_NAME} # 关联到目标 + POST_BUILD # 在目标构建完成后执行 + COMMAND ${CMAKE_COMMAND} -E copy_if_different # 使用 cmake 内建命令复制文件 (仅在文件不同时复制) + "${ABS_RESOURCE_FILE}" # 源文件 (绝对路径) + "${DESTINATION_DIR}/${RESOURCE_FILENAME}" # **目标路径** (可执行文件目录 + 文件名) + COMMENT "正在复制资源 ${RESOURCE_FILENAME} 到 ${TARGET_NAME} 的输出目录" # 构建时显示的注释 + VERBATIM # 确保特殊字符被正确处理 + ) + + # (可选) 将资源文件添加到目标源文件中,以便在某些 IDE 中显示 + # 注意:如果文件不在源目录树下,某些 IDE 可能不会正确显示 + # target_sources(${TARGET_NAME} PRIVATE "${ABS_RESOURCE_FILE}") + + endforeach() endfunction() \ No newline at end of file diff --git a/example/src/main.cpp b/example/src/main.cpp index c9d8d6c..7201781 100644 --- a/example/src/main.cpp +++ b/example/src/main.cpp @@ -11,6 +11,10 @@ int main(int argc, char* argv[]) { mirage_app::get().init(); + auto c1 = linear_color::from_string("#FF0000"); + auto c2 = linear_color::from_string("rgb(255, 0, 0)"); + auto c3 = linear_color::from_string("rgba(255, 0, 0, 255)"); + auto& manager = font_manager::instance(); manager.add_font(L"C:/Users/46944/AppData/Local/Microsoft/Windows/Fonts/MapleMono-NF-CN-Regular.ttf"); manager.add_font(L"C:/Windows/Fonts/msyh.ttc"); diff --git a/src/mirage_app/mirage.cpp b/src/mirage_app/mirage.cpp index ff079f0..aa8c207 100644 --- a/src/mirage_app/mirage.cpp +++ b/src/mirage_app/mirage.cpp @@ -8,6 +8,7 @@ #include "window/mwindow.h" #include "misc/mirage_scoped_duration_timer.h" #include "platform_window/platform_window.h" +#include "style/mirage_style.h" void mirage_log(const char* tag, uint32_t log_level, uint32_t log_item_id, const char* message_or_null, uint32_t line_nr, const char* filename_or_null, void* user_data) { @@ -57,6 +58,9 @@ void mirage_app::init() { mirage_scoped_duration_timer timer(duration); last_time = get_current_time(); + if (!mirage_style::get().load_config("default_style.toml")) { + fprintf(stderr, "mirage: 无法加载样式配置\n"); + } render_context = mirage_create_render_context(); render_context->init(); const sg_desc desc = { @@ -70,7 +74,7 @@ void mirage_app::init() { render_context->end_init(); } // 初始化用时 - std::cout << "mirage: " << "Initialization took " << std::chrono::duration_cast(duration).count() << "ms" << std::endl; + fprintf(stderr, "mirage: 初始化耗时 %lld ms\n", std::chrono::duration_cast(duration).count()); } diff --git a/src/mirage_core/misc/name.h b/src/mirage_core/misc/name.h index 983045f..d1ccfc7 100644 --- a/src/mirage_core/misc/name.h +++ b/src/mirage_core/misc/name.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include /** * name_t diff --git a/src/mirage_image/color.cpp b/src/mirage_image/color.cpp index 214f143..973a097 100644 --- a/src/mirage_image/color.cpp +++ b/src/mirage_image/color.cpp @@ -3,108 +3,156 @@ #include #include -linear_color linear_color::from_string(const std::string& in_str) { - // **定义一个表示无效输入的默认返回值 (C++20 designated initializers)** - // Made static constexpr for potential compile-time use if needed elsewhere - static linear_color invalid_color = { 0.0f, 0.0f, 0.0f, 0.0f }; - - std::string str = in_str; - - // **1. 去除前后空格 (No major C++23 change here, std::find_if is efficient)** - auto trim_func = [](unsigned char ch){ return !std::isspace(ch); }; - str.erase(str.begin(), std::ranges::find_if(str, trim_func)); - str.erase(std::find_if(str.rbegin(), str.rend(), trim_func).base(), str.end()); - - // **2. 检查格式前缀和后缀 (C++20 starts_with/ends_with)** - bool is_rgba = false; - std::string_view value_part_sv; - - if (str.starts_with("rgb(") && str.ends_with(')')) { - is_rgba = false; - value_part_sv = std::string_view(str).substr(4, str.length() - 5); // Skip "rgb(" and ")" - } else if (str.starts_with("rgba(") && str.ends_with(')')) { - is_rgba = true; - value_part_sv = std::string_view(str).substr(5, str.length() - 6); // Skip "rgba(" and ")" - } else { - return invalid_color; // **无效格式前缀/后缀** - } - - if (value_part_sv.empty() && (is_rgba ? 4 : 3) > 0) { - // Handle cases like "rgb()" or "rgba()" which are invalid if components are expected - return invalid_color; // **括号内为空** - } - - // **3. 分割数值字符串 (Using stringstream, C++23 views could be an alternative but more complex here)** - std::vector values; - std::stringstream ss; - ss << value_part_sv; - std::string segment; - - while (std::getline(ss, segment, ',')) { - // **4. 处理每个分量** - // 去除分量前后的空格 - segment.erase(segment.begin(), std::ranges::find_if(segment, trim_func)); - segment.erase(std::find_if(segment.rbegin(), segment.rend(), trim_func).base(), segment.end()); - - if (segment.empty()) { - return invalid_color; // **空分量值** - } - - const char* const first = segment.data(); - const char* const last = first + segment.size(); - float component_value = 0.0f; - - // **C++23 std::string::contains to check for decimal point** - if (segment.contains('.')) { - // **处理浮点数 [0, 1]** - float val = 0.0f; - auto [ptr, ec] = std::from_chars(first, last, val); - - // Check for errors: conversion failed OR not the entire segment was parsed - if (ec != std::errc() || ptr != last) { - return invalid_color; // **无效浮点数格式** - } - - // Check range [0, 1] for floats - if (val < 0.0f || val > 1.0f) { - return invalid_color; // **浮点数值超出 [0, 1] 范围** - } - component_value = val; - - } else { - // **处理整数 [0, 255]** - int int_val = 0; - auto [ptr, ec] = std::from_chars(first, last, int_val); - - // Check for errors: conversion failed OR not the entire segment was parsed - if (ec != std::errc() || ptr != last) { - return invalid_color; // **无效整数格式** - } - - // Check range [0, 255] for integers - if (int_val < 0 || int_val > 255) { - return invalid_color; // **整数值超出 [0, 255] 范围** - } - - // **归一化: 将 uint8 范围的值映射到 [0, 1] float** - component_value = static_cast(int_val) / 255.0f; - } - - values.push_back(component_value); - } - - // **5. 检查分量数量是否匹配** - if (is_rgba) { - if (values.size() != 4) { - return invalid_color; // **rgba 分量数量错误** - } - return { values[0], values[1], values[2], values[3] }; - } - - // rgb - if (values.size() != 3) { - return invalid_color; // **rgb 分量数量错误** - } - // **对于 rgb,alpha 默认为 1.0f** - return { values[0], values[1], values[2], 1.f }; +/** + * @brief 将十六进制字符转换为整数值 + * + * @param hex 十六进制字符 (0-9, A-F, a-f) + * @return 对应的整数值 (0-15),如果字符无效则返回-1 + */ +int hex_char_to_int(char hex) { + if (hex >= '0' && hex <= '9') + return hex - '0'; + if (hex >= 'A' && hex <= 'F') + return hex - 'A' + 10; + if (hex >= 'a' && hex <= 'f') + return hex - 'a' + 10; + return -1; // 无效字符 +} + +std::optional linear_color::from_string(const std::string& in_str) { + linear_color result{}; + + // 处理十六进制格式 + if (!in_str.empty() && in_str[0] == '#') { + // **支持的十六进制格式:** #RGB, #RGBA, #RRGGBB, #RRGGBBAA + size_t length = in_str.length(); + + // 检查格式并解析 + if (length == 4) { // #RGB 格式 + // 检查所有字符是否都是有效的十六进制数字 + for (int i = 1; i < 4; i++) { + if (!std::isxdigit(in_str[i])) + return std::nullopt; + } + + // 对于 #RGB 格式,每个字符需要重复:R -> RR, G -> GG, B -> BB + int r = hex_char_to_int(in_str[1]); + int g = hex_char_to_int(in_str[2]); + int b = hex_char_to_int(in_str[3]); + + // 将 0-15 的值扩展到 0-255 的范围 + result.r = (r * 16 + r) / 255.0f; + result.g = (g * 16 + g) / 255.0f; + result.b = (b * 16 + b) / 255.0f; + result.a = 1.0f; // 默认为完全不透明 + + return result; + } + if (length == 5) { // #RGBA 格式 + // 检查所有字符是否都是有效的十六进制数字 + for (int i = 1; i < 5; i++) { + if (!std::isxdigit(in_str[i])) + return std::nullopt; + } + + int r = hex_char_to_int(in_str[1]); + int g = hex_char_to_int(in_str[2]); + int b = hex_char_to_int(in_str[3]); + int a = hex_char_to_int(in_str[4]); + + // 将 0-15 的值扩展到 0-255 的范围 + result.r = (r * 16 + r) / 255.0f; + result.g = (g * 16 + g) / 255.0f; + result.b = (b * 16 + b) / 255.0f; + result.a = (a * 16 + a) / 255.0f; + + return result; + } + if (length == 7) { // #RRGGBB 格式 + // 检查所有字符是否都是有效的十六进制数字 + for (int i = 1; i < 7; i++) { + if (!std::isxdigit(in_str[i])) + return std::nullopt; + } + + int r = (hex_char_to_int(in_str[1]) << 4) + hex_char_to_int(in_str[2]); + int g = (hex_char_to_int(in_str[3]) << 4) + hex_char_to_int(in_str[4]); + int b = (hex_char_to_int(in_str[5]) << 4) + hex_char_to_int(in_str[6]); + + result.r = r / 255.0f; + result.g = g / 255.0f; + result.b = b / 255.0f; + result.a = 1.0f; // 默认为完全不透明 + + return result; + } + if (length == 9) { // #RRGGBBAA 格式 + // 检查所有字符是否都是有效的十六进制数字 + for (int i = 1; i < 9; i++) { + if (!std::isxdigit(in_str[i])) + return std::nullopt; + } + + int r = (hex_char_to_int(in_str[1]) << 4) + hex_char_to_int(in_str[2]); + int g = (hex_char_to_int(in_str[3]) << 4) + hex_char_to_int(in_str[4]); + int b = (hex_char_to_int(in_str[5]) << 4) + hex_char_to_int(in_str[6]); + int a = (hex_char_to_int(in_str[7]) << 4) + hex_char_to_int(in_str[8]); + + result.r = r / 255.0f; + result.g = g / 255.0f; + result.b = b / 255.0f; + result.a = a / 255.0f; + + return result; + } + // 无效的十六进制格式长度 + return std::nullopt; + } + + // 如果不是十六进制格式,继续处理 RGB/RGBA 格式 + int r_int, g_int, b_int, a_int = 255; // Alpha 默认值为 255 (对应 1.0f) + int components_read = 0; + char closing_paren = '\0'; + + // 尝试匹配 rgba(r,g,b,a) 格式 + components_read = std::sscanf(in_str.c_str(), "rgba(%d,%d,%d,%d %c", + &r_int, &g_int, &b_int, &a_int, &closing_paren); + + if (components_read == 5 && closing_paren == ')') { + // 验证数值范围 [0, 255] + if (r_int < 0 || r_int > 255 || g_int < 0 || g_int > 255 || + b_int < 0 || b_int > 255 || a_int < 0 || a_int > 255) { + return std::nullopt; // 数值超出范围 + } + + // 归一化到 [0.0, 1.0] + result.r = static_cast(r_int) / 255.0f; + result.g = static_cast(g_int) / 255.0f; + result.b = static_cast(b_int) / 255.0f; + result.a = static_cast(a_int) / 255.0f; + return result; + } + + // 如果 RGBA 格式不匹配,尝试匹配 rgb(r,g,b) 格式 + closing_paren = '\0'; // 重置 + components_read = std::sscanf(in_str.c_str(), "rgb(%d,%d,%d %c", + &r_int, &g_int, &b_int, &closing_paren); + + if (components_read == 4 && closing_paren == ')') { + // 验证数值范围 [0, 255] + if (r_int < 0 || r_int > 255 || g_int < 0 || g_int > 255 || + b_int < 0 || b_int > 255) { + return std::nullopt; // 数值超出范围 + } + + // 归一化到 [0.0, 1.0],Alpha 使用默认值 1.0f + result.r = static_cast(r_int) / 255.0f; + result.g = static_cast(g_int) / 255.0f; + result.b = static_cast(b_int) / 255.0f; + result.a = 1.0f; + return result; + } + + // 所有格式都不匹配 + return std::nullopt; } diff --git a/src/mirage_image/color.h b/src/mirage_image/color.h index 28b10ae..710b890 100644 --- a/src/mirage_image/color.h +++ b/src/mirage_image/color.h @@ -148,7 +148,7 @@ public: * @note **如果字符串格式无效或颜色分量值无效/超出范围,则返回 linear_color{0.0f, 0.0f, 0.0f, 0.0f}。** * 使用 [[nodiscard]] 提示调用者应检查返回值。 */ - [[nodiscard]] static linear_color from_string(const std::string& in_str); + [[nodiscard]] static std::optional from_string(const std::string& in_str); //-------------- 成员变量 -------------- diff --git a/src/mirage_widget/CMakeLists.txt b/src/mirage_widget/CMakeLists.txt index c5f0095..6f5bcd3 100644 --- a/src/mirage_widget/CMakeLists.txt +++ b/src/mirage_widget/CMakeLists.txt @@ -8,3 +8,5 @@ add_subdirectory(third_party/tomlplusplus) add_library(${PROJECT_NAME} STATIC ${SRC_FILES}) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(${PROJECT_NAME} PUBLIC mirage_core mirage_render tomlplusplus::tomlplusplus) + +add_resources(${PROJECT_NAME} src/style/default_style.toml) diff --git a/src/mirage_widget/src/style/default_style.toml b/src/mirage_widget/src/style/default_style.toml index 4eb25dd..4d7f257 100644 --- a/src/mirage_widget/src/style/default_style.toml +++ b/src/mirage_widget/src/style/default_style.toml @@ -4,6 +4,7 @@ version = "0.0.1" description = "mirage的默认样式" author = "奶酪" + [button] hover_color = "rgb(31, 31, 31)" pressed_color = "rgb(31, 31, 31)" diff --git a/src/mirage_widget/src/style/mirage_style.cpp b/src/mirage_widget/src/style/mirage_style.cpp index 7d7f67c..12f80fd 100644 --- a/src/mirage_widget/src/style/mirage_style.cpp +++ b/src/mirage_widget/src/style/mirage_style.cpp @@ -6,5 +6,7 @@ bool mirage_style::load_config(const std::filesystem::path& in_filename) { } catch (const toml::parse_error& err) { fprintf(stderr, "%s\n", err.what()); + return false; } + return true; }