diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..a65df8a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,99 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main", "beta", "alpha" ] + pull_request: + branches: [ "main", "beta", "alpha" ] + schedule: + - cron: '42 23 * * 3' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ad9bf76..c8bf401 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -7,9 +7,9 @@ permissions: on: push: - branches: [ "main" ] + branches: [ "main", "beta", "alpha" ] pull_request: - branches: [ "main" ] + branches: [ "main", "beta", "alpha" ] jobs: build: diff --git a/.scripts/Setup.ps1 b/.scripts/Setup.ps1 index 4171528..4bfa893 100644 --- a/.scripts/Setup.ps1 +++ b/.scripts/Setup.ps1 @@ -43,9 +43,14 @@ $projectsList = @( @{ Module = "Core" Projects = @( + @{ Folder = "Core"; Name = "CatalystUI.Attributes" }, + @{ Folder = "Core"; Name = "CatalystUI.Collections" }, + @{ Folder = "Core"; Name = "CatalystUI.Threading" }, + @{ Folder = "Tooling"; Name = "CatalystUI.Analyzers" }, + @{ Folder = "Tooling"; Name = "CatalystUI.CodeFix" }, @{ Folder = "Core"; Name = "CatalystUI.Core" } ) - PromptIgnore = $false + PromptIgnore = $true } ) diff --git a/CatalystUI/.editorconfig b/CatalystUI/.editorconfig new file mode 100644 index 0000000..355e106 --- /dev/null +++ b/CatalystUI/.editorconfig @@ -0,0 +1,300 @@ +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = -------------------------------------------------------------------------------------------------\nCatalystUI Framework for .NET Core - https://catalystui.org/\nCopyright (c) 2025 CatalystUI LLC. All rights reserved.\n\nThis file is part of CatalystUI and is provided as part of an early-access release.\nUnauthorized commercial use, distribution, or modification is strictly prohibited.\n\nThis software is not open source and is not publicly licensed.\nFor full terms, see the LICENSE and NOTICE files in the project root.\n------------------------------------------------------------------------------------------------- + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.all_upper.required_prefix = +dotnet_naming_style.all_upper.required_suffix = +dotnet_naming_style.all_upper.word_separator = _ +dotnet_naming_style.all_upper.capitalization = all_upper + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.begins_with_underscore.required_prefix = _ +dotnet_naming_style.begins_with_underscore.required_suffix = +dotnet_naming_style.begins_with_underscore.word_separator = +dotnet_naming_style.begins_with_underscore.capitalization = camel_case + +# Naming rule for constant fields +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = public +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly +dotnet_naming_rule.static_readonly_fields_should_be_upper_case.severity = suggestion +dotnet_naming_rule.static_readonly_fields_should_be_upper_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_should_be_upper_case.style = all_upper +dotnet_naming_symbols.const_fields.applicable_kinds = field +dotnet_naming_symbols.const_fields.applicable_accessibilities = public +dotnet_naming_symbols.const_fields.required_modifiers = const +dotnet_naming_rule.const_fields_should_be_upper_case.severity = suggestion +dotnet_naming_rule.const_fields_should_be_upper_case.symbols = const_fields +dotnet_naming_rule.const_fields_should_be_upper_case.style = all_upper + +# Naming correction for enum members (only supports ReSharper; see https://github.com/dotnet/roslyn/issues/24209) +resharper_csharp_naming_rule.enum_member = AaBb + +# Naming rule for instance fields +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = * +dotnet_naming_symbols.private_fields.required_modifiers = +dotnet_naming_rule.private_fields_should_be_begins_with_underscore.severity = suggestion +dotnet_naming_rule.private_fields_should_be_begins_with_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_begins_with_underscore.style = begins_with_underscore + +# ReSharper properties +resharper_blank_lines_after_multiline_statements = 0 +resharper_blank_lines_after_start_comment = 1 +resharper_blank_lines_after_using_list = 1 +resharper_blank_lines_around_auto_property = 1 +resharper_blank_lines_around_field = 1 +resharper_blank_lines_around_invocable = 1 +resharper_blank_lines_around_namespace = 1 +resharper_blank_lines_around_property = 1 +resharper_blank_lines_around_region = 1 +resharper_blank_lines_around_single_line_type = 0 +resharper_blank_lines_around_type = 1 +resharper_blank_lines_before_control_transfer_statements = 0 +resharper_blank_lines_inside_namespace = 1 +resharper_blank_lines_inside_type = 1 +resharper_csharp_remove_spaces_on_blank_lines = false +resharper_space_within_array_access_brackets = false +resharper_space_within_array_rank_brackets = false +resharper_space_within_list_pattern_brackets = true \ No newline at end of file diff --git a/CatalystUI/CatalystUI.sln b/CatalystUI/CatalystUI.sln index c3a3324..956b7d8 100644 --- a/CatalystUI/CatalystUI.sln +++ b/CatalystUI/CatalystUI.sln @@ -4,6 +4,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{7EC51871-4 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Core", "Core\CatalystUI.Core\CatalystUI.Core.csproj", "{68F496AC-9438-40F1-9DF8-97363033D661}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tooling", "Tooling", "{5D38F696-8C11-4C9A-B50E-2C33AA7FAA6C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Profiling", "Tooling\CatalystUI.Profiling\CatalystUI.Profiling.csproj", "{10856DCF-AD1F-45C2-B995-E36CA4F8751B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Collections", "Core\CatalystUI.Collections\CatalystUI.Collections.csproj", "{9B36BF4B-52A9-4881-8D01-391627D51AB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Threading", "Core\CatalystUI.Threading\CatalystUI.Threading.csproj", "{BFC8674D-AE56-4FF4-94B1-ACF5D6B2A4FA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Analyzers", "Tooling\CatalystUI.Analyzers\CatalystUI.Analyzers.csproj", "{A3936CB7-DC31-414B-9E40-CB9436391068}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Attributes", "Core\CatalystUI.Attributes\CatalystUI.Attributes.csproj", "{44E8E3D2-FE47-49EA-A397-EB680E33AA2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.CodeFix", "Tooling\CatalystUI.CodeFix\CatalystUI.CodeFix.csproj", "{E5319DB6-E93C-4A7D-9B3B-F219206BBC54}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -14,8 +28,38 @@ Global {68F496AC-9438-40F1-9DF8-97363033D661}.Debug|Any CPU.Build.0 = Debug|Any CPU {68F496AC-9438-40F1-9DF8-97363033D661}.Release|Any CPU.ActiveCfg = Release|Any CPU {68F496AC-9438-40F1-9DF8-97363033D661}.Release|Any CPU.Build.0 = Release|Any CPU + {10856DCF-AD1F-45C2-B995-E36CA4F8751B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10856DCF-AD1F-45C2-B995-E36CA4F8751B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10856DCF-AD1F-45C2-B995-E36CA4F8751B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10856DCF-AD1F-45C2-B995-E36CA4F8751B}.Release|Any CPU.Build.0 = Release|Any CPU + {9B36BF4B-52A9-4881-8D01-391627D51AB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B36BF4B-52A9-4881-8D01-391627D51AB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B36BF4B-52A9-4881-8D01-391627D51AB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B36BF4B-52A9-4881-8D01-391627D51AB9}.Release|Any CPU.Build.0 = Release|Any CPU + {BFC8674D-AE56-4FF4-94B1-ACF5D6B2A4FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFC8674D-AE56-4FF4-94B1-ACF5D6B2A4FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFC8674D-AE56-4FF4-94B1-ACF5D6B2A4FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFC8674D-AE56-4FF4-94B1-ACF5D6B2A4FA}.Release|Any CPU.Build.0 = Release|Any CPU + {A3936CB7-DC31-414B-9E40-CB9436391068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3936CB7-DC31-414B-9E40-CB9436391068}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3936CB7-DC31-414B-9E40-CB9436391068}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3936CB7-DC31-414B-9E40-CB9436391068}.Release|Any CPU.Build.0 = Release|Any CPU + {44E8E3D2-FE47-49EA-A397-EB680E33AA2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44E8E3D2-FE47-49EA-A397-EB680E33AA2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44E8E3D2-FE47-49EA-A397-EB680E33AA2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44E8E3D2-FE47-49EA-A397-EB680E33AA2E}.Release|Any CPU.Build.0 = Release|Any CPU + {E5319DB6-E93C-4A7D-9B3B-F219206BBC54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5319DB6-E93C-4A7D-9B3B-F219206BBC54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5319DB6-E93C-4A7D-9B3B-F219206BBC54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5319DB6-E93C-4A7D-9B3B-F219206BBC54}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {68F496AC-9438-40F1-9DF8-97363033D661} = {7EC51871-49A8-4991-BDAF-F43A4E9B8C9D} + {10856DCF-AD1F-45C2-B995-E36CA4F8751B} = {5D38F696-8C11-4C9A-B50E-2C33AA7FAA6C} + {9B36BF4B-52A9-4881-8D01-391627D51AB9} = {7EC51871-49A8-4991-BDAF-F43A4E9B8C9D} + {BFC8674D-AE56-4FF4-94B1-ACF5D6B2A4FA} = {7EC51871-49A8-4991-BDAF-F43A4E9B8C9D} + {A3936CB7-DC31-414B-9E40-CB9436391068} = {5D38F696-8C11-4C9A-B50E-2C33AA7FAA6C} + {44E8E3D2-FE47-49EA-A397-EB680E33AA2E} = {7EC51871-49A8-4991-BDAF-F43A4E9B8C9D} + {E5319DB6-E93C-4A7D-9B3B-F219206BBC54} = {5D38F696-8C11-4C9A-B50E-2C33AA7FAA6C} EndGlobalSection EndGlobal diff --git a/CatalystUI/Core/CatalystUI.Attributes/CatalystUI.Attributes.csproj b/CatalystUI/Core/CatalystUI.Attributes/CatalystUI.Attributes.csproj new file mode 100644 index 0000000..f1c9654 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Attributes/CatalystUI.Attributes.csproj @@ -0,0 +1,32 @@ + + + + + + Catalyst.Attributes + Catalyst.Attributes + + + CatalystUI Attributes + 1.0.0 + beta.2 + CatalystUI LLC + Attributes API provided by the CatalystUI library. + CatalystUI,attributes + + + + + netstandard2.0 + latest + + + + + false + false + false + none + + + \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Attributes/Threading/CachedDelegateAttribute.cs b/CatalystUI/Core/CatalystUI.Attributes/Threading/CachedDelegateAttribute.cs new file mode 100644 index 0000000..282cbac --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Attributes/Threading/CachedDelegateAttribute.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System; + +namespace Catalyst.Attributes.Threading { + + /// + /// Generates a readonly, cached reference to the annotated method as a delegate. + /// + /// + /// + /// The name for the cached delegate field is prefixed with either _cachedAction or _cachedFunction, + /// depending on whether the method returns void or a value. + /// + /// + /// Valid signatures include: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class CachedDelegateAttribute : Attribute { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Collections/CatalystUI.Collections.csproj b/CatalystUI/Core/CatalystUI.Collections/CatalystUI.Collections.csproj new file mode 100644 index 0000000..111d7d0 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Collections/CatalystUI.Collections.csproj @@ -0,0 +1,18 @@ + + + + + + Catalyst.Collections + Catalyst.Collections + + + CatalystUI Collections + 1.0.0 + beta.2 + CatalystUI LLC + Collections API provided by the CatalystUI library. + CatalystUI,collections + + + \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Collections/StaticArrayPool.cs b/CatalystUI/Core/CatalystUI.Collections/StaticArrayPool.cs new file mode 100644 index 0000000..ebeb3d2 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Collections/StaticArrayPool.cs @@ -0,0 +1,212 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Catalyst.Collections { + + /// + /// A fixed-size pool implementation with zero memory + /// allocations after initialization. + /// + /// + /// + /// Allocations should be made using the method, + /// which returns a reference to the next available slot in the pool. Once + /// updated, the method should be fired, which + /// returns the index of the newly allocated element and increments + /// the count by one. + /// + /// + /// To release an allocated element, the + /// method should be called with the index of the element to release. + /// + /// + /// The type of elements stored in the pool. + public sealed class StaticArrayPool : IReadOnlyCollection where T : struct { + + /// + /// The array that holds the elements of the pool. + /// + private readonly T[] _arr; + + /// + /// A boolean array that indicates which elements in the pool are free. + /// + private readonly bool[] _free; + + /// + /// A stack which holds the indices of allocated elements. + /// + private readonly StaticArrayStack _allocated; + + /// + /// The current number of elements in the pool. + /// + private int _count; + + /// + /// Gets the current number of elements in the pool. + /// + /// The pool's current element count. + public int Count => _count; + + /// + /// Gets the total allocated capacity of the pool. + /// + /// The pool's total allocated capacity. + public int Capacity => _arr.Length; + + /// + /// Gets the underlying array of the pool. + /// + /// + /// + /// INTERNAL USE ONLY. + ///
+ /// Version consistency is not guaranteed, + /// and the property may be changed without notice. + ///
+ /// + /// Warning: Fetching the underlying array + /// provides access to the raw data of the pool, meaning + /// the return value will provide both valid and invalid + /// instances of the underlying struct type. Prefer + /// enumeration via whenever + /// possible. This property is provided for advanced use cases + /// and/or disposal of the underlying structures, and should + /// be used with special care, attention, and caution. + /// + ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)] + public T[] Items => _arr; + + /// + /// Gets a flag indicating if the pool is empty. + /// + /// if the pool is empty; otherwise, . + public bool IsEmpty => _count == 0; + + /// + /// Gets a flag indicating if the pool is full. + /// + /// if the pool is full; otherwise, . + public bool IsFull => _count == Capacity; + + /// + /// Gets a reference to the element at the specified index. + /// + /// The index of the element to access. + /// Thrown if the pool is empty, the index is out of range, or the index is out of bounds. + public ref T this[int index] { + get { + if (IsEmpty) throw new IndexOutOfRangeException("Cannot access an empty pool."); + if (index < 0 || index >= Capacity) throw new IndexOutOfRangeException("Index is out of bounds of the pool."); + if (_free[index]) throw new IndexOutOfRangeException("Index is not allocated in the pool."); + return ref _arr[index]; + } + } + + /// + /// Constructs a new . + /// + /// The allocated capacity of the pool. + public StaticArrayPool(int capacity) { + _arr = new T[capacity]; + _free = new bool[capacity]; + _allocated = new(capacity); + for (int i = capacity - 1; i >= 0; i--) { + ref int index = ref _allocated.PeekPush(); + index = i; + _allocated.Push(); + _free[i] = true; + } + _count = 0; + } + + /// + /// Peeks at the next available slot in the pool. + /// + /// A reference to the next available slot in the pool. + public ref T PeekNext() { + if (IsFull) throw new IndexOutOfRangeException("Cannot peek at next element in a full pool."); + return ref _arr[_allocated.PeekPop()]; + } + + /// + /// Increments the count of allocated elements and + /// returns the index of the newly allocated element. + /// + /// The index of the newly allocated element. + public int Allocate() { + if (IsFull) throw new IndexOutOfRangeException("Cannot allocate elements in a full pool."); + ref int index = ref _allocated.PeekPop(); + _allocated.Pop(); + _free[index] = false; + _count++; + return index; + } + + /// + /// Releases an allocated element in the pool by its index. + /// + /// The index of the element to release. + public void Release(int index) { + if (IsEmpty) throw new IndexOutOfRangeException("Cannot release elements from an empty pool."); + ref int slot = ref _allocated.PeekPush(); + slot = index; + _allocated.Push(); + _free[index] = true; + _count--; + } + + /// + /// Clears the queue by resetting all elements to their defaults. + /// + public void Clear() { + for (int i = 0; i < _arr.Length; i++) { + _arr[i] = default; + _free[i] = true; + } + _allocated.Clear(); + for (int i = _arr.Length - 1; i >= 0; i--) { + ref int index = ref _allocated.PeekPush(); + index = i; + _allocated.Push(); + } + _count = 0; + } + + /// + public IEnumerator GetEnumerator() { + if (IsEmpty) yield break; + if (IsFull) { + for (int i = 0; i < _arr.Length; i++) { + yield return _arr[i]; + } + } else { + for (int i = 0; i < _arr.Length; i++) { + if (!_free[i]) yield return _arr[i]; + } + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Collections/StaticArrayQueue.cs b/CatalystUI/Core/CatalystUI.Collections/StaticArrayQueue.cs new file mode 100644 index 0000000..7716578 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Collections/StaticArrayQueue.cs @@ -0,0 +1,211 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Catalyst.Collections { + + /// + /// A fixed-size queue implementation with zero memory + /// allocations after initialization. + /// + /// + /// + /// Allocations should be made using the method, + /// which returns a reference to the next available slot in the queue. Once + /// updated, the method should be fired to increment + /// the write counter by one. + /// + /// + /// Similarly, the method returns a reference to the + /// front-most enqueued slot in the queue. Once read, the + /// method should be called to increment the read counter by one. + /// + /// + /// The type of elements stored in the queue. + public sealed class StaticArrayQueue : IReadOnlyCollection where T : struct { + + /// + /// The array used to store the queue elements. + /// + private readonly T[] _arr; + + /// + /// The next index to write into the queue. + /// + private int _write; + + /// + /// The next index to read from the queue. + /// + private int _read; + + /// + /// The current number of elements in the queue. + /// + private int _count; + + /// + /// Gets the current number of elements in the queue. + /// + /// The queue's current element count. + public int Count => _count; + + /// + /// Gets the total allocated capacity of the queue. + /// + /// The queue's total allocated capacity. + public int Capacity => _arr.Length; + + /// + /// Gets the underlying array of the queue. + /// + /// + /// + /// INTERNAL USE ONLY. + ///
+ /// Version consistency is not guaranteed, + /// and the property may be changed without notice. + ///
+ /// + /// Warning: Fetching the underlying array + /// provides access to the raw data of the queue, meaning + /// the return value will provide both valid and invalid + /// instances of the underlying struct type. Prefer + /// index-based access via the indexer + /// or enumeration via whenever + /// possible. This property is provided for advanced use cases + /// and/or disposal of the underlying structures, and should + /// be used with special care, attention, and caution. + /// + ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)] + public T[] Items => _arr; + + /// + /// Gets a flag indicating if the queue is empty. + /// + /// if the queue is empty; otherwise, . + public bool IsEmpty => _count == 0; + + /// + /// Gets a flag indicating if the queue is full. + /// + /// if the queue is full; otherwise, . + public bool IsFull => _count == Capacity; + + /// + /// Gets a reference to the element at the specified index. + /// + /// The index of the element to access. + /// Thrown if the queue is empty, the index is out of range, or the index is out of bounds. + public ref T this[int index] { + get { + if (IsEmpty) throw new IndexOutOfRangeException("Cannot access an empty queue."); + if (index < 0 || index >= _count) throw new IndexOutOfRangeException("Index is out of bounds of the queue."); + int adjustedIndex = (_read + index) % _arr.Length; + return ref _arr[adjustedIndex]; + } + } + + /// + /// Constructs a new . + /// + /// The allocated capacity of the queue. + public StaticArrayQueue(int capacity) { + if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be a number greater than zero!"); + _arr = new T[capacity]; + _write = 0; + _read = 0; + _count = 0; + } + + /// + /// Peeks at the next available slot in the queue. + /// + /// A reference to the next available slot in the queue. + /// Thrown if the queue is full. + public ref T PeekEnqueue() { + if (IsFull) throw new IndexOutOfRangeException("Cannot peek the back of a full queue."); + return ref _arr[_write]; + } + + /// + /// Increments the write counter by one. + /// + /// Thrown if the queue is full. + public void Enqueue() { + if (IsFull) throw new IndexOutOfRangeException("Cannot enqueue to a full queue."); + _write = (_write + 1) % _arr.Length; + _count++; + } + + /// + /// Peeks at the front-most element in the queue. + /// + /// A reference to the front-most element in the queue. + /// Thrown if the queue is empty. + public ref T PeekDequeue() { + if (IsEmpty) throw new IndexOutOfRangeException("Cannot peek the front of an empty queue."); + return ref _arr[_read]; + } + + /// + /// Increments the read counter by one. + /// + /// Thrown if the queue is empty. + public void Dequeue() { + if (IsEmpty) throw new IndexOutOfRangeException("Cannot dequeue from an empty queue."); + _read = (_read + 1) % _arr.Length; + _count--; + } + + /// + /// Clears the queue by resetting all values to their defaults. + /// + public void Clear() { + for (int i = 0; i < _arr.Length; i++) { + ref T item = ref _arr[i]; + item = default; + } + _write = 0; + _read = 0; + _count = 0; + } + + /// + public IEnumerator GetEnumerator() { + if (IsEmpty) yield break; + if (IsFull) { + for (int i = 0; i < _arr.Length; i++) { + yield return _arr[i]; + } + } else { + int remaining = _count; + int index = _read; + for (int i = 0; i < remaining; i++) { + yield return _arr[index]; + index = (index + 1) % _arr.Length; + } + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Collections/StaticArrayStack.cs b/CatalystUI/Core/CatalystUI.Collections/StaticArrayStack.cs new file mode 100644 index 0000000..3b8e3e2 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Collections/StaticArrayStack.cs @@ -0,0 +1,182 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Catalyst.Collections { + + /// + /// A fixed-size stack implementation with zero memory + /// allocations after initialization. + /// + /// + /// + /// Allocations should be made using the method, + /// which returns a reference to the next available slot in the stack. Once + /// updated, the method should be fired to increment + /// the count by one. + /// + /// + /// Similarly, the method returns a reference to the + /// top-most element in the stack. Once read, the + /// method should be called to decrement the count by one. + /// + /// + /// The type of elements stored in the stack. + public sealed class StaticArrayStack : IReadOnlyCollection where T : struct { + + /// + /// The array used to store the stack elements. + /// + private readonly T[] _arr; + + /// + /// The current number of elements in the stack. + /// + private int _count; + + /// + /// Gets the current number of elements in the stack. + /// + /// The stack's current element count. + public int Count => _count; + + /// + /// Gets the total allocated capacity of the stack. + /// + /// The stack's total allocated capacity. + public int Capacity => _arr.Length; + + /// + /// Gets the underlying array of the stack. + /// + /// + /// + /// INTERNAL USE ONLY. + ///
+ /// Version consistency is not guaranteed, + /// and the property may be changed without notice. + ///
+ /// + /// Warning: Fetching the underlying array + /// provides access to the raw data of the stack, meaning + /// the return value will provide both valid and invalid + /// instances of the underlying struct type. Prefer + /// enumeration via whenever + /// possible. This property is provided for advanced use cases + /// and/or disposal of the underlying structures, and should + /// be used with special care, attention, and caution. + /// + ///
+ public T[] Items => _arr; + + /// + /// Gets a flag indicating whether the stack is empty. + /// + /// if the stack is empty; otherwise, . + public bool IsEmpty => _count == 0; + + /// + /// Gets a flag indicating whether the stack is full. + /// + /// if the stack is full; otherwise, . + public bool IsFull => _count == _arr.Length; + + /// + /// Gets a reference to the element at the specified index. + /// + /// The index of the element to access. + /// Thrown if the stack is empty, the index is out of range, or the index is out of bounds. + public ref T this[int index] { + get { + if (IsEmpty) throw new IndexOutOfRangeException("Cannot access an empty stack."); + if (index >= _count) throw new IndexOutOfRangeException("Index is out of bounds of the stack."); + return ref _arr[index]; + } + } + + /// + /// Constructs a new . + /// + /// The allocated capacity of the stack. + public StaticArrayStack(int capacity) { + if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be a number greater than zero!"); + _arr = new T[capacity]; + _count = 0; + } + + /// + /// Peeks at the next available slot in the stack. + /// + /// A reference to the next available slot in the stack. + /// Thrown if the stack is full. + public ref T PeekPush() { + if (IsFull) throw new IndexOutOfRangeException("Stack is full, cannot peek next element!"); + return ref _arr[_count]; + } + + /// + /// Increments the head counter by one. + /// + /// Thrown if the stack is full. + public void Push() { + if (IsFull) throw new IndexOutOfRangeException("Stack is full, cannot push new element!"); + _count++; + } + + /// + /// Peeks at the top element of the stack. + /// + /// A reference to the top element of the stack. + /// Thrown if the stack is empty. + public ref T PeekPop() { + if (IsEmpty) throw new IndexOutOfRangeException("Stack is empty, cannot peek top element!"); + return ref _arr[_count - 1]; + } + + /// + /// Decrements the head counter by one. + /// + /// Thrown if the stack is empty. + public void Pop() { + if (IsEmpty) throw new IndexOutOfRangeException("Stack is empty, cannot pop top element!"); + _count--; + } + + /// + /// Clears the stack by resetting all values to their defaults. + /// + public void Clear() { + for (int i = 0; i < _count; i++) { + ref T item = ref _arr[i]; + item = default; + } + _count = 0; + } + + /// + public IEnumerator GetEnumerator() { + if (IsEmpty) yield break; + for (int i = _count - 1; i >= 0; i--) { + yield return _arr[i]; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/CatalystUI.Core.csproj b/CatalystUI/Core/CatalystUI.Core/CatalystUI.Core.csproj index 0621225..339ec99 100644 --- a/CatalystUI/Core/CatalystUI.Core/CatalystUI.Core.csproj +++ b/CatalystUI/Core/CatalystUI.Core/CatalystUI.Core.csproj @@ -10,14 +10,18 @@ CatalystUI Core 1.0.0 beta.2 - FireController#1847 + CatalystUI LLC Core API provided by the CatalystUI library. CatalystUI,core - + + + + + \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/IAdapterConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/IAdapterConnector.cs new file mode 100644 index 0000000..445da68 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/IAdapterConnector.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using Catalyst.Layers; + +namespace Catalyst.Connectors { + + /// + /// Represents the adapter connector in the CatalystUI model. + /// + /// + public interface IAdapterConnector : IConnector where TLayerLow : IFrameLayer where TLayerHigh : IComponentsLayer { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/IBridgeConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/IBridgeConnector.cs new file mode 100644 index 0000000..2b675b0 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/IBridgeConnector.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using Catalyst.Layers; + +namespace Catalyst.Connectors { + + /// + /// Represents the bridge connector in the CatalystUI model. + /// + /// + public interface IBridgeConnector : IConnector where TLayerLow : IRendererLayer where TLayerHigh : IFrameLayer { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs new file mode 100644 index 0000000..da20dbc --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using Catalyst.Layers; + +namespace Catalyst.Connectors { + + /// + /// Represents the data connector in the CatalystUI model. + /// + /// + public interface IDataConnector : IConnector where TLayerLow : ISemanticsLayer where TLayerHigh : IDataLayer { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs new file mode 100644 index 0000000..b16e6ed --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using Catalyst.Layers; + +namespace Catalyst.Connectors { + + /// + /// Represents the native connector in the CatalystUI model. + /// + /// + public interface INativeConnector : IConnector where TLayerLow : ISystemLayer where TLayerHigh : IWindowLayer { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/IParserConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/IParserConnector.cs new file mode 100644 index 0000000..6d2e5fc --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/IParserConnector.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using Catalyst.Layers; + +namespace Catalyst.Connectors { + + /// + /// Represents the parser connector in the CatalystUI model. + /// + /// + public interface IParserConnector : IConnector where TLayerLow : IComponentsLayer where TLayerHigh : ISemanticsLayer { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/ISurfaceConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/ISurfaceConnector.cs new file mode 100644 index 0000000..3116d6d --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/ISurfaceConnector.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using Catalyst.Layers; + +namespace Catalyst.Connectors { + + /// + /// Represents the surface connector in the CatalystUI model. + /// + /// + public interface ISurfaceConnector : IConnector where TLayerLow : IWindowLayer where TLayerHigh : IRendererLayer { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebug.cs b/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebug.cs new file mode 100644 index 0000000..e4d4f8b --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebug.cs @@ -0,0 +1,382 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; + +namespace Catalyst.Debugging { + + /// + /// Container class for CatalystUI debugging utilities. + /// + public static class CatalystDebug { + + /// + /// The environment variable prefix for CatalystUI debugging settings. + /// + public const string ENV_PREFIX = "CATALYST_DEBUG_"; + + /// + /// The name of the CatalystUI debugging configuration file. + /// + public const string CONFIG_FILE_NAME = "catdebug.ini"; + + /// + /// The name of the CatalystUI debugging output log file. + /// + public const string OUTPUT_FILE_NAME = "catdebug.log"; + + // The following is a list of primary sections and keys which + // are used in the configuration file for Catalyst debugging. +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public const string DEBUG_SECTION_LOGGER = "Logger"; + public const string DEBUG_KVP_LOGGER_MINIMUM_LEVEL = "MinimumLevel"; + public const string DEBUG_KVP_LOGGER_SHOW_THREAD = "ShowThread"; + public const string DEBUG_KVP_LOGGER_SHOW_FILENAME = "ShowFileName"; + public const string DEBUG_KVP_LOGGER_SHOW_METHOD_NAME = "ShowMethodName"; + public const string DEBUG_KVP_LOGGER_SHOW_LINE_NUMBER = "ShowLineNumber"; + public const string DEBUG_KVP_LOGGER_SHOW_STACK_TRACE = "ShowStackTrace"; + public const string DEBUG_SECTION_ENABLED_SCOPES = "EnabledScopes"; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + /// + /// Gets or sets the current CatalystUI debugging configuration. + /// + /// The current instance. + public static CatalystDebugConfiguration? DebugConfiguration { get; private set; } + + /// + /// Gets or sets the current CatalystUI debugging options. + /// + /// The current instance. + public static CatalystDebugOptions DebugOptions { get; private set; } + + /// + /// A cache of configuration properties which + /// have been attempted to be loaded. + /// + private static readonly ConcurrentDictionary _configCache; + + /// + /// A dictionary of enabled scopes for debugging. + /// + private static readonly ConcurrentDictionary _enabledScopes; + + /// + /// A constructor for creating instances. + /// + private static Func? _debugContextConstructor; + + /// + /// Static initializer for . + /// + static CatalystDebug() { + // Fields + _configCache = new(); + _enabledScopes = new(); + _debugContextConstructor = null; + + // Properties + DebugConfiguration = LoadDebugConfiguration(); + DebugOptions = LoadDebugOptions(); + + // Preload scopes + PreloadScopes(); + } + + /// + /// Loads the CatalystUI debugging configuration from file and environment variables. + /// + /// The loaded instance. + private static CatalystDebugConfiguration? LoadDebugConfiguration() { + try { + if (!File.Exists(CONFIG_FILE_NAME)) return null; + using FileStream stream = File.OpenRead(CONFIG_FILE_NAME); + + // The following code is adapted from previous iterations + // of the CatalystUI Arcane project. To prevent interdependency, + // only the necessary code has been copied here. + + // COPIED CODE START + + // Read and parse the file into basic structures + Dictionary fileEntries = [ ]; + Dictionary> fileSections = [ ]; + using StreamReader reader = new(stream, leaveOpen: true); + string? section = null; + while (reader.ReadLine() is { } line) { + line = line.Trim(); + if (string.IsNullOrWhiteSpace(line) || line.StartsWith(';')) continue; // Skip empty lines and comments + + // Parse section tag + if (line.StartsWith('[') && line.EndsWith(']')) { + // New section + section = line[1..^1].Trim(); + if (string.IsNullOrWhiteSpace(section)) section = null; // don't accept empty sections + continue; + } + + // Split key-value pairs + int index = line.IndexOf('='); + if (index < 0) continue; // No key-value pair found + string key = line[..index].Trim(); + string? value = line[(index + 1)..].Trim(); + if (string.IsNullOrWhiteSpace(value)) value = null; // Treat empty values as null + if (value != null && (value = value.Trim()).Length >= 4) { // Check for "null" value + int idx = value.IndexOf("null", StringComparison.OrdinalIgnoreCase); + if (idx >= 0 && + (idx == 0 || value[..idx].All(c => !char.IsLetterOrDigit(c))) && + (idx + 4 == value.Length || value[(idx + 4)..].All(c => !char.IsLetterOrDigit(c)))) + value = null; + } + + // If we have a section, add to it; otherwise, add to global entries + if (section != null) { + if (!fileSections.TryGetValue(section, out Dictionary? sectionEntries)) { + sectionEntries = new(); + fileSections[section] = sectionEntries; + } + sectionEntries[key] = value; + } else { + fileEntries[key] = value; + } + } + + // COPIED CODE END + + // Cast sections to read-only dictionaries + Dictionary> readonlySections = + fileSections.ToDictionary( + kvp => kvp.Key, + IReadOnlyDictionary (kvp) => kvp.Value + ); + + // Return the constructed configuration + return new CatalystDebugConfiguration( + globalEntries: fileEntries, + sections: readonlySections + ); + } catch (Exception e) { + Trace.TraceWarning($"⚠️ [Catalyst.Debugging] Failed to load the debug configuration file. {e.Message}{Environment.NewLine}{e.StackTrace}"); + return null; + } + } + + /// + /// Gets a configuration value from the CatalystUI debugging configuration. + /// + /// The section in the configuration file to look for, or to use the global section. + /// The key to retrieve the value for. + /// The value retrieved from the configuration file or environment variable. + /// If , skips logging the retrieval of the configuration value. + /// if the value was found; otherwise, . + public static bool TryGetConfigValue(string? section, string key, [NotNullWhen(true)] out string? value, bool skipLog = false) { + // First, check the cache + if (_configCache.TryGetValue(key, out value)) { + return value != null; // the value is always found if it was cached + } + + // First, check the environment variable + string env; + if (section != null) { + env = ENV_PREFIX + section.ToUpperInvariant() + "_" + key.ToUpperInvariant(); + } else { + env = ENV_PREFIX + key.ToUpperInvariant(); + } + value = Environment.GetEnvironmentVariable(env); + if (value != null) { + if (!skipLog) { + Trace.WriteLine($"ℹ️ [Catalyst.Debugging] Configuration value '{key}' found in environment variable '{env}' with value: {value}"); + } + _configCache.TryAdd(key, value); + return true; + } + + // Second, check the configuration file + if (DebugConfiguration != null) { + CatalystDebugConfiguration configFile = DebugConfiguration.Value; + if (!configFile.GlobalEntries.TryGetValue(key, out value) && section != null) { + if (configFile.Sections.TryGetValue(section, out IReadOnlyDictionary? sectionEntries)) { + _ = sectionEntries.TryGetValue(key, out value); + } + } + if (value != null) { + _configCache.TryAdd(key, value); + if (!skipLog) { + Trace.WriteLine($"ℹ️ [Catalyst.Debugging] Configuration value '{key}' found in section '{section ?? "Global"}' with value: {value}"); + } + return true; + } + } + + // Otherwise, we couldn't find it + value = null; + _configCache.TryAdd(key, null); + return false; + } + + /// + /// Determines the log event level from a string representation. + /// + /// The string representation of the log level. + /// The corresponding log event level or if not recognized. + public static LogLevel? LogLevelFromString(string level) { + return level.Trim().ToLowerInvariant() switch { + "critical" => LogLevel.Critical, + "fatal" => LogLevel.Critical, // Alias for Critical + "error" => LogLevel.Error, + "exception" => LogLevel.Error, // Alias for Error + "warning" => LogLevel.Warning, + "information" => LogLevel.Info, + "info" => LogLevel.Info, // Alias for Information + "debug" => LogLevel.Debug, + "verbose" => LogLevel.Verbose, + _ => null + }; + } + + /// + /// Loads the CatalystUI debugging options from configuration. + /// + /// The loaded instance. + private static CatalystDebugOptions LoadDebugOptions() { + LogLevel level = LogLevel.Debug; + bool showThread = true; + bool showFileName = false; + bool showMethodName = false; + bool showLineNumber = false; + bool showStackTrace = false; + + // Fetch each associated configuration value + if (TryGetConfigValue(DEBUG_SECTION_LOGGER, DEBUG_KVP_LOGGER_MINIMUM_LEVEL, out string? configLogLevel)) { + if (LogLevelFromString(configLogLevel) is LogLevel parsedLevel) { + level = parsedLevel; + } + } + if (TryGetConfigValue(DEBUG_SECTION_LOGGER, DEBUG_KVP_LOGGER_SHOW_THREAD, out string? configShowThread)) { + if (bool.TryParse(configShowThread, out bool parsedShowThread)) { + showThread = parsedShowThread; + } + } + if (TryGetConfigValue(DEBUG_SECTION_LOGGER, DEBUG_KVP_LOGGER_SHOW_FILENAME, out string? configShowFileName)) { + if (bool.TryParse(configShowFileName, out bool parsedShowFileName)) { + showFileName = parsedShowFileName; + } + } + if (TryGetConfigValue(DEBUG_SECTION_LOGGER, DEBUG_KVP_LOGGER_SHOW_METHOD_NAME, out string? configShowMethodName)) { + if (bool.TryParse(configShowMethodName, out bool parsedShowMethodName)) { + showMethodName = parsedShowMethodName; + } + } + if (TryGetConfigValue(DEBUG_SECTION_LOGGER, DEBUG_KVP_LOGGER_SHOW_LINE_NUMBER, out string? configShowLineNumber)) { + if (bool.TryParse(configShowLineNumber, out bool parsedShowLineNumber)) { + showLineNumber = parsedShowLineNumber; + } + } + if (TryGetConfigValue(DEBUG_SECTION_LOGGER, DEBUG_KVP_LOGGER_SHOW_STACK_TRACE, out string? configShowStackTrace)) { + if (bool.TryParse(configShowStackTrace, out bool parsedShowStackTrace)) { + showStackTrace = parsedShowStackTrace; + } + } + + // Return the constructed options + return new( + minimumLogLevel: level, + showsThread: showThread, + showsFileName: showFileName, + showsMethodName: showMethodName, + showsLineNumber: showLineNumber, + showsStackTrace: showStackTrace + ); + } + + /// + /// Determines the log level for a given scope. + /// + /// The scope to determine the log level for. + /// The determined for the scope. + public static LogLevel DetermineScopeLevel(string scope) { + if (!_enabledScopes.TryGetValue(scope, out LogLevel level)) { + level = DebugOptions.MinimumLogLevel; + if (TryGetConfigValue(DEBUG_SECTION_ENABLED_SCOPES, scope, out string? configValue, true)) { + LogLevel? parsedResult = LogLevelFromString(configValue); + if (parsedResult == null) throw new InvalidOperationException($"Invalid log level '{configValue}' for scope '{scope}'. Using default level: {DebugOptions.MinimumLogLevel}"); + level = parsedResult.Value; + _enabledScopes.TryAdd(scope, level); + } else { + Trace.WriteLine($"⚠️ Scope '{scope}' is not defined in the configuration file. Using default level: {DebugOptions.MinimumLogLevel}"); + } + + // Scope output + if (DebugOptions.MinimumLogLevel >= LogLevel.Debug) { + Trace.WriteLine($"ℹ️ Determined scope '{scope}' level: {level}"); + } + } + return level; + } + + /// + /// Preloads all existing scopes from the configuration. + /// + private static void PreloadScopes() { + // Scan through scopes and check if they are enabled + if (DebugConfiguration != null && DebugConfiguration.Value.Sections.TryGetValue(DEBUG_SECTION_ENABLED_SCOPES, out IReadOnlyDictionary? sectionEntries)) { + foreach (KeyValuePair entry in sectionEntries) { + // Determine the scope level + try { + DetermineScopeLevel(entry.Key); + } catch { + // ignore failed scope level determination + // special configuration values may use this section to store additional + // information for each scope, so we don't want to throw an error here + // + // an error is thrown in ForContext() if the scope level is invalid, + // however, since the property is expected to be used as a log level + // for the associated scope + } + } + } + } + + /// + /// Injects a custom constructor for creating instances. + /// + /// + /// Custom implementations of can be used to provide custom logging or debugging behavior. + /// The constructor should return a new instance of or + /// an extended variety that overrides the default behavior. + /// + /// A function that constructs a new . + public static void InjectDebugContext(Func constructor) { + if (_debugContextConstructor != null) { + Trace.WriteLine("⚠️ A debug constructor has already been set. The previous constructor will be replaced with the new one."); + Trace.WriteLine(constructor.GetType().GetGenericArguments()[1].FullName); + } + _debugContextConstructor = constructor; + } + + /// + /// Constructs a new debug context for the specified scope. + /// + /// The scope of the debug context. + /// A new instance of for the specified scope. + public static DebugContext ForContext(string scope) { + return _debugContextConstructor == null ? new NoopDebugContext(scope) : _debugContextConstructor(scope); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebugConfiguration.cs b/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebugConfiguration.cs new file mode 100644 index 0000000..63a691c --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebugConfiguration.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Catalyst.Debugging { + + /// + /// The debug configuration file for CatalystUI. + /// + public readonly record struct CatalystDebugConfiguration { + + /// + /// Gets a raw list of parsed global entries from the configuration file. + /// + /// The global entries dictionary. + public required IReadOnlyDictionary GlobalEntries { get; init; } + + /// + /// Gets a raw list of parsed sections from the configuration file. + /// + /// The sections dictionary. + public required IReadOnlyDictionary> Sections { get; init; } + + /// + /// Constructs a new . + /// + /// The global entries. + /// The sections. + [SetsRequiredMembers] + public CatalystDebugConfiguration( + IReadOnlyDictionary globalEntries, + IReadOnlyDictionary> sections) { + GlobalEntries = globalEntries; + Sections = sections; + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebugOptions.cs b/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebugOptions.cs new file mode 100644 index 0000000..b1f90ed --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Debugging/CatalystDebugOptions.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System.Diagnostics.CodeAnalysis; + +namespace Catalyst.Debugging { + + /// + /// Options which can be set to modify the way Catalyst debugging works. + /// + public readonly record struct CatalystDebugOptions { + + /// + /// Gets the minimum logging level for Catalyst debugging. + /// + /// The minimum logging level. + public required LogLevel MinimumLogLevel { get; init; } + + /// + /// Gets a flag indicating whether thread information should be shown in log entries. + /// + /// if thread information should be shown; otherwise, . + public required bool ShowsThread { get; init; } + + /// + /// Gets a flag indicating whether file names should be shown in log entries. + /// + /// if file names should be shown; otherwise, . + public required bool ShowsFileName { get; init; } + + /// + /// Gets a flag indicating whether method names should be shown in log entries. + /// + /// if method names should be shown; otherwise, . + public required bool ShowsMethodName { get; init; } + + /// + /// Gets a flag indicating whether line numbers should be shown in log entries. + /// + /// if line numbers should be shown; otherwise, . + public required bool ShowsLineNumber { get; init; } + + /// + /// Gets a flag indicating whether stack traces should be shown in log entries. + /// + /// if stack traces should be shown; otherwise, . + public required bool ShowsStackTrace { get; init; } + + /// + /// Constructs a new with + /// default settings. + /// + [SetsRequiredMembers] + public CatalystDebugOptions() { + MinimumLogLevel = LogLevel.Debug; + ShowsThread = true; + ShowsFileName = false; + ShowsMethodName = false; + ShowsLineNumber = false; + ShowsStackTrace = false; + } + + /// + /// Constructs a new with + /// the specified settings. + /// + [SetsRequiredMembers] + public CatalystDebugOptions( + LogLevel minimumLogLevel, + bool showsThread, + bool showsFileName, + bool showsMethodName, + bool showsLineNumber, + bool showsStackTrace) { + MinimumLogLevel = minimumLogLevel; + ShowsThread = showsThread; + ShowsFileName = showsFileName; + ShowsMethodName = showsMethodName; + ShowsLineNumber = showsLineNumber; + ShowsStackTrace = showsStackTrace; + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Debugging/DebugContext.cs b/CatalystUI/Core/CatalystUI.Core/Debugging/DebugContext.cs new file mode 100644 index 0000000..e0d6606 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Debugging/DebugContext.cs @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System.Diagnostics; + +namespace Catalyst.Debugging { + + /// + /// A context for debugging operations within the CatalystUI framework. + /// + public abstract class DebugContext { + + /// + /// Gets the scope of the debug context. + /// + /// The scope as a string. + public string Scope { get; } + + /// + /// Gets or sets the prefix for debug messages. + /// + /// + /// No additional formatting is applied to the prefix. + /// + /// The prefix as a string. + public string? Prefix { get; protected set; } + + /// + /// Gets or sets the log level for the debug context. + /// + /// The log level. + public LogLevel Level { get; protected set; } + + /// + /// Constructs a new with + /// the specified scope. + /// + /// + protected DebugContext(string scope) { + Scope = scope; + Prefix = null; + Level = CatalystDebug.DebugOptions.MinimumLogLevel; + } + + /// + /// Logs to the logger with the specified log level, message, and optional prefix and arguments. + /// + /// The log level for the message. + /// The message to log. + /// An optional prefix to prepend to the message. + /// Optional arguments to stringify at the end of the message. + [DebuggerHidden] + public virtual void Log(LogLevel level, string message, string? prefix = null, params object[] args) { + // no-op since internal library calls will not be filtered out by the compiler + // this is an unfortunate compromise to allow for debug logging from the library itself + // in addition to providing the debug functionality for the user + // any calls to the utility methods will be stripped by the compiler in release builds + // of both the library and the user's application, which is intended functionality + } + + /// + /// Logs a fatal error with the specified message and optional arguments. + /// + /// + [Conditional("DEBUG")] + public void LogFatal(string message, params object[] args) { + if (Level < LogLevel.Critical) return; + Log(LogLevel.Critical, message, null, args); + } + + /// + [Conditional("DEBUG")] + public void LogCritical(string message, params object[] args) => LogFatal(message, args); + + /// + /// Logs an error with the specified message and optional arguments. + /// + /// + /// If an exception is provided as an argument, the exception's will be included in the log output. + /// If the message is null or empty, it will be replaced with the exception's + /// + /// + [Conditional("DEBUG")] + public void LogError(string message, params object[] args) { + if (Level < LogLevel.Error) return; + Log(LogLevel.Error, message, null, args); + } + + /// + [Conditional("DEBUG")] + public void LogException(string message, params object[] args) => LogError(message, args); + + /// + /// Logs a warning with the specified message and optional arguments. + /// + /// + [Conditional("DEBUG")] + public void LogWarning(string message, params object[] args) { + if (Level < LogLevel.Warning) return; + Log(LogLevel.Warning, message, null, args); + } + + /// + /// Logs an informational message with the specified message and optional arguments. + /// + /// + [Conditional("DEBUG")] + public void LogInfo(string message, params object[] args) { + if (Level < LogLevel.Info) return; + Log(LogLevel.Info, message, null, args); + } + + /// + /// Logs a debug message with the specified message and optional arguments. + /// + /// + [Conditional("DEBUG")] + public void LogDebug(string message, params object[] args) { + if (Level < LogLevel.Debug) return; + Log(LogLevel.Debug, message, null, args); + } + + /// + /// Logs a verbose message with the specified message and optional arguments. + /// + /// + [Conditional("DEBUG")] + public void LogVerbose(string message, params object[] args) { + if (Level < LogLevel.Verbose) return; + Log(LogLevel.Verbose, message, null, args); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Debugging/LogLevel.cs b/CatalystUI/Core/CatalystUI.Core/Debugging/LogLevel.cs new file mode 100644 index 0000000..b4bbb8d --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Debugging/LogLevel.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + + +namespace Catalyst.Debugging { + + /// + /// A list of recognized log levels for debugging and logging within the CatalystUI framework. + /// + public enum LogLevel { + + /// + /// No logs are emitted or processed. + /// + None = 0, + + /// + /// Logs only critical errors that require immediate attention, such as application crashes or severe failures. + /// + Critical = 1, + + /// + /// Logs critical errors and errors that occur but do not require immediate attention. + /// + Error = 2, + + /// + /// Logs errors and additional warnings that may indicate potential issues. + /// + Warning = 3, + + /// + /// Logs errors, warnings, and informational messages that provide insights into the application's state. + /// + Info = 4, + + /// + /// Logs errors, warnings, informational messages, and detailed debugging information useful for diagnosing issues. + /// + Debug = 5, + + /// + /// Logs all messages, including detailed debug information, performance metrics, and verbose output for in-depth analysis. + /// + Verbose = 6 + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Debugging/NoopDebugContext.cs b/CatalystUI/Core/CatalystUI.Core/Debugging/NoopDebugContext.cs new file mode 100644 index 0000000..1a1aa7d --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Debugging/NoopDebugContext.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Debugging { + + /// + /// Should be avoided whenever possible, a fallback debug context which is used + /// by the static class when no custom debug context + /// is provided. + /// + public sealed class NoopDebugContext : DebugContext { + + /// + internal NoopDebugContext(string scope) : base(scope) { + // ... + } + + /// + public override void Log(LogLevel level, string message, string? prefix = null, params object[] args) { + // no-op + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Domains/IAuditoryDomain.cs b/CatalystUI/Core/CatalystUI.Core/Domains/IAuditoryDomain.cs new file mode 100644 index 0000000..6039fe7 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Domains/IAuditoryDomain.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Domains { + + /// + /// Represents the auditory domain in the CatalystUI model. + /// + public interface IAuditoryDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Domains/IGustatoryDomain.cs b/CatalystUI/Core/CatalystUI.Core/Domains/IGustatoryDomain.cs new file mode 100644 index 0000000..a62b5e4 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Domains/IGustatoryDomain.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Domains { + + /// + /// Represents the gustatory domain in the CatalystUI model. + /// + public interface IGustatoryDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Domains/IMultisensoryDomain.cs b/CatalystUI/Core/CatalystUI.Core/Domains/IMultisensoryDomain.cs new file mode 100644 index 0000000..673c826 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Domains/IMultisensoryDomain.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Domains { + + /// + /// Represents the multisensory domain in the CatalystUI model. + /// + public interface IMultisensoryDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Domains/IOlfactoryDomain.cs b/CatalystUI/Core/CatalystUI.Core/Domains/IOlfactoryDomain.cs new file mode 100644 index 0000000..e9af3f2 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Domains/IOlfactoryDomain.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Domains { + + /// + /// Represents the olfactory domain in the CatalystUI model. + /// + public interface IOlfactoryDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Domains/ISymbolicDomain.cs b/CatalystUI/Core/CatalystUI.Core/Domains/ISymbolicDomain.cs new file mode 100644 index 0000000..9b0ba52 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Domains/ISymbolicDomain.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Domains { + + /// + /// Represents the symbolic domain in the CatalystUI model. + /// + public interface ISymbolicDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Domains/ITactileDomain.cs b/CatalystUI/Core/CatalystUI.Core/Domains/ITactileDomain.cs new file mode 100644 index 0000000..408c636 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Domains/ITactileDomain.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Domains { + + /// + /// Represents the tactile domain in the CatalystUI model. + /// + public interface ITactileDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Domains/IVisualDomain.cs b/CatalystUI/Core/CatalystUI.Core/Domains/IVisualDomain.cs new file mode 100644 index 0000000..c7f657a --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Domains/IVisualDomain.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Domains { + + /// + /// Represents the visual domain in the CatalystUI model. + /// + public interface IVisualDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/IConnector.cs b/CatalystUI/Core/CatalystUI.Core/IConnector.cs new file mode 100644 index 0000000..8fd590d --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/IConnector.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using Catalyst.Layers; +using System; + +// ReSharper disable once CheckNamespace +namespace Catalyst.Connectors { + + /// + /// Represents a connector in the CatalystUI model. + /// + /// The layer type associated with the lower-level abstraction. + /// The layer type associated with the higher-level abstraction. + public interface IConnector where TLayerLow : ILayer where TLayerHigh : ILayer { + + /// + /// Gets the type of the higher layer of the connector. + /// + /// The connector's high layer type. + static virtual Type HighLayer => typeof(TLayerHigh); + + /// + /// Gets the type of the lower layer of the connector. + /// + /// The connector's low layer type. + static virtual Type LowLayer => typeof(TLayerLow); + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/IDomain.cs b/CatalystUI/Core/CatalystUI.Core/IDomain.cs new file mode 100644 index 0000000..669a223 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/IDomain.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +// ReSharper disable once CheckNamespace +namespace Catalyst.Domains { + + /// + /// Represents a domain in the CatalystUI model. + /// + public interface IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/ILayer.cs b/CatalystUI/Core/CatalystUI.Core/ILayer.cs new file mode 100644 index 0000000..fa43891 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/ILayer.cs @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; +using System; + +// ReSharper disable once CheckNamespace +namespace Catalyst.Layers { + + /// + /// Represents a layer in the CatalystUI model. + /// + /// The domain type associated with the layer. + public interface ILayer where TDomain : IDomain { + + /// + /// Gets the type of the domain of the layer. + /// + /// The layer's domain type. + static virtual Type Domain => typeof(TDomain); + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Interactions/IInputDevice.cs b/CatalystUI/Core/CatalystUI.Core/Interactions/IInputDevice.cs new file mode 100644 index 0000000..c8358c7 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Interactions/IInputDevice.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Interactions { + + /// + /// Represents the originating input device for an interaction. + /// + public interface IInputDevice { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Interactions/IInteraction.cs b/CatalystUI/Core/CatalystUI.Core/Interactions/IInteraction.cs new file mode 100644 index 0000000..b5a1d22 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Interactions/IInteraction.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Interactions { + + /// + /// Represents an interaction in the CatalystUI model. + /// + public interface IInteraction { + + /// + /// Gets the input device that originated the interaction. + /// + /// The input device, or if the origin is unknown. + IInputDevice? InputDevice { get; } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Interactions/Input/IHardwareDevice.cs b/CatalystUI/Core/CatalystUI.Core/Interactions/Input/IHardwareDevice.cs new file mode 100644 index 0000000..097c8a7 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Interactions/Input/IHardwareDevice.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Interactions.Input { + + /// + /// Represents a physical hardware input device, + /// such as a mouse, keyboard, or touch screen. + /// + public interface IHardwareDevice : IInputDevice { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Interactions/Input/ILogicalDevice.cs b/CatalystUI/Core/CatalystUI.Core/Interactions/Input/ILogicalDevice.cs new file mode 100644 index 0000000..ee1cd44 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Interactions/Input/ILogicalDevice.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Interactions.Input { + + /// + /// Represents a logical input device, indicating the interaction + /// did not originate from a physical hardware device. + /// + public interface ILogicalDevice : IInputDevice { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Layers/IComponentsLayer.cs b/CatalystUI/Core/CatalystUI.Core/Layers/IComponentsLayer.cs new file mode 100644 index 0000000..d0a619a --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Layers/IComponentsLayer.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + + +using Catalyst.Domains; + +namespace Catalyst.Layers { + + /// + /// Represents the components layer in the CatalystUI model. + /// + /// + public interface IComponentsLayer : ILayer where TDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Layers/IDataLayer.cs b/CatalystUI/Core/CatalystUI.Core/Layers/IDataLayer.cs new file mode 100644 index 0000000..fc07364 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Layers/IDataLayer.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + + +using Catalyst.Domains; + +namespace Catalyst.Layers { + + /// + /// Represents the data layer in the CatalystUI model. + /// + /// + public interface IDataLayer : ILayer where TDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Layers/IFrameLayer.cs b/CatalystUI/Core/CatalystUI.Core/Layers/IFrameLayer.cs new file mode 100644 index 0000000..3dd29a4 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Layers/IFrameLayer.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + + +using Catalyst.Domains; + +namespace Catalyst.Layers { + + /// + /// Represents the frame layer in the CatalystUI model. + /// + /// + public interface IFrameLayer : ILayer where TDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Layers/IRendererLayer.cs b/CatalystUI/Core/CatalystUI.Core/Layers/IRendererLayer.cs new file mode 100644 index 0000000..c4f4e7f --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Layers/IRendererLayer.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; + +namespace Catalyst.Layers { + + /// + /// Represents the renderer layer in the CatalystUI model. + /// + /// + public interface IRendererLayer : ILayer where TDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Layers/ISemanticsLayer.cs b/CatalystUI/Core/CatalystUI.Core/Layers/ISemanticsLayer.cs new file mode 100644 index 0000000..158c2ce --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Layers/ISemanticsLayer.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; + +namespace Catalyst.Layers { + + /// + /// Represents the semantics layer in the CatalystUI model. + /// + /// + public interface ISemanticsLayer : ILayer where TDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Layers/ISystemLayer.cs b/CatalystUI/Core/CatalystUI.Core/Layers/ISystemLayer.cs new file mode 100644 index 0000000..a9367a4 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Layers/ISystemLayer.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; + +namespace Catalyst.Layers { + + /// + /// Represents the system layer in the CatalystUI model. + /// + /// + public interface ISystemLayer : ILayer where TDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Core/Layers/IWindowLayer.cs b/CatalystUI/Core/CatalystUI.Core/Layers/IWindowLayer.cs new file mode 100644 index 0000000..a34a35d --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Core/Layers/IWindowLayer.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Domains; + +namespace Catalyst.Layers { + + /// + /// Represents the window layer in the CatalystUI model. + /// + /// + public interface IWindowLayer : ILayer where TDomain : IDomain { + + // ... + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Threading/CatalystUI.Threading.csproj b/CatalystUI/Core/CatalystUI.Threading/CatalystUI.Threading.csproj new file mode 100644 index 0000000..11aef9a --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Threading/CatalystUI.Threading.csproj @@ -0,0 +1,24 @@ + + + + + + Catalyst.Threading + Catalyst.Threading + true + + + CatalystUI Threading + 1.0.0 + beta.2 + CatalystUI LLC + Threading API provided by the CatalystUI library. + CatalystUI,threading + + + + + + + + \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs b/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs new file mode 100644 index 0000000..f448e02 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs @@ -0,0 +1,384 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Collections; +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Threading; + +namespace Catalyst.Threading { + + /// + /// A thread-safe which enqueues and dequeues delegates for execution. + /// + public sealed class DelegateQueue : IDisposable { + + /// + /// An event handler delegate for events related to the . + /// + public delegate void DelegateQueueEventHandler(DelegateQueue queue, EnqueuedDelegate @delegate); + + /// + /// Invoked when a delegate is enqueued but prior to its execution. + /// + /// + /// Invoked on the calling thread, not the executing thread. + ///

+ /// During invocation, the queue lock is held, so avoid long-running + /// operations or deadlocks may occur. + ///
+ public event DelegateQueueEventHandler? DelegateEnqueued; + + /// + /// Invoked when a delegate is dequeued for execution but prior to its execution. + /// + /// + /// Invoked on the executing thread, not the calling thread. + ///

+ /// During invocation, the queue lock is held, so avoid long-running + /// operations or deadlocks may occur. + ///
+ public event DelegateQueueEventHandler? DelegateDequeued; + + /// + /// Invoked after a delegate has been executed. + /// + /// + /// Invoked on the executing thread, not the calling thread. + ///

+ /// During invocation, the queue lock is held, so avoid long-running + /// operations or deadlocks may occur. + ///
+ public event DelegateQueueEventHandler? DelegateExecuted; + + /// + /// The queue which is used to store enqueued delegates. + /// + private readonly StaticArrayQueue _queue; + + /// + /// A wait handle that can be used to block until the queue is not full. + /// + private readonly ManualResetEvent _queueFullWaitHandle; + + /// + /// The ID of the thread currently executing the queue. + /// + private int _executingThreadId; + + /// + /// A flag indicating whether the object has been disposed of. + /// + private volatile bool _disposed; + + /// + /// A lock used to ensure thread-safe access to the object. + /// + private readonly Lock _lock; + + /// + /// Gets a read-only collection for the underlying queue of enqueued delegates. + /// + /// The underlying queue of enqueued delegates. + public IReadOnlyCollection Queue => _queue; + + /// + /// Constructs a new . + /// + /// The size of the queue, or to use the default value of . + public DelegateQueue(int? size = null) { + size ??= Environment.ProcessorCount; + + // Fields + _queue = new(size.Value); + _queueFullWaitHandle = new(false); + _executingThreadId = -1; + _disposed = false; + _lock = new(); + } + + /// + /// Disposes of the . + /// + ~DelegateQueue() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + /// + /// Attempts to enqueue a delegate for execution, and + /// waits for it to complete if specified. + /// + /// The delegate to enqueue. + /// A pointer to the caller of the delegate. + /// A pointer to the parameters of the delegate. + /// A pointer to the return value of the delegate. + /// Whether to wait for the delegate to complete execution. + /// The timeout in milliseconds to wait for the delegate to complete execution, or (-1) to wait indefinitely. + /// if the delegate executed within the timeout period; otherwise, . + public bool Enqueue(Delegate @delegate, nint caller, nint parameters, out nint @return, bool wait = false, int timeout = -1) { + ObjectDisposedException.ThrowIf(_disposed, this); + + // Enter the lock + _lock.Enter(); + + // Avoid recursive episodes + if (Environment.CurrentManagedThreadId == _executingThreadId) { + try { + throw new InvalidOperationException($"Enqueuing a delegate from the same-thread (recursion) when a delegate is currently being executed in a {nameof(DelegateQueue)} is not allowed!"); + } finally { + _lock.Exit(); + } + } + + // Wait if the queue is full + while (_queue.IsFull) { + _queueFullWaitHandle.Reset(); + _lock.Exit(); // exit the lock to allow other threads to enqueue + if (!_queueFullWaitHandle.WaitOne(timeout)) { + throw new TimeoutException("The delegate queue is full and the operation timed out."); + } + _lock.Enter(); // re-enter the lock after waiting + } + + // Fetch and enqueue the delegate + ref EnqueuedDelegate entry = ref _queue.PeekEnqueue(); + entry.Delegate = @delegate; + entry.Caller = caller; + entry.Parameters = parameters; + entry.Return = nint.Zero; + entry.Exception = null; + if (wait) { + if (entry.WaitHandle == null) { + entry.WaitHandle = new(false); + } else { + entry.WaitHandle.Reset(); + } + } + entry.HasAwaiter = wait; + _queue.Enqueue(); + + // Why would we not exit the lock here? + // Because we're still in the process of enqueuing, + // and even in the nanosecond timestamp of checking + // the boolean wait and comparing the timeout, + // I have run into race conditions where the + // delegate is executed before the lock is exited, + // resulting in a deadlock. Yeah. Figure that one out. + // ... I had to and it took me days. + + // Wait for the delegate to complete if specified + bool result = true; + if (wait) { + if (timeout != Timeout.Infinite) { + OnDelegateEnqueued(entry); + _lock.Exit(); // exit the lock + result = entry.WaitHandle!.WaitOne(timeout); + } else { + OnDelegateEnqueued(entry); + _lock.Exit(); // exit the lock + entry.WaitHandle!.WaitOne(); // wait indefinitely + } + @return = entry.Return; + if (entry.Exception != null) ExceptionDispatchInfo.Capture(entry.Exception).Throw(); + } else { + OnDelegateEnqueued(entry); + _lock.Exit(); // exit the lock + @return = nint.Zero; + } + return result; + } + + /// + /// Loops through the queue and executes all enqueued delegates. + /// + public void Execute() { + ObjectDisposedException.ThrowIf(_disposed, this); + _lock.Enter(); + try { + // If there's nothing to execute, return immediately + if (_queue.IsEmpty) return; + + // Loop through the queue and execute each delegate + _executingThreadId = Environment.CurrentManagedThreadId; + while (_queue.Count != 0) { + ref EnqueuedDelegate entry = ref _queue.PeekDequeue(); + OnDelegateDequeued(entry); + _lock.Exit(); // leave the lock for delegate execution + try { + entry.Execute(); + } finally { + _lock.Enter(); // re-enter the lock after execution + OnDelegateExecuted(entry); + // if we disposed of ourselves or the queue while executing, + // it could have become empty, so we check again + if (!_disposed && !_queue.IsEmpty) { + entry.WaitHandle?.Set(); + _queue.Dequeue(); + } + } + if (_disposed) return; // if we disposed of ourselves while executing, exit the loop + } + + // Cleanup executing state + _queueFullWaitHandle.Set(); // signal that the queue is not full anymore + _executingThreadId = -1; // reset the executing thread ID + } finally { + _lock.Exit(); // finally exit the lock + } + } + + /// + private void OnDelegateEnqueued(EnqueuedDelegate @delegate) { + DelegateEnqueued?.Invoke(this, @delegate); + } + + /// + private void OnDelegateDequeued(EnqueuedDelegate @delegate) { + DelegateDequeued?.Invoke(this, @delegate); + } + + /// + private void OnDelegateExecuted(EnqueuedDelegate @delegate) { + DelegateExecuted?.Invoke(this, @delegate); + } + + /// + /// Disposes of the . + /// + public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// if disposal is being performed by the garbage collector, otherwise + /// + private void Dispose(bool disposing) { + _lock.Enter(); + try { + if (_disposed) return; + + // Dispose managed state (managed objects) + if (disposing) { + // Dispose of the underlying wait handles + EnqueuedDelegate[] items = _queue.Items; + for (int i = 0; i < items.Length; i++) { + ref EnqueuedDelegate entry = ref items[i]; + entry.WaitHandle?.Dispose(); + } + _queue.Clear(); + + // Dispose the queue full wait handle + _queueFullWaitHandle.Dispose(); + } + + // Dispose unmanaged state (unmanaged objects) + // ... + + // Indicate disposal completion + _disposed = true; + } finally { + _lock.Exit(); + } + } + + /// + /// Represents a delegate which has been enqueued for execution. + /// + public record struct EnqueuedDelegate { + + /// + /// Gets or sets the delegate to be executed. + /// + /// The delegate to execute. + public required Delegate Delegate { get; set; } + + /// + /// Gets or sets a pointer to the caller of the delegate. + /// + /// The delegate's caller pointer. + public required nint Caller { get; set; } + + /// + /// Gets or sets a pointer to the parameters of the delegate. + /// + /// The delegate's parameters pointer. + public required nint Parameters { get; set; } + + /// + /// Gets or sets a pointer to the return value of the delegate. + /// + /// The delegate's return pointer. + public required nint Return { get; set; } + + /// + /// Gets or sets an exception that occurred during execution of the delegate, if any. + /// + /// The exception that occurred during execution, or if no exception occurred. + public required Exception? Exception { get; set; } + + /// + /// Gets or sets a wait handle that can be used to wait for the delegate to complete execution. + /// + /// The wait handle for the delegate. + public required ManualResetEvent? WaitHandle { get; set; } + + /// + /// Gets or sets a flag indicating whether the delegate has an awaiter. + /// + /// if the delegate has an awaiter; otherwise, . + public required bool HasAwaiter { get; set; } + + /// + /// Executes the enqueued delegate. + /// + public void Execute() { + try { + switch (Delegate) { + case Action action: + action(); + break; + case Action action2: + action2(Caller); + break; + case Action action3: + action3(Caller, Parameters); + break; + case Func func: + Return = func(); + break; + case Func func2: + Return = func2(Caller); + break; + case Func func3: + Return = func3(Caller, Parameters); + break; + default: + throw new NotSupportedException($"The delegate type {Delegate.GetType()} is not supported by the {nameof(EnqueuedDelegate)} structure."); + } + } catch (Exception e) { + // If we have an awaiter, pass the exception to it + // Otherwise, it's an unhandled exception + if (HasAwaiter) { + Exception = e; + } else { + throw; + } + } + } + + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Threading/RequiresMainThreadException.cs b/CatalystUI/Core/CatalystUI.Threading/RequiresMainThreadException.cs new file mode 100644 index 0000000..aa2a58b --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Threading/RequiresMainThreadException.cs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using System.ComponentModel; + +namespace Catalyst.Threading { + + /// + /// Represents an exception that is thrown + /// when the operation requires the main thread + /// to be captured, but it has not been captured. + /// + public class RequiresMainThreadException : InvalidAsynchronousStateException { + + /// + /// Constructs a new . + /// + /// The name of the class that contains the method requiring the main thread to be captured. + /// The name of the method that requires the main thread to be captured. + public RequiresMainThreadException(string className, string methodName) : base(GetMessage(className, methodName)) { + // ... + } + + /// + /// Gets the message for the exception. + /// + /// The name of the class that contains the method requiring the main thread to be captured. + /// The name of the method that requires the main thread to be captured. + /// A formatted message indicating the method that requires the main thread to be captured. + private static string GetMessage(string className, string methodName) { + return $"The method '{className}#{methodName}' requires the main thread to be captured, but it has not been captured!"; + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs b/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs new file mode 100644 index 0000000..66a3e0f --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs @@ -0,0 +1,760 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Collections; +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace Catalyst.Threading { + + /// + /// Provides delegate execution synchronization for a given thread. + /// + public sealed class ThreadDelegateDispatcher : IDisposable { + + /// + /// An event handler delegate for events related to the . + /// + public delegate void DispatcherEventHandler(ThreadDelegateDispatcher dispatcher); + + /// + /// An event handler delegate for queue events related to the . + /// + public delegate void DispatcherQueueEventHandler(ThreadDelegateDispatcher dispatcher, Delegate @delegate); + + /// + /// Invoked prior to the execution of the delegate queue. + /// + /// + /// Always invoked from the dispatcher's associated thread. + /// + public event DispatcherEventHandler? PreExecute; + + /// + /// Invoked after the execution of the delegate queue. + /// + /// + /// Always invoked from the dispatcher's associated thread. + /// + public event DispatcherEventHandler? PostExecute; + + /// + public event DispatcherQueueEventHandler? DelegateEnqueued; + + /// + public event DispatcherQueueEventHandler? DelegateDequeued; + + /// + public event DispatcherQueueEventHandler? DelegateExecuted; + + /// + /// The default sizing for the delegate caches when using the typed execute methods. + /// + public const int DEFAULT_DELEGATE_CACHE_SIZE = 8; + + /// + /// The maximum number of milliseconds (ms) an enqueued delegate + /// should remain unresponsive before timing out. + /// + /// + /// + /// The timeout is a utility value which can be used by + /// any user of a dispatcher to ensure that their + /// awaited executions do not block indefinitely. + /// It is a commonly suggested value, but is not + /// required to be used, since some delegates may + /// take longer to execute than others. + /// + /// + /// The value is larger when in debug mode to allow + /// for debuggers to attach and inspect the code's + /// execution. In release mode, it is set to a + /// value of 250ms for fast failures. + /// + /// + public static int LockoutTimeout => Debugger.IsAttached ? Timeout.Infinite : 250; + + /// + /// Gets the main thread dispatcher fetched via . + /// + /// The main thread dispatcher, or if it has not been captured. + public static ThreadDelegateDispatcher? MainThreadDispatcher { get; private set; } + + /// + /// Gets a flag indicating if the main thread has been captured. + /// + /// if the main thread has been captured, otherwise . + [MemberNotNullWhen(true, nameof(MainThreadDispatcher))] + public static bool IsMainThreadCaptured => MainThreadDispatcher != null; + + /// + /// The dispatcher's worker thread, or if it is associated with the main thread. + /// + private readonly Thread? _thread; + + /// + /// The ID of the dispatcher's worker thread. + /// + private readonly int _threadId; + + /// + /// The queue of delegates to be executed on the thread. + /// + private readonly DelegateQueue _queue; + + /// + /// A flag indicating whether the object has been disposed of. + /// + private volatile bool _disposed; + + /// + /// A lock used to ensure thread-safe access to the object. + /// + private readonly Lock _lock; + + /// + /// Gets the worker thread of the dispatcher. + /// + /// The dispatcher's worker thread, or if it is associated with the main thread. + public Thread? Thread => _thread; + + /// + /// Gets the managed thread ID of dispatcher's worker thread. + /// + /// The dispatcher's worker thread managed thread ID. + public int ThreadId => _threadId; + + /// + /// Gets the number of delegates currently enqueued in the dispatcher. + /// + public int Enqueued => _queue.Queue.Count; + + /// + /// Constructs a new . + /// + /// The thread to associate with the dispatcher. + /// The managed thread ID of the thread. + /// The size of the delegate queue, or to use the default value of . + private ThreadDelegateDispatcher(Thread thread, int threadId, int? queueSize = null) { + // Fields + _thread = thread; + _threadId = threadId; + _queue = new(queueSize); + _queue.DelegateEnqueued += HandleDelegateEnqueued; + _queue.DelegateDequeued += HandleDelegateDequeued; + _queue.DelegateExecuted += HandleDelegateExecuted; + _disposed = false; + _lock = new(); + } + + /// + /// Constructs a new + /// by creating a new background worker thread with the + /// specified name. + /// + /// + /// To end the execution of the dispatcher, simply + /// remove all references to it and ensure no + /// further delegates are enqueued. The newly + /// created worker thread is marked as a background + /// thread. You can also call the + /// method on the dispatcher to immediately + /// end execution, although this is not + /// encouraged as it could result in undefined + /// behavior if the thread is still in the process + /// of executing an enqueued delegate. + /// + /// The name of the thread to be created, or to use the default thread name. + /// The size of the delegate queue, or to use the default value of . + /// A new instance of . + public static ThreadDelegateDispatcher New(string? threadName = null, int? queueSize = null) { + ThreadDelegateDispatcher? dispatcher = null; + + // ReSharper disable AccessToDisposedClosure + // ReSharper disable AccessToModifiedClosure + // ReSharper disable LoopVariableIsNeverChangedInsideLoop + // Construct a new worker thread prior to constructing the dispatcher + using ManualResetEvent spinUpWaitHandle = new(false); + Thread thread = new(() => { + spinUpWaitHandle.Set(); + while (dispatcher == null) { + Thread.SpinWait(1); + Thread.Yield(); + } + dispatcher.WorkerThread(); + }) { + Name = threadName, IsBackground = true + }; + // ReSharper restore LoopVariableIsNeverChangedInsideLoop + // ReSharper restore AccessToDisposedClosure + // ReSharper restore AccessToModifiedClosure + + // Construct the dispatcher, start the thread, and return + dispatcher = new(thread, thread.ManagedThreadId, queueSize); + thread.Start(); + spinUpWaitHandle.WaitOne(); // wait for the thread to spin up + return dispatcher; + } + + /// + /// Constructs a new + /// by capturing the current thread and enqueuing the specified + /// callback as the first delegate to be executed. + /// + /// + /// To end execution of the dispatcher, execute the + /// method somewhere within + /// the callback. If the captured thread is not marked + /// as a background thread, such as the main thread, + /// the application will not close until the dispatcher + /// is disposed of. To instead create a new background + /// worker thread, prefer the + /// method. + /// + /// The callback to be executed on the thread. + /// The size of the delegate queue, or to use the default value of . + /// Whether the thread to be captured is the main thread. If , the dispatcher will be set as the main thread dispatcher. + public static void Capture(Action callback, int? queueSize = null, bool isMainThread = false) { + if (isMainThread && MainThreadDispatcher != null) throw new InvalidOperationException("The main thread dispatcher has already been captured!"); + ThreadDelegateDispatcher dispatcher = new( + Thread.CurrentThread, + Environment.CurrentManagedThreadId, + queueSize + ); + if (isMainThread) MainThreadDispatcher = dispatcher; + // manually enqueue to prevent our smart thread id checking from just calling it directly + dispatcher._queue.Enqueue(() => callback(dispatcher), nint.Zero, nint.Zero, out _); + dispatcher.WorkerThread(); + } + + /// + /// Disposes of the . + /// + ~ThreadDelegateDispatcher() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + /// + public bool Execute(in Action @delegate, TCaller caller, TParameters parameters, bool wait = false, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + ActionCache.Lock.Enter(); + bool shouldUnlock = true; + try { + if (Environment.CurrentManagedThreadId == _threadId) { + @delegate(caller, parameters); + return true; + } else { + // Request a context from the pool + StaticArrayPool.Context> pool = ActionCache.Pool; + ref ActionCache.Context ctx = ref pool.PeekNext(); + + // Assign the context + ctx._delegate = @delegate; + ctx._caller = caller; + ctx._parameters = parameters; + + // Allocate context in the pool + int index = pool.Allocate(); + + // Enqueue and return + if (wait) { + shouldUnlock = false; + ActionCache.Lock.Exit(); + } + + return _queue.Enqueue( + ActionCache.Action, + index, + nint.Zero, + out nint _, + wait, + timeout + ); + } + } finally { + if (shouldUnlock) ActionCache.Lock.Exit(); + } + } + + /// + public bool Execute(in Action @delegate, TCaller caller, bool wait = false, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + ActionCache.Lock.Enter(); + bool shouldUnlock = true; + try { + if (Environment.CurrentManagedThreadId == _threadId) { + @delegate(caller); + + return true; + } else { + // Request a context from the pool + StaticArrayPool.Context> pool = ActionCache.Pool; + ref ActionCache.Context ctx = ref pool.PeekNext(); + + // Assign the context + ctx._delegate = @delegate; + ctx._caller = caller; + + // Allocate context in the pool + int index = pool.Allocate(); + + // Enqueue and return + if (wait) { + shouldUnlock = false; + ActionCache.Lock.Exit(); + } + + return _queue.Enqueue( + ActionCache.Action, + index, + nint.Zero, + out nint _, + wait, + timeout + ); + } + } finally { + if (shouldUnlock) ActionCache.Lock.Exit(); + } + } + + /// + public bool Execute(in Action @delegate, nint caller, nint parameters, bool wait = false, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + if (Environment.CurrentManagedThreadId == _threadId) { + @delegate(caller, parameters); + return true; + } else { + return _queue.Enqueue(@delegate, caller, parameters, out _, wait, timeout); + } + } + + /// + public bool Execute(in Action @delegate, nint caller, bool wait = false, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + if (Environment.CurrentManagedThreadId == _threadId) { + @delegate(caller); + return true; + } else { + return _queue.Enqueue(@delegate, caller, nint.Zero, out _, wait, timeout); + } + } + + /// + public bool Execute(in Action @delegate, bool wait = false, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + if (Environment.CurrentManagedThreadId == _threadId) { + @delegate(); + return true; + } else { + return _queue.Enqueue(@delegate, nint.Zero, nint.Zero, out _, wait, timeout); + } + } + + /// + public bool Execute(in Func @delegate, TCaller caller, TParameters parameters, out TReturn @return, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + FunctionCache.Lock.Enter(); + bool shouldUnlock = true; + try { + if (Environment.CurrentManagedThreadId == _threadId) { + @return = @delegate(caller, parameters); + return true; + } else { + // Request a context from the pool + StaticArrayPool.Context> pool = FunctionCache.Pool; + ref FunctionCache.Context ctx = ref pool.PeekNext(); + + // Assign the context + ctx._delegate = @delegate; + ctx._caller = caller; + ctx._parameters = parameters; + ctx._return = default!; + + // Allocate context in the pool + int index = pool.Allocate(); + shouldUnlock = false; + FunctionCache.Lock.Exit(); + bool success = _queue.Enqueue( + FunctionCache.Function, + index, + nint.Zero, + out nint _, + true, + timeout + ); + shouldUnlock = true; + FunctionCache.Lock.Enter(); + @return = ctx._return; + return success; + } + } finally { + if (shouldUnlock) FunctionCache.Lock.Exit(); + } + } + + /// + public bool Execute(in Func @delegate, TCaller caller, out TReturn @return, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + FunctionCache.Lock.Enter(); + bool shouldUnlock = true; + try { + if (Environment.CurrentManagedThreadId == _threadId) { + @return = @delegate(caller); + + return true; + } else { + // Request a context from the pool + StaticArrayPool.Context> pool = FunctionCache.Pool; + ref FunctionCache.Context ctx = ref pool.PeekNext(); + + // Assign the context + ctx._delegate = @delegate; + ctx._caller = caller; + ctx._return = default!; + + // Allocate context in the pool + int index = pool.Allocate(); + shouldUnlock = false; + FunctionCache.Lock.Exit(); + bool success = _queue.Enqueue( + FunctionCache.Function, + index, + nint.Zero, + out nint _, + true, + timeout + ); + shouldUnlock = true; + FunctionCache.Lock.Enter(); + @return = ctx._return; + return success; + } + } finally { + if (shouldUnlock) FunctionCache.Lock.Exit(); + } + } + + /// + public bool Execute(in Func @delegate, out TReturn @return, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + FunctionCache.Lock.Enter(); + bool shouldUnlock = true; + try { + if (Environment.CurrentManagedThreadId == _threadId) { + @return = @delegate(); + return true; + } else { + // Request a context from the pool + StaticArrayPool.Context> pool = FunctionCache.Pool; + ref FunctionCache.Context ctx = ref pool.PeekNext(); + + // Assign the context + ctx._delegate = @delegate; + ctx._return = default!; + + // Allocate context in the pool + int index = pool.Allocate(); + shouldUnlock = false; + FunctionCache.Lock.Exit(); + bool success = _queue.Enqueue( + FunctionCache.Function, + index, + nint.Zero, + out nint _, + true, + timeout + ); + shouldUnlock = true; + FunctionCache.Lock.Enter(); + @return = ctx._return; + return success; + } + } finally { + if (shouldUnlock) FunctionCache.Lock.Exit(); + } + } + + /// + public bool Execute(in Func @delegate, nint caller, nint parameters, out nint @return, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + if (Environment.CurrentManagedThreadId == _threadId) { + @return = @delegate(caller, parameters); + return true; + } else { + return _queue.Enqueue(@delegate, caller, parameters, out @return, true, timeout); + } + } + + /// + public bool Execute(in Func @delegate, nint caller, out nint @return, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + if (Environment.CurrentManagedThreadId == _threadId) { + @return = @delegate(caller); + return true; + } else { + return _queue.Enqueue(@delegate, caller, nint.Zero, out @return, true, timeout); + } + } + + /// + public bool Execute(in Func @delegate, out nint @return, int timeout = -2) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (timeout == -2) timeout = LockoutTimeout; + if (Environment.CurrentManagedThreadId == _threadId) { + @return = @delegate(); + return true; + } else { + return _queue.Enqueue(@delegate, nint.Zero, nint.Zero, out @return, true, timeout); + } + } + + /// + private void HandleDelegateEnqueued(DelegateQueue queue, DelegateQueue.EnqueuedDelegate @delegate) { + OnDelegateEnqueued(@delegate.Delegate); + } + + /// + private void HandleDelegateDequeued(DelegateQueue queue, DelegateQueue.EnqueuedDelegate @delegate) { + OnDelegateDequeued(@delegate.Delegate); + } + + /// + private void HandleDelegateExecuted(DelegateQueue queue, DelegateQueue.EnqueuedDelegate @delegate) { + OnDelegateExecuted(@delegate.Delegate); + } + + /// + private void OnPreExecute() { + PreExecute?.Invoke(this); + } + + /// + private void OnPostExecute() { + PostExecute?.Invoke(this); + } + + /// + private void OnDelegateEnqueued(Delegate @delegate) { + DelegateEnqueued?.Invoke(this, @delegate); + } + + /// + private void OnDelegateDequeued(Delegate @delegate) { + DelegateDequeued?.Invoke(this, @delegate); + } + + /// + private void OnDelegateExecuted(Delegate @delegate) { + DelegateExecuted?.Invoke(this, @delegate); + } + + /// + /// The worker thread method that processes the delegate queue. + /// + private void WorkerThread() { + while (true) { + _lock.Enter(); + try { + if (_disposed) break; + OnPreExecute(); + if (_disposed) break; + _queue.Execute(); + if (_disposed) break; + OnPostExecute(); + } finally { + _lock.Exit(); + } + } + } + + /// + /// Disposes of the . + /// + public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// if disposal is being performed by the garbage collector, otherwise + /// + private void Dispose(bool disposing) { + _lock.Enter(); + try { + if (_disposed) return; + + // Dispose managed state (managed objects) + if (disposing) { + _queue.DelegateEnqueued -= HandleDelegateEnqueued; + _queue.DelegateDequeued -= HandleDelegateDequeued; + _queue.DelegateExecuted -= HandleDelegateExecuted; + _queue.Dispose(); + } + + // Dispose unmanaged state (unmanaged objects) + // ... + + // Indicate disposal completion + _disposed = true; + } finally { + _lock.Exit(); + } + } + +#pragma warning disable + // ReSharper disable All + internal static class ActionCache { + + internal static readonly Lock Lock = new(); + internal static readonly StaticArrayPool Pool = new(DEFAULT_DELEGATE_CACHE_SIZE); + + internal static readonly Action Action = (poolIndex, _) => { + ref Context ctx = ref Pool[(int) poolIndex]; + try { + ctx.Execute(); + } finally { + Lock.Enter(); + try { + Pool.Release((int) poolIndex); + } finally { + Lock.Exit(); + } + } + }; + + internal struct Context { + + internal Action _delegate; + internal TCaller _caller; + internal TParameters _parameters; + internal void Execute() => _delegate(_caller, _parameters); + + } + + } + + internal static class ActionCache { + + internal static readonly Lock Lock = new(); + internal static readonly StaticArrayPool Pool = new(DEFAULT_DELEGATE_CACHE_SIZE); + + internal static readonly Action Action = poolIndex => { + ref Context ctx = ref Pool[(int) poolIndex]; + try { + ctx.Execute(); + } finally { + Lock.Enter(); + try { + Pool.Release((int) poolIndex); + } finally { + Lock.Exit(); + } + } + }; + + internal struct Context { + + internal Action _delegate; + internal TCaller _caller; + internal void Execute() => _delegate(_caller); + + } + + } + + internal static class FunctionCache { + + internal static readonly Lock Lock = new(); + internal static readonly StaticArrayPool Pool = new(DEFAULT_DELEGATE_CACHE_SIZE); + + internal static readonly Func Function = (poolIndex, _) => { + ref Context ctx = ref Pool[(int) poolIndex]; + ctx.Execute(); + + return nint.Zero; + }; + + internal struct Context { + + internal Func _delegate; + internal TCaller _caller; + internal TParameters _parameters; + internal TReturn _return; + internal void Execute() => _return = _delegate(_caller, _parameters); + + } + + } + + internal static class FunctionCache { + + internal static readonly Lock Lock = new(); + internal static readonly StaticArrayPool Pool = new(DEFAULT_DELEGATE_CACHE_SIZE); + + internal static readonly Func Function = (poolIndex, _) => { + ref Context ctx = ref Pool[(int) poolIndex]; + ctx.Execute(); + + return nint.Zero; + }; + + internal struct Context { + + internal Func _delegate; + internal TCaller _caller; + internal TReturn _return; + internal void Execute() => _return = _delegate(_caller); + + } + + } + + internal static class FunctionCache { + + internal static readonly Lock Lock = new(); + internal static readonly StaticArrayPool Pool = new(DEFAULT_DELEGATE_CACHE_SIZE); + + internal static readonly Func Function = (poolIndex, _) => { + ref Context ctx = ref Pool[(int) poolIndex]; + ctx.Execute(); + + return nint.Zero; + }; + + internal struct Context { + + internal Func _delegate; + internal TReturn _return; + internal void Execute() => _return = _delegate(); + + } + + } + // ReSharper restore All +#pragma warning restore + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Analyzers/AnalyzerReleases.Shipped.md b/CatalystUI/Tooling/CatalystUI.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..445db9f --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,10 @@ +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes + ---------|----------|----------|---------------------------------------------------- + CTL001 | Usage | Warning | CTL001_Threading_TDDAnalyzer +CTL002 | Usage | Error | CTL002_Threading_Attributes_CacheDelegateAttribute +CTL003 | Usage | Error | CTL003_Threading_Attributes_CacheDelegateAttribute +CTL004 | Usage | Error | CTL003_Threading_Attributes_CacheDelegateAttribute \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Analyzers/AnalyzerReleases.Unshipped.md b/CatalystUI/Tooling/CatalystUI.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..e69de29 diff --git a/CatalystUI/Tooling/CatalystUI.Analyzers/CatalystUI.Analyzers.csproj b/CatalystUI/Tooling/CatalystUI.Analyzers/CatalystUI.Analyzers.csproj new file mode 100644 index 0000000..76b2b0f --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Analyzers/CatalystUI.Analyzers.csproj @@ -0,0 +1,58 @@ + + + + + true + + + + + + Catalyst.Analyzers + Catalyst.Analyzers + false + + + CatalystUI Analyzers + 1.0.0 + beta.2 + CatalystUI LLC + Analyzers API provided by the CatalystUI library. + CatalystUI,analyzers + + + + + + + + + + + netstandard2.0 + latest + + + + + false + false + false + none + + + + + + + + + + + \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Analyzers/Descriptors.cs b/CatalystUI/Tooling/CatalystUI.Analyzers/Descriptors.cs new file mode 100644 index 0000000..b585f1c --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Analyzers/Descriptors.cs @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Microsoft.CodeAnalysis; + +namespace Catalyst.Analyzers { + + /// + /// Contains all diagnostic descriptors for the CatalystUI analyzers. + /// + public static class Descriptors { + + /// + /// CTL001: Ignored return value of awaited dispatched execution. + /// + public static readonly DiagnosticDescriptor CTL001 = new( + "CTL001", + "Ignored return value of awaited execution", + "The return value of the '{0}' method on '{1}' should not be ignored when the '{2}' parameter is set to true", + "Usage", + DiagnosticSeverity.Warning, + true, + "Ignoring the return value of an awaited execution can lead to unexpected behavior if the execution fails or does not complete as expected. If ignoring the return value is intentional, explicitly ignore it using '_'." + ); + + /// + /// CTL002: Partial class required for cached delegate. + /// + public static readonly DiagnosticDescriptor CTL002 = new( + "CTL002", + "Partial class required for cached delegate", + "Any class containing a method annotated with the '{0}' must be declared as 'partial'", + "Usage", + DiagnosticSeverity.Error, + true, + "Cached delegates require the containing class to be partial to allow the source generator to inject the necessary code." + ); + + /// + /// CTL003: Backing method for cached delegate must be static. + /// + public static readonly DiagnosticDescriptor CTL003 = new( + "CTL003", + "Backing method for cached delegate must be static", + "A method annotated with the '{0}' must be declared as 'static'", + "Usage", + DiagnosticSeverity.Error, + true, + "Cached delegates are designed to be static to ensure they can be accessed without an instance of the class. This allows for efficient caching and execution across threads." + ); + + /// + /// CTL004: Too many parameters for a cached delegate. + /// + public static readonly DiagnosticDescriptor CTL004 = new( + "CTL004", + "Too many parameters for a cached delegate", + "A method annotated with the '{0}' must contain no more than 2 parameters: 'caller' and 'parameters'", + "Usage", + DiagnosticSeverity.Error, + true, + "Dispatched delegates are passed between threads and require minimal data to retain performance and efficiency. Methods annotated with the '{0}' attribute must adhere to this constraint." + ); + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/CachedDelegateGenerator.cs b/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/CachedDelegateGenerator.cs new file mode 100644 index 0000000..a0680a5 --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/CachedDelegateGenerator.cs @@ -0,0 +1,162 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Attributes.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Linq; + +namespace Catalyst.Analyzers.Threading { + + /// + /// The source generator for the . + /// + [Generator] + public sealed class CachedDelegateGenerator : IIncrementalGenerator { + + /// + /// Gets the prefix for a cached action field. + /// + private const string CACHED_ACTION_PREFIX = "_cachedAction"; + + /// + /// Gets the prefix for a cached function field. + /// + private const string CACHED_FUNCTION_PREFIX = "_cachedFunction"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) { + // Fetch the methods annotated with the CacheDelegateAttribute + IncrementalValuesProvider<(MethodDeclarationSyntax, IMethodSymbol)> methods = + context.SyntaxProvider.ForAttributeWithMetadataName( + $"Catalyst.Attributes.Threading.{nameof(CachedDelegateAttribute)}", + static (node, _) => node is MethodDeclarationSyntax, + static (ctx, _) => ( + (MethodDeclarationSyntax) ctx.TargetNode, + (IMethodSymbol) ctx.SemanticModel.GetDeclaredSymbol(ctx.TargetNode)! + ) + ).Where(ctx => ctx is { Item1: not null, Item2: not null }); + + // Register the source generation for the methods + context.RegisterSourceOutput(methods, GenerateCacheDelegateSource); + } + + /// + /// Generates the source code for the cached delegate. + /// + private static void GenerateCacheDelegateSource(SourceProductionContext context, (MethodDeclarationSyntax, IMethodSymbol) pair) { + MethodDeclarationSyntax methodSyntax = pair.Item1; + IMethodSymbol methodSymbol = pair.Item2; + + // Verify the containing class is partial + INamedTypeSymbol classSymbol = methodSymbol.ContainingType; + if (!classSymbol.DeclaringSyntaxReferences.Any(sr => sr.GetSyntax() is ClassDeclarationSyntax cds && cds.Modifiers.Any(m => m.Text == "partial"))) { + context.ReportDiagnostic(Diagnostic.Create( + Descriptors.CTL002, + methodSyntax.GetLocation(), + nameof(CachedDelegateAttribute) + )); + return; + } + + // Verify the method is static + if (!methodSymbol.IsStatic) { + context.ReportDiagnostic(Diagnostic.Create( + Descriptors.CTL003, + methodSyntax.GetLocation(), + nameof(CachedDelegateAttribute) + )); + return; + } + + // Verify the method does not contain too many parameters + if (methodSymbol.Parameters.Length > 2) { + context.ReportDiagnostic(Diagnostic.Create( + Descriptors.CTL004, + methodSyntax.GetLocation(), + nameof(CachedDelegateAttribute) + )); + return; + } + + // Prepare variables for the namespace and class + string ns = classSymbol.ContainingNamespace.ToDisplayString(); + string classAccess = ConvertAccessibility(classSymbol.DeclaredAccessibility); + string className = classSymbol.Name; + bool classStatic = classSymbol.IsStatic; + + // Prepare variables for the method + string methodAccess = ConvertAccessibility(methodSymbol.DeclaredAccessibility); + string methodName = methodSymbol.Name; + string prefix = methodSymbol.ReturnsVoid ? CACHED_ACTION_PREFIX : CACHED_FUNCTION_PREFIX; + string fieldName = $"{prefix}{methodName}"; + string delegateType = methodSymbol.ReturnsVoid ? + methodSymbol.Parameters.Length switch { + 0 => "Action", + 1 => $"Action<{methodSymbol.Parameters[0].Type.ToDisplayString()}>", + 2 => $"Action<{methodSymbol.Parameters[0].Type.ToDisplayString()}, {methodSymbol.Parameters[1].Type.ToDisplayString()}>", + _ => throw new NotImplementedException() + } : + methodSymbol.Parameters.Length switch { + 0 => $"Func<{methodSymbol.ReturnType.ToDisplayString()}>", + 1 => $"Func<{methodSymbol.Parameters[0].Type.ToDisplayString()}, {methodSymbol.ReturnType.ToDisplayString()}>", + 2 => $"Func<{methodSymbol.Parameters[0].Type.ToDisplayString()}, {methodSymbol.Parameters[1].Type.ToDisplayString()}, {methodSymbol.ReturnType.ToDisplayString()}>", + _ => throw new NotImplementedException() + }; + + // Prepare variables for the comments + string classCref = $"{ns}.{className}"; + string methodCref = $"{ns}.{className}.{methodName}("; + methodCref += string.Join(", ", methodSymbol.Parameters.Select(p => p.Type.ToDisplayString())); + methodCref += ")"; + + // Partial class source code generation + string source = + $$""" + #nullable enable + + // + namespace {{ns}} { + + {{classAccess}} {{(classStatic ? "static " : "")}}partial class {{className}} { + + /// + /// A cached {{(methodSymbol.ReturnsVoid ? "action" : "function")}} for the method . + /// + /// + {{methodAccess}} static {{delegateType}} {{fieldName}} = {{methodName}}; + + } + + } + """; + + // Add the generated source to the compilation + context.AddSource($"{className}_{methodName}_CachedDelegates.g.cs", source); + } + + private static string ConvertAccessibility(Accessibility accessibility) { + return accessibility switch { + Accessibility.Public => "public", + Accessibility.Private => "private", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + Accessibility.ProtectedOrInternal => "protected internal", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.NotApplicable => "", + _ => throw new ArgumentOutOfRangeException(nameof(accessibility), accessibility, null) + }; + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/ThreadDelegateDispatcherAnalyzer.cs b/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/ThreadDelegateDispatcherAnalyzer.cs new file mode 100644 index 0000000..a3fb6a5 --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/ThreadDelegateDispatcherAnalyzer.cs @@ -0,0 +1,72 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using System.Collections.Immutable; +using System.Linq; + +namespace Catalyst.Analyzers.Threading { + + /// + /// Analyzes use cases for the Thread Delegate Dispatcher (TDD) in CatalystUI. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ThreadDelegateDispatcherAnalyzer : DiagnosticAnalyzer { + + /// + public override ImmutableArray SupportedDiagnostics => [ Descriptors.CTL001 ]; + + /// + public override void Initialize(AnalysisContext context) { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + } + + /// + /// Analyze invocations of the Thread Delegate Dispatcher (TDD) methods. + /// + /// The operation analysis context. + private static void AnalyzeInvocation(OperationAnalysisContext context) { + IInvocationOperation invocation = (IInvocationOperation) context.Operation; + + // If it is not an expression statement, ignore it + if (invocation.Parent is not IExpressionStatementOperation) return; + + // If it is not our method, ignore it + IMethodSymbol method = invocation.TargetMethod; + if (method.Name != "Execute" || method.ReturnType.SpecialType != SpecialType.System_Boolean) return; + + // If the 'wait' parameter is not true, ignore it + IParameterSymbol? waitParam = method.Parameters.FirstOrDefault(p => p.Name == "wait" && p.Type.SpecialType == SpecialType.System_Boolean); + bool shouldReport = false; + if (waitParam == null) { + shouldReport = true; + } else { + IArgumentOperation? waitArg = invocation.Arguments.FirstOrDefault(arg => SymbolEqualityComparer.Default.Equals(arg.Parameter, waitParam)); + if (waitArg == null) return; + if (waitArg.Value.ConstantValue is { HasValue: true, Value: true }) { + shouldReport = true; + } + } + + // Report diagnostic if the 'wait' parameter is true + if (shouldReport) { + Diagnostic diagnostic = Diagnostic.Create(Descriptors.CTL001, invocation.Syntax.GetLocation(), method.Name, method.ContainingType.ToDisplayString(), "wait"); + context.ReportDiagnostic(diagnostic); + } + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.CodeFix/CatalystUI.CodeFix.csproj b/CatalystUI/Tooling/CatalystUI.CodeFix/CatalystUI.CodeFix.csproj new file mode 100644 index 0000000..b31334c --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.CodeFix/CatalystUI.CodeFix.csproj @@ -0,0 +1,52 @@ + + + + + + Catalyst.CodeFix + Catalyst.CodeFix + false + + + CatalystUI CodeFix + 1.0.0 + beta.2 + CatalystUI LLC + CodeFix API provided by the CatalystUI library. + CatalystUI,codefix + + + + + + + + + + + netstandard2.0 + latest + + + + + false + false + false + none + + + + + + + + + + \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.CodeFix/Threading/CachedDelegateCodeFixProvider.cs b/CatalystUI/Tooling/CatalystUI.CodeFix/Threading/CachedDelegateCodeFixProvider.cs new file mode 100644 index 0000000..99b6ca8 --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.CodeFix/Threading/CachedDelegateCodeFixProvider.cs @@ -0,0 +1,162 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Analyzers; +using Catalyst.Attributes.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Catalyst.CodeFix.Threading { + + /// + /// Provides code fixes for the CacheDelegateAttribute (CDA) source generator. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CachedDelegateCodeFixProvider))] + [Shared] + public class CachedDelegateCodeFixProvider : CodeFixProvider { + + /// + public override ImmutableArray FixableDiagnosticIds => [ + Descriptors.CTL002.Id, + Descriptors.CTL003.Id + ]; + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { + foreach (Diagnostic diagnostic in context.Diagnostics) { + // CTL002: Partial class required for cached delegate + if (diagnostic.Id == Descriptors.CTL002.Id) { + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode? node = root?.FindNode(diagnostic.Location.SourceSpan); + if (node == null) continue; + + // Add mark class as partial code fix + context.RegisterCodeFix( + CodeAction.Create( + "Mark class as partial", + ct => MarkClassAsPartialAsync(context.Document, node, ct), + nameof(CachedDelegateCodeFixProvider) + "_MarkPartial" + ), + diagnostic + ); + + // CTL003: Backing method for cached delegate must be static + } else if (diagnostic.Id == Descriptors.CTL003.Id) { + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode? node = root?.FindNode(diagnostic.Location.SourceSpan); + if (node == null) continue; + + // Add make method static and pass caller code fix + if (node is MethodDeclarationSyntax methodNode && node.AncestorsAndSelf().OfType().FirstOrDefault() is ClassDeclarationSyntax classNode) { + string className = classNode.Identifier.ValueText; + if (methodNode.ParameterList.Parameters.All(p => p.Type?.ToString() != className)) { + context.RegisterCodeFix( + CodeAction.Create( + "Make method static and pass caller", + ct => MakeMethodStaticAndAddCaller(context.Document, node, ct), + nameof(CachedDelegateCodeFixProvider) + "_MakeStaticAndPassCaller" + ), + diagnostic + ); + } + } + + // Add make method static code fix + context.RegisterCodeFix( + CodeAction.Create( + "Make method static", + ct => MakeMethodStaticAsync(context.Document, node, ct), + nameof(CachedDelegateCodeFixProvider) + "_MakeStatic" + ), + diagnostic + ); + } + } + } + + /// + /// Marks the class containing the method annotated with as partial. + /// + private static async Task MarkClassAsPartialAsync(Document document, SyntaxNode node, CancellationToken cancellationToken) { + if (node.AncestorsAndSelf().OfType().FirstOrDefault() is not ClassDeclarationSyntax classNode) return document; + if (classNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) return document; + + // Add the partial modifier to the class + SyntaxToken partial = SyntaxFactory.Token(SyntaxKind.PartialKeyword).WithTrailingTrivia(SyntaxFactory.Space); + ClassDeclarationSyntax newClassNode = classNode.WithModifiers(classNode.Modifiers.Add(partial)).WithAdditionalAnnotations(Formatter.Annotation); + + // Apply changes to the document + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode? newRoot = root?.ReplaceNode(classNode, newClassNode); + return newRoot == null ? document : document.WithSyntaxRoot(newRoot); + } + + /// + /// Makes the method annotated with static and adds a caller parameter. + /// + private static async Task MakeMethodStaticAndAddCaller(Document document, SyntaxNode node, CancellationToken cancellationToken) { + if (node is not MethodDeclarationSyntax methodNode) return document; + if (node.AncestorsAndSelf().OfType().FirstOrDefault() is not ClassDeclarationSyntax classNode) return document; + if (methodNode.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) return document; + + // Add the static modifier to the method + SyntaxToken staticModifier = SyntaxFactory.Token(SyntaxKind.StaticKeyword).WithTrailingTrivia(SyntaxFactory.Space); + MethodDeclarationSyntax newMethodNode = methodNode.WithModifiers(methodNode.Modifiers.Add(staticModifier)).WithAdditionalAnnotations(Formatter.Annotation); + + // Add the caller parameter + ParameterSyntax callerParameter = SyntaxFactory.Parameter( + SyntaxFactory.Identifier("caller") + ).WithType(SyntaxFactory.ParseTypeName(classNode.Identifier.Text)).WithDefault(null); + SeparatedSyntaxList parameters = methodNode.ParameterList.Parameters; + parameters = parameters.Insert(0, callerParameter); + newMethodNode = newMethodNode.WithParameterList( + SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(parameters)) + ); + + // Apply changes to the document + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode? newRoot = root?.ReplaceNode(methodNode, newMethodNode); + return newRoot == null ? document : document.WithSyntaxRoot(newRoot); + } + + /// + /// Makes the method annotated with static. + /// + private static async Task MakeMethodStaticAsync(Document document, SyntaxNode node, CancellationToken cancellationToken) { + if (node is not MethodDeclarationSyntax methodNode) return document; + if (node.AncestorsAndSelf().OfType().FirstOrDefault() is not ClassDeclarationSyntax classNode) return document; + if (methodNode.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) return document; + + // Add the static modifier to the method + SyntaxToken staticModifier = SyntaxFactory.Token(SyntaxKind.StaticKeyword).WithTrailingTrivia(SyntaxFactory.Space); + MethodDeclarationSyntax newMethodNode = methodNode.WithModifiers(methodNode.Modifiers.Add(staticModifier)).WithAdditionalAnnotations(Formatter.Annotation); + + // Apply changes to the document + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode? newRoot = root?.ReplaceNode(methodNode, newMethodNode); + return newRoot == null ? document : document.WithSyntaxRoot(newRoot); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.CodeFix/Threading/ThreadDelegateDispatcherCodeFixProvider.cs b/CatalystUI/Tooling/CatalystUI.CodeFix/Threading/ThreadDelegateDispatcherCodeFixProvider.cs new file mode 100644 index 0000000..238c7f8 --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.CodeFix/Threading/ThreadDelegateDispatcherCodeFixProvider.cs @@ -0,0 +1,108 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Analyzers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Catalyst.CodeFix.Threading { + + /// + /// Provides code fixes for the Thread Delegate Dispatcher (TDD) analyzer. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ThreadDelegateDispatcherCodeFixProvider))] + [Shared] + public class ThreadDelegateDispatcherCodeFixProvider : CodeFixProvider { + + /// + public override ImmutableArray FixableDiagnosticIds => [ Descriptors.CTL001.Id ]; + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { + Diagnostic diagnostic = context.Diagnostics.First(); + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode? node = root?.FindNode(diagnostic.Location.SourceSpan); + if (node == null) return; + + // Add check against return value code fix + context.RegisterCodeFix( + CodeAction.Create( + "Check against return value", + c => CheckAgainstReturnValueAsync(context.Document, node, c), + nameof(ThreadDelegateDispatcherCodeFixProvider) + ), + diagnostic + ); + + // Add discard assignment code fix + context.RegisterCodeFix( + CodeAction.Create( + "Assign result to discard", + c => AddDiscardAssignmentAsync(context.Document, node, c), + nameof(ThreadDelegateDispatcherCodeFixProvider) + ), + diagnostic + ); + } + + private static async Task CheckAgainstReturnValueAsync(Document document, SyntaxNode node, CancellationToken cancellationToken) { + if (node is not InvocationExpressionSyntax { Parent: ExpressionStatementSyntax expression } invocation) return document; + PrefixUnaryExpressionSyntax negated = SyntaxFactory.PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + invocation.WithoutTrailingTrivia() + ); + ThrowStatementSyntax throwStatement = SyntaxFactory.ThrowStatement( + SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.IdentifierName("NotImplementedException") + ).WithArgumentList(SyntaxFactory.ArgumentList()) + ).WithAdditionalAnnotations(new SyntaxAnnotation("CaretTarget")) + .WithTrailingTrivia(); + BlockSyntax block = SyntaxFactory.Block(throwStatement); + IfStatementSyntax ifStatement = SyntaxFactory.IfStatement(negated, block) + .WithTriviaFrom(expression) + .WithAdditionalAnnotations(Formatter.Annotation); + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode? newRoot = root?.ReplaceNode(expression, ifStatement); + return newRoot == null ? document : document.WithSyntaxRoot(newRoot); + } + + /// + /// Assigns the result of a thread delegate dispatcher call to a discard variable. + /// + private static async Task AddDiscardAssignmentAsync(Document document, SyntaxNode node, CancellationToken cancellationToken) { + if (node is not InvocationExpressionSyntax invocation) return document; + ExpressionStatementSyntax newExpression = SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("_"), + invocation + ) + ).WithTriviaFrom(invocation.Parent!); + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode? newRoot = root?.ReplaceNode(invocation.Parent!, newExpression); + return newRoot == null ? document : document.WithSyntaxRoot(newRoot); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Profiling/CatalystUI.Profiling.csproj b/CatalystUI/Tooling/CatalystUI.Profiling/CatalystUI.Profiling.csproj new file mode 100644 index 0000000..5da9d12 --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Profiling/CatalystUI.Profiling.csproj @@ -0,0 +1,48 @@ + + + + + + Exe + Catalyst.Profiling + Catalyst.Profiling + + + + + + + + + + + + + + $(DefineConstants);PROFILE_EXISTS + + + + + + + + + + + + + + + + + + + + + PreserveNewest + catdebug.ini + + + + \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Profiling/Profile.template.cs b/CatalystUI/Tooling/CatalystUI.Profiling/Profile.template.cs new file mode 100644 index 0000000..337546f --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Profiling/Profile.template.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +using Catalyst.Builders; +using Catalyst.Builders.Extensions; +using Catalyst.Debugging; + +namespace Catalyst.Profiling { + + /// + /// A template for profiling CatalystUI or a CatalystUI-based application. + /// + public static class ProfileTemplate { + + /// + /// The debug context for profiling operations. + /// + private static DebugContext _debug; + + /// + /// Static constructor to initialize the debug context. + /// + static Profile() { + _debug = null!; + } + + /// + /// Acts as the main entry point for the profiling code. + /// + public static void Entry(string[] args) { + new CatalystAppBuilder() +#if DEBUG + .UseCatalystDebug() +#endif + .Build(Run); + } + + /// + /// Runs the profiling code. + /// + public static void Run(CatalystApp app) { + _debug = CatalystDebug.ForContext("Profiling"); + _debug.LogInfo("Hello, world!"); + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Profiling/Program.cs b/CatalystUI/Tooling/CatalystUI.Profiling/Program.cs new file mode 100644 index 0000000..a3bc2dc --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Profiling/Program.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------------- +// CatalystUI Framework for .NET Core - https://catalystui.org/ +// Copyright (c) 2025 CatalystUI LLC. All rights reserved. +// +// This file is part of CatalystUI and is provided as part of an early-access release. +// Unauthorized commercial use, distribution, or modification is strictly prohibited. +// +// This software is not open source and is not publicly licensed. +// For full terms, see the LICENSE and NOTICE files in the project root. +// ------------------------------------------------------------------------------------------------- + +namespace Catalyst.Profiling { + + /// + /// Profiling program for CatalystUI or CatalystUI-based applications. + /// + /// + /// For usage information on this tool, please refer to the README.md file + /// in the project root or the official documentation. + /// + public static class Program { + + /// + /// Main entry point for the profiling tool. + /// + /// + /// Do not modify this method. Please refer to the official documentation for usage instructions. + /// + /// The command-line arguments. + public static void Main(string[] args) { +#if PROFILE_EXISTS + Profile.Entry(args); +#endif + } + + } + +} \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Profiling/Properties/PublishProfiles/PublishProfiling.pubxml b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/PublishProfiles/PublishProfiling.pubxml new file mode 100644 index 0000000..35659c6 --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/PublishProfiles/PublishProfiling.pubxml @@ -0,0 +1,37 @@ + + + + + + + Exe + true + true + true + true + true + true + + + ConsoleApp + bin\Release\net9.0\_publish\ + + + win-x64 + + + osx-x64 + + + linux-x64 + + + + + + + + + + + \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Profiling/Properties/catdebug.template.ini b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/catdebug.template.ini new file mode 100644 index 0000000..075aa9c --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/catdebug.template.ini @@ -0,0 +1,12 @@ +[Logger] +MinimumLevel=Debug +ShowThread=true +ShowFileName=false +ShowMethodName=false +ShowLineNumber=false +ShowStackTrace=false + +[EnabledScopes] +Debugging=info +Application=info +Profiling=info \ No newline at end of file diff --git a/CatalystUI/Tooling/CatalystUI.Profiling/Properties/catdeps.template.props b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/catdeps.template.props new file mode 100644 index 0000000..1d3d73e --- /dev/null +++ b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/catdeps.template.props @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index f107bde..732c1ec 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CatalystUI Framework for .NET Core ![Static Badge](https://img.shields.io/badge/Powered_by-.NET-blue?style=flat-square&logo=sharp&logoColor=%23ffffff) +# CatalystUI Framework for .NET Core ![Static Badge](https://img.shields.io/badge/Powered_by-.NET-blue?style=flat-square&logo=sharp&logoColor=%23ffffff) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/CatalystUI/NetCore/dotnet.yml?branch=main&style=flat-square) A lightweight implementation of the CatalystUI Model for .NET Core.