├── .clang-format ├── .clang-tidy ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── data ├── Tactile.icns ├── icon.png ├── lang │ ├── en.ini │ ├── en.json │ ├── en_GB.ini │ ├── en_GB.json │ ├── sv.ini │ └── sv.json ├── meta │ ├── icon.afdesign │ ├── icon.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@x2.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@x2.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@x2.png │ ├── screenshots │ │ ├── v010.png │ │ ├── v020-alpha.png │ │ ├── v020.png │ │ └── v020_1.png │ ├── splash.afdesign │ └── splash │ │ ├── splash-0.2.0.png │ │ ├── splash-0.3.0.png │ │ └── splash-0.4.0.png └── test │ └── core │ └── test.ini ├── docs ├── dev │ ├── README.md │ ├── compiling.md │ ├── macos-icon.md │ └── vulkan.md ├── godot.md ├── tiled.md └── yaml-format.md ├── src ├── app │ ├── CMakeLists.txt │ └── tactile.main.cpp ├── core │ ├── CMakeLists.txt │ ├── common │ │ ├── common-containers.cppm │ │ ├── common-fs.cppm │ │ ├── common-memory.cppm │ │ ├── common-platform.cppm │ │ ├── common-primitives.cppm │ │ ├── common-result.cppm │ │ ├── common-strings.cppm │ │ ├── common-time.cppm │ │ ├── common.cppm │ │ ├── result.cpp │ │ └── strings.cpp │ ├── core.cppm │ ├── io │ │ ├── base64.cpp │ │ ├── compression.cpp │ │ ├── io-base64.cppm │ │ ├── io-compression.cppm │ │ └── io.cppm │ ├── layer │ │ ├── annotation_layer.cpp │ │ ├── group_layer.cpp │ │ ├── layer-annotation_layer.cppm │ │ ├── layer-group_layer.cppm │ │ ├── layer-interfaces.cppm │ │ ├── layer-layer_info.cppm │ │ ├── layer-tile_layer.cppm │ │ ├── layer.cppm │ │ ├── layer_info.cpp │ │ └── tile_layer.cpp │ ├── log │ │ ├── console_log_sink.cpp │ │ ├── file_log_sink.cpp │ │ ├── log-buffer.cppm │ │ ├── log-console_log_sink.cppm │ │ ├── log-file_log_sink.cppm │ │ ├── log-level.cppm │ │ ├── log-logger.cppm │ │ ├── log-sink.cppm │ │ ├── log.cppm │ │ └── logger.cpp │ ├── meta │ │ ├── attr.cpp │ │ ├── meta-attr.cppm │ │ ├── meta-color.cppm │ │ └── meta.cppm │ ├── numeric │ │ ├── numeric-casts.cppm │ │ ├── numeric-checked.cppm │ │ ├── numeric-concepts.cppm │ │ ├── numeric-constants.cppm │ │ ├── numeric-hash.cppm │ │ ├── numeric-random.cppm │ │ ├── numeric-vec.cppm │ │ ├── numeric.cppm │ │ └── random.cpp │ ├── runtime │ │ ├── runtime-interfaces.cppm │ │ ├── runtime-null.cppm │ │ ├── runtime.cpp │ │ └── runtime.cppm │ ├── save │ │ └── save.cppm │ ├── tile │ │ ├── tile.cpp │ │ ├── tile.cppm │ │ └── tile_animation.cpp │ └── util │ │ ├── util-defer.cppm │ │ ├── util-validation.cppm │ │ └── util.cppm ├── editor │ ├── CMakeLists.txt │ ├── app │ │ ├── app.cppm │ │ └── launch.cpp │ ├── cli │ │ ├── cli.cpp │ │ └── cli.cppm │ ├── command │ │ ├── command-interfaces.cppm │ │ ├── command-stack.cppm │ │ ├── command.cppm │ │ └── command_stack.cpp │ └── editor.cppm ├── ui │ ├── CMakeLists.txt │ ├── ui-imgui.cppm │ └── ui.cppm └── zlib │ ├── CMakeLists.txt │ ├── zlib.cpp │ └── zlib.cppm ├── tests ├── core │ ├── CMakeLists.txt │ ├── common │ │ ├── error.test.cpp │ │ └── strings.test.cpp │ ├── io │ │ └── base64.test.cpp │ ├── layer │ │ ├── annotation_layer.test.cpp │ │ ├── group_layer.test.cpp │ │ ├── layer.test.cpp │ │ ├── layer_info.test.cpp │ │ └── tile_layer.test.cpp │ ├── meta │ │ └── attr.test.cpp │ ├── numeric │ │ ├── casts.test.cpp │ │ ├── checked.test.cpp │ │ └── vec.test.cpp │ ├── tactile_core_tests.main.cpp │ └── util │ │ ├── defer.test.cpp │ │ └── validation.test.cpp ├── editor │ ├── CMakeLists.txt │ ├── command │ │ └── command_stack.test.cpp │ └── tactile_editor_tests.main.cpp └── zlib │ ├── CMakeLists.txt │ ├── tactile_zlib_tests.main.cpp │ └── zlib_compression_format.test.cpp ├── tools ├── cmake │ └── tactile.cmake ├── scripts │ ├── homebrew-llvm-fix-libc++-modules-path.diff │ └── install_assets.py └── vcpkg │ ├── toolchains │ └── arm64-osx-homebrew-llvm.cmake │ └── triplets │ ├── arm64-osx-tactile.cmake │ ├── x64-linux-tactile.cmake │ └── x64-windows-tactile.cmake └── vcpkg.json /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | 3 | # Language mode 4 | Language: Cpp 5 | Standard: Latest 6 | 7 | # Basic 8 | UseTab: Never 9 | ColumnLimit: 85 10 | IndentWidth: 2 11 | ContinuationIndentWidth: 4 12 | MaxEmptyLinesToKeep: 1 13 | 14 | # Pointers and references 15 | DerivePointerAlignment: false 16 | PointerAlignment: Left 17 | ReferenceAlignment: Left 18 | 19 | # Qualifiers 20 | QualifierAlignment: Custom 21 | QualifierOrder: [ 'friend', 'inline', 'constexpr', 'static', 'const', 'volatile', 'restrict', 'type' ] 22 | SpaceAroundPointerQualifiers: Default 23 | 24 | # Braces 25 | InsertBraces: false 26 | BracedInitializerIndentWidth: 2 27 | Cpp11BracedListStyle: true 28 | SpaceBeforeCpp11BracedList: true 29 | AllowShortBlocksOnASingleLine: Never 30 | AlignArrayOfStructures: None 31 | BreakBeforeBraces: Custom 32 | BraceWrapping: 33 | IndentBraces: false 34 | AfterNamespace: false 35 | AfterExternBlock: true 36 | AfterClass: true 37 | AfterStruct: true 38 | AfterEnum: true 39 | AfterUnion: true 40 | AfterFunction: true 41 | AfterCaseLabel: false 42 | AfterControlStatement: Never 43 | BeforeElse: true 44 | BeforeCatch: true 45 | BeforeWhile: false 46 | BeforeLambdaBody: false 47 | SplitEmptyNamespace: false 48 | SplitEmptyFunction: false 49 | SplitEmptyRecord: false 50 | 51 | # Spaces 52 | SpaceBeforeParens: ControlStatementsExceptControlMacros 53 | SpaceBeforeSquareBrackets: false 54 | SpaceBeforeRangeBasedForLoopColon: true 55 | SpaceAfterCStyleCast: true 56 | SpaceAfterLogicalNot: false 57 | SpacesInParens: Never 58 | SpacesInAngles: Never 59 | SpacesInSquareBrackets: false 60 | SpaceInEmptyBlock: false 61 | 62 | # Preprocessor 63 | AlignConsecutiveMacros: None 64 | AlignEscapedNewlines: Left 65 | IndentPPDirectives: BeforeHash 66 | PPIndentWidth: 2 67 | SortIncludes: CaseSensitive 68 | IncludeBlocks: Regroup 69 | IncludeCategories: 70 | # Standard headers, located in <> with no extension. 71 | - Regex: '<[A-Za-z0-9\_\-]+>' 72 | SortPriority: 1 73 | Priority: 1 74 | 75 | # Headers in <> with extension and optional prefix. 76 | - Regex: '<.+>' 77 | SortPriority: 2 78 | Priority: 2 79 | 80 | # Headers in "" with extension. 81 | - Regex: '".+"' 82 | SortPriority: 3 83 | Priority: 3 84 | 85 | # Operators 86 | AlignOperands: Align 87 | AlignConsecutiveDeclarations: None 88 | AlignConsecutiveAssignments: None 89 | SpaceBeforeAssignmentOperators: true 90 | BreakBeforeTernaryOperators: true 91 | BreakBeforeBinaryOperators: None 92 | 93 | # Control statements 94 | AllowShortIfStatementsOnASingleLine: Never 95 | AllowShortLoopsOnASingleLine: false 96 | 97 | # Attributes 98 | BreakAfterAttributes: Leave 99 | 100 | # Namespaces 101 | NamespaceIndentation: None 102 | FixNamespaceComments: true 103 | 104 | # Access modifiers 105 | IndentAccessModifiers: false 106 | AccessModifierOffset: -1 107 | EmptyLineBeforeAccessModifier: Always 108 | EmptyLineAfterAccessModifier: Never 109 | 110 | # Constructors 111 | PackConstructorInitializers: Never 112 | SpaceBeforeCtorInitializerColon: true 113 | ConstructorInitializerIndentWidth: 2 114 | BreakConstructorInitializers: BeforeColon 115 | 116 | # Inheritance 117 | BreakInheritanceList: AfterColon 118 | SpaceBeforeInheritanceColon: true 119 | 120 | # Enums 121 | AllowShortEnumsOnASingleLine: false 122 | 123 | # Functions 124 | AlwaysBreakAfterReturnType: None 125 | AllowShortFunctionsOnASingleLine: None 126 | IndentWrappedFunctionNames: false 127 | RemoveSemicolon: false 128 | BinPackParameters: false 129 | BinPackArguments: false 130 | AllowAllParametersOfDeclarationOnNextLine: false 131 | AllowAllArgumentsOnNextLine: false 132 | AllowBreakBeforeNoexceptSpecifier: OnlyWithParen 133 | 134 | # Lambdas 135 | LambdaBodyIndentation: Signature 136 | AllowShortLambdasOnASingleLine: All 137 | 138 | # Templates 139 | SpaceAfterTemplateKeyword: true 140 | AlwaysBreakTemplateDeclarations: Yes 141 | 142 | # Concepts 143 | IndentRequiresClause: true 144 | BreakBeforeConceptDeclarations: Always 145 | RequiresClausePosition: OwnLine 146 | RequiresExpressionIndentation: OuterScope 147 | AllowShortCompoundRequirementOnASingleLine: true 148 | 149 | # Switches 150 | AllowShortCaseLabelsOnASingleLine: true 151 | SpaceBeforeCaseColon: false 152 | IndentCaseLabels: true 153 | IndentCaseBlocks: false 154 | AlignConsecutiveShortCaseStatements: 155 | Enabled: true 156 | AcrossEmptyLines: false 157 | AcrossComments: false 158 | AlignCaseColons: false 159 | 160 | # Bit fields 161 | AlignConsecutiveBitFields: Consecutive 162 | BitFieldColonSpacing: Both 163 | 164 | # Digit separators 165 | IntegerLiteralSeparator: 166 | Binary: -1 167 | Decimal: 0 168 | Hex: -1 169 | 170 | # Strings 171 | BreakStringLiterals: false 172 | BreakAdjacentStringLiterals: false 173 | AlwaysBreakBeforeMultilineStrings: false 174 | 175 | # Comments 176 | SpacesBeforeTrailingComments: 2 177 | ReflowComments: true 178 | AlignTrailingComments: 179 | Kind: Always 180 | OverEmptyLines: 0 181 | 182 | # Miscellaneous 183 | AlignAfterOpenBracket: Align 184 | BreakArrays: false 185 | SpacesInContainerLiterals: false 186 | IndentGotoLabels: false 187 | IndentExternBlock: Indent 188 | KeepEmptyLinesAtTheStartOfBlocks: false 189 | InsertNewlineAtEOF: true 190 | SortUsingDeclarations: Lexicographic 191 | RemoveParentheses: Leave 192 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | FormatStyle: google 2 | 3 | Checks: > 4 | bugprone-*, 5 | cppcoreguidelines-*, 6 | clang-analyzer-*, 7 | modernize-*, 8 | misc-*, 9 | performance-*, 10 | portability-*, 11 | readability-*, 12 | -*-easily-swappable-parameters, 13 | -*-switch-missing-default-case, 14 | -*-owning-memory, 15 | -*-avoid-do-while, 16 | -*-pro-type-vararg, 17 | -*-avoid-c-arrays, 18 | -*-identifier-length, 19 | -*-uppercase-literal-suffix, 20 | -*-named-parameter, 21 | -*-magic-numbers, 22 | -*-redundant-member-init, 23 | -*-static-accessed-through-instance, 24 | -*-math-missing-parentheses, 25 | 26 | CheckOptions: 27 | - key: readability-identifier-naming.ClassCase 28 | value: CamelCase 29 | 30 | - key: readability-identifier-naming.StructCase 31 | value: CamelCase 32 | 33 | - key: readability-identifier-naming.EnumCase 34 | value: CamelCase 35 | 36 | - key: readability-identifier-naming.EnumConstantCase 37 | value: CamelCase 38 | 39 | - key: readability-identifier-naming.EnumConstantIgnoredRegexp 40 | value: "(_|.)+" 41 | 42 | - key: readability-identifier-naming.FunctionCase 43 | value: lower_case 44 | 45 | - key: readability-identifier-naming.FunctionIgnoredRegexp 46 | value: "^_.*" 47 | 48 | - key: readability-identifier-naming.MethodCase 49 | value: lower_case 50 | 51 | - key: readability-identifier-naming.PrivateMethodPrefix 52 | value: _ 53 | 54 | - key: readability-identifier-naming.ParameterCase 55 | value: lower_case 56 | 57 | - key: readability-identifier-naming.LocalVariableCase 58 | value: lower_case 59 | 60 | - key: readability-identifier-naming.GlobalVariableCase 61 | value: CamelCase 62 | 63 | - key: readability-identifier-naming.GlobalVariablePrefix 64 | value: g 65 | 66 | - key: readability-identifier-naming.GlobalConstantCase 67 | value: CamelCase 68 | 69 | - key: readability-identifier-naming.GlobalConstantPrefix 70 | value: k 71 | 72 | - key: readability-identifier-naming.ConstexprVariableCase 73 | value: CamelCase 74 | 75 | - key: readability-identifier-naming.ConstexprVariableIgnoredRegexp 76 | value: "([a-z0-9]+|_)+" 77 | 78 | - key: readability-identifier-naming.ConstexprVariablePrefix 79 | value: k 80 | 81 | - key: readability-identifier-naming.StructMemberCase 82 | value: lower_case 83 | 84 | - key: readability-identifier-naming.ClassMemberCase 85 | value: CamelCase 86 | 87 | - key: readability-identifier-naming.ClassMemberPrefix 88 | value: m 89 | 90 | - key: readability-identifier-naming.TypeAliasIgnoredRegexp 91 | value: "^[a-z].*" 92 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | docs/** linguist-documentation 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # macOS folder settings 35 | .DS_Store 36 | 37 | # Visual Studio files 38 | .vs 39 | 40 | # Visual Studio Code files 41 | .vscode 42 | 43 | # JetBrains files 44 | .idea 45 | 46 | # JetBrains Fleet files 47 | .fleet 48 | 49 | # Generated Doxygen output 50 | docs/doxygen/html 51 | 52 | # Build output 53 | build 54 | cmake-build-* 55 | 56 | # Miscellaneous 57 | .cache 58 | 59 | # Asset files 60 | data/fonts 61 | data/images 62 | 63 | # Cached script files 64 | tools/scripts/cache 65 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.30) 2 | 3 | message(DEBUG "CMAKE_TOOLCHAIN_FILE: ${CMAKE_TOOLCHAIN_FILE}") 4 | message(DEBUG "CMAKE_SKIP_INSTALL_ALL_DEPENDENCY: ${CMAKE_SKIP_INSTALL_ALL_DEPENDENCY}") 5 | message(DEBUG "CMAKE_EXPERIMENTAL_CXX_IMPORT_STD: ${CMAKE_EXPERIMENTAL_CXX_IMPORT_STD}") 6 | message(DEBUG "CMAKE_CXX_MODULE_STD: ${CMAKE_CXX_MODULE_STD}") 7 | message(DEBUG "VCPKG_OVERLAY_TRIPLETS: ${VCPKG_OVERLAY_TRIPLETS}") 8 | message(DEBUG "VCPKG_TARGET_TRIPLET: ${VCPKG_TARGET_TRIPLET}") 9 | 10 | option(TACTILE_ENABLE_TESTS "Enable tests" OFF) 11 | option(TACTILE_ENABLE_EDITOR "Enable the editor application" OFF) 12 | option(TACTILE_ENABLE_OPENGL "Enable OpenGL renderer" OFF) 13 | option(TACTILE_ENABLE_VULKAN "Enable Vulkan renderer" OFF) 14 | option(TACTILE_ENABLE_ZLIB "Enable Zlib compression support" OFF) 15 | option(TACTILE_ENABLE_ZSTD "Enable Zstd compression support" OFF) 16 | option(TACTILE_ENABLE_YAML "Enable Tactile YAML format support" OFF) 17 | option(TACTILE_ENABLE_TMJ "Enable Tiled TMJ format support" OFF) 18 | option(TACTILE_ENABLE_TMX "Enable Tiled TMJ format support" OFF) 19 | option(TACTILE_ENABLE_TSCN "Enable Godot TSCN format support" OFF) 20 | option(TACTILE_ENABLE_LTO "Enable link-time optimizations" OFF) 21 | option(TACTILE_ENABLE_CLION_IMPORT_STD_WORKAROUND "Enable CLion 'import std;' workaround" OFF) 22 | 23 | if (NOT TACTILE_RENDERER STREQUAL "Null" AND 24 | NOT TACTILE_RENDERER STREQUAL "OpenGL" AND 25 | NOT TACTILE_RENDERER STREQUAL "Vulkan") 26 | message(FATAL_ERROR "Invalid renderer: ${TACTILE_RENDERER}") 27 | endif () 28 | 29 | message(DEBUG "TACTILE_ENABLE_TESTS: ${TACTILE_ENABLE_TESTS}") 30 | message(DEBUG "TACTILE_ENABLE_EDITOR: ${TACTILE_ENABLE_EDITOR}") 31 | message(DEBUG "TACTILE_ENABLE_OPENGL: ${TACTILE_ENABLE_OPENGL}") 32 | message(DEBUG "TACTILE_ENABLE_VULKAN: ${TACTILE_ENABLE_VULKAN}") 33 | message(DEBUG "TACTILE_ENABLE_ZLIB: ${TACTILE_ENABLE_ZLIB}") 34 | message(DEBUG "TACTILE_ENABLE_ZSTD: ${TACTILE_ENABLE_ZSTD}") 35 | message(DEBUG "TACTILE_ENABLE_YAML: ${TACTILE_ENABLE_YAML}") 36 | message(DEBUG "TACTILE_ENABLE_TMJ: ${TACTILE_ENABLE_TMJ}") 37 | message(DEBUG "TACTILE_ENABLE_TMX: ${TACTILE_ENABLE_TMX}") 38 | message(DEBUG "TACTILE_ENABLE_TSCN: ${TACTILE_ENABLE_TSCN}") 39 | message(DEBUG "TACTILE_ENABLE_LTO: ${TACTILE_ENABLE_LTO}") 40 | message(DEBUG "TACTILE_ENABLE_CLION_IMPORT_STD_WORKAROUND: ${TACTILE_ENABLE_CLION_IMPORT_STD_WORKAROUND}") 41 | message(DEBUG "TACTILE_RENDERER: ${TACTILE_RENDERER}") 42 | 43 | if (TACTILE_ENABLE_TESTS) 44 | list(APPEND VCPKG_MANIFEST_FEATURES "tests") 45 | endif () 46 | 47 | if (TACTILE_ENABLE_EDITOR) 48 | list(APPEND VCPKG_MANIFEST_FEATURES "editor") 49 | endif () 50 | 51 | if (TACTILE_ENABLE_OPENGL) 52 | list(APPEND VCPKG_MANIFEST_FEATURES "opengl") 53 | endif () 54 | 55 | if (TACTILE_ENABLE_VULKAN) 56 | list(APPEND VCPKG_MANIFEST_FEATURES "vulkan") 57 | endif () 58 | 59 | if (TACTILE_ENABLE_ZLIB) 60 | list(APPEND VCPKG_MANIFEST_FEATURES "zlib") 61 | endif () 62 | 63 | if (TACTILE_ENABLE_ZSTD) 64 | list(APPEND VCPKG_MANIFEST_FEATURES "zstd") 65 | endif () 66 | 67 | if (TACTILE_ENABLE_YAML) 68 | list(APPEND VCPKG_MANIFEST_FEATURES "yaml") 69 | endif () 70 | 71 | if (TACTILE_ENABLE_TMJ) 72 | list(APPEND VCPKG_MANIFEST_FEATURES "tmj") 73 | endif () 74 | 75 | if (TACTILE_ENABLE_TMX) 76 | list(APPEND VCPKG_MANIFEST_FEATURES "tmx") 77 | endif () 78 | 79 | message(DEBUG "VCPKG_MANIFEST_FEATURES: ${VCPKG_MANIFEST_FEATURES}") 80 | 81 | project(tactile 82 | HOMEPAGE_URL "https://github.com/albin-johansson/tactile" 83 | VERSION 0.5.0 84 | LANGUAGES CXX 85 | ) 86 | 87 | # Determine build type, e.g. "debug" or "release". 88 | string(TOLOWER "${CMAKE_BUILD_TYPE}" TACTILE_BUILD_TYPE) 89 | if (NOT (TACTILE_BUILD_TYPE MATCHES "debug|release|asan")) 90 | message(FATAL_ERROR "Unsupported build type: ${CMAKE_BUILD_TYPE}") 91 | endif () 92 | message(DEBUG "TACTILE_BUILD_TYPE: ${TACTILE_BUILD_TYPE}") 93 | 94 | set(TACTILE_BINARY_DIR "${PROJECT_SOURCE_DIR}/build/${TACTILE_BUILD_TYPE}/output") 95 | message(DEBUG "TACTILE_BINARY_DIR: ${TACTILE_BINARY_DIR}") 96 | 97 | install(DIRECTORY "${PROJECT_SOURCE_DIR}/data" DESTINATION "${TACTILE_BINARY_DIR}") 98 | 99 | include("tools/cmake/tactile.cmake") 100 | 101 | find_package(Boost REQUIRED COMPONENTS safe_numerics) 102 | find_package(FastFloat CONFIG REQUIRED) 103 | find_path(CPPCODEC_INCLUDE_DIRS "cppcodec/base32_crockford.hpp") 104 | 105 | add_subdirectory("src/core") 106 | 107 | if (TACTILE_ENABLE_EDITOR) 108 | find_package(argparse CONFIG REQUIRED) 109 | find_package(imgui CONFIG REQUIRED) 110 | 111 | add_subdirectory("src/ui") 112 | 113 | if (TACTILE_ENABLE_ZLIB) 114 | find_package(ZLIB REQUIRED) 115 | add_subdirectory("src/zlib") 116 | endif () 117 | 118 | add_subdirectory("src/editor") 119 | add_subdirectory("src/app") 120 | endif () 121 | 122 | if (TACTILE_ENABLE_TESTS) 123 | find_package(GTest CONFIG REQUIRED) 124 | 125 | add_subdirectory("tests/core") 126 | 127 | if (TACTILE_ENABLE_EDITOR) 128 | if (TACTILE_ENABLE_ZLIB) 129 | add_subdirectory("tests/zlib") 130 | endif () 131 | 132 | add_subdirectory("tests/editor") 133 | endif () 134 | endif () 135 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 10, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 30 6 | }, 7 | "configurePresets": [ 8 | { 9 | "name": "default", 10 | "hidden": true, 11 | "generator": "Ninja", 12 | "cacheVariables": { 13 | "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", 14 | "CMAKE_SKIP_INSTALL_ALL_DEPENDENCY": "ON", 15 | "CMAKE_EXPERIMENTAL_CXX_IMPORT_STD": "0e5b6991-d74f-4b3d-a41c-cf096e0b2508", 16 | "CMAKE_CXX_MODULE_STD": "ON", 17 | "VCPKG_OVERLAY_TRIPLETS": "${sourceDir}/tools/vcpkg/triplets", 18 | "TACTILE_ENABLE_TESTS": "ON", 19 | "TACTILE_ENABLE_EDITOR": "ON", 20 | "TACTILE_ENABLE_OPENGL": "OFF", 21 | "TACTILE_ENABLE_VULKAN": "OFF", 22 | "TACTILE_ENABLE_ZLIB": "ON", 23 | "TACTILE_ENABLE_ZSTD": "ON", 24 | "TACTILE_ENABLE_YAML": "ON", 25 | "TACTILE_ENABLE_TMJ": "ON", 26 | "TACTILE_ENABLE_TMX": "ON", 27 | "TACTILE_ENABLE_TSCN": "ON", 28 | "TACTILE_ENABLE_LTO": "OFF", 29 | "TACTILE_ENABLE_CLION_IMPORT_STD_WORKAROUND": "OFF", 30 | "TACTILE_RENDERER": "Null" 31 | } 32 | }, 33 | { 34 | "name": "default-debug", 35 | "hidden": true, 36 | "inherits": [ 37 | "default" 38 | ], 39 | "binaryDir": "${sourceDir}/build/debug", 40 | "cacheVariables": { 41 | "CMAKE_BUILD_TYPE": "Debug" 42 | } 43 | }, 44 | { 45 | "name": "default-release", 46 | "hidden": true, 47 | "inherits": [ 48 | "default" 49 | ], 50 | "binaryDir": "${sourceDir}/build/release", 51 | "cacheVariables": { 52 | "CMAKE_BUILD_TYPE": "Release", 53 | "TACTILE_ENABLE_LTO": "ON" 54 | } 55 | }, 56 | { 57 | "name": "use-opengl-renderer", 58 | "hidden": true, 59 | "cacheVariables": { 60 | "TACTILE_ENABLE_OPENGL": "ON", 61 | "TACTILE_RENDERER": "OpenGL" 62 | } 63 | }, 64 | { 65 | "name": "use-mold-linker", 66 | "hidden": true, 67 | "cacheVariables": { 68 | "CMAKE_EXE_LINKER_FLAGS": "-fuse-ld=mold", 69 | "CMAKE_SHARED_LINKER_FLAGS": "-fuse-ld=mold" 70 | } 71 | }, 72 | { 73 | "name": "arm64-osx-homebrew-llvm", 74 | "hidden": true, 75 | "cacheVariables": { 76 | "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/tools/vcpkg/toolchains/arm64-osx-homebrew-llvm.cmake", 77 | "VCPKG_TARGET_TRIPLET": "arm64-osx-tactile" 78 | } 79 | }, 80 | { 81 | "name": "arm64-osx-homebrew-llvm-debug-null", 82 | "inherits": [ 83 | "arm64-osx-homebrew-llvm", 84 | "default-debug" 85 | ] 86 | }, 87 | { 88 | "name": "arm64-osx-homebrew-llvm-release-null", 89 | "inherits": [ 90 | "arm64-osx-homebrew-llvm", 91 | "default-release" 92 | ] 93 | }, 94 | { 95 | "name": "arm64-osx-homebrew-llvm-debug-opengl", 96 | "inherits": [ 97 | "use-opengl-renderer", 98 | "arm64-osx-homebrew-llvm-debug-null" 99 | ] 100 | }, 101 | { 102 | "name": "arm64-osx-homebrew-llvm-release-opengl", 103 | "inherits": [ 104 | "use-opengl-renderer", 105 | "arm64-osx-homebrew-llvm-release-null" 106 | ] 107 | }, 108 | { 109 | "name": "x64-linux", 110 | "hidden": true, 111 | "inherits": [ 112 | "use-mold-linker" 113 | ], 114 | "cacheVariables": { 115 | "VCPKG_TARGET_TRIPLET": "x64-linux-tactile" 116 | } 117 | }, 118 | { 119 | "name": "x64-linux-debug-null", 120 | "inherits": [ 121 | "x64-linux", 122 | "default-debug" 123 | ] 124 | }, 125 | { 126 | "name": "x64-linux-release-null", 127 | "inherits": [ 128 | "x64-linux", 129 | "default-release" 130 | ] 131 | }, 132 | { 133 | "name": "x64-linux-debug-opengl", 134 | "inherits": [ 135 | "use-opengl-renderer", 136 | "x64-linux-debug-null" 137 | ] 138 | }, 139 | { 140 | "name": "x64-linux-release-opengl", 141 | "inherits": [ 142 | "use-opengl-renderer", 143 | "x64-linux-release-null" 144 | ] 145 | }, 146 | { 147 | "name": "x64-windows", 148 | "hidden": true, 149 | "cacheVariables": { 150 | "VCPKG_TARGET_TRIPLET": "x64-windows-tactile" 151 | } 152 | }, 153 | { 154 | "name": "x64-windows-debug-null", 155 | "inherits": [ 156 | "x64-windows", 157 | "default-debug" 158 | ] 159 | }, 160 | { 161 | "name": "x64-windows-release-null", 162 | "inherits": [ 163 | "x64-windows", 164 | "default-release" 165 | ] 166 | }, 167 | { 168 | "name": "x64-windows-debug-opengl", 169 | "inherits": [ 170 | "use-opengl-renderer", 171 | "x64-windows-debug-null" 172 | ] 173 | }, 174 | { 175 | "name": "x64-windows-release-opengl", 176 | "inherits": [ 177 | "use-opengl-renderer", 178 | "x64-windows-release-null" 179 | ] 180 | } 181 | ] 182 | } 183 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Albin Johansson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tactile 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | [![version](https://img.shields.io/github/v/release/albin-johansson/tactile)](https://github.com/albin-johansson/tactile/releases) 5 | [![CI](https://github.com/albin-johansson/tactile/actions/workflows/ci.yml/badge.svg)](https://github.com/albin-johansson/tactile/actions/workflows/ci.yml) 6 | 7 | A tilemap editor that aims to be simple, fast and lightweight. 8 | 9 | ![Splash](data/meta/splash/splash-0.4.0.png) 10 | 11 | ## Aim 12 | 13 | * Easy to learn *and* easy to use 14 | * Workflow optimized for common actions 15 | * Well documented and high-quality source code 16 | * Lightweight feel and scalable performance 17 | * Cross-platform: works on Windows, macOS, and Linux 18 | 19 | ## Features 20 | 21 | * Extensive and intuitive undo/redo support 22 | * Various layer types 23 | * Tile layers 24 | * Object layers 25 | * Group layers 26 | * Supports an intuitive and human-readable YAML map format 27 | * Read and write support for the JSON and XML map formats used by [Tiled](https://www.mapeditor.org/) 28 | * Export maps as Godot scenes (see [godot.md](docs/godot.md)) 29 | * Intuitive mouse tools 30 | * Tile stamp tool 31 | * Eraser tool 32 | * Bucket fill tool 33 | * Rectangle tool 34 | * Ellipse tool 35 | * Point tool 36 | * Components (attachable bundles of attributes, as commonly found in game engines) 37 | * Properties that can be attached to almost anything: maps, layers, objects, tiles, etc. 38 | * Vector properties, with support for 2D/3D/4D vectors of both integers and floats 39 | * Tile animations 40 | * Tile compression support, using Base64 encoding with either Zlib or Zstd 41 | * Helpful error messages when things go wrong, e.g. when parsing corrupted maps 42 | * Language support for American English, British English, and Swedish 43 | * Various editor themes, both dark and light 44 | 45 | ## Documentation 46 | 47 | More documentation can be found in the [docs](./docs) directory. 48 | -------------------------------------------------------------------------------- /data/Tactile.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/Tactile.icns -------------------------------------------------------------------------------- /data/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/icon.png -------------------------------------------------------------------------------- /data/lang/en.ini: -------------------------------------------------------------------------------- 1 | [misc] 2 | ok = OK 3 | 4 | [verb] 5 | cancel = Cancel 6 | apply = Apply 7 | close = Close 8 | create = Create 9 | rename = Rename 10 | undo = Undo 11 | redo = Redo 12 | save = Save 13 | quit = Quit 14 | 15 | [noun] 16 | int = Integer 17 | int2 = Integer (2D) 18 | int3 = Integer (3D) 19 | int4 = Integer (4D) 20 | float = Float 21 | float2 = Float (2D) 22 | float3 = Float (3D) 23 | float4 = Float (4D) 24 | bool = Boolean 25 | string = String 26 | color = Color 27 | object = Object 28 | path = Path 29 | row = Row 30 | rows = Rows 31 | column = Column 32 | columns = Columns 33 | orientation = Orientation 34 | name = Name 35 | type = Type 36 | value = Value 37 | tile_width = Tile width 38 | tile_height = Tile height 39 | light_themes = Light themes 40 | dark_themes = Dark themes 41 | font = Font 42 | default = Default 43 | version = Version 44 | project_dir = Project directory 45 | file_menu = File 46 | edit_menu = Edit 47 | view_menu = View 48 | map_menu = Map 49 | tileset_menu = Tileset 50 | help_menu = Help 51 | debug_menu = Debug 52 | recent_files_menu = Recent Files 53 | widgets_menu = Widgets 54 | theme_menu = Theme 55 | export_as_menu = Export As 56 | document_dock = Documents 57 | layer_dock = Layers 58 | tileset_dock = Tilesets 59 | property_dock = Properties 60 | component_dock = Components 61 | animation_dock = Animation 62 | log_dock = Log 63 | component_editor_dialog = Component Editor 64 | settings_dialog = Settings 65 | about_dialog = About Tactile 66 | credits_dialog = Credits 67 | create_map_dialog = Create Map 68 | create_tileset_dialog = Create Tileset 69 | godot_export_dialog = Export Godot Scene 70 | style_editor = Style Editor 71 | 72 | [adjective] 73 | orthogonal = Orthogonal 74 | hexagonal = Hexagonal 75 | 76 | [action] 77 | create_map = Create Map... 78 | create_tileset = Create Tileset... 79 | create_layer = Create Layer... 80 | open = Open... 81 | open_map = Open Map... 82 | open_tileset = Open Tileset... 83 | open_component_editor = Open Component Editor... 84 | open_settings = Open Settings... 85 | save_as = Save As... 86 | reopen_last_closed_file = Reopen Last Closed File 87 | clear_file_history = Clear File History 88 | reset_layout = Reset Layout 89 | increase_font_size = Increase Font Size 90 | decrease_font_size = Decrease Font Size 91 | reset_font_size = Reset Font Size 92 | increase_zoom = Increase Zoom 93 | decrease_zoom = Decrease Zoom 94 | reset_zoom = Reset Zoom 95 | center_viewport = Center Viewport 96 | toggle_grid = Toggle Grid 97 | pan_up = Pan Up 98 | pan_down = Pan Down 99 | pan_left = Pan Left 100 | pan_right = Pan Right 101 | highlight_active_layer = Highlight Active Layer 102 | toggle_ui = Toggle UI 103 | stamp_tool = Stamp Tool 104 | eraser_tool = Eraser Tool 105 | bucket_tool = Bucket Tool 106 | object_selector_tool = Object Selector Tool 107 | rectangle_tool = Rectangle Tool 108 | ellipse_tool = Ellipse Tool 109 | point_tool = Point Tool 110 | add_tileset = Add Tileset... 111 | add_row = Add Row 112 | add_column = Add Column 113 | remove_row = Remove Row 114 | remove_column = Remove Column 115 | resize = Resize... 116 | create_property = Create Property... 117 | remove_property = Remove Property 118 | rename_property = Rename Property... 119 | change_property_type = Change Property Type... 120 | select_image = Select Image... 121 | fix_invalid_tiles = Fix Invalid Tiles 122 | show_metadata = Show Metadata 123 | report_bug = Report a Bug... 124 | about_tactile = About Tactile... 125 | open_credits = Open Credits... 126 | open_debugger = Open Debugger... 127 | open_style_editor = Open Style Editor... 128 | open_demo_window = Open Demo Window... 129 | open_storage_dir = Open Storage Directory... 130 | tile_layer_item = Tile Layer... 131 | object_layer_item = Object Layer... 132 | group_layer_item = Group Layer... 133 | 134 | [hint] 135 | context_has_no_properties = This context has no properties 136 | context_has_no_components = This context has no components 137 | map_has_no_layers = This map has no layers 138 | map_has_no_tilesets = This map has no tilesets 139 | select_tileset_image = Select an image that contains the tiles aligned in a grid 140 | -------------------------------------------------------------------------------- /data/lang/en_GB.ini: -------------------------------------------------------------------------------- 1 | [misc] 2 | ok = OK 3 | 4 | [verb] 5 | cancel = Cancel 6 | apply = Apply 7 | close = Close 8 | create = Create 9 | rename = Rename 10 | undo = Undo 11 | redo = Redo 12 | save = Save 13 | quit = Quit 14 | 15 | [noun] 16 | int = Integer 17 | int2 = Integer (2D) 18 | int3 = Integer (3D) 19 | int4 = Integer (4D) 20 | float = Float 21 | float2 = Float (2D) 22 | float3 = Float (3D) 23 | float4 = Float (4D) 24 | bool = Boolean 25 | string = String 26 | color = Colour 27 | object = Object 28 | path = Path 29 | row = Row 30 | rows = Rows 31 | column = Column 32 | columns = Columns 33 | orientation = Orientation 34 | name = Name 35 | type = Type 36 | value = Value 37 | tile_width = Tile width 38 | tile_height = Tile height 39 | light_themes = Light themes 40 | dark_themes = Dark themes 41 | font = Font 42 | default = Default 43 | version = Version 44 | project_dir = Project directory 45 | file_menu = File 46 | edit_menu = Edit 47 | view_menu = View 48 | map_menu = Map 49 | tileset_menu = Tileset 50 | help_menu = Help 51 | debug_menu = Debug 52 | recent_files_menu = Recent Files 53 | widgets_menu = Widgets 54 | theme_menu = Theme 55 | export_as_menu = Export As 56 | document_dock = Documents 57 | layer_dock = Layers 58 | tileset_dock = Tilesets 59 | property_dock = Properties 60 | component_dock = Components 61 | animation_dock = Animation 62 | log_dock = Log 63 | component_editor_dialog = Component Editor 64 | settings_dialog = Settings 65 | about_dialog = About Tactile 66 | credits_dialog = Credits 67 | create_map_dialog = Create Map 68 | create_tileset_dialog = Create Tileset 69 | godot_export_dialog = Export Godot Scene 70 | style_editor = Style Editor 71 | 72 | [adjective] 73 | orthogonal = Orthogonal 74 | hexagonal = Hexagonal 75 | 76 | [action] 77 | create_map = Create Map... 78 | create_tileset = Create Tileset... 79 | create_layer = Create Layer... 80 | open = Open... 81 | open_map = Open Map... 82 | open_tileset = Open Tileset... 83 | open_component_editor = Open Component Editor... 84 | open_settings = Open Settings... 85 | save_as = Save As... 86 | reopen_last_closed_file = Reopen Last Closed File 87 | clear_file_history = Clear File History 88 | reset_layout = Reset Layout 89 | increase_font_size = Increase Font Size 90 | decrease_font_size = Decrease Font Size 91 | reset_font_size = Reset Font Size 92 | increase_zoom = Increase Zoom 93 | decrease_zoom = Decrease Zoom 94 | reset_zoom = Reset Zoom 95 | center_viewport = Centre Viewport 96 | toggle_grid = Toggle Grid 97 | pan_up = Pan Up 98 | pan_down = Pan Down 99 | pan_left = Pan Left 100 | pan_right = Pan Right 101 | highlight_active_layer = Highlight Active Layer 102 | toggle_ui = Toggle UI 103 | stamp_tool = Stamp Tool 104 | eraser_tool = Eraser Tool 105 | bucket_tool = Bucket Tool 106 | object_selector_tool = Object Selector Tool 107 | rectangle_tool = Rectangle Tool 108 | ellipse_tool = Ellipse Tool 109 | point_tool = Point Tool 110 | add_tileset = Add Tileset... 111 | add_row = Add Row 112 | add_column = Add Column 113 | remove_row = Remove Row 114 | remove_column = Remove Column 115 | resize = Resize... 116 | create_property = Create Property... 117 | remove_property = Remove Property 118 | rename_property = Rename Property... 119 | change_property_type = Change Property Type... 120 | select_image = Select Image... 121 | fix_invalid_tiles = Fix Invalid Tiles 122 | show_metadata = Show Metadata 123 | report_bug = Report a Bug... 124 | about_tactile = About Tactile... 125 | open_credits = Open Credits... 126 | open_debugger = Open Debugger... 127 | open_style_editor = Open Style Editor... 128 | open_demo_window = Open Demo Window... 129 | open_storage_dir = Open Storage Directory... 130 | tile_layer_item = Tile Layer... 131 | object_layer_item = Object Layer... 132 | group_layer_item = Group Layer... 133 | 134 | [hint] 135 | context_has_no_properties = This context has no properties 136 | context_has_no_components = This context has no components 137 | map_has_no_layers = This map has no layers 138 | map_has_no_tilesets = This map has no tilesets 139 | select_tileset_image = Select an image that contains the tiles aligned in a grid 140 | -------------------------------------------------------------------------------- /data/lang/en_GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "center-viewport": "Centre Viewport" 4 | }, 5 | "settings": { 6 | "behavior-tab": "Behaviour", 7 | "viewport-bg-color": "Viewport background colour", 8 | "grid-color": "Grid colour" 9 | }, 10 | "misc": { 11 | "type-color": "Colour" 12 | } 13 | } -------------------------------------------------------------------------------- /data/lang/sv.ini: -------------------------------------------------------------------------------- 1 | [misc] 2 | ok = OK 3 | 4 | [verb] 5 | cancel = Avbryt 6 | apply = Applicera 7 | close = Stäng 8 | create = Skapa 9 | rename = Ändra Namn 10 | undo = Ångra 11 | redo = Gör om 12 | save = Spara 13 | quit = Avsluta 14 | 15 | [noun] 16 | int = Heltal 17 | int2 = Heltal (2D) 18 | int3 = Heltal (3D) 19 | int4 = Heltal (4D) 20 | float = Reellt tal 21 | float2 = Reellt tal (2D) 22 | float3 = Reellt tal (3D) 23 | float4 = Reellt tal (4D) 24 | bool = Boolesk 25 | string = Sträng 26 | color = Färg 27 | object = Objekt 28 | path = Filsökväg 29 | row = Rad 30 | rows = Rader 31 | column = Kolumn 32 | columns = Kolumner 33 | orientation = Orientering 34 | name = Namn 35 | type = Typ 36 | value = Värde 37 | tile_width = Tilebredd 38 | tile_height = Tilehöjd 39 | light_themes = Ljusa teman 40 | dark_themes = Mörka teman 41 | font = Typsnitt 42 | default = Standard 43 | version = Version 44 | project_dir = Projektmapp 45 | file_menu = Fil 46 | edit_menu = Ändra 47 | view_menu = Vy 48 | map_menu = Karta 49 | tileset_menu = Tilesamling 50 | help_menu = Hjälp 51 | debug_menu = Debugga 52 | recent_files_menu = Nyligen Ändrade Filer 53 | widgets_menu = Fönster 54 | theme_menu = Tema 55 | export_as_menu = Exportera Som 56 | document_dock = Dokument 57 | layer_dock = Lager 58 | tileset_dock = Tilesamlingar 59 | property_dock = Attributer 60 | component_dock = Komponenter 61 | animation_dock = Animation 62 | log_dock = Logg 63 | component_editor_dialog = Komponenthanterare 64 | settings_dialog = Inställningar 65 | about_dialog = Om Tactile 66 | credits_dialog = Tredjeparter 67 | create_map_dialog = Skapa Karta 68 | create_tileset_dialog = Skapa Tilesamling 69 | godot_export_dialog = Exportera Godotscen 70 | style_editor = Stilhanterare 71 | 72 | [adjective] 73 | orthogonal = Ortogonal 74 | hexagonal = Hexagonal 75 | 76 | [action] 77 | create_map = Skapa Karta... 78 | create_tileset = Skapa Tilesamling... 79 | create_layer = Skapa Lager... 80 | open = Öppna... 81 | open_map = Öppna Karta... 82 | open_tileset = Öppna Tilesamling... 83 | open_component_editor = Öppna Komponenthanterare... 84 | open_settings = Öppna Inställningar... 85 | save_as = Spara Som... 86 | reopen_last_closed_file = Öppna Senast Stängda Fil 87 | clear_file_history = Rensa Filhistorik 88 | reset_layout = Återställ Layout 89 | increase_font_size = Öka Textstorlek 90 | decrease_font_size = Minska Textstorlek 91 | reset_font_size = Återställ Textstorlek 92 | increase_zoom = Öka Zoom 93 | decrease_zoom = Minska Zoom 94 | reset_zoom = Återställ Zoom 95 | center_viewport = Centrera Viewport 96 | toggle_grid = Visa Rutnät 97 | pan_up = Panorera Uppåt 98 | pan_down = Panorera Nedåt 99 | pan_left = Panorera Vänster 100 | pan_right = Panorera Höger 101 | highlight_active_layer = Highlighta Aktivt Lager 102 | toggle_ui = Toggla Användargränssnitt 103 | stamp_tool = Stämpelverktyg 104 | eraser_tool = Suddverktyg 105 | bucket_tool = Hinkverktyg 106 | object_selector_tool = Objektväljarverktyg 107 | rectangle_tool = Rektangelverktyg 108 | ellipse_tool = Ellipsverktyg 109 | point_tool = Punktverktyg 110 | add_tileset = Lägg Till Tilesamling... 111 | add_row = Lägg Till Rad 112 | add_column = Lägg Till Kolumn 113 | remove_row = Ta Bort Rad 114 | remove_column = Ta Bort Kolumn 115 | resize = Ändra Storlek... 116 | create_property = Skapa Attribut... 117 | remove_property = Ta Bort Attribut 118 | rename_property = Döp Om Attribut... 119 | change_property_type = Ändra Attributtyp... 120 | select_image = Välj Bild... 121 | fix_invalid_tiles = Fixa Ogiltiga Tiles 122 | show_metadata = Visa Metadata 123 | report_bug = Rapportera Bugg... 124 | about_tactile = Om Tactile... 125 | open_credits = Tredjeparter... 126 | open_debugger = Öppna Debugger... 127 | open_style_editor = Öppna Stilhanterare... 128 | open_demo_window = Öppna Demofönster... 129 | open_storage_dir = Öppna Lagringsmapp... 130 | tile_layer_item = Tilelager... 131 | object_layer_item = Objektlager... 132 | group_layer_item = Grupplager... 133 | 134 | [hint] 135 | context_has_no_properties = Denna kontext har inga attributer 136 | context_has_no_components = Denna kontext har inga komponenter 137 | map_has_no_layers = Denna karta har inga lager 138 | map_has_no_tilesets = Denna karta har inga tilesamlingar 139 | select_tileset_image = Välj en bild som innehåller alla tiles arrangerade i ett rutnät 140 | -------------------------------------------------------------------------------- /data/meta/icon.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.afdesign -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_128x128@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_128x128@x2.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_256x256@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_256x256@x2.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /data/meta/icon.iconset/icon_512x512@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/icon.iconset/icon_512x512@x2.png -------------------------------------------------------------------------------- /data/meta/screenshots/v010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/screenshots/v010.png -------------------------------------------------------------------------------- /data/meta/screenshots/v020-alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/screenshots/v020-alpha.png -------------------------------------------------------------------------------- /data/meta/screenshots/v020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/screenshots/v020.png -------------------------------------------------------------------------------- /data/meta/screenshots/v020_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/screenshots/v020_1.png -------------------------------------------------------------------------------- /data/meta/splash.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/splash.afdesign -------------------------------------------------------------------------------- /data/meta/splash/splash-0.2.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/splash/splash-0.2.0.png -------------------------------------------------------------------------------- /data/meta/splash/splash-0.3.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/splash/splash-0.3.0.png -------------------------------------------------------------------------------- /data/meta/splash/splash-0.4.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albin-johansson/tactile/fddfe4ce39d7ee102172317b4ca753ecf548ebb2/data/meta/splash/splash-0.4.0.png -------------------------------------------------------------------------------- /data/test/core/test.ini: -------------------------------------------------------------------------------- 1 | ; qwerty 2 | 3 | [s1] 4 | a = s1.a 5 | b = s1.b 6 | ABC = 123 7 | zxc = z x c 8 | 9 | ; abcdef 10 | 11 | [s2] 12 | a = s2.a 13 | b = s2.b 14 | c = s2.c 15 | 16 | [s3] 17 | ; foobar 18 | -------------------------------------------------------------------------------- /docs/dev/README.md: -------------------------------------------------------------------------------- 1 | # Developer documentation 2 | 3 | This directory provides various guides relevant to the development of Tactile. 4 | This content is not intended to be useful for end users. -------------------------------------------------------------------------------- /docs/dev/compiling.md: -------------------------------------------------------------------------------- 1 | # Compiling 2 | 3 | This document provides a guide for how to build the project. 4 | 5 | - [Compiling](#compiling) 6 | - [Install a C++ compiler](#install-a-c-compiler) 7 | - [Install Vcpkg](#install-vcpkg) 8 | - [Building the project](#building-the-project) 9 | 10 | Tactile uses [Vcpkg](https://github.com/microsoft/vcpkg), an open-source dependency manager for C++ libraries, developed by Microsoft. 11 | This makes building the Tactile editor really quite straightforward. 12 | 13 | ## Install a C++ compiler 14 | 15 | On both Windows and macOS, you can install a C++ compiler by installing their primary IDEs, i.e. Visual Studio and Xcode. 16 | For Linux, you should use your distributions package manager, e.g. `sudo apt install g++`. 17 | 18 | ## Install Vcpkg 19 | 20 | The process of installing Vcpkg really just boils down to cloning a GitHub repository, running a configuration script, and setting an environment variable. 21 | 22 | Enter the following commands in your shell in the directory you'd like to install Vcpkg. 23 | Note, the below example works on Unix systems. On Windows, you probably need to run `.\bootstrap-vcpkg.bat -disableMetrics` instead. 24 | The `-disableMetrics` flag is optional. 25 | 26 | ```bash 27 | > git clone https://github.com/microsoft/vcpkg 28 | > cd vcpkg 29 | > ./bootstrap-vcpkg.sh -disableMetrics 30 | ``` 31 | 32 | It is recommended to set the environment variable `VCPKG_ROOT` to point to the directory where you installed Vcpkg, this will make your CMake build command simpler and less error-prone. 33 | 34 | ```bash 35 | > echo $VCPKG_ROOT # Possible output: '/Users/steve/vcpkg' 36 | ``` 37 | 38 | ## Building the project 39 | 40 | Given a successful Vcpkg installation, building the project should be a simple as entering the following commands, starting in the root directory of the repository. 41 | Use the correct preset in the `CMakePresets.json` for your system. 42 | The following works for ARM-based macOS systems. 43 | 44 | ```bash 45 | > mkdir build 46 | > cd build 47 | > cmake .. --preset arm64-osx-homebrew-llvm-debug-opengl 48 | > ninja 49 | ``` 50 | 51 | Depending on the version of CMake you're using, it might be necessary to override the value of 52 | `CMAKE_EXPERIMENTAL_CXX_IMPORT_STD`, see [this](https://github.com/Kitware/CMake/blob/master/Help/dev/experimental.rst) 53 | page for the correct value. 54 | -------------------------------------------------------------------------------- /docs/dev/macos-icon.md: -------------------------------------------------------------------------------- 1 | # macOS icons 2 | 3 | In order to generate a new `.icns` file for icon updates, use the following steps. 4 | 5 | Create the following versions of the icon. 6 | 7 | | Name | Size | 8 | |----------------------:|:----------| 9 | | `icon_16x16.png` | 16x16 | 10 | | `icon_16x16@x2.png` | 32x32 | 11 | | `icon_32x32.png` | 32x32 | 12 | | `icon_32x32@x2.png` | 64x64 | 13 | | `icon_128x128.png` | 128x128 | 14 | | `icon_128x128@x2.png` | 256x256 | 15 | | `icon_512x512@x2.png` | 512x512 | 16 | | `icon_512x512@x2.png` | 1024x1024 | 17 | 18 | Then, move these icons into a folder called `icons.iconset`. 19 | 20 | Finally, create a `.icns` file by using the following command. 21 | 22 | ```bash 23 | iconutil -c icns icon.iconset 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/dev/vulkan.md: -------------------------------------------------------------------------------- 1 | # Vulkan 2 | 3 | This document provides information related to the development of the Vulkan renderer. 4 | 5 | ## Installation 6 | 7 | The easiest option is to install the [Vulkan SDK](https://vulkan.lunarg.com/), which will work on most platforms. 8 | You can validate your local Vulkan installation with the following command. 9 | 10 | ```shell 11 | vulkaninfo --summary 12 | ``` 13 | 14 | ### Homebrew (macOS) 15 | 16 | On macOS, it's possible to avoid the Vulkan SDK by using MoltenVK directly via Homebrew. 17 | 18 | ```shell 19 | brew install molten-vk vulkan-validationlayers 20 | ``` 21 | 22 | However, this requires manually setting some additional environment variables. 23 | 24 | ```shell 25 | export VULKAN_SDK=$(brew --prefix molten-vk) 26 | export VK_ICD_FILENAMES=$VULKAN_SDK/share/vulkan/icd.d/MoltenVK_icd.json 27 | export VK_LAYER_PATH=$VK_LAYER_PATH:$(brew --prefix vulkan-validationlayers)/share/vulkan/explicit_layer.d 28 | export DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix vulkan-validationlayers)/lib 29 | ``` 30 | 31 | ### Troubleshooting 32 | 33 | If you experience problems with your Vulkan installation, such as issues with locating the validation layers, it can be useful to tell the Vulkan loader to emit debug messages. 34 | 35 | ```shell 36 | export VK_LOADER_DEBUG=all 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/godot.md: -------------------------------------------------------------------------------- 1 | # Godot Support 2 | 3 | It is possible to export maps created using Tactile as Godot scenes. 4 | However, certain Tactile features, such as ellipse objects, cannot be directly translated. 5 | In these cases, Tactile will do its best to approximate the map as it is described in the Tactile editor. 6 | 7 | ## Limitations 8 | 9 | You do not have to design your maps any differently to export them as Godot scenes, but there are certain limitations due to implementation differences between Tactile and Godot. 10 | 11 | - In Godot, a `TileMap` (which is really a tile layer) may only feature one associated `TileSet`, so Tactile will merge all tilesets associated with the map into a single `TileSet` definition. 12 | - Ellipse objects are approximated as polygons. 13 | - Godot tile animations cannot have frame-specific durations, so Tactile always uses the duration of the first frame. 14 | - Properties and components are only provided as metadata. 15 | - 4D vector properties are saved as arrays. 16 | 17 | ## Usage 18 | 19 | Once you are happy with your map, you can export it as a Godot scene using the action `Map -> Export As -> Godot Scene...`. 20 | This will open a dialog in which you may fill in the necessary information about your Godot project. 21 | 22 | ## Translation 23 | 24 | When you export a map as a Godot scene, Tactile will do its best to translate the contents of the map to corresponding nodes available in Godot. 25 | This section covers the overall translation strategy. 26 | 27 | ### Tilesets 28 | 29 | No matter how many tilesets your map uses, Tactile will only generate a single Godot `TileSet` definition. 30 | This information is written to a separate `tileset.tres` file. 31 | The `TileSet` resource will contain the merged data from all of your tilesets. 32 | Images used by your tilesets will also be automatically copied to your Godot project. 33 | Consider disabling the `Import Defaults -> Texture -> Filter` option in your Godot project, to prevent blurry tiles by default. 34 | 35 | ### Tile Layers 36 | 37 | Tile layers are fully supported, and are exported as `TileMap` nodes. All `TileMap` nodes will use the same `TileSet` resource. 38 | 39 | ### Object Layers & Objects 40 | 41 | Each object layer is exported as a `Node2D` node, which have their objects stored as immediate children nodes. 42 | 43 | Rectangle objects are exported using `Area2D` and `CollisionShape2D` nodes, using `RectangleShape2D` as the collision shape. 44 | Ellipse objects are not directly supported in Godot, so they are approximated as polygons using `Area2D` and `CollisionPolygon2D` nodes. 45 | Lastly, point objects are simply converted to plain `Node2D` nodes. 46 | 47 | ### Group Layers 48 | 49 | Group layers are naturally mapped to simple `Node2D` nodes, with all child layers attached as child nodes. 50 | -------------------------------------------------------------------------------- /docs/tiled.md: -------------------------------------------------------------------------------- 1 | # Tiled Support 2 | 3 | Tactile maintains general compatibility with the Tiled JSON and XML file formats. 4 | This document outlines the general limitations of this compatibility. 5 | 6 | ## Unsupported Tiled features 7 | 8 | The following Tiled features have no corresponding feature in Tactile and are thus unsupported. 9 | 10 | * Image layers 11 | * Wang tiles 12 | * User-defined property types 13 | * `class` properties 14 | * Tileset transformations 15 | * Tileset tile offsets 16 | * Text objects 17 | * Infinite maps (chunks) 18 | * Template objects 19 | 20 | ## Import 21 | 22 | The following are the limitations when importing Tiled maps. 23 | 24 | * Most unsupported features are simply ignored during parsing 25 | * Features introduced after version 1.7 of both Tiled JSON/XML formats are generally not supported 26 | 27 | ## Export 28 | 29 | The following are the limitations when exporting Tactile maps in a Tiled format. 30 | 31 | * Vector properties are exported as plain string properties 32 | * Components are ignored when using Tiled formats 33 | -------------------------------------------------------------------------------- /src/app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_app_project CXX) 2 | 3 | add_executable(tactile_app) 4 | 5 | target_sources(tactile_app 6 | PRIVATE 7 | "tactile.main.cpp" 8 | ) 9 | 10 | tactile_set_target_properties(tactile_app) 11 | 12 | set_target_properties(tactile_app 13 | PROPERTIES 14 | OUTPUT_NAME "tactile" 15 | ) 16 | 17 | target_link_libraries(tactile_app 18 | PRIVATE 19 | tactile_interface_target 20 | tactile_core 21 | tactile_editor 22 | ) 23 | -------------------------------------------------------------------------------- /src/app/tactile.main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | import tactile.editor; 4 | 5 | auto main(const int argc, char* argv[]) -> int 6 | { 7 | return tactile::editor::launch(argc, argv); 8 | } 9 | -------------------------------------------------------------------------------- /src/core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_core_project CXX) 2 | 3 | add_library(tactile_core STATIC) 4 | 5 | target_sources(tactile_core 6 | PUBLIC FILE_SET "CXX_MODULES" FILES 7 | "common/common.cppm" 8 | "common/common-containers.cppm" 9 | "common/common-fs.cppm" 10 | "common/common-memory.cppm" 11 | "common/common-platform.cppm" 12 | "common/common-primitives.cppm" 13 | "common/common-result.cppm" 14 | "common/common-strings.cppm" 15 | "common/common-time.cppm" 16 | "io/io.cppm" 17 | "io/io-base64.cppm" 18 | "io/io-compression.cppm" 19 | "layer/layer.cppm" 20 | "layer/layer-annotation_layer.cppm" 21 | "layer/layer-group_layer.cppm" 22 | "layer/layer-interfaces.cppm" 23 | "layer/layer-layer_info.cppm" 24 | "layer/layer-tile_layer.cppm" 25 | "log/log.cppm" 26 | "log/log-buffer.cppm" 27 | "log/log-console_log_sink.cppm" 28 | "log/log-file_log_sink.cppm" 29 | "log/log-level.cppm" 30 | "log/log-logger.cppm" 31 | "log/log-sink.cppm" 32 | "meta/meta.cppm" 33 | "meta/meta-attr.cppm" 34 | "meta/meta-color.cppm" 35 | "numeric/numeric.cppm" 36 | "numeric/numeric-casts.cppm" 37 | "numeric/numeric-checked.cppm" 38 | "numeric/numeric-concepts.cppm" 39 | "numeric/numeric-constants.cppm" 40 | "numeric/numeric-hash.cppm" 41 | "numeric/numeric-random.cppm" 42 | "numeric/numeric-vec.cppm" 43 | "runtime/runtime.cppm" 44 | "runtime/runtime-interfaces.cppm" 45 | "runtime/runtime-null.cppm" 46 | "save/save.cppm" 47 | "tile/tile.cppm" 48 | "util/util.cppm" 49 | "util/util-defer.cppm" 50 | "util/util-validation.cppm" 51 | "core.cppm" 52 | 53 | PRIVATE 54 | "common/result.cpp" 55 | "common/strings.cpp" 56 | "io/base64.cpp" 57 | "io/compression.cpp" 58 | "layer/annotation_layer.cpp" 59 | "layer/group_layer.cpp" 60 | "layer/layer_info.cpp" 61 | "layer/tile_layer.cpp" 62 | "log/console_log_sink.cpp" 63 | "log/file_log_sink.cpp" 64 | "log/logger.cpp" 65 | "meta/attr.cpp" 66 | "numeric/random.cpp" 67 | "runtime/runtime.cpp" 68 | "tile/tile.cpp" 69 | "tile/tile_animation.cpp" 70 | ) 71 | 72 | tactile_set_target_properties(tactile_core) 73 | 74 | target_link_libraries(tactile_core 75 | PRIVATE 76 | FastFloat::fast_float 77 | 78 | PUBLIC 79 | tactile_interface_target 80 | Boost::safe_numerics 81 | ) 82 | 83 | target_include_directories(tactile_core 84 | PRIVATE 85 | "${CPPCODEC_INCLUDE_DIRS}" 86 | ) 87 | -------------------------------------------------------------------------------- /src/core/common/common-containers.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:containers; 4 | 5 | export import std; 6 | export import :primitives; 7 | 8 | export namespace tactile { 9 | 10 | template 11 | using Span = std::span; 12 | 13 | template 14 | using Array = std::array; 15 | 16 | template 17 | using Vector = std::vector; 18 | 19 | template 20 | using Deque = std::deque; 21 | 22 | template 23 | using HashMap = std::unordered_map; 24 | 25 | template 26 | using TreeMap = std::map>; 27 | 28 | template 29 | using Option = std::optional; 30 | 31 | template 32 | using Expected = std::expected; 33 | 34 | template 35 | using Unexpected = std::unexpected; 36 | 37 | inline constexpr std::nullopt_t kNone {std::nullopt}; 38 | 39 | } // namespace tactile 40 | -------------------------------------------------------------------------------- /src/core/common/common-fs.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:fs; 4 | 5 | export import std; 6 | 7 | export namespace tactile { 8 | 9 | using Path = std::filesystem::path; 10 | 11 | namespace fs = std::filesystem; 12 | 13 | } // namespace tactile 14 | -------------------------------------------------------------------------------- /src/core/common/common-memory.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:memory; 4 | 5 | export import std; 6 | 7 | export namespace tactile { 8 | 9 | template > 10 | using Unique = std::unique_ptr; 11 | 12 | template 13 | using Shared = std::shared_ptr; 14 | 15 | template 16 | using Weak = std::weak_ptr; 17 | 18 | using std::make_shared; 19 | using std::make_unique; 20 | 21 | } // namespace tactile 22 | -------------------------------------------------------------------------------- /src/core/common/common-platform.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:platform; 4 | 5 | export import :primitives; 6 | 7 | export namespace tactile { 8 | 9 | enum class Platform : u8 10 | { 11 | kLinux, 12 | kMacOS, 13 | kWindows, 14 | }; 15 | 16 | #ifdef NDEBUG 17 | inline constexpr bool kIsDebugBuild {false}; 18 | #else 19 | inline constexpr bool kIsDebugBuild {true}; 20 | #endif 21 | 22 | #if defined(_WIN32) 23 | inline constexpr auto kCurrentPlatform = Platform::kWindows; 24 | #elif defined(__APPLE__) && defined(__MACH__) 25 | inline constexpr auto kCurrentPlatform = Platform::kMacOS; 26 | #elif defined(__linux__) 27 | inline constexpr auto kCurrentPlatform = Platform::kLinux; 28 | #else 29 | #error "Unsupported operating system" 30 | #endif 31 | 32 | } // namespace tactile 33 | -------------------------------------------------------------------------------- /src/core/common/common-primitives.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:primitives; 4 | 5 | export import std; 6 | 7 | export namespace tactile { 8 | 9 | using usize = std::size_t; 10 | using isize = std::ptrdiff_t; 11 | 12 | using uchar = unsigned char; 13 | using ushort = unsigned short int; 14 | using uint = unsigned int; 15 | using ulong = unsigned long int; 16 | using ulonglong = unsigned long long int; 17 | 18 | using u8 = std::uint8_t; 19 | using u16 = std::uint16_t; 20 | using u32 = std::uint32_t; 21 | using u64 = std::uint64_t; 22 | 23 | using i8 = std::int8_t; 24 | using i16 = std::int16_t; 25 | using i32 = std::int32_t; 26 | using i64 = std::int64_t; 27 | 28 | using f32 = float; 29 | using f64 = double; 30 | 31 | static_assert(sizeof(f32) == 4); 32 | static_assert(sizeof(f64) == 8); 33 | 34 | } // namespace tactile 35 | -------------------------------------------------------------------------------- /src/core/common/common-result.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:result; 4 | 5 | export import std; 6 | export import :containers; 7 | export import :primitives; 8 | export import :strings; 9 | 10 | export namespace tactile { 11 | 12 | /// Provides common error codes. 13 | enum class Error : u8 14 | { 15 | /// An unknown error occurred. 16 | kUnknown, 17 | 18 | /// Not enough memory. 19 | kOutOfMemory, 20 | 21 | /// A feature isn't supported. 22 | kUnsupportedFeature, 23 | 24 | /// An invalid operation was attempted. 25 | kInvalidOp, 26 | 27 | /// An invalid argument was detected. 28 | kInvalidArg, 29 | 30 | /// An invalid file was detected. 31 | kInvalidFile, 32 | 33 | /// A file doesn't exist. 34 | kNoSuchFile, 35 | 36 | /// Requested an out of range value. 37 | kOutOfRange, 38 | 39 | /// An arithmetic overflow was detected. 40 | kArithmeticOverflow, 41 | 42 | /// An arithmetic underflow was detected. 43 | kArithmeticUnderflow, 44 | 45 | /// An arithmetic precision error was detected. 46 | kArithmeticPrecision, 47 | 48 | /// An invalid arithmetic value was detected. 49 | kArithmeticInvalidValue, 50 | 51 | /// A stack overflow was detected. 52 | kStackOverflow, 53 | 54 | /// A stack underflow was detected. 55 | kStackUnderflow, 56 | 57 | /// An initialization error occurred. 58 | kCouldNotInitialize, 59 | 60 | /// Could not parse a file. 61 | kCouldNotParseFile, 62 | 63 | /// Could not compress data. 64 | kCouldNotCompress, 65 | 66 | /// Could not decompress data. 67 | kCouldNotDecompress, 68 | }; 69 | 70 | template 71 | using Result = Expected; 72 | 73 | [[nodiscard]] 74 | constexpr auto ok() noexcept -> Result 75 | { 76 | return Result {}; 77 | } 78 | 79 | template 80 | [[nodiscard]] constexpr auto ok(T&& value) noexcept -> Result 81 | { 82 | return Result {std::forward(value)}; 83 | } 84 | 85 | [[nodiscard]] 86 | constexpr auto err(const Error err) noexcept -> Unexpected 87 | { 88 | return Unexpected {err}; 89 | } 90 | 91 | /// Returns a textual representation of a given error code. 92 | [[nodiscard]] 93 | auto to_string(Error error) -> StringView; 94 | 95 | /// Returns a human-readable string that describes a given error code. 96 | [[nodiscard]] 97 | auto describe(Error error) -> StringView; 98 | 99 | } // namespace tactile 100 | -------------------------------------------------------------------------------- /src/core/common/common-strings.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:strings; 4 | 5 | export import std; 6 | export import :containers; 7 | export import :primitives; 8 | 9 | export namespace tactile { 10 | 11 | using String = std::string; 12 | 13 | using StringView = std::string_view; 14 | 15 | /// Parses an `i64` from a given string. 16 | [[nodiscard]] 17 | auto parse_i64(StringView str, int base = 10) noexcept -> Option; 18 | 19 | /// Parses a `u64` from a given string. 20 | [[nodiscard]] 21 | auto parse_u64(StringView str, int base = 10) noexcept -> Option; 22 | 23 | /// Parses an `f64` from a given string. 24 | [[nodiscard]] 25 | auto parse_f64(StringView str) noexcept -> Option; 26 | 27 | } // namespace tactile 28 | -------------------------------------------------------------------------------- /src/core/common/common-time.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.common:time; 4 | 5 | export import std; 6 | 7 | export namespace tactile { 8 | 9 | using Seconds = std::chrono::seconds; 10 | using Milliseconds = std::chrono::milliseconds; 11 | using Microseconds = std::chrono::microseconds; 12 | 13 | using SteadyClock = std::chrono::steady_clock; 14 | using SystemClock = std::chrono::system_clock; 15 | 16 | using std::chrono::duration_cast; 17 | 18 | } // namespace tactile 19 | -------------------------------------------------------------------------------- /src/core/common/common.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides common vocabulary types and functions used throughout the Tactile 4 | /// codebase. As such, this module cannot depend on other Tactile modules. 5 | export module tactile.core.common; 6 | 7 | export import :containers; 8 | export import :fs; 9 | export import :memory; 10 | export import :platform; 11 | export import :primitives; 12 | export import :result; 13 | export import :strings; 14 | export import :time; 15 | -------------------------------------------------------------------------------- /src/core/common/result.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.common; 4 | 5 | namespace tactile { 6 | 7 | auto to_string(const Error error) -> StringView 8 | { 9 | switch (error) { 10 | case Error::kUnknown: return "unknown"; 11 | case Error::kOutOfMemory: return "out_of_memory"; 12 | case Error::kUnsupportedFeature: return "unsupported_feature"; 13 | case Error::kInvalidOp: return "invalid_op"; 14 | case Error::kInvalidArg: return "invalid_arg"; 15 | case Error::kInvalidFile: return "invalid_file"; 16 | case Error::kNoSuchFile: return "no_such_file"; 17 | case Error::kOutOfRange: return "out_of_range"; 18 | case Error::kArithmeticOverflow: return "arithmetic_overflow"; 19 | case Error::kArithmeticUnderflow: return "arithmetic_underflow"; 20 | case Error::kArithmeticPrecision: return "arithmetic_precision"; 21 | case Error::kArithmeticInvalidValue: return "arithmetic_invalid_value"; 22 | case Error::kStackOverflow: return "stack_overflow"; 23 | case Error::kStackUnderflow: return "stack_underflow"; 24 | case Error::kCouldNotInitialize: return "could_not_initialize"; 25 | case Error::kCouldNotParseFile: return "could_not_parse_file"; 26 | case Error::kCouldNotCompress: return "could_not_compress"; 27 | case Error::kCouldNotDecompress: return "could_not_decompress"; 28 | } 29 | 30 | return "?"; 31 | } 32 | 33 | auto describe(const Error error) -> StringView 34 | { 35 | switch (error) { 36 | case Error::kUnknown: return "an unknown error occurred"; 37 | case Error::kOutOfMemory: return "out of memory"; 38 | case Error::kUnsupportedFeature: return "a feature isn't supported"; 39 | case Error::kInvalidOp: return "attempted an invalid operation"; 40 | case Error::kInvalidArg: return "detected an invalid argument"; 41 | case Error::kInvalidFile: return "detected an invalid file"; 42 | case Error::kNoSuchFile: return "an expected file didn't exist"; 43 | case Error::kOutOfRange: return "requested an out of range value"; 44 | case Error::kArithmeticOverflow: return "detected arithmetic overflow"; 45 | case Error::kArithmeticUnderflow: return "detected arithmetic underflow"; 46 | case Error::kArithmeticPrecision: return "detected loss of arithmetic precision"; 47 | case Error::kArithmeticInvalidValue: return "detected invalid arithmetic value"; 48 | case Error::kStackOverflow: return "detected stack overflow"; 49 | case Error::kStackUnderflow: return "detected stack underflow"; 50 | case Error::kCouldNotInitialize: return "an initialization error occurred"; 51 | case Error::kCouldNotParseFile: return "could not parse a file"; 52 | case Error::kCouldNotCompress: return "could not compress data"; 53 | case Error::kCouldNotDecompress: return "could not decompress data"; 54 | } 55 | 56 | return "?"; 57 | } 58 | 59 | } // namespace tactile 60 | -------------------------------------------------------------------------------- /src/core/common/strings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module; 4 | 5 | #include 6 | 7 | module tactile.core.common; 8 | 9 | namespace tactile { 10 | namespace { 11 | 12 | template 13 | auto _parse_int(const StringView str, const int base) noexcept -> Option 14 | { 15 | const auto* const begin = str.data(); 16 | const auto* const end = begin + str.size(); 17 | 18 | T value {}; 19 | const auto [ptr, err] = std::from_chars(begin, end, value, base); 20 | 21 | if (err == std::errc {} && ptr == end) { 22 | return value; 23 | } 24 | 25 | return kNone; 26 | } 27 | 28 | } // namespace 29 | 30 | auto parse_i64(const StringView str, const int base) noexcept -> Option 31 | { 32 | return _parse_int(str, base); 33 | } 34 | 35 | auto parse_u64(const StringView str, const int base) noexcept -> Option 36 | { 37 | return _parse_int(str, base); 38 | } 39 | 40 | auto parse_f64(const StringView str) noexcept -> Option 41 | { 42 | const auto* const begin = str.data(); 43 | const auto* const end = begin + str.size(); 44 | 45 | f64 value {}; 46 | const auto [ptr, err] = fast_float::from_chars(begin, end, value); 47 | 48 | if (err == std::errc {} && ptr == end) { 49 | return value; 50 | } 51 | 52 | return kNone; 53 | } 54 | 55 | } // namespace tactile 56 | -------------------------------------------------------------------------------- /src/core/core.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core; 4 | 5 | export import tactile.core.common; 6 | export import tactile.core.io; 7 | export import tactile.core.layer; 8 | export import tactile.core.log; 9 | export import tactile.core.meta; 10 | export import tactile.core.numeric; 11 | export import tactile.core.runtime; 12 | export import tactile.core.save; 13 | export import tactile.core.tile; 14 | export import tactile.core.util; 15 | -------------------------------------------------------------------------------- /src/core/io/base64.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module; 4 | 5 | #include 6 | 7 | module tactile.core.io; 8 | 9 | namespace tactile { 10 | 11 | void base64_encode(const Span data, String& encoded_data) 12 | { 13 | base64::encode(encoded_data, data); 14 | } 15 | 16 | void base64_decode(const StringView data, Vector& decoded_data) 17 | { 18 | base64::decode(decoded_data, data); 19 | } 20 | 21 | } // namespace tactile 22 | -------------------------------------------------------------------------------- /src/core/io/compression.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.io; 4 | 5 | import std; 6 | import tactile.core.log; 7 | 8 | namespace tactile { 9 | 10 | auto Compressor::compress_with(const CompressionAlgorithm algorithm, 11 | const Span data) const -> Result> 12 | { 13 | const auto iter = m_formats.find(algorithm); 14 | 15 | if (iter == m_formats.end()) { 16 | get_logger().log(LogLevel::kError, 17 | "Tried to compress data with unsupported algorithm"); 18 | return err(Error::kInvalidOp); 19 | } 20 | 21 | return iter->second->compress(data); 22 | } 23 | 24 | auto Compressor::decompress_with(const CompressionAlgorithm algorithm, 25 | const Span data) const 26 | -> Result> 27 | { 28 | const auto iter = m_formats.find(algorithm); 29 | 30 | if (iter == m_formats.end()) { 31 | get_logger().log(LogLevel::kError, 32 | "Tried to decompress data with unsupported algorithm"); 33 | return err(Error::kInvalidOp); 34 | } 35 | 36 | return iter->second->decompress(data); 37 | } 38 | 39 | void Compressor::set_format(const CompressionAlgorithm algorithm, 40 | Unique format) 41 | { 42 | m_formats.insert_or_assign(algorithm, std::move(format)); 43 | } 44 | 45 | } // namespace tactile 46 | -------------------------------------------------------------------------------- /src/core/io/io-base64.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.io:base64; 4 | 5 | export import tactile.core.common; 6 | 7 | export namespace tactile { 8 | 9 | /// Encodes raw bytes as a Base64 string. 10 | void base64_encode(Span data, String& encoded_data); 11 | 12 | /// Decodes a Base64 string to raw bytes. 13 | void base64_decode(StringView data, Vector& decoded_data); 14 | 15 | } // namespace tactile 16 | -------------------------------------------------------------------------------- /src/core/io/io-compression.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.io:compression; 4 | 5 | export import tactile.core.common; 6 | 7 | export namespace tactile { 8 | 9 | /// Represents supported compression algorithms. 10 | enum class CompressionAlgorithm : u8 11 | { 12 | kZlib, 13 | kZstd, 14 | }; 15 | 16 | /// Interface for compression format implementations. 17 | class ICompressionFormat 18 | { 19 | protected: 20 | ICompressionFormat() = default; 21 | 22 | ICompressionFormat(ICompressionFormat&&) noexcept = default; 23 | 24 | ICompressionFormat(const ICompressionFormat&) = default; 25 | 26 | auto operator=(ICompressionFormat&&) noexcept -> ICompressionFormat& = default; 27 | 28 | auto operator=(const ICompressionFormat&) -> ICompressionFormat& = default; 29 | 30 | public: 31 | virtual ~ICompressionFormat() noexcept = default; 32 | 33 | /// Compresses a stream of bytes. 34 | [[nodiscard]] 35 | virtual auto compress(Span data) const -> Result> = 0; 36 | 37 | /// Decompresses a stream of compressed bytes. 38 | [[nodiscard]] 39 | virtual auto decompress(Span data) const -> Result> = 0; 40 | }; 41 | 42 | /// A thin wrapper over a collection of compression formats. 43 | class Compressor final 44 | { 45 | public: 46 | /// Compresses a stream of bytes using a given algorithm. 47 | [[nodiscard]] 48 | auto compress_with(CompressionAlgorithm algorithm, Span data) const 49 | -> Result>; 50 | 51 | /// Decompresses a stream of bytes using a given algorithm. 52 | [[nodiscard]] 53 | auto decompress_with(CompressionAlgorithm algorithm, Span data) const 54 | -> Result>; 55 | 56 | /// Sets the compression format implementation for a given algorithm. 57 | void set_format(CompressionAlgorithm algorithm, Unique format); 58 | 59 | private: 60 | HashMap> m_formats {}; 61 | }; 62 | 63 | } // namespace tactile 64 | -------------------------------------------------------------------------------- /src/core/io/io.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides the low-level I/O API. 4 | export module tactile.core.io; 5 | 6 | export import :base64; 7 | export import :compression; 8 | -------------------------------------------------------------------------------- /src/core/layer/annotation_layer.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.layer; 4 | 5 | import std; 6 | 7 | namespace tactile { 8 | 9 | auto make_annotation(const AnnotationID id, const AnnotationKind kind) 10 | -> Unique 11 | { 12 | auto annotation = make_unique(); 13 | 14 | annotation->id = id; 15 | annotation->kind = kind; 16 | annotation->position = Float2 {0, 0}; 17 | annotation->size = Float2 {0, 0}; 18 | annotation->visible = true; 19 | 20 | return annotation; 21 | } 22 | 23 | AnnotationLayer::AnnotationLayer(const LayerID id) 24 | : m_info {id} 25 | {} 26 | 27 | void AnnotationLayer::visit(ILayerVisitor& visitor) 28 | { 29 | visitor.on_annotation_layer(*this); 30 | } 31 | 32 | void AnnotationLayer::visit(IConstLayerVisitor& visitor) const 33 | { 34 | visitor.on_annotation_layer(*this); 35 | } 36 | 37 | auto AnnotationLayer::info() -> LayerInfo& 38 | { 39 | return m_info; 40 | } 41 | 42 | auto AnnotationLayer::info() const -> const LayerInfo& 43 | { 44 | return m_info; 45 | } 46 | 47 | void AnnotationLayer::add_annotation(Unique annotation) 48 | { 49 | if (annotation == nullptr) { 50 | throw std::invalid_argument {"tried to add null annotation"}; 51 | } 52 | 53 | m_annotations.push_back(std::move(annotation)); 54 | } 55 | 56 | auto AnnotationLayer::remove_annotation(const AnnotationID id) -> Unique 57 | { 58 | const auto iter = std::ranges::find_if( 59 | m_annotations, 60 | [id](const Unique& annotation) { return annotation->id == id; }); 61 | 62 | if (iter == m_annotations.end()) { 63 | return nullptr; 64 | } 65 | 66 | auto removed_annotation = std::move(*iter); 67 | m_annotations.erase(iter); 68 | 69 | return removed_annotation; 70 | } 71 | 72 | auto AnnotationLayer::annotation_at(const usize index) -> Annotation& 73 | { 74 | return *m_annotations.at(index); 75 | } 76 | 77 | auto AnnotationLayer::annotation_at(const usize index) const -> const Annotation& 78 | { 79 | return *m_annotations.at(index); 80 | } 81 | 82 | auto AnnotationLayer::find_annotation(const AnnotationID id) -> Annotation* 83 | { 84 | const auto iter = std::ranges::find_if( 85 | m_annotations, 86 | [id](const Unique& annotation) { return annotation->id == id; }); 87 | return iter == m_annotations.end() ? nullptr : iter->get(); 88 | } 89 | 90 | auto AnnotationLayer::find_annotation(const AnnotationID id) const 91 | -> const Annotation* 92 | { 93 | const auto iter = std::ranges::find_if( 94 | m_annotations, 95 | [id](const Unique& annotation) { return annotation->id == id; }); 96 | return iter == m_annotations.end() ? nullptr : iter->get(); 97 | } 98 | 99 | auto AnnotationLayer::annotation_count() const -> usize 100 | { 101 | return m_annotations.size(); 102 | } 103 | 104 | auto AnnotationLayer::begin() const -> const_iterator 105 | { 106 | return m_annotations.begin(); 107 | } 108 | 109 | auto AnnotationLayer::end() const -> const_iterator 110 | { 111 | return m_annotations.end(); 112 | } 113 | 114 | } // namespace tactile 115 | -------------------------------------------------------------------------------- /src/core/layer/layer-annotation_layer.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.layer:annotation_layer; 4 | 5 | export import tactile.core.common; 6 | export import tactile.core.numeric; 7 | export import :interfaces; 8 | export import :layer_info; 9 | 10 | export namespace tactile { 11 | 12 | /// Type used for annotation object identifiers. 13 | using AnnotationID = i32; 14 | 15 | /// Represents different kinds of annotation objects. 16 | enum class AnnotationKind : u8 17 | { 18 | kPoint, 19 | kRect, 20 | kEllipse, 21 | }; 22 | 23 | /// Describes an annotation in an annotation layer. 24 | struct Annotation final 25 | { 26 | /// The unique identifier associated with the annotation. 27 | AnnotationID id; 28 | 29 | /// The type of the annotation. 30 | AnnotationKind kind; 31 | 32 | /// The logical position. 33 | Float2 position; 34 | 35 | /// The logical size. 36 | Float2 size; 37 | 38 | /// The user-defined name. 39 | String name; 40 | 41 | /// Indicates whether the annotation is rendered. 42 | bool visible; 43 | }; 44 | 45 | /// Creates an annotation object. 46 | [[nodiscard]] 47 | auto make_annotation(AnnotationID id, AnnotationKind kind) -> Unique; 48 | 49 | /// A layer variant consisting of zero or more annotations. 50 | class AnnotationLayer final : public ILayer 51 | { 52 | public: 53 | using storage_type = Vector>; 54 | using const_iterator = storage_type::const_iterator; 55 | 56 | /// Creates an empty annotation layer. 57 | explicit AnnotationLayer(LayerID id); 58 | 59 | void visit(ILayerVisitor& visitor) override; 60 | 61 | void visit(IConstLayerVisitor& visitor) const override; 62 | 63 | [[nodiscard]] 64 | auto info() -> LayerInfo& override; 65 | 66 | [[nodiscard]] 67 | auto info() const -> const LayerInfo& override; 68 | 69 | /// Adds an annotation to the layer. 70 | /// 71 | /// Complexity: O(1) 72 | void add_annotation(Unique annotation); 73 | 74 | /// Removes an annotation from the layer. 75 | /// 76 | /// Complexity: O(N) 77 | auto remove_annotation(AnnotationID id) -> Unique; 78 | 79 | /// Searches for an annotation with a given identifier. 80 | /// 81 | /// Complexity: O(N) 82 | [[nodiscard]] 83 | auto find_annotation(AnnotationID id) -> Annotation*; 84 | 85 | /// Searches for an annotation with a given identifier. 86 | /// 87 | /// Complexity: O(N) 88 | [[nodiscard]] 89 | auto find_annotation(AnnotationID id) const -> const Annotation*; 90 | 91 | /// Returns the annotation at a given index. 92 | /// 93 | /// Complexity: O(1) 94 | [[nodiscard]] 95 | auto annotation_at(usize index) -> Annotation&; 96 | 97 | /// Returns the annotation at a given index. 98 | /// 99 | /// Complexity: O(1) 100 | [[nodiscard]] 101 | auto annotation_at(usize index) const -> const Annotation&; 102 | 103 | /// Returns the number of annotations in the layer. 104 | /// 105 | /// Complexity: O(1) 106 | [[nodiscard]] 107 | auto annotation_count() const -> usize; 108 | 109 | /// Returns an iterator to the beginning of the layer. 110 | [[nodiscard]] 111 | auto begin() const -> const_iterator; 112 | 113 | /// Returns an iterator to the end of the layer. 114 | [[nodiscard]] 115 | auto end() const -> const_iterator; 116 | 117 | private: 118 | LayerInfo m_info; 119 | storage_type m_annotations {}; 120 | }; 121 | 122 | } // namespace tactile 123 | -------------------------------------------------------------------------------- /src/core/layer/layer-group_layer.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.layer:group_layer; 4 | 5 | export import :interfaces; 6 | export import :layer_info; 7 | 8 | export namespace tactile { 9 | 10 | /// A layer type that provides recursive layer storage. 11 | /// 12 | /// Complexity comments use N to denote the total number of layers in the group. 13 | class GroupLayer final : public ILayer 14 | { 15 | public: 16 | /// Creates an empty group layer. 17 | explicit GroupLayer(LayerID id); 18 | 19 | void visit(ILayerVisitor& visitor) override; 20 | 21 | void visit(IConstLayerVisitor& visitor) const override; 22 | 23 | [[nodiscard]] 24 | auto info() -> LayerInfo& override; 25 | 26 | [[nodiscard]] 27 | auto info() const -> const LayerInfo& override; 28 | 29 | /// Appends a layer to the group. 30 | /// 31 | /// Complexity: O(N) 32 | void append_layer(Unique layer); 33 | 34 | /// Appends a layer to a nested group. 35 | /// 36 | /// Complexity: O(N) 37 | void append_layer_to(LayerID parent_id, Unique layer); 38 | 39 | /// Removes a layer from the group. 40 | /// 41 | /// Complexity: O(N) 42 | auto remove_layer(LayerID id) -> Unique; 43 | 44 | /// Raises a layer relative to its sibling layers. 45 | /// 46 | /// Complexity: O(N) 47 | [[nodiscard]] 48 | auto raise_layer(LayerID id) -> Result; 49 | 50 | /// Lowers a layer relative to its sibling layers. 51 | /// 52 | /// Complexity: O(N) 53 | [[nodiscard]] 54 | auto lower_layer(LayerID id) -> Result; 55 | 56 | /// Returns the relative index of a layer. 57 | /// 58 | /// Complexity: O(N) 59 | [[nodiscard]] 60 | auto layer_index_rel(LayerID id) const -> isize; 61 | 62 | /// Returns the absolute index of a layer. 63 | /// 64 | /// Complexity: O(N) 65 | [[nodiscard]] 66 | auto layer_index_abs(LayerID id) const -> isize; 67 | 68 | /// Recursively searches for a layer with a given ID. 69 | /// 70 | /// Complexity: O(N) 71 | [[nodiscard]] 72 | auto find_layer(LayerID id) -> ILayer*; 73 | 74 | /// Recursively searches for a layer with a given ID. 75 | /// 76 | /// Complexity: O(N) 77 | [[nodiscard]] 78 | auto find_layer(LayerID id) const -> const ILayer*; 79 | 80 | /// Recursively searches for the parent of a layer with a given ID. 81 | /// 82 | /// Complexity: O(N) 83 | [[nodiscard]] 84 | auto find_parent_layer(LayerID id) -> GroupLayer*; 85 | 86 | /// Recursively searches for the parent of a layer with a given ID. 87 | /// 88 | /// Complexity: O(N) 89 | [[nodiscard]] 90 | auto find_parent_layer(LayerID id) const -> const GroupLayer*; 91 | 92 | /// Recursively counts the number of descendants of this group. 93 | /// 94 | /// Complexity: O(N) 95 | [[nodiscard]] 96 | auto layer_count() const -> isize; 97 | 98 | private: 99 | LayerInfo m_info; 100 | Vector> m_layers {}; 101 | 102 | struct FindLayerResult final 103 | { 104 | GroupLayer* parent_layer; 105 | isize rel_index; 106 | isize abs_index; 107 | bool found; 108 | }; 109 | 110 | struct FindConstLayerResult final 111 | { 112 | const GroupLayer* parent_layer; 113 | isize rel_index; 114 | isize abs_index; 115 | bool found; 116 | }; 117 | 118 | /// Complexity: O(N) 119 | [[nodiscard]] 120 | auto _find_layer(LayerID id, isize abs_index = 0) -> FindLayerResult; 121 | 122 | /// Complexity: O(N) 123 | [[nodiscard]] 124 | auto _find_layer(LayerID id, isize abs_index = 0) const -> FindConstLayerResult; 125 | }; 126 | 127 | } // namespace tactile 128 | -------------------------------------------------------------------------------- /src/core/layer/layer-interfaces.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.layer:interfaces; 4 | 5 | export import :layer_info; 6 | 7 | export namespace tactile { 8 | 9 | class GroupLayer; 10 | class TileLayer; 11 | class AnnotationLayer; 12 | 13 | /// Visitor interface for layer types. 14 | /// 15 | /// Layers are visited in depth-first order. 16 | class ILayerVisitor 17 | { 18 | protected: 19 | ILayerVisitor() = default; 20 | 21 | ILayerVisitor(ILayerVisitor&&) noexcept = default; 22 | 23 | ILayerVisitor(const ILayerVisitor&) = default; 24 | 25 | auto operator=(ILayerVisitor&&) noexcept -> ILayerVisitor& = default; 26 | 27 | auto operator=(const ILayerVisitor&) -> ILayerVisitor& = default; 28 | 29 | public: 30 | virtual ~ILayerVisitor() noexcept = default; 31 | 32 | /// Called for each group layer. 33 | virtual void on_group_layer(GroupLayer& layer) = 0; 34 | 35 | /// Called for each tile layer. 36 | virtual void on_tile_layer(TileLayer& layer) = 0; 37 | 38 | /// Called for each annotation layer. 39 | virtual void on_annotation_layer(AnnotationLayer& layer) = 0; 40 | }; 41 | 42 | /// Read-only visitor interface for layer types. 43 | /// 44 | /// Layers are visited in depth-first order. 45 | class IConstLayerVisitor 46 | { 47 | protected: 48 | IConstLayerVisitor() = default; 49 | 50 | IConstLayerVisitor(IConstLayerVisitor&&) noexcept = default; 51 | 52 | IConstLayerVisitor(const IConstLayerVisitor&) = default; 53 | 54 | auto operator=(IConstLayerVisitor&&) noexcept -> IConstLayerVisitor& = default; 55 | 56 | auto operator=(const IConstLayerVisitor&) -> IConstLayerVisitor& = default; 57 | 58 | public: 59 | virtual ~IConstLayerVisitor() noexcept = default; 60 | 61 | /// Called for each group layer. 62 | virtual void on_group_layer(const GroupLayer& layer) = 0; 63 | 64 | /// Called for each tile layer. 65 | virtual void on_tile_layer(const TileLayer& layer) = 0; 66 | 67 | /// Called for each annotation layer. 68 | virtual void on_annotation_layer(const AnnotationLayer& layer) = 0; 69 | }; 70 | 71 | /// Interface for layer types. 72 | class ILayer 73 | { 74 | protected: 75 | ILayer() = default; 76 | 77 | ILayer(ILayer&&) noexcept = default; 78 | 79 | ILayer(const ILayer&) = default; 80 | 81 | auto operator=(ILayer&&) noexcept -> ILayer& = default; 82 | 83 | auto operator=(const ILayer&) -> ILayer& = default; 84 | 85 | public: 86 | virtual ~ILayer() noexcept = default; 87 | 88 | /// Visits the layer. 89 | virtual void visit(ILayerVisitor& visitor) = 0; 90 | 91 | /// Visits the layer. 92 | virtual void visit(IConstLayerVisitor& visitor) const = 0; 93 | 94 | /// Returns the common layer information. 95 | [[nodiscard]] 96 | virtual auto info() -> LayerInfo& = 0; 97 | 98 | /// Returns the common layer information. 99 | [[nodiscard]] 100 | virtual auto info() const -> const LayerInfo& = 0; 101 | }; 102 | 103 | } // namespace tactile 104 | -------------------------------------------------------------------------------- /src/core/layer/layer-layer_info.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.layer:layer_info; 4 | 5 | export import tactile.core.common; 6 | 7 | export namespace tactile { 8 | 9 | using LayerID = i32; 10 | 11 | /// Provides information featured by all layer variants. 12 | class LayerInfo final 13 | { 14 | public: 15 | explicit LayerInfo(LayerID id); 16 | 17 | /// Sets the opacity of the layer when rendered. 18 | void set_opacity(f32 opacity); 19 | 20 | /// Sets whether the layer is rendered. 21 | void set_visible(bool visible); 22 | 23 | /// Returns the associated identifier. 24 | [[nodiscard]] 25 | auto id() const noexcept -> LayerID; 26 | 27 | /// Returns the opacity of the layer when rendered. 28 | [[nodiscard]] 29 | auto opacity() const noexcept -> f32; 30 | 31 | /// Indicates whether the layer is rendered. 32 | [[nodiscard]] 33 | auto visible() const noexcept -> bool; 34 | 35 | private: 36 | LayerID m_id; 37 | f32 m_opacity {1.0f}; 38 | bool m_visible {true}; 39 | }; 40 | 41 | } // namespace tactile 42 | -------------------------------------------------------------------------------- /src/core/layer/layer-tile_layer.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.layer:tile_layer; 4 | 5 | export import :interfaces; 6 | export import :layer_info; 7 | 8 | export namespace tactile { 9 | 10 | /// A layer variant consisting of a grid of tile identifiers. 11 | class TileLayer final : public ILayer 12 | { 13 | public: 14 | explicit TileLayer(LayerID id); 15 | 16 | void visit(ILayerVisitor& visitor) override; 17 | 18 | void visit(IConstLayerVisitor& visitor) const override; 19 | 20 | [[nodiscard]] 21 | auto info() -> LayerInfo& override; 22 | 23 | [[nodiscard]] 24 | auto info() const -> const LayerInfo& override; 25 | 26 | private: 27 | LayerInfo m_info; 28 | }; 29 | 30 | } // namespace tactile 31 | -------------------------------------------------------------------------------- /src/core/layer/layer.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides APIs related to map layers. 4 | /// 5 | /// The primary API exposed by this module is the ILayer interface. There are 6 | /// currently three implementations of this interface: GroupLayer, TileLayer, and 7 | /// AnnotationLayer. 8 | export module tactile.core.layer; 9 | 10 | export import :annotation_layer; 11 | export import :group_layer; 12 | export import :interfaces; 13 | export import :layer_info; 14 | export import :tile_layer; 15 | -------------------------------------------------------------------------------- /src/core/layer/layer_info.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.layer; 4 | 5 | import std; 6 | 7 | namespace tactile { 8 | 9 | LayerInfo::LayerInfo(const LayerID id) 10 | : m_id {id} 11 | {} 12 | 13 | void LayerInfo::set_opacity(const f32 opacity) 14 | { 15 | m_opacity = std::clamp(opacity, 0.0f, 1.0f); 16 | } 17 | 18 | void LayerInfo::set_visible(const bool visible) 19 | { 20 | m_visible = visible; 21 | } 22 | 23 | auto LayerInfo::id() const noexcept -> LayerID 24 | { 25 | return m_id; 26 | } 27 | 28 | auto LayerInfo::opacity() const noexcept -> f32 29 | { 30 | return m_opacity; 31 | } 32 | 33 | auto LayerInfo::visible() const noexcept -> bool 34 | { 35 | return m_visible; 36 | } 37 | 38 | } // namespace tactile 39 | -------------------------------------------------------------------------------- /src/core/layer/tile_layer.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.layer; 4 | 5 | namespace tactile { 6 | 7 | TileLayer::TileLayer(const LayerID id) 8 | : m_info {id} 9 | {} 10 | 11 | void TileLayer::visit(ILayerVisitor& visitor) 12 | { 13 | visitor.on_tile_layer(*this); 14 | } 15 | 16 | void TileLayer::visit(IConstLayerVisitor& visitor) const 17 | { 18 | visitor.on_tile_layer(*this); 19 | } 20 | 21 | auto TileLayer::info() -> LayerInfo& 22 | { 23 | return m_info; 24 | } 25 | 26 | auto TileLayer::info() const -> const LayerInfo& 27 | { 28 | return m_info; 29 | } 30 | 31 | } // namespace tactile 32 | -------------------------------------------------------------------------------- /src/core/log/console_log_sink.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.log; 4 | 5 | import std; 6 | 7 | namespace tactile { 8 | namespace { 9 | 10 | constexpr StringView kAnsiColorReset = "\x1B[0m"; 11 | constexpr StringView kAnsiColorFgRedBold = "\x1B[31m\x1B[1m"; 12 | constexpr StringView kAnsiColorFgYellow = "\x1B[33m"; 13 | constexpr StringView kAnsiColorFgMagenta = "\x1B[35m"; 14 | constexpr StringView kAnsiColorFgCyan = "\x1B[36m"; 15 | constexpr StringView kAnsiColorFgWhite = "\x1B[37m"; 16 | 17 | [[nodiscard]] 18 | auto _get_ansi_color(const LogLevel level) noexcept -> StringView 19 | { 20 | switch (level) { 21 | case LogLevel::kTrace: return kAnsiColorFgMagenta; 22 | case LogLevel::kDebug: return kAnsiColorFgCyan; 23 | case LogLevel::kInfo: return kAnsiColorFgWhite; 24 | case LogLevel::kWarn: return kAnsiColorFgYellow; 25 | case LogLevel::kError: return kAnsiColorFgRedBold; 26 | } 27 | 28 | return kAnsiColorFgWhite; 29 | } 30 | 31 | } // namespace 32 | 33 | void ConsoleLogSink::log(const LogMessage& msg) 34 | { 35 | if (m_use_colors) { 36 | std::clog << _get_ansi_color(msg.level); 37 | } 38 | 39 | std::clog << msg.prefix << ' ' << msg.text; 40 | 41 | if (m_use_colors) { 42 | std::clog << kAnsiColorReset; 43 | } 44 | 45 | std::clog << '\n'; 46 | } 47 | 48 | void ConsoleLogSink::flush() 49 | { 50 | std::clog.flush(); 51 | } 52 | 53 | void ConsoleLogSink::set_use_colors(const bool use_colors) 54 | { 55 | m_use_colors = use_colors; 56 | } 57 | 58 | } // namespace tactile 59 | -------------------------------------------------------------------------------- /src/core/log/file_log_sink.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.log; 4 | 5 | namespace tactile { 6 | 7 | FileLogSink::FileLogSink(const Path& log_file) 8 | : m_stream {log_file, std::ios::out | std::ios::trunc} 9 | { 10 | if (!m_stream.good()) { 11 | throw std::runtime_error {"could not create log file"}; 12 | } 13 | } 14 | 15 | void FileLogSink::log(const LogMessage& msg) 16 | { 17 | m_stream << msg.prefix << ' ' << msg.text << '\n'; 18 | } 19 | 20 | void FileLogSink::flush() 21 | { 22 | m_stream.flush(); 23 | } 24 | 25 | } // namespace tactile 26 | -------------------------------------------------------------------------------- /src/core/log/log-buffer.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.log:buffer; 4 | 5 | export import tactile.core.common; 6 | 7 | namespace tactile { 8 | 9 | template 10 | class LogBuffer final 11 | { 12 | public: 13 | using value_type = char; 14 | using iterator = char*; 15 | using const_iterator = const char*; 16 | 17 | LogBuffer() noexcept = default; 18 | 19 | LogBuffer(LogBuffer&&) = delete; 20 | 21 | LogBuffer(const LogBuffer&) = delete; 22 | 23 | ~LogBuffer() noexcept = default; 24 | 25 | auto operator=(LogBuffer&&) -> LogBuffer& = delete; 26 | 27 | auto operator=(const LogBuffer&) -> LogBuffer& = delete; 28 | 29 | void clear() noexcept 30 | { 31 | m_size = 0; 32 | } 33 | 34 | void push_back(const char ch) noexcept 35 | { 36 | if (m_size < N) { 37 | m_data[m_size] = ch; 38 | ++m_size; 39 | } 40 | } 41 | 42 | [[nodiscard]] 43 | auto view() const noexcept -> StringView 44 | { 45 | return StringView {m_data.data(), m_size}; 46 | } 47 | 48 | [[nodiscard]] 49 | auto begin() noexcept -> iterator 50 | { 51 | return m_data.data(); 52 | } 53 | 54 | [[nodiscard]] 55 | auto begin() const noexcept -> const_iterator 56 | { 57 | return m_data.data(); 58 | } 59 | 60 | [[nodiscard]] 61 | auto end() noexcept -> iterator 62 | { 63 | return begin() + m_size; 64 | } 65 | 66 | [[nodiscard]] 67 | auto end() const noexcept -> const_iterator 68 | { 69 | return begin() + m_size; 70 | } 71 | 72 | private: 73 | Array m_data {}; 74 | usize m_size {}; 75 | }; 76 | 77 | } // namespace tactile 78 | -------------------------------------------------------------------------------- /src/core/log/log-console_log_sink.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.log:console_log_sink; 4 | 5 | export import :sink; 6 | 7 | export namespace tactile { 8 | 9 | /// A log sink that writes messages to the console. 10 | class ConsoleLogSink final : public ILogSink 11 | { 12 | public: 13 | void log(const LogMessage& msg) override; 14 | 15 | void flush() override; 16 | 17 | void set_use_colors(bool use_colors); 18 | 19 | private: 20 | bool m_use_colors {true}; 21 | }; 22 | 23 | } // namespace tactile 24 | -------------------------------------------------------------------------------- /src/core/log/log-file_log_sink.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.log:file_log_sink; 4 | 5 | export import std; 6 | export import :sink; 7 | 8 | export namespace tactile { 9 | 10 | /// A log sink that writes messages to a file. 11 | class FileLogSink final : public ILogSink 12 | { 13 | public: 14 | explicit FileLogSink(const Path& log_file); 15 | 16 | void log(const LogMessage& msg) override; 17 | 18 | void flush() override; 19 | 20 | private: 21 | std::ofstream m_stream; 22 | }; 23 | 24 | } // namespace tactile 25 | -------------------------------------------------------------------------------- /src/core/log/log-level.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.log:level; 4 | 5 | export import tactile.core.common; 6 | 7 | export namespace tactile { 8 | 9 | /// The supported log level categories. 10 | /// 11 | /// The underlying value increases with severity. 12 | enum class LogLevel : u8 13 | { 14 | /// For verbose messages only useful for detailed debugging. 15 | kTrace, 16 | 17 | /// For messages that provide useful developer information. 18 | kDebug, 19 | 20 | /// For messages that provide general high-level information. 21 | kInfo, 22 | 23 | /// For messages that warn about potential issues. 24 | kWarn, 25 | 26 | /// For messages that report errors. 27 | kError, 28 | }; 29 | 30 | } // namespace tactile 31 | -------------------------------------------------------------------------------- /src/core/log/log-logger.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.log:logger; 4 | 5 | export import std; 6 | export import tactile.core.common; 7 | export import :level; 8 | export import :sink; 9 | import :buffer; 10 | 11 | export namespace tactile { 12 | 13 | /// A sink based logger. 14 | class Logger final 15 | { 16 | public: 17 | using clock_type = SteadyClock; 18 | 19 | Logger() noexcept = default; 20 | 21 | Logger(Logger&&) = delete; 22 | 23 | Logger(const Logger&) = delete; 24 | 25 | ~Logger() noexcept = default; 26 | 27 | auto operator=(Logger&&) -> Logger& = delete; 28 | 29 | auto operator=(const Logger&) -> Logger& = delete; 30 | 31 | /// Logs a formatted message. 32 | template 33 | void log(const LogLevel level, 34 | const std::format_string fmt, 35 | const Args&... args) noexcept 36 | { 37 | if (level >= m_min_level) { 38 | _log(level, fmt.get(), std::make_format_args(args...)); 39 | } 40 | } 41 | 42 | /// Disposes all associated sinks. 43 | void reset(); 44 | 45 | /// Adds a sink to the logger. 46 | void add_sink(Unique sink); 47 | 48 | /// Sets the minimum level of logged messages. 49 | void set_min_level(LogLevel level); 50 | 51 | /// Sets the level at which messages will be explicitly flushed. 52 | void set_flush_level(LogLevel level); 53 | 54 | /// Sets the start time (used to customize timestamps). 55 | void set_start_time(Option start_time); 56 | 57 | private: 58 | LogLevel m_min_level {LogLevel::kInfo}; 59 | LogLevel m_flush_level {LogLevel::kError}; 60 | Option m_start_time {}; 61 | Vector> m_sinks {}; 62 | LogBuffer<1024> m_text_buffer {}; 63 | LogBuffer<32> m_prefix_buffer {}; 64 | 65 | void _log(LogLevel level, StringView fmt, std::format_args args) noexcept; 66 | }; 67 | 68 | /// Returns the global logger instance. 69 | [[nodiscard]] 70 | auto get_logger() noexcept -> Logger&; 71 | 72 | } // namespace tactile 73 | -------------------------------------------------------------------------------- /src/core/log/log-sink.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.log:sink; 4 | 5 | export import tactile.core.common; 6 | export import :level; 7 | 8 | export namespace tactile { 9 | 10 | /// A view of a logged message. 11 | struct LogMessage final 12 | { 13 | /// The severity of the message. 14 | LogLevel level; 15 | 16 | /// A string that usually encodes the severity and timestamp. 17 | StringView prefix; 18 | 19 | /// The formatted log message. 20 | StringView text; 21 | }; 22 | 23 | /// Interface for logger sinks. 24 | /// 25 | /// A logger may feature several associated sinks which are called whenever a message 26 | /// is logged. This allows the application to control exactly what happens with 27 | /// logged messages, e.g., whether to print them to a console or store them in a 28 | /// file. 29 | class ILogSink 30 | { 31 | protected: 32 | ILogSink() = default; 33 | 34 | ILogSink(ILogSink&&) noexcept = default; 35 | 36 | ILogSink(const ILogSink&) = default; 37 | 38 | auto operator=(ILogSink&&) noexcept -> ILogSink& = default; 39 | 40 | auto operator=(const ILogSink&) -> ILogSink& = default; 41 | 42 | public: 43 | virtual ~ILogSink() noexcept = default; 44 | 45 | /// Logs an incoming message. 46 | virtual void log(const LogMessage& msg) = 0; 47 | 48 | /// Flushes any pending messages, most likely via an associated I/O stream. 49 | virtual void flush() = 0; 50 | }; 51 | 52 | } // namespace tactile 53 | -------------------------------------------------------------------------------- /src/core/log/log.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides the common logging API. 4 | /// 5 | /// The primary API of this module is the Logger class. While the Logger class isn't 6 | /// a singleton, there is a global logger instance, accessible via the get_logger 7 | /// function. Tactile code should use this function to log information. 8 | /// 9 | /// Note that you need to install one or more "sinks" to the logger to see any output 10 | /// from the logger. Two sink implementations are provided by this module: 11 | /// ConsoleLogSink, and FileLogSink. 12 | /// 13 | /// This module cannot depend on any Tactile modules other than "tactile.core.common" 14 | /// to make it usable within as many modules as possible. 15 | export module tactile.core.log; 16 | 17 | export import :console_log_sink; 18 | export import :file_log_sink; 19 | export import :level; 20 | export import :logger; 21 | export import :sink; 22 | -------------------------------------------------------------------------------- /src/core/log/logger.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.log; 4 | 5 | import std; 6 | 7 | namespace tactile { 8 | namespace { 9 | 10 | [[nodiscard]] 11 | auto _to_prefix(const LogLevel level) noexcept -> StringView 12 | { 13 | switch (level) { 14 | case LogLevel::kDebug: return "DEBUG"; 15 | case LogLevel::kInfo: return "INFO"; 16 | case LogLevel::kWarn: return "WARN"; 17 | case LogLevel::kError: return "ERROR"; 18 | default: return "???"; 19 | } 20 | } 21 | 22 | } // namespace 23 | 24 | void Logger::_log(const LogLevel level, 25 | const StringView fmt, 26 | const std::format_args args) noexcept 27 | { 28 | try { 29 | if (m_sinks.empty()) { 30 | return; 31 | } 32 | 33 | const auto log_instant = clock_type::now(); 34 | const auto elapsed_time = duration_cast( 35 | log_instant - m_start_time.value_or(clock_type::time_point {})); 36 | 37 | m_text_buffer.clear(); 38 | m_prefix_buffer.clear(); 39 | 40 | std::vformat_to(std::back_inserter(m_text_buffer), fmt, args); 41 | std::format_to(std::back_inserter(m_prefix_buffer), 42 | "[{}] ({:%Q}):", 43 | _to_prefix(level), 44 | elapsed_time); 45 | 46 | const LogMessage message { 47 | .level = level, 48 | .prefix = m_prefix_buffer.view(), 49 | .text = m_text_buffer.view(), 50 | }; 51 | 52 | const auto should_flush = level >= m_flush_level; 53 | for (const auto& sink : m_sinks) { 54 | sink->log(message); 55 | 56 | if (should_flush) { 57 | sink->flush(); 58 | } 59 | } 60 | } 61 | catch (const std::exception& error) { 62 | std::printf("LOGGER ERROR: %s\n", error.what()); 63 | } 64 | catch (...) { 65 | std::printf("UNKNOWN LOGGER ERROR\n"); 66 | } 67 | } 68 | 69 | void Logger::reset() 70 | { 71 | m_sinks.clear(); 72 | } 73 | 74 | void Logger::add_sink(Unique sink) 75 | { 76 | m_sinks.push_back(std::move(sink)); 77 | } 78 | 79 | void Logger::set_min_level(const LogLevel level) 80 | { 81 | m_min_level = level; 82 | } 83 | 84 | void Logger::set_flush_level(const LogLevel level) 85 | { 86 | m_flush_level = level; 87 | } 88 | 89 | void Logger::set_start_time(const Option start_time) 90 | { 91 | m_start_time = start_time; 92 | } 93 | 94 | auto get_logger() noexcept -> Logger& 95 | { 96 | static Logger logger {}; 97 | return logger; 98 | } 99 | 100 | } // namespace tactile 101 | -------------------------------------------------------------------------------- /src/core/meta/attr.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.meta; 4 | 5 | namespace tactile { 6 | 7 | Attr::Attr(const AttrKind kind) 8 | { 9 | reset(kind); 10 | } 11 | 12 | void Attr::reset(const AttrKind kind) 13 | { 14 | switch (kind) { 15 | case AttrKind::kInt: emplace(); break; 16 | case AttrKind::kInt2: emplace(); break; 17 | case AttrKind::kInt3: emplace(); break; 18 | case AttrKind::kInt4: emplace(); break; 19 | case AttrKind::kFloat: emplace(); break; 20 | case AttrKind::kFloat2: emplace(); break; 21 | case AttrKind::kFloat3: emplace(); break; 22 | case AttrKind::kFloat4: emplace(); break; 23 | case AttrKind::kBool: emplace(); break; 24 | case AttrKind::kString: emplace(); break; 25 | case AttrKind::kPath: emplace(); break; 26 | case AttrKind::kColor: emplace(); break; 27 | default: throw std::invalid_argument {"bad attribute kind"}; 28 | } 29 | } 30 | 31 | auto Attr::as_int() -> int_type* 32 | { 33 | return std::get_if(&m_value); 34 | } 35 | 36 | auto Attr::as_int() const -> const int_type* 37 | { 38 | return std::get_if(&m_value); 39 | } 40 | 41 | auto Attr::as_int2() -> int2_type* 42 | { 43 | return std::get_if(&m_value); 44 | } 45 | 46 | auto Attr::as_int2() const -> const int2_type* 47 | { 48 | return std::get_if(&m_value); 49 | } 50 | 51 | auto Attr::as_int3() -> int3_type* 52 | { 53 | return std::get_if(&m_value); 54 | } 55 | 56 | auto Attr::as_int3() const -> const int3_type* 57 | { 58 | return std::get_if(&m_value); 59 | } 60 | 61 | auto Attr::as_int4() -> int4_type* 62 | { 63 | return std::get_if(&m_value); 64 | } 65 | 66 | auto Attr::as_int4() const -> const int4_type* 67 | { 68 | return std::get_if(&m_value); 69 | } 70 | 71 | auto Attr::as_float() -> float_type* 72 | { 73 | return std::get_if(&m_value); 74 | } 75 | 76 | auto Attr::as_float() const -> const float_type* 77 | { 78 | return std::get_if(&m_value); 79 | } 80 | 81 | auto Attr::as_float2() -> float2_type* 82 | { 83 | return std::get_if(&m_value); 84 | } 85 | 86 | auto Attr::as_float2() const -> const float2_type* 87 | { 88 | return std::get_if(&m_value); 89 | } 90 | 91 | auto Attr::as_float3() -> float3_type* 92 | { 93 | return std::get_if(&m_value); 94 | } 95 | 96 | auto Attr::as_float3() const -> const float3_type* 97 | { 98 | return std::get_if(&m_value); 99 | } 100 | 101 | auto Attr::as_float4() -> float4_type* 102 | { 103 | return std::get_if(&m_value); 104 | } 105 | 106 | auto Attr::as_float4() const -> const float4_type* 107 | { 108 | return std::get_if(&m_value); 109 | } 110 | 111 | auto Attr::as_bool() -> bool* 112 | { 113 | return std::get_if(&m_value); 114 | } 115 | 116 | auto Attr::as_bool() const -> const bool* 117 | { 118 | return std::get_if(&m_value); 119 | } 120 | 121 | auto Attr::as_string() -> string_type* 122 | { 123 | return std::get_if(&m_value); 124 | } 125 | 126 | auto Attr::as_string() const -> const string_type* 127 | { 128 | return std::get_if(&m_value); 129 | } 130 | 131 | auto Attr::as_path() -> path_type* 132 | { 133 | return std::get_if(&m_value); 134 | } 135 | 136 | auto Attr::as_path() const -> const path_type* 137 | { 138 | return std::get_if(&m_value); 139 | } 140 | 141 | auto Attr::as_color() -> color_type* 142 | { 143 | return std::get_if(&m_value); 144 | } 145 | 146 | auto Attr::as_color() const -> const color_type* 147 | { 148 | return std::get_if(&m_value); 149 | } 150 | 151 | auto Attr::kind() const -> AttrKind 152 | { 153 | switch (m_value.index()) { 154 | case kIntTypeIndex: return AttrKind::kInt; 155 | case kInt2TypeIndex: return AttrKind::kInt2; 156 | case kInt3TypeIndex: return AttrKind::kInt3; 157 | case kInt4TypeIndex: return AttrKind::kInt4; 158 | case kFloatTypeIndex: return AttrKind::kFloat; 159 | case kFloat2TypeIndex: return AttrKind::kFloat2; 160 | case kFloat3TypeIndex: return AttrKind::kFloat3; 161 | case kFloat4TypeIndex: return AttrKind::kFloat4; 162 | case kBoolTypeIndex: return AttrKind::kBool; 163 | case kStringTypeIndex: return AttrKind::kString; 164 | case kPathTypeIndex: return AttrKind::kPath; 165 | case kColorTypeIndex: return AttrKind::kColor; 166 | } 167 | 168 | throw std::logic_error {"bad attribute value index"}; 169 | } 170 | 171 | auto Attr::is_vector() const -> bool 172 | { 173 | switch (kind()) { 174 | case AttrKind::kInt: 175 | case AttrKind::kFloat: 176 | case AttrKind::kBool: 177 | case AttrKind::kString: 178 | case AttrKind::kPath: [[fallthrough]]; 179 | case AttrKind::kColor: return false; 180 | case AttrKind::kInt2: 181 | case AttrKind::kInt3: 182 | case AttrKind::kInt4: 183 | case AttrKind::kFloat2: 184 | case AttrKind::kFloat3: [[fallthrough]]; 185 | case AttrKind::kFloat4: return true; 186 | } 187 | 188 | throw std::logic_error {"bad attribute kind"}; 189 | } 190 | 191 | } // namespace tactile 192 | -------------------------------------------------------------------------------- /src/core/meta/meta-attr.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.meta:attr; 4 | 5 | export import std; 6 | export import tactile.core.common; 7 | export import tactile.core.numeric; 8 | export import :color; 9 | 10 | export namespace tactile { 11 | 12 | /// Represents different kinds of attributes. 13 | enum class AttrKind : u8 14 | { 15 | kInt, 16 | kInt2, 17 | kInt3, 18 | kInt4, 19 | kFloat, 20 | kFloat2, 21 | kFloat3, 22 | kFloat4, 23 | kBool, 24 | kString, 25 | kPath, 26 | kColor, 27 | }; 28 | 29 | /// Represents an attribute value. 30 | class Attr final 31 | { 32 | public: 33 | using int_type = i32; 34 | using int2_type = Int2; 35 | using int3_type = Int3; 36 | using int4_type = Int4; 37 | using float_type = f32; 38 | using float2_type = Float2; 39 | using float3_type = Float3; 40 | using float4_type = Float4; 41 | using string_type = String; 42 | using path_type = Path; 43 | using color_type = Color; 44 | 45 | /// Creates an attribute of a given kind. 46 | explicit Attr(AttrKind kind = AttrKind::kInt); 47 | 48 | /// Resets the value of the attribute to default value of a given attribute kind. 49 | void reset(AttrKind kind); 50 | 51 | /// Constructs a value in the attribute in-place. 52 | template 53 | auto emplace(Args&&... args) -> T& 54 | { 55 | return m_value.emplace(std::forward(args)...); 56 | } 57 | 58 | /// Returns a pointer to the underlying integer value, if there is one. 59 | [[nodiscard]] 60 | auto as_int() -> int_type*; 61 | 62 | /// Returns a pointer to the underlying integer value, if there is one. 63 | [[nodiscard]] 64 | auto as_int() const -> const int_type*; 65 | 66 | /// Returns a pointer to underlying 2D integer vector value, if there is one. 67 | [[nodiscard]] 68 | auto as_int2() -> int2_type*; 69 | 70 | /// Returns a pointer to underlying 2D integer vector value, if there is one. 71 | [[nodiscard]] 72 | auto as_int2() const -> const int2_type*; 73 | 74 | /// Returns a pointer to underlying 3D integer vector value, if there is one. 75 | [[nodiscard]] 76 | auto as_int3() -> int3_type*; 77 | 78 | /// Returns a pointer to underlying 3D integer vector value, if there is one. 79 | [[nodiscard]] 80 | auto as_int3() const -> const int3_type*; 81 | 82 | /// Returns a pointer to underlying 4D integer vector value, if there is one. 83 | [[nodiscard]] 84 | auto as_int4() -> int4_type*; 85 | 86 | /// Returns a pointer to underlying 4D integer vector value, if there is one. 87 | [[nodiscard]] 88 | auto as_int4() const -> const int4_type*; 89 | 90 | /// Returns a pointer to underlying float value, if there is one. 91 | [[nodiscard]] 92 | auto as_float() -> float_type*; 93 | 94 | /// Returns a pointer to underlying float value, if there is one. 95 | [[nodiscard]] 96 | auto as_float() const -> const float_type*; 97 | 98 | /// Returns a pointer to underlying 2D float vector value, if there is one. 99 | [[nodiscard]] 100 | auto as_float2() -> float2_type*; 101 | 102 | /// Returns a pointer to underlying 2D float vector value, if there is one. 103 | [[nodiscard]] 104 | auto as_float2() const -> const float2_type*; 105 | 106 | /// Returns a pointer to underlying 3D float vector value, if there is one. 107 | [[nodiscard]] 108 | auto as_float3() -> float3_type*; 109 | 110 | /// Returns a pointer to underlying 3D float vector value, if there is one. 111 | [[nodiscard]] 112 | auto as_float3() const -> const float3_type*; 113 | 114 | /// Returns a pointer to underlying 4D float vector value, if there is one. 115 | [[nodiscard]] 116 | auto as_float4() -> float4_type*; 117 | 118 | /// Returns a pointer to underlying 4D float vector value, if there is one. 119 | [[nodiscard]] 120 | auto as_float4() const -> const float4_type*; 121 | 122 | /// Returns a pointer to underlying boolean value, if there is one. 123 | [[nodiscard]] 124 | auto as_bool() -> bool*; 125 | 126 | /// Returns a pointer to underlying boolean value, if there is one. 127 | [[nodiscard]] 128 | auto as_bool() const -> const bool*; 129 | 130 | /// Returns a pointer to underlying string value, if there is one. 131 | [[nodiscard]] 132 | auto as_string() -> string_type*; 133 | 134 | /// Returns a pointer to underlying string value, if there is one. 135 | [[nodiscard]] 136 | auto as_string() const -> const string_type*; 137 | 138 | /// Returns a pointer to underlying path value, if there is one. 139 | [[nodiscard]] 140 | auto as_path() -> path_type*; 141 | 142 | /// Returns a pointer to underlying path value, if there is one. 143 | [[nodiscard]] 144 | auto as_path() const -> const path_type*; 145 | 146 | /// Returns a pointer to underlying color value, if there is one. 147 | [[nodiscard]] 148 | auto as_color() -> color_type*; 149 | 150 | /// Returns a pointer to underlying color value, if there is one. 151 | [[nodiscard]] 152 | auto as_color() const -> const color_type*; 153 | 154 | /// Returns the kind of the attribute. 155 | [[nodiscard]] 156 | auto kind() const -> AttrKind; 157 | 158 | /// Indicates whether the attribute has the default value for its kind. 159 | [[nodiscard]] 160 | auto has_default_value() const -> bool 161 | { 162 | const auto visitor = [](const T& value) -> bool { 163 | return value == T {}; 164 | }; 165 | return std::visit(visitor, m_value); 166 | } 167 | 168 | /// Indicates whether the attribute has a vector value. 169 | [[nodiscard]] 170 | auto is_vector() const -> bool; 171 | 172 | [[nodiscard]] 173 | auto operator==(const Attr&) const -> bool = default; 174 | 175 | private: 176 | // These are indices into the value_type variant type parameters. 177 | constexpr static usize kIntTypeIndex {0}; 178 | constexpr static usize kInt2TypeIndex {1}; 179 | constexpr static usize kInt3TypeIndex {2}; 180 | constexpr static usize kInt4TypeIndex {3}; 181 | constexpr static usize kFloatTypeIndex {4}; 182 | constexpr static usize kFloat2TypeIndex {5}; 183 | constexpr static usize kFloat3TypeIndex {6}; 184 | constexpr static usize kFloat4TypeIndex {7}; 185 | constexpr static usize kBoolTypeIndex {8}; 186 | constexpr static usize kStringTypeIndex {9}; 187 | constexpr static usize kPathTypeIndex {10}; 188 | constexpr static usize kColorTypeIndex {11}; 189 | 190 | using value_type = std::variant; 202 | 203 | value_type m_value {}; 204 | }; 205 | 206 | } // namespace tactile 207 | -------------------------------------------------------------------------------- /src/core/meta/meta-color.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.meta:color; 4 | 5 | export import tactile.core.common; 6 | 7 | export namespace tactile { 8 | 9 | /// Represents an RGBA color with 8-bit precision. 10 | struct Color final 11 | { 12 | u8 red {0x00u}; 13 | u8 green {0x00u}; 14 | u8 blue {0x00u}; 15 | u8 alpha {0xFFu}; 16 | 17 | [[nodiscard]] 18 | constexpr auto operator==(const Color&) const noexcept -> bool = default; 19 | }; 20 | 21 | } // namespace tactile 22 | -------------------------------------------------------------------------------- /src/core/meta/meta.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides APIs related to metadata, such as properties and components. 4 | /// 5 | /// Many objects, such as maps and layers, support user-defined properties and 6 | /// components. A property, in this context, means a named value of some kind. The 7 | /// Attr class is used to represent a single property value. A component is a named 8 | /// collection of properties that can be directly attached to any object that may 9 | /// feature properties. 10 | export module tactile.core.meta; 11 | 12 | export import :attr; 13 | export import :color; 14 | -------------------------------------------------------------------------------- /src/core/numeric/numeric-casts.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.numeric:casts; 4 | 5 | export import std; 6 | 7 | export namespace tactile { 8 | 9 | /// Performs a checked narrowing conversion of an integral value. 10 | /// 11 | /// Throws if the original value isn't representable using the destination type. 12 | template 13 | requires(sizeof(To) <= sizeof(From)) 14 | [[nodiscard]] constexpr auto checked_cast(const From from) -> To 15 | { 16 | if constexpr (!std::same_as) { 17 | if constexpr (std::signed_integral) { 18 | if (std::cmp_less(from, std::numeric_limits::min())) [[unlikely]] { 19 | throw std::underflow_error {"integral narrowing conversion would be lossy"}; 20 | } 21 | } 22 | 23 | if (std::cmp_greater(from, std::numeric_limits::max())) [[unlikely]] { 24 | throw std::overflow_error {"integral narrowing conversion would be lossy"}; 25 | } 26 | } 27 | 28 | return static_cast(from); 29 | } 30 | 31 | /// Performs a checked conversion of an unsigned integer to a signed integer. 32 | template 33 | [[nodiscard]] constexpr auto to_signed(const T value) -> std::make_signed_t 34 | { 35 | return checked_cast>(value); 36 | } 37 | 38 | /// Performs a checked conversion of a signed integer to an unsigned integer. 39 | template 40 | [[nodiscard]] constexpr auto to_unsigned(const T value) -> std::make_unsigned_t 41 | { 42 | return checked_cast>(value); 43 | } 44 | 45 | /// Performs a saturating narrowing conversion of an integral value. 46 | template 47 | [[nodiscard]] constexpr auto saturate_cast(const From from) noexcept -> To 48 | { 49 | constexpr auto kToMin = std::numeric_limits::min(); 50 | constexpr auto kToMax = std::numeric_limits::max(); 51 | 52 | if constexpr (!std::same_as) { 53 | if constexpr (std::signed_integral) { 54 | if (std::cmp_less(from, kToMin)) { 55 | return kToMin; 56 | } 57 | } 58 | 59 | if (std::cmp_greater(from, kToMax)) { 60 | return kToMax; 61 | } 62 | } 63 | 64 | return static_cast(from); 65 | } 66 | 67 | } // namespace tactile 68 | -------------------------------------------------------------------------------- /src/core/numeric/numeric-checked.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module; 4 | 5 | #include // Needed for boost::safe_numerics 6 | 7 | #include 8 | 9 | export module tactile.core.numeric:checked; 10 | 11 | export import tactile.core.common; 12 | export import :concepts; 13 | 14 | namespace tactile { 15 | 16 | [[nodiscard]] 17 | constexpr auto to_error( 18 | const boost::safe_numerics::safe_numerics_error error) noexcept -> Error 19 | { 20 | using enum boost::safe_numerics::safe_numerics_error; 21 | switch (error) { 22 | case positive_overflow_error: return Error::kArithmeticOverflow; 23 | case negative_overflow_error: [[fallthrough]]; 24 | case underflow_error: return Error::kArithmeticUnderflow; 25 | case precision_overflow_error: return Error::kArithmeticPrecision; 26 | case negative_value_shift: [[fallthrough]]; 27 | case negative_shift: [[fallthrough]]; 28 | case shift_too_large: return Error::kInvalidOp; 29 | case range_error: [[fallthrough]]; 30 | case domain_error: [[fallthrough]]; 31 | case uninitialized_value: return Error::kArithmeticInvalidValue; 32 | case success: [[fallthrough]]; 33 | default: return Error::kUnknown; 34 | } 35 | } 36 | 37 | } // namespace tactile 38 | 39 | export namespace tactile { 40 | 41 | /// Performs checked addition of two numerical values. 42 | template 43 | [[nodiscard]] constexpr auto checked_add(const T lhs, const T rhs) noexcept 44 | -> Result 45 | { 46 | const auto result = boost::safe_numerics::checked::add(lhs, rhs); 47 | 48 | if (result.exception()) [[unlikely]] { 49 | return err(to_error(result.m_e)); 50 | } 51 | 52 | return result.m_contents.m_r; 53 | } 54 | 55 | /// Performs checked subtraction of two numerical values. 56 | template 57 | [[nodiscard]] constexpr auto checked_sub(const T lhs, const T rhs) noexcept 58 | -> Result 59 | { 60 | const auto result = boost::safe_numerics::checked::subtract(lhs, rhs); 61 | 62 | if (result.exception()) [[unlikely]] { 63 | return err(to_error(result.m_e)); 64 | } 65 | 66 | return result.m_contents.m_r; 67 | } 68 | 69 | /// Performs checked multiplication of two numerical values. 70 | template 71 | [[nodiscard]] constexpr auto checked_mul(const T lhs, const T rhs) noexcept 72 | -> Result 73 | { 74 | const auto result = boost::safe_numerics::checked::multiply(lhs, rhs); 75 | 76 | if (result.exception()) [[unlikely]] { 77 | return err(to_error(result.m_e)); 78 | } 79 | 80 | return result.m_contents.m_r; 81 | } 82 | 83 | /// Performs checked division of two numerical values. 84 | template 85 | [[nodiscard]] constexpr auto checked_div(const T lhs, const T rhs) noexcept 86 | -> Result 87 | { 88 | const auto result = boost::safe_numerics::checked::divide(lhs, rhs); 89 | 90 | if (result.exception()) [[unlikely]] { 91 | return err(to_error(result.m_e)); 92 | } 93 | 94 | return result.m_contents.m_r; 95 | } 96 | 97 | } // namespace tactile 98 | -------------------------------------------------------------------------------- /src/core/numeric/numeric-concepts.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.numeric:concepts; 4 | 5 | export import std; 6 | 7 | export namespace tactile { 8 | 9 | template 10 | concept Numeric = std::integral || std::floating_point; 11 | 12 | } // namespace tactile 13 | -------------------------------------------------------------------------------- /src/core/numeric/numeric-constants.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.numeric:constants; 4 | 5 | export import std; 6 | export import tactile.core.common; 7 | 8 | export namespace tactile { 9 | 10 | inline constexpr auto kMinI8 = std::numeric_limits::min(); 11 | inline constexpr auto kMinI16 = std::numeric_limits::min(); 12 | inline constexpr auto kMinI32 = std::numeric_limits::min(); 13 | inline constexpr auto kMinI64 = std::numeric_limits::min(); 14 | 15 | inline constexpr auto kMaxI8 = std::numeric_limits::max(); 16 | inline constexpr auto kMaxI16 = std::numeric_limits::max(); 17 | inline constexpr auto kMaxI32 = std::numeric_limits::max(); 18 | inline constexpr auto kMaxI64 = std::numeric_limits::max(); 19 | 20 | // Provided for consistency, these are of course all zero. 21 | inline constexpr auto kMinU8 = std::numeric_limits::min(); 22 | inline constexpr auto kMinU16 = std::numeric_limits::min(); 23 | inline constexpr auto kMinU32 = std::numeric_limits::min(); 24 | inline constexpr auto kMinU64 = std::numeric_limits::min(); 25 | 26 | inline constexpr auto kMaxU8 = std::numeric_limits::max(); 27 | inline constexpr auto kMaxU16 = std::numeric_limits::max(); 28 | inline constexpr auto kMaxU32 = std::numeric_limits::max(); 29 | inline constexpr auto kMaxU64 = std::numeric_limits::max(); 30 | 31 | } // namespace tactile 32 | -------------------------------------------------------------------------------- /src/core/numeric/numeric-hash.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.numeric:hash; 4 | 5 | export import std; 6 | export import tactile.core.common; 7 | 8 | export namespace tactile { 9 | 10 | /// Hashes a value and combines the result with an existing hash value. 11 | /// 12 | /// See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0814r2.pdf 13 | template 14 | constexpr void hash_combine(usize& seed, const T& value) noexcept 15 | { 16 | const auto value_hash = std::hash {}(value); 17 | seed ^= value_hash + 0x9E3779B9uz + (seed << 6uz) + (seed >> 2uz); 18 | } 19 | 20 | /// Hashes a generic collection of values. 21 | [[nodiscard]] 22 | constexpr auto hash_combine(const auto&... args) noexcept -> usize 23 | { 24 | auto seed = 0uz; 25 | (hash_combine(seed, args), ...); 26 | return seed; 27 | } 28 | 29 | } // namespace tactile 30 | -------------------------------------------------------------------------------- /src/core/numeric/numeric-random.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.numeric:random; 4 | 5 | export import tactile.core.common; 6 | 7 | export namespace tactile { 8 | 9 | /// Forces initialization of the RNG engine for the current thread. 10 | void random_init(); 11 | 12 | /// Returns a pseudo-random `i32` within the interval [min, max]. 13 | [[nodiscard]] 14 | auto random_i32(i32 min, i32 max) -> i32; 15 | 16 | /// Returns a pseudo-random `u32` within the interval [min, max]. 17 | [[nodiscard]] 18 | auto random_u32(u32 min, u32 max) -> u32; 19 | 20 | /// Returns a pseudo-random `f32` within the interval [min, max]. 21 | [[nodiscard]] 22 | auto random_f32(f32 min, f32 max) -> f32; 23 | 24 | /// Returns a pseudo-random boolean value. 25 | [[nodiscard]] 26 | auto random_bool() -> bool; 27 | 28 | } // namespace tactile 29 | -------------------------------------------------------------------------------- /src/core/numeric/numeric.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides common numeric APIs. 4 | /// 5 | /// This module serves as a fundamental building block for other modules, and must 6 | /// therefore only depend on the "tactile.common" module. 7 | export module tactile.core.numeric; 8 | 9 | export import :casts; 10 | export import :checked; 11 | export import :concepts; 12 | export import :constants; 13 | export import :hash; 14 | export import :random; 15 | export import :vec; 16 | -------------------------------------------------------------------------------- /src/core/numeric/random.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.numeric; 4 | 5 | import std; 6 | import tactile.core.common; 7 | 8 | namespace tactile { 9 | namespace { 10 | 11 | using RandomEngine = std::mt19937; 12 | 13 | [[nodiscard]] 14 | auto _make_random_engine() -> RandomEngine 15 | { 16 | std::random_device entropy_source {}; 17 | const auto seed = entropy_source(); 18 | return RandomEngine {seed}; 19 | } 20 | 21 | [[nodiscard]] 22 | auto _get_random_engine() -> RandomEngine& 23 | { 24 | thread_local auto engine = _make_random_engine(); 25 | return engine; 26 | } 27 | 28 | } // namespace 29 | 30 | void random_init() 31 | { 32 | (void) _get_random_engine(); 33 | } 34 | 35 | auto random_i32(const i32 min, const i32 max) -> i32 36 | { 37 | auto& engine = _get_random_engine(); 38 | return std::uniform_int_distribution {min, max}(engine); 39 | } 40 | 41 | auto random_u32(const u32 min, const u32 max) -> u32 42 | { 43 | auto& engine = _get_random_engine(); 44 | return std::uniform_int_distribution {min, max}(engine); 45 | } 46 | 47 | auto random_f32(const f32 min, const f32 max) -> f32 48 | { 49 | auto& engine = _get_random_engine(); 50 | return std::uniform_real_distribution {min, max}(engine); 51 | } 52 | 53 | auto random_bool() -> bool 54 | { 55 | return random_i32(0, 1) == 1; 56 | } 57 | 58 | } // namespace tactile 59 | -------------------------------------------------------------------------------- /src/core/runtime/runtime-interfaces.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.runtime:interfaces; 4 | 5 | export import tactile.core.common; 6 | 7 | export namespace tactile { 8 | 9 | /// Type used for texture identifiers. 10 | using TextureID = i32; 11 | 12 | /// Interface for a texture loaded by a backend. 13 | class ITexture 14 | { 15 | protected: 16 | ITexture() = default; 17 | 18 | ITexture(ITexture&&) noexcept = default; 19 | 20 | ITexture(const ITexture&) = default; 21 | 22 | auto operator=(ITexture&&) noexcept -> ITexture& = default; 23 | 24 | auto operator=(const ITexture&) -> ITexture& = default; 25 | 26 | public: 27 | virtual ~ITexture() noexcept = default; 28 | 29 | /// Returns the identifier associated with the texture. 30 | [[nodiscard]] 31 | virtual auto id() const -> TextureID = 0; 32 | }; 33 | 34 | /// Interface for a texture manager associated with a backend. 35 | class ITextureManager 36 | { 37 | protected: 38 | ITextureManager() = default; 39 | 40 | ITextureManager(ITextureManager&&) noexcept = default; 41 | 42 | ITextureManager(const ITextureManager&) = default; 43 | 44 | auto operator=(ITextureManager&&) noexcept -> ITextureManager& = default; 45 | 46 | auto operator=(const ITextureManager&) -> ITextureManager& = default; 47 | 48 | public: 49 | virtual ~ITextureManager() noexcept = default; 50 | 51 | /// Loads a texture from the filesystem. 52 | [[nodiscard]] 53 | virtual auto load_texture(const Path& path) -> Result = 0; 54 | 55 | /// Removes a previously loaded texture. 56 | virtual void erase_texture(TextureID id) = 0; 57 | 58 | /// Returns the texture associated with a given identifier, if any. 59 | [[nodiscard]] 60 | virtual auto find_texture(TextureID id) const -> const ITexture* = 0; 61 | }; 62 | 63 | /// Interface for applications usable with backend implementations (see IBackend). 64 | class IApp 65 | { 66 | protected: 67 | IApp() = default; 68 | 69 | IApp(IApp&&) noexcept = default; 70 | 71 | IApp(const IApp&) = default; 72 | 73 | auto operator=(IApp&&) noexcept -> IApp& = default; 74 | 75 | auto operator=(const IApp&) -> IApp& = default; 76 | 77 | public: 78 | virtual ~IApp() noexcept = default; 79 | 80 | /// Called once per event loop iteration, immediately after polling OS events. 81 | [[nodiscard]] 82 | virtual auto on_update() -> Result = 0; 83 | 84 | /// Renders UI elements. 85 | /// 86 | /// This is the only function in this interface that is allowed to call into 87 | /// Dear ImGui APIs. Backends should aim to follow each call to on_update with a 88 | /// call to this function. However, there's no guarantee that this is the case. 89 | /// In other words, there may be several calls to on_update for every on_render 90 | /// call. 91 | virtual void on_render() const = 0; 92 | 93 | /// Called when the application is starting up, before the first on_update call. 94 | /// 95 | /// It's safe for an application to store the passed pointer. 96 | [[nodiscard]] 97 | virtual auto on_startup(ITextureManager* texture_manager) -> Result = 0; 98 | 99 | /// Called when the application is shutting down, after the last on_update call. 100 | [[nodiscard]] 101 | virtual auto on_shutdown() -> Result = 0; 102 | 103 | /// Called once per event loop iteration to check if the application wants to stop. 104 | [[nodiscard]] 105 | virtual auto should_stop() const -> bool = 0; 106 | }; 107 | 108 | /// Interface for backend implementations. 109 | class IBackend 110 | { 111 | protected: 112 | IBackend() = default; 113 | 114 | IBackend(IBackend&&) noexcept = default; 115 | 116 | IBackend(const IBackend&) = default; 117 | 118 | auto operator=(IBackend&&) noexcept -> IBackend& = default; 119 | 120 | auto operator=(const IBackend&) -> IBackend& = default; 121 | 122 | public: 123 | virtual ~IBackend() noexcept = default; 124 | 125 | /// Runs the given application. 126 | [[nodiscard]] 127 | virtual auto run(IApp& app) -> Result = 0; 128 | }; 129 | 130 | } // namespace tactile 131 | -------------------------------------------------------------------------------- /src/core/runtime/runtime-null.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.runtime:null; 4 | 5 | export import :interfaces; 6 | 7 | export namespace tactile { 8 | 9 | /// A null backend implementation. 10 | class NullBackend final : public IBackend 11 | { 12 | public: 13 | [[nodiscard]] 14 | auto run(IApp&) -> Result override 15 | { 16 | return ok(); 17 | } 18 | }; 19 | 20 | } // namespace tactile 21 | -------------------------------------------------------------------------------- /src/core/runtime/runtime.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.runtime; 4 | 5 | namespace tactile { 6 | 7 | void Runtime::set_save_encoder(const SaveFormatKind kind, ISaveEncoder* encoder) 8 | { 9 | m_save_encoders.insert_or_assign(kind, encoder); 10 | } 11 | 12 | void Runtime::set_save_decoder(const SaveFormatKind kind, ISaveDecoder* decoder) 13 | { 14 | m_save_decoders.insert_or_assign(kind, decoder); 15 | } 16 | 17 | auto Runtime::find_save_encoder(const SaveFormatKind kind) -> ISaveEncoder* 18 | { 19 | const auto iter = m_save_encoders.find(kind); 20 | return iter != m_save_encoders.end() ? iter->second : nullptr; 21 | } 22 | 23 | auto Runtime::find_save_encoder(const SaveFormatKind kind) const 24 | -> const ISaveEncoder* 25 | { 26 | const auto iter = m_save_encoders.find(kind); 27 | return iter != m_save_encoders.end() ? iter->second : nullptr; 28 | } 29 | 30 | auto Runtime::find_save_decoder(const SaveFormatKind kind) -> ISaveDecoder* 31 | { 32 | const auto iter = m_save_decoders.find(kind); 33 | return iter != m_save_decoders.end() ? iter->second : nullptr; 34 | } 35 | 36 | auto Runtime::find_save_decoder(const SaveFormatKind kind) const 37 | -> const ISaveDecoder* 38 | { 39 | const auto iter = m_save_decoders.find(kind); 40 | return iter != m_save_decoders.end() ? iter->second : nullptr; 41 | } 42 | 43 | auto Runtime::compressor() -> Compressor& 44 | { 45 | return m_compressor; 46 | } 47 | 48 | auto Runtime::compressor() const -> const Compressor& 49 | { 50 | return m_compressor; 51 | } 52 | 53 | auto get_runtime() -> Runtime& 54 | { 55 | static Runtime runtime {}; 56 | return runtime; 57 | } 58 | 59 | } // namespace tactile 60 | -------------------------------------------------------------------------------- /src/core/runtime/runtime.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// This module provides the global runtime context, accessible via the get_runtime 4 | /// function. The runtime context is used by other Tactile targets to access and 5 | /// configure common resources, such as save format implementations. 6 | /// 7 | /// This module is not used by any of the other 'tactile.core' modules. 8 | export module tactile.core.runtime; 9 | 10 | export import tactile.core.common; 11 | export import tactile.core.save; 12 | export import tactile.core.io; 13 | export import :interfaces; 14 | export import :null; 15 | 16 | export namespace tactile { 17 | 18 | /// Represents the Tactile runtime context. 19 | class Runtime final 20 | { 21 | public: 22 | /// Adds or removes a save encoder implementation for a given format. 23 | void set_save_encoder(SaveFormatKind kind, ISaveEncoder* encoder); 24 | 25 | /// Adds or removes a save decoder implementation for a given format. 26 | void set_save_decoder(SaveFormatKind kind, ISaveDecoder* decoder); 27 | 28 | /// Returns the registered save encoder implementation for a given format. 29 | [[nodiscard]] 30 | auto find_save_encoder(SaveFormatKind kind) -> ISaveEncoder*; 31 | 32 | /// Returns the registered save encoder implementation for a given format. 33 | [[nodiscard]] 34 | auto find_save_encoder(SaveFormatKind kind) const -> const ISaveEncoder*; 35 | 36 | /// Returns the registered save decoder implementation for a given format. 37 | [[nodiscard]] 38 | auto find_save_decoder(SaveFormatKind kind) -> ISaveDecoder*; 39 | 40 | /// Returns the registered save decoder implementation for a given format. 41 | [[nodiscard]] 42 | auto find_save_decoder(SaveFormatKind kind) const -> const ISaveDecoder*; 43 | 44 | /// Returns the associated compressor. 45 | [[nodiscard]] 46 | auto compressor() -> Compressor&; 47 | 48 | /// Returns the associated compressor. 49 | [[nodiscard]] 50 | auto compressor() const -> const Compressor&; 51 | 52 | private: 53 | HashMap m_save_encoders {}; 54 | HashMap m_save_decoders {}; 55 | Compressor m_compressor {}; 56 | }; 57 | 58 | /// Returns the global runtime context. 59 | [[nodiscard]] 60 | auto get_runtime() -> Runtime&; 61 | 62 | } // namespace tactile 63 | -------------------------------------------------------------------------------- /src/core/save/save.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides the save format API. 4 | export module tactile.core.save; 5 | 6 | export import tactile.core.common; 7 | export import tactile.core.io; 8 | 9 | export namespace tactile { 10 | 11 | /// Represents supported save format types. 12 | enum class SaveFormatKind : u8 13 | { 14 | kTactileYaml, 15 | kTiledJson, 16 | kTiledXml, 17 | kGodotScene, 18 | }; 19 | 20 | /// Provides options for decoding save files. 21 | struct SaveDecoderOptions final 22 | {}; 23 | 24 | /// Provides options for encoding save files. 25 | struct SaveEncoderOptions final 26 | {}; 27 | 28 | /// Interface for save format decoder implementations. 29 | class ISaveDecoder 30 | { 31 | protected: 32 | ISaveDecoder() = default; 33 | 34 | ISaveDecoder(ISaveDecoder&&) noexcept = default; 35 | 36 | ISaveDecoder(const ISaveDecoder&) = default; 37 | 38 | auto operator=(ISaveDecoder&&) noexcept -> ISaveDecoder& = default; 39 | 40 | auto operator=(const ISaveDecoder&) -> ISaveDecoder& = default; 41 | 42 | public: 43 | virtual ~ISaveDecoder() noexcept = default; 44 | 45 | /// Reads a map from a file. 46 | [[nodiscard]] 47 | virtual auto decode_map(const Path& path, 48 | const Compressor& compressor, 49 | const SaveDecoderOptions& options) -> Result = 0; 50 | }; 51 | 52 | /// Interface for save format encoder implementations. 53 | class ISaveEncoder 54 | { 55 | protected: 56 | ISaveEncoder() = default; 57 | 58 | ISaveEncoder(ISaveEncoder&&) noexcept = default; 59 | 60 | ISaveEncoder(const ISaveEncoder&) = default; 61 | 62 | auto operator=(ISaveEncoder&&) noexcept -> ISaveEncoder& = default; 63 | 64 | auto operator=(const ISaveEncoder&) -> ISaveEncoder& = default; 65 | 66 | public: 67 | virtual ~ISaveEncoder() noexcept = default; 68 | }; 69 | 70 | } // namespace tactile 71 | -------------------------------------------------------------------------------- /src/core/tile/tile.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.tile; 4 | 5 | namespace tactile { 6 | 7 | Tile::Tile(const TileID id) 8 | : m_id {id} 9 | {} 10 | 11 | auto Tile::id() const -> TileID 12 | { 13 | return m_id; 14 | } 15 | 16 | auto Tile::animation() -> Option& 17 | { 18 | return m_animation; 19 | } 20 | 21 | auto Tile::animation() const -> const Option& 22 | { 23 | return m_animation; 24 | } 25 | 26 | } // namespace tactile 27 | -------------------------------------------------------------------------------- /src/core/tile/tile.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides APIs related to tiles and tilesets. 4 | export module tactile.core.tile; 5 | 6 | export import tactile.core.common; 7 | export import tactile.core.io; 8 | export import tactile.core.numeric; 9 | 10 | export namespace tactile { 11 | 12 | /// Alias for global tile identifiers. 13 | using TileID = i32; 14 | 15 | /// The tile identifier used by empty tiles. 16 | inline constexpr TileID kEmptyTileId {0}; 17 | 18 | /// Represents a single frame in a tile animation. 19 | struct TileAnimationFrame final 20 | { 21 | /// The identifier of the tile showed during the frame. 22 | TileID tile_id {}; 23 | 24 | /// The length of time the frame is shown. 25 | Milliseconds duration {}; 26 | }; 27 | 28 | /// Represents a sequential tile animation. 29 | /// 30 | /// Animations are based on an initial frame that represents the parent tile. It's 31 | /// not possible to remove this initial frame from an animation. 32 | class TileAnimation final 33 | { 34 | public: 35 | explicit TileAnimation(const TileAnimationFrame& first_frame); 36 | 37 | /// Updates the state of the animation. 38 | void update(SteadyClock::time_point update_time); 39 | 40 | /// Inserts a frame at a given position in the animation. 41 | /// 42 | /// This function can also be used to append frames if the specified index is equal 43 | /// to the number of frames. However, note that it's not allowed to insert frames 44 | /// at index 0. 45 | /// 46 | /// If successful, this function always resets the progress of the animation. 47 | [[nodiscard]] 48 | auto insert_frame(isize index, const TileAnimationFrame& frame) -> Result; 49 | 50 | /// Inserts a frame at the end of the animation. 51 | void append_frame(const TileAnimationFrame& frame); 52 | 53 | /// Removes a frame at a given index. 54 | /// 55 | /// It's not possible to remove the frame at index 0. 56 | [[nodiscard]] 57 | auto erase_frame(isize index) -> Result; 58 | 59 | /// Returns the currently active frame. 60 | [[nodiscard]] 61 | auto current_frame() const -> const TileAnimationFrame&; 62 | 63 | /// Returns the number of frames in the animation. 64 | [[nodiscard]] 65 | auto frame_count() const -> usize; 66 | 67 | private: 68 | Vector m_frames {}; 69 | usize m_current_frame {0uz}; 70 | SteadyClock::time_point m_last_update {SteadyClock::now()}; 71 | }; 72 | 73 | /// Represents a tile definition. 74 | class Tile final 75 | { 76 | public: 77 | /// Creates a tile definition. 78 | explicit Tile(TileID id); 79 | 80 | /// Returns the associated identifier. 81 | [[nodiscard]] 82 | auto id() const -> TileID; 83 | 84 | /// Returns the associated animation. 85 | [[nodiscard]] 86 | auto animation() -> Option&; 87 | 88 | /// Returns the associated animation. 89 | [[nodiscard]] 90 | auto animation() const -> const Option&; 91 | 92 | private: 93 | TileID m_id; 94 | Option m_animation {}; 95 | }; 96 | 97 | } // namespace tactile 98 | -------------------------------------------------------------------------------- /src/core/tile/tile_animation.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.core.tile; 4 | 5 | import tactile.core.log; 6 | 7 | namespace tactile { 8 | 9 | TileAnimation::TileAnimation(const TileAnimationFrame& first_frame) 10 | { 11 | m_frames.push_back(first_frame); 12 | } 13 | 14 | void TileAnimation::update(const SteadyClock::time_point update_time) 15 | { 16 | const auto& frame = current_frame(); 17 | 18 | const auto elapsed_time = update_time - m_last_update; 19 | if (elapsed_time >= frame.duration) { 20 | m_current_frame = (m_current_frame + 1uz) % m_frames.size(); 21 | m_last_update = update_time; 22 | } 23 | } 24 | 25 | auto TileAnimation::insert_frame(const isize index, const TileAnimationFrame& frame) 26 | -> Result 27 | { 28 | const auto frame_count = m_frames.size(); 29 | 30 | if (index == 0 || std::cmp_greater(index, frame_count)) { 31 | get_logger().log(LogLevel::kError, 32 | "Cannot insert tile animation frame at index {}", 33 | index); 34 | return err(Error::kInvalidArg); 35 | } 36 | 37 | if (std::cmp_equal(index, frame_count)) { 38 | append_frame(frame); 39 | } 40 | else { 41 | m_frames.insert(m_frames.begin() + index, frame); 42 | } 43 | 44 | m_last_update = SteadyClock::now(); 45 | m_current_frame = 0; 46 | 47 | return ok(); 48 | } 49 | 50 | void TileAnimation::append_frame(const TileAnimationFrame& frame) 51 | { 52 | m_frames.push_back(frame); 53 | } 54 | 55 | auto TileAnimation::erase_frame(const isize index) -> Result 56 | { 57 | if (index == 0 || index >= m_frames.size()) { 58 | return err(Error::kInvalidArg); 59 | } 60 | 61 | auto frame = m_frames.at(to_unsigned(index)); 62 | m_frames.erase(m_frames.begin() + index); 63 | 64 | return frame; 65 | } 66 | 67 | auto TileAnimation::current_frame() const -> const TileAnimationFrame& 68 | { 69 | return m_frames.at(m_current_frame); 70 | } 71 | 72 | auto TileAnimation::frame_count() const -> usize 73 | { 74 | return m_frames.size(); 75 | } 76 | 77 | } // namespace tactile 78 | -------------------------------------------------------------------------------- /src/core/util/util-defer.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.util:defer; 4 | 5 | export import std; 6 | export import tactile.core.log; 7 | 8 | export namespace tactile { 9 | 10 | /// Utility for creating inline destructors. 11 | template 12 | class Defer final 13 | { 14 | public: 15 | [[nodiscard]] 16 | explicit Defer(T callback) noexcept 17 | : m_callback {std::move(callback)} 18 | {} 19 | 20 | Defer(Defer&&) = delete; 21 | 22 | Defer(const Defer&) = delete; 23 | 24 | ~Defer() noexcept 25 | { 26 | try { 27 | m_callback(); 28 | } 29 | catch (const std::exception& error) { 30 | get_logger().log(LogLevel::kError, 31 | "Defer destructor threw exception: {}", 32 | error.what()); 33 | } 34 | catch (...) { 35 | get_logger().log(LogLevel::kError, "Defer destructor threw exception"); 36 | } 37 | } 38 | 39 | auto operator=(Defer&&) -> Defer& = delete; 40 | 41 | auto operator=(const Defer&) -> Defer& = delete; 42 | 43 | private: 44 | T m_callback; 45 | }; 46 | 47 | } // namespace tactile 48 | -------------------------------------------------------------------------------- /src/core/util/util-validation.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.core.util:validation; 4 | 5 | import std; 6 | 7 | export namespace tactile { 8 | 9 | /// Returns a given pointer if it's not null, throws an exception otherwise. 10 | template 11 | [[nodiscard]] auto require_not_null(T ptr) -> T 12 | { 13 | if (ptr == nullptr) [[unlikely]] { 14 | throw std::invalid_argument {"unexpected null pointer"}; 15 | } 16 | return ptr; 17 | } 18 | 19 | } // namespace tactile 20 | -------------------------------------------------------------------------------- /src/core/util/util.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides miscellaneous utilities. 4 | export module tactile.core.util; 5 | 6 | export import :defer; 7 | export import :validation; 8 | -------------------------------------------------------------------------------- /src/editor/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_editor_project CXX) 2 | 3 | add_library(tactile_editor STATIC) 4 | 5 | target_sources(tactile_editor 6 | PUBLIC FILE_SET "CXX_MODULES" FILES 7 | "app/app.cppm" 8 | "cli/cli.cppm" 9 | "command/command.cppm" 10 | "command/command-interfaces.cppm" 11 | "command/command-stack.cppm" 12 | "editor.cppm" 13 | 14 | PRIVATE 15 | "app/launch.cpp" 16 | "cli/cli.cpp" 17 | "command/command_stack.cpp" 18 | ) 19 | 20 | tactile_set_target_properties(tactile_editor) 21 | 22 | target_link_libraries(tactile_editor 23 | PUBLIC 24 | tactile_interface_target 25 | tactile_core 26 | 27 | PRIVATE 28 | argparse::argparse 29 | ) 30 | -------------------------------------------------------------------------------- /src/editor/app/app.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.editor.app; 4 | 5 | export namespace tactile::editor { 6 | 7 | /// Launches the Tactile editor application. 8 | [[nodiscard]] 9 | auto launch(int argc, char* argv[]) -> int; 10 | 11 | } // namespace tactile::editor 12 | -------------------------------------------------------------------------------- /src/editor/app/launch.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module; 4 | 5 | #include 6 | 7 | module tactile.editor.app; 8 | 9 | import tactile.editor.cli; 10 | 11 | namespace tactile::editor { 12 | 13 | auto launch(const int argc, char* argv[]) -> int 14 | { 15 | const auto cli_options = parse_cli_options(argc, argv); 16 | if (!cli_options.has_value()) { 17 | return EXIT_FAILURE; 18 | } 19 | 20 | if (!cli_options->should_launch) { 21 | return EXIT_SUCCESS; 22 | } 23 | 24 | return EXIT_SUCCESS; 25 | } 26 | 27 | } // namespace tactile::editor 28 | -------------------------------------------------------------------------------- /src/editor/cli/cli.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module; 4 | 5 | #include 6 | 7 | module tactile.editor.cli; 8 | 9 | import std; 10 | 11 | namespace tactile::editor { 12 | namespace { 13 | 14 | constexpr const char* kVersionNumber = "0.5.0"; 15 | 16 | constexpr const char* kHelpMessage = 17 | R"(Usage: tactile [--help] [--version] [--log-level=] 18 | 19 | Options: 20 | -h, --help Print this message 21 | -v, --version Print the version number 22 | --log-level The log verbosity (default: "inf"))"; 23 | 24 | void _init_parser(argparse::ArgumentParser& parser, CliOptions& options) 25 | { 26 | parser.set_assign_chars("="); 27 | 28 | parser.add_argument("-h", "--help").nargs(0).action([&](const String&) { 29 | std::println("{}", kHelpMessage); 30 | options.should_launch = false; 31 | }); 32 | 33 | parser.add_argument("-v", "--version").nargs(0).action([&](const String&) { 34 | std::println("{}", kVersionNumber); 35 | options.should_launch = false; 36 | }); 37 | 38 | parser.add_argument("--log-level") 39 | .nargs(1) 40 | .choices("trc", "dbg", "inf", "wrn", "err") 41 | .default_value("inf") 42 | .action([&](const String& value) { 43 | if (value == "trc") { 44 | options.log_level = LogLevel::kTrace; 45 | } 46 | else if (value == "dbg") { 47 | options.log_level = LogLevel::kDebug; 48 | } 49 | else if (value == "inf") { 50 | options.log_level = LogLevel::kInfo; 51 | } 52 | else if (value == "wrn") { 53 | options.log_level = LogLevel::kWarn; 54 | } 55 | else if (value == "err") { 56 | options.log_level = LogLevel::kError; 57 | } 58 | }); 59 | } 60 | 61 | } // namespace 62 | 63 | auto make_default_cli_options() -> CliOptions 64 | { 65 | return { 66 | .log_level = LogLevel::kInfo, 67 | .should_launch = true, 68 | }; 69 | } 70 | 71 | auto parse_cli_options(const int argc, char* argv[]) -> Result 72 | { 73 | argparse::ArgumentParser parser {"tactile", 74 | kVersionNumber, 75 | argparse::default_arguments::none}; 76 | 77 | auto options = make_default_cli_options(); 78 | _init_parser(parser, options); 79 | 80 | try { 81 | parser.parse_args(argc, argv); 82 | } 83 | catch (const std::exception& error) { 84 | std::println(std::cerr, "ERROR: {}", error.what()); 85 | return err(Error::kInvalidArg); 86 | } 87 | catch (...) { 88 | std::println(std::cerr, "ERROR: Unknown"); 89 | return err(Error::kUnknown); 90 | } 91 | 92 | return options; 93 | } 94 | 95 | } // namespace tactile::editor 96 | -------------------------------------------------------------------------------- /src/editor/cli/cli.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.editor.cli; 4 | 5 | export import tactile.core; 6 | 7 | export namespace tactile::editor { 8 | 9 | /// Represents command line options. 10 | struct CliOptions final 11 | { 12 | /// The minimum log level to use. 13 | LogLevel log_level; 14 | 15 | /// Indicates whether the editor should be launched. 16 | bool should_launch; 17 | }; 18 | 19 | /// Returns the default CLI options. 20 | auto make_default_cli_options() -> CliOptions; 21 | 22 | /// Parses command line options. 23 | auto parse_cli_options(int argc, char* argv[]) -> Result; 24 | 25 | } // namespace tactile::editor 26 | -------------------------------------------------------------------------------- /src/editor/command/command-interfaces.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.editor.command:interfaces; 4 | 5 | export namespace tactile::editor { 6 | 7 | /// Interface for editor commands. 8 | class ICommand 9 | { 10 | protected: 11 | ICommand() = default; 12 | 13 | ICommand(ICommand&&) noexcept = default; 14 | 15 | ICommand(const ICommand&) = default; 16 | 17 | auto operator=(ICommand&&) noexcept -> ICommand& = default; 18 | 19 | auto operator=(const ICommand&) -> ICommand& = default; 20 | 21 | public: 22 | virtual ~ICommand() noexcept = default; 23 | 24 | /// Executes the command. 25 | virtual void redo() = 0; 26 | 27 | /// Reverts the effects of the redo function. 28 | virtual void undo() = 0; 29 | 30 | /// Attempts to merge another command into this command. 31 | /// 32 | /// This function can be used to combine consecutive high-frequency commands of 33 | /// the same type. For example, the user changing a color property via a color 34 | /// picker may trigger many "update property" events in a short amount of time. 35 | /// However, most users would consider the color change as a single action, so 36 | /// calling undo should revert the color to the initial value, skipping any 37 | /// intermediate values. 38 | [[nodiscard]] 39 | virtual auto merge([[maybe_unused]] const ICommand& other) -> bool 40 | { 41 | return false; 42 | } 43 | }; 44 | 45 | } // namespace tactile::editor 46 | -------------------------------------------------------------------------------- /src/editor/command/command-stack.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.editor.command:stack; 4 | 5 | export import std; 6 | export import tactile.core; 7 | export import :interfaces; 8 | 9 | export namespace tactile::editor { 10 | 11 | /// Manages a fixed-size history of editor commands. 12 | class CommandStack final 13 | { 14 | public: 15 | /// Creates an empty command stack with the given capacity. 16 | explicit CommandStack(usize capacity); 17 | 18 | /// Marks the current state as clean. 19 | void mark_as_clean(); 20 | 21 | /// Reverts the most recent command. 22 | void undo(); 23 | 24 | /// Executes the most recently reverted command. 25 | void redo(); 26 | 27 | /// Executes a command and stores it. 28 | template T, typename... Args> 29 | void execute(Args&&... args) 30 | { 31 | if (size() == capacity()) { 32 | _remove_oldest_command(); 33 | } 34 | 35 | _remove_commands_after_cursor(); 36 | 37 | // We initially allocate the command on the stack because we won't need to store 38 | // the command if it gets merged with the most recent command. 39 | T command {std::forward(args)...}; 40 | command.redo(); 41 | 42 | // If the command can't be merged, store it. 43 | if (m_commands.empty() || !m_commands.back()->merge(command)) { 44 | m_commands.push_back(make_unique(std::move(command))); 45 | _increment_cursor(); 46 | } 47 | else { 48 | m_clean_cursor.reset(); 49 | m_has_clean_state = false; 50 | } 51 | } 52 | 53 | /// Stores a command without executing it. 54 | void store(Unique command); 55 | 56 | /// Indicates whether the current state is considered dirty. 57 | [[nodiscard]] 58 | auto is_dirty() const -> bool; 59 | 60 | /// Indicates whether a command can be reverted. 61 | [[nodiscard]] 62 | auto can_undo() const -> bool; 63 | 64 | /// Indicates whether a command can be re-executed. 65 | [[nodiscard]] 66 | auto can_redo() const -> bool; 67 | 68 | /// Returns the size of the stack. 69 | [[nodiscard]] 70 | auto size() const -> usize; 71 | 72 | /// Returns the capacity of the stack. 73 | [[nodiscard]] 74 | auto capacity() const -> usize; 75 | 76 | /// Returns the index of the command cursor. 77 | [[nodiscard]] 78 | auto cursor() const -> Option; 79 | 80 | /// Returns the index of the clean command cursor. 81 | [[nodiscard]] 82 | auto clean_cursor() const -> Option; 83 | 84 | /// Returns a pointer to the cursor command, if there is one. 85 | [[nodiscard]] 86 | auto cursor_command() const -> const ICommand*; 87 | 88 | private: 89 | usize m_capacity; 90 | Deque> m_commands {}; 91 | Option m_cursor {}; 92 | Option m_clean_cursor {}; 93 | bool m_has_clean_state {false}; 94 | 95 | [[nodiscard]] 96 | auto _next_cursor_index() const -> usize; 97 | 98 | void _remove_oldest_command(); 99 | 100 | void _remove_commands_after_cursor(); 101 | 102 | void _increment_cursor(); 103 | 104 | void _reset_or_decrement_cursor(); 105 | }; 106 | 107 | } // namespace tactile::editor 108 | -------------------------------------------------------------------------------- /src/editor/command/command.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.editor.command; 4 | 5 | export import :interfaces; 6 | export import :stack; 7 | -------------------------------------------------------------------------------- /src/editor/command/command_stack.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module tactile.editor.command; 4 | 5 | import std; 6 | 7 | namespace tactile::editor { 8 | 9 | CommandStack::CommandStack(const usize capacity) 10 | : m_capacity {capacity} 11 | { 12 | if (m_capacity < 1) { 13 | throw std::invalid_argument {"invalid command stack capacity"}; 14 | } 15 | } 16 | 17 | void CommandStack::mark_as_clean() 18 | { 19 | m_clean_cursor = m_cursor; 20 | m_has_clean_state = true; 21 | } 22 | 23 | void CommandStack::undo() 24 | { 25 | if (!can_undo()) { 26 | throw std::runtime_error {"couldn't undo command"}; 27 | } 28 | 29 | const auto& command = m_commands.at(m_cursor.value()); 30 | command->undo(); 31 | 32 | _reset_or_decrement_cursor(); 33 | } 34 | 35 | void CommandStack::redo() 36 | { 37 | if (!can_redo()) { 38 | throw std::runtime_error {"couldn't redo command"}; 39 | } 40 | 41 | const auto& command = m_commands.at(_next_cursor_index()); 42 | command->redo(); 43 | 44 | _increment_cursor(); 45 | } 46 | 47 | void CommandStack::store(Unique command) 48 | { 49 | if (size() == capacity()) { 50 | _remove_oldest_command(); 51 | } 52 | 53 | _remove_commands_after_cursor(); 54 | 55 | m_commands.push_back(std::move(command)); 56 | _increment_cursor(); 57 | } 58 | 59 | auto CommandStack::is_dirty() const -> bool 60 | { 61 | return !m_has_clean_state || m_cursor != m_clean_cursor; 62 | } 63 | 64 | auto CommandStack::can_undo() const -> bool 65 | { 66 | return !m_commands.empty() && m_cursor.has_value(); 67 | } 68 | 69 | auto CommandStack::can_redo() const -> bool 70 | { 71 | return !m_commands.empty() && m_cursor < size() - 1uz; 72 | } 73 | 74 | auto CommandStack::size() const -> usize 75 | { 76 | return m_commands.size(); 77 | } 78 | 79 | auto CommandStack::capacity() const -> usize 80 | { 81 | return m_capacity; 82 | } 83 | 84 | auto CommandStack::cursor() const -> Option 85 | { 86 | return m_cursor; 87 | } 88 | 89 | auto CommandStack::clean_cursor() const -> Option 90 | { 91 | return m_clean_cursor; 92 | } 93 | 94 | auto CommandStack::cursor_command() const -> const ICommand* 95 | { 96 | return m_cursor.has_value() ? m_commands.at(*m_cursor).get() : nullptr; 97 | } 98 | 99 | auto CommandStack::_next_cursor_index() const -> usize 100 | { 101 | return m_cursor.has_value() ? *m_cursor + 1uz : 0uz; 102 | } 103 | 104 | void CommandStack::_remove_oldest_command() 105 | { 106 | m_commands.pop_front(); 107 | _reset_or_decrement_cursor(); 108 | } 109 | 110 | void CommandStack::_remove_commands_after_cursor() 111 | { 112 | const auto start_index = _next_cursor_index(); 113 | 114 | if (m_clean_cursor >= start_index) { 115 | m_clean_cursor.reset(); 116 | m_has_clean_state = false; 117 | } 118 | 119 | const auto command_count = size(); 120 | for (auto index = start_index; index < command_count; ++index) { 121 | m_commands.pop_back(); 122 | } 123 | } 124 | 125 | void CommandStack::_increment_cursor() 126 | { 127 | m_cursor = _next_cursor_index(); 128 | } 129 | 130 | void CommandStack::_reset_or_decrement_cursor() 131 | { 132 | if (!m_cursor.has_value()) { 133 | return; 134 | } 135 | 136 | if (m_cursor == 0) { 137 | m_cursor.reset(); 138 | } 139 | else { 140 | m_cursor = *m_cursor - 1; 141 | } 142 | } 143 | 144 | } // namespace tactile::editor 145 | -------------------------------------------------------------------------------- /src/editor/editor.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | export module tactile.editor; 4 | 5 | export import tactile.editor.app; 6 | export import tactile.editor.cli; 7 | export import tactile.editor.command; 8 | -------------------------------------------------------------------------------- /src/ui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_ui_project CXX) 2 | 3 | add_library(tactile_ui STATIC) 4 | 5 | target_sources(tactile_ui 6 | PUBLIC FILE_SET "CXX_MODULES" FILES 7 | "ui.cppm" 8 | "ui-imgui.cppm" 9 | ) 10 | 11 | tactile_set_target_properties(tactile_ui) 12 | 13 | target_link_libraries(tactile_ui 14 | PUBLIC 15 | tactile_interface_target 16 | tactile_core 17 | imgui::imgui 18 | ) 19 | 20 | if (TACTILE_ENABLE_OPENGL) 21 | target_compile_definitions(tactile_ui PRIVATE "TACTILE_ENABLE_OPENGL") 22 | endif () 23 | -------------------------------------------------------------------------------- /src/ui/ui-imgui.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module; 4 | 5 | #include 6 | #include 7 | 8 | #ifdef TACTILE_ENABLE_OPENGL 9 | #include 10 | #endif 11 | 12 | export module tactile.ui:imgui; 13 | 14 | // NOLINTBEGIN(*-unused-using-decls) 15 | 16 | export namespace ImGui { 17 | 18 | using ImGui::Begin; 19 | using ImGui::CreateContext; 20 | using ImGui::DestroyContext; 21 | using ImGui::End; 22 | using ImGui::GetDrawData; 23 | using ImGui::GetIO; 24 | using ImGui::NewFrame; 25 | using ImGui::Render; 26 | using ImGui::ShowDemoWindow; 27 | 28 | } // namespace ImGui 29 | 30 | export { 31 | using ::ImGuiContext; 32 | using ::ImVec2; 33 | using ::ImVec4; 34 | 35 | using ::ImGui_ImplSDL3_InitForOpenGL; 36 | using ::ImGui_ImplSDL3_InitForVulkan; 37 | using ::ImGui_ImplSDL3_NewFrame; 38 | using ::ImGui_ImplSDL3_ProcessEvent; 39 | using ::ImGui_ImplSDL3_Shutdown; 40 | 41 | #ifdef TACTILE_ENABLE_OPENGL 42 | using ::ImGui_ImplOpenGL3_CreateDeviceObjects; 43 | using ::ImGui_ImplOpenGL3_CreateFontsTexture; 44 | using ::ImGui_ImplOpenGL3_DestroyDeviceObjects; 45 | using ::ImGui_ImplOpenGL3_DestroyFontsTexture; 46 | using ::ImGui_ImplOpenGL3_Init; 47 | using ::ImGui_ImplOpenGL3_NewFrame; 48 | using ::ImGui_ImplOpenGL3_RenderDrawData; 49 | using ::ImGui_ImplOpenGL3_Shutdown; 50 | #endif 51 | } 52 | 53 | // NOLINTEND(*-unused-using-decls) 54 | -------------------------------------------------------------------------------- /src/ui/ui.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides low-level UI APIs. 4 | export module tactile.ui; 5 | 6 | export import :imgui; 7 | -------------------------------------------------------------------------------- /src/zlib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_zlib_project CXX) 2 | 3 | add_library(tactile_zlib STATIC) 4 | 5 | target_sources(tactile_zlib 6 | PUBLIC FILE_SET "CXX_MODULES" FILES 7 | "zlib.cppm" 8 | 9 | PRIVATE 10 | "zlib.cpp" 11 | ) 12 | 13 | tactile_set_target_properties(tactile_zlib) 14 | 15 | target_link_libraries(tactile_zlib 16 | PUBLIC 17 | tactile_interface_target 18 | tactile_core 19 | 20 | PRIVATE 21 | ZLIB::ZLIB 22 | ) 23 | -------------------------------------------------------------------------------- /src/zlib/zlib.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | module; 4 | 5 | #define Z_PREFIX_SET 6 | #include 7 | 8 | module tactile.zlib; 9 | 10 | import std; 11 | 12 | namespace tactile { 13 | namespace { 14 | 15 | // Zlib has inconsistently named typedefs, we use these instead. 16 | using z_byte = ::Bytef; 17 | using z_uint = ::uInt; 18 | using z_ulong = ::uLong; 19 | 20 | /// Type used for staging buffers used to batch data processing. 21 | using StagingBuffer = Array; 22 | 23 | /// Provides callbacks that controls the behavior of stream processing functions. 24 | struct ZlibCallbacks final 25 | { 26 | using start_fn = int (*)(z_stream*); 27 | using estimate_output_buffer_size_fn = usize (*)(z_stream*, usize); 28 | using update_fn = int (*)(z_stream*, int); 29 | using finish_fn = int (*)(z_stream*); 30 | 31 | start_fn start {}; 32 | estimate_output_buffer_size_fn estimate_output_buffer_size {}; 33 | update_fn update {}; 34 | finish_fn finish {}; 35 | }; 36 | 37 | /// Initializes a Zlib stream. 38 | [[nodiscard]] 39 | auto zlib_start(z_stream& stream, 40 | const ZlibCallbacks& callbacks, 41 | const Span input_data, 42 | StagingBuffer& staging_buffer) -> Result 43 | { 44 | stream.next_in = const_cast(input_data.data()); // NOLINT 45 | stream.avail_in = checked_cast(input_data.size_bytes()); 46 | stream.next_out = staging_buffer.data(); 47 | stream.avail_out = checked_cast(staging_buffer.size()); 48 | 49 | const auto init_stream_result = callbacks.start(&stream); 50 | if (init_stream_result != Z_OK) { 51 | get_logger().log(LogLevel::kError, 52 | "Could not initialize z_stream: {}", 53 | zError(init_stream_result)); 54 | return err(Error::kInvalidOp); 55 | } 56 | 57 | return ok(); 58 | } 59 | 60 | /// Processes a stream in chunks. 61 | [[nodiscard]] 62 | auto zlib_process(z_stream& stream, 63 | const ZlibCallbacks& callbacks, 64 | StagingBuffer& staging_buffer, 65 | Vector& output_buffer) -> Result 66 | { 67 | const auto copy_processed_batch_to_output_buffer = [&] { 68 | const auto written_bytes = staging_buffer.size() - stream.avail_out; 69 | output_buffer.insert(output_buffer.end(), 70 | staging_buffer.data(), 71 | staging_buffer.data() + written_bytes); 72 | }; 73 | 74 | while (true) { 75 | const auto update_result = callbacks.update(&stream, Z_FINISH); 76 | 77 | if (update_result == Z_STREAM_END) { 78 | copy_processed_batch_to_output_buffer(); 79 | break; 80 | } 81 | 82 | if (update_result == Z_OK || update_result == Z_BUF_ERROR) { 83 | // We ran out of space in the staging buffer, so we need to flush and 84 | // reuse it. 85 | copy_processed_batch_to_output_buffer(); 86 | stream.next_out = staging_buffer.data(); 87 | stream.avail_out = checked_cast(staging_buffer.size()); 88 | } 89 | else { 90 | get_logger().log(LogLevel::kError, 91 | "Could not process Zlib chunk: {}", 92 | zError(update_result)); 93 | return err(Error::kUnknown); 94 | } 95 | } 96 | 97 | return ok(); 98 | } 99 | 100 | /// Finalizes a Zlib stream. 101 | [[nodiscard]] 102 | auto zlib_finish(z_stream& stream, const ZlibCallbacks& callbacks) -> Result 103 | { 104 | const auto end_stream_result = callbacks.finish(&stream); 105 | 106 | if (end_stream_result != Z_OK) { 107 | get_logger().log(LogLevel::kError, 108 | "Could not finalize z_stream: {}", 109 | zError(end_stream_result)); 110 | return err(Error::kUnknown); 111 | } 112 | 113 | return ok(); 114 | } 115 | 116 | /// Runs Zlib on a given stream of bytes. 117 | [[nodiscard]] 118 | auto zlib_apply(const Span data, const ZlibCallbacks& callbacks) 119 | -> Result> 120 | { 121 | z_stream stream {}; 122 | StagingBuffer staging_buffer {}; 123 | Vector output_buffer {}; 124 | 125 | return zlib_start(stream, callbacks, data, staging_buffer) 126 | .and_then([&] { 127 | const auto output_buffer_size = 128 | callbacks.estimate_output_buffer_size(&stream, data.size()); 129 | output_buffer.reserve(output_buffer_size); 130 | return zlib_process(stream, callbacks, staging_buffer, output_buffer); 131 | }) 132 | .and_then([&] { return zlib_finish(stream, callbacks); }) 133 | .transform([&] { return std::move(output_buffer); }); 134 | } 135 | 136 | } // namespace 137 | 138 | auto ZlibCompressionFormat::compress(const Span data) const 139 | -> Result> 140 | { 141 | ZlibCallbacks callbacks {}; 142 | callbacks.start = [](z_stream* stream) { 143 | return z_deflateInit(stream, Z_DEFAULT_COMPRESSION); 144 | }; 145 | callbacks.estimate_output_buffer_size = [](z_stream* stream, 146 | const usize data_size) -> usize { 147 | return deflateBound(stream, checked_cast(data_size)); 148 | }; 149 | callbacks.update = &deflate; 150 | callbacks.finish = &deflateEnd; 151 | 152 | return zlib_apply(data, callbacks); 153 | } 154 | 155 | auto ZlibCompressionFormat::decompress(const Span data) const 156 | -> Result> 157 | { 158 | ZlibCallbacks callbacks {}; 159 | callbacks.start = [](z_stream* stream) { return z_inflateInit(stream); }; 160 | callbacks.estimate_output_buffer_size = 161 | [](z_stream*, const usize data_size) -> usize { return data_size * 2uz; }; 162 | callbacks.update = &inflate; 163 | callbacks.finish = &inflateEnd; 164 | 165 | return zlib_apply(data, callbacks); 166 | } 167 | 168 | } // namespace tactile 169 | -------------------------------------------------------------------------------- /src/zlib/zlib.cppm: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | /// Provides a compression format implementation using Zlib. 4 | export module tactile.zlib; 5 | 6 | export import tactile.core; 7 | 8 | export namespace tactile { 9 | 10 | /// Provides compression using the Zlib library. 11 | /// 12 | /// See https://github.com/madler/zlib. 13 | class ZlibCompressionFormat final : public ICompressionFormat 14 | { 15 | public: 16 | [[nodiscard]] 17 | auto compress(Span data) const -> Result> override; 18 | 19 | [[nodiscard]] 20 | auto decompress(Span data) const -> Result> override; 21 | }; 22 | 23 | } // namespace tactile 24 | -------------------------------------------------------------------------------- /tests/core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_core_tests_project CXX) 2 | 3 | add_executable(tactile_core_tests) 4 | 5 | target_sources(tactile_core_tests 6 | PRIVATE 7 | "common/error.test.cpp" 8 | "common/strings.test.cpp" 9 | "io/base64.test.cpp" 10 | "layer/annotation_layer.test.cpp" 11 | "layer/group_layer.test.cpp" 12 | "layer/layer.test.cpp" 13 | "layer/layer_info.test.cpp" 14 | "layer/tile_layer.test.cpp" 15 | "meta/attr.test.cpp" 16 | "numeric/casts.test.cpp" 17 | "numeric/checked.test.cpp" 18 | "numeric/vec.test.cpp" 19 | "util/defer.test.cpp" 20 | "util/validation.test.cpp" 21 | "tactile_core_tests.main.cpp" 22 | ) 23 | 24 | tactile_set_target_properties(tactile_core_tests) 25 | 26 | target_link_libraries(tactile_core_tests 27 | PRIVATE 28 | tactile_interface_target 29 | tactile_core 30 | GTest::gtest 31 | ) 32 | -------------------------------------------------------------------------------- /tests/core/common/error.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import tactile.core.common; 6 | 7 | namespace tactile::tests { 8 | namespace { 9 | 10 | class ErrorTest : public testing::Test 11 | {}; 12 | 13 | TEST_F(ErrorTest, ToString) 14 | { 15 | EXPECT_EQ(to_string(Error::kUnknown), "unknown"); 16 | EXPECT_EQ(to_string(Error::kOutOfMemory), "out_of_memory"); 17 | EXPECT_EQ(to_string(Error::kUnsupportedFeature), "unsupported_feature"); 18 | EXPECT_EQ(to_string(Error::kInvalidOp), "invalid_op"); 19 | EXPECT_EQ(to_string(Error::kInvalidArg), "invalid_arg"); 20 | EXPECT_EQ(to_string(Error::kInvalidFile), "invalid_file"); 21 | EXPECT_EQ(to_string(Error::kNoSuchFile), "no_such_file"); 22 | EXPECT_EQ(to_string(Error::kOutOfRange), "out_of_range"); 23 | EXPECT_EQ(to_string(Error::kArithmeticOverflow), "arithmetic_overflow"); 24 | EXPECT_EQ(to_string(Error::kArithmeticUnderflow), "arithmetic_underflow"); 25 | EXPECT_EQ(to_string(Error::kArithmeticPrecision), "arithmetic_precision"); 26 | EXPECT_EQ(to_string(Error::kArithmeticInvalidValue), "arithmetic_invalid_value"); 27 | EXPECT_EQ(to_string(Error::kStackOverflow), "stack_overflow"); 28 | EXPECT_EQ(to_string(Error::kStackUnderflow), "stack_underflow"); 29 | EXPECT_EQ(to_string(Error::kCouldNotInitialize), "could_not_initialize"); 30 | EXPECT_EQ(to_string(Error::kCouldNotParseFile), "could_not_parse_file"); 31 | EXPECT_EQ(to_string(Error::kCouldNotCompress), "could_not_compress"); 32 | EXPECT_EQ(to_string(Error::kCouldNotDecompress), "could_not_decompress"); 33 | } 34 | 35 | TEST_F(ErrorTest, Describe) 36 | { 37 | EXPECT_EQ(describe(Error::kUnknown), "an unknown error occurred"); 38 | EXPECT_EQ(describe(Error::kOutOfMemory), "out of memory"); 39 | EXPECT_EQ(describe(Error::kUnsupportedFeature), "a feature isn't supported"); 40 | EXPECT_EQ(describe(Error::kInvalidOp), "attempted an invalid operation"); 41 | EXPECT_EQ(describe(Error::kInvalidArg), "detected an invalid argument"); 42 | EXPECT_EQ(describe(Error::kInvalidFile), "detected an invalid file"); 43 | EXPECT_EQ(describe(Error::kNoSuchFile), "an expected file didn't exist"); 44 | EXPECT_EQ(describe(Error::kOutOfRange), "requested an out of range value"); 45 | EXPECT_EQ(describe(Error::kArithmeticOverflow), "detected arithmetic overflow"); 46 | EXPECT_EQ(describe(Error::kArithmeticUnderflow), "detected arithmetic underflow"); 47 | EXPECT_EQ(describe(Error::kArithmeticPrecision), 48 | "detected loss of arithmetic precision"); 49 | EXPECT_EQ(describe(Error::kArithmeticInvalidValue), 50 | "detected invalid arithmetic value"); 51 | EXPECT_EQ(describe(Error::kStackOverflow), "detected stack overflow"); 52 | EXPECT_EQ(describe(Error::kStackUnderflow), "detected stack underflow"); 53 | EXPECT_EQ(describe(Error::kCouldNotInitialize), 54 | "an initialization error occurred"); 55 | EXPECT_EQ(describe(Error::kCouldNotParseFile), "could not parse a file"); 56 | EXPECT_EQ(describe(Error::kCouldNotCompress), "could not compress data"); 57 | EXPECT_EQ(describe(Error::kCouldNotDecompress), "could not decompress data"); 58 | } 59 | 60 | } // namespace 61 | } // namespace tactile::tests 62 | -------------------------------------------------------------------------------- /tests/core/common/strings.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import std; 6 | import tactile.core.common; 7 | 8 | namespace tactile::tests { 9 | namespace { 10 | 11 | class StringOpsTest : public testing::Test 12 | {}; 13 | 14 | TEST_F(StringOpsTest, ParseU64) 15 | { 16 | EXPECT_EQ(parse_u64("42"), 42U); 17 | 18 | EXPECT_EQ(parse_u64("0"), std::numeric_limits::min()); 19 | EXPECT_FALSE(parse_u64("-1").has_value()); 20 | 21 | EXPECT_EQ(parse_u64("18446744073709551615"), std::numeric_limits::max()); 22 | EXPECT_FALSE(parse_u64("18446744073709551616").has_value()); 23 | 24 | EXPECT_FALSE(parse_u64("foobar").has_value()); 25 | } 26 | 27 | TEST_F(StringOpsTest, ParseI64) 28 | { 29 | EXPECT_EQ(parse_i64("42"), 42); 30 | EXPECT_EQ(parse_i64("-123"), -123); 31 | 32 | EXPECT_EQ(parse_i64("-9223372036854775808"), std::numeric_limits::min()); 33 | EXPECT_FALSE(parse_i64("-9223372036854775809").has_value()); 34 | 35 | EXPECT_EQ(parse_i64("9223372036854775807"), std::numeric_limits::max()); 36 | EXPECT_FALSE(parse_i64("9223372036854775808").has_value()); 37 | 38 | EXPECT_FALSE(parse_i64("foobar").has_value()); 39 | } 40 | 41 | TEST_F(StringOpsTest, ParseF64) 42 | { 43 | EXPECT_EQ(parse_f64("0"), 0.0); 44 | EXPECT_EQ(parse_f64("4.2"), 4.2); 45 | EXPECT_EQ(parse_f64("-10"), -10.0); 46 | 47 | EXPECT_FALSE(parse_f64("foobar").has_value()); 48 | } 49 | 50 | } // namespace 51 | } // namespace tactile::tests 52 | -------------------------------------------------------------------------------- /tests/core/io/base64.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | #include 5 | 6 | import tactile.core.common; 7 | import tactile.core.io; 8 | 9 | namespace tactile::tests { 10 | namespace { 11 | 12 | class Base64Test : public testing::Test 13 | {}; 14 | 15 | TEST_F(Base64Test, EncodeDecode) 16 | { 17 | // See https://en.m.wikipedia.org/wiki/Base64#Examples 18 | const Vector data { 19 | 'M', 'a', 'n', 'y', ' ', 'h', 'a', 'n', 'd', 's', ' ', 'm', 'a', 'k', 20 | 'e', ' ', 'l', 'i', 'g', 'h', 't', ' ', 'w', 'o', 'r', 'k', '.', 21 | }; 22 | 23 | String encoded_data {}; 24 | base64_encode(data, encoded_data); 25 | 26 | ASSERT_EQ(encoded_data, "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu"); 27 | 28 | Vector decoded_data {}; 29 | base64_decode(encoded_data, decoded_data); 30 | 31 | EXPECT_THAT(decoded_data, testing::ContainerEq(data)); 32 | } 33 | 34 | } // namespace 35 | } // namespace tactile::tests 36 | -------------------------------------------------------------------------------- /tests/core/layer/annotation_layer.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | #include 5 | 6 | import std; 7 | import tactile.core.layer; 8 | 9 | namespace tactile::tests { 10 | namespace { 11 | 12 | struct TestAnnotations final 13 | { 14 | AnnotationID point_id; 15 | AnnotationID rect_id; 16 | AnnotationID ellipse_id; 17 | AnnotationID invalid_id; 18 | }; 19 | 20 | class AnnotationLayerTest : public testing::Test 21 | { 22 | protected: 23 | AnnotationLayer m_layer {LayerID {42}}; 24 | AnnotationID m_next_annotation_id {1}; 25 | 26 | [[nodiscard]] 27 | auto prepare_annotations() -> TestAnnotations 28 | { 29 | TestAnnotations annotations {}; 30 | annotations.point_id = m_next_annotation_id++; 31 | annotations.rect_id = m_next_annotation_id++; 32 | annotations.ellipse_id = m_next_annotation_id++; 33 | annotations.invalid_id = AnnotationID {100}; 34 | 35 | m_layer.add_annotation( 36 | make_annotation(annotations.point_id, AnnotationKind::kPoint)); 37 | m_layer.add_annotation( 38 | make_annotation(annotations.rect_id, AnnotationKind::kRect)); 39 | m_layer.add_annotation( 40 | make_annotation(annotations.ellipse_id, AnnotationKind::kEllipse)); 41 | 42 | return annotations; 43 | } 44 | }; 45 | 46 | TEST_F(AnnotationLayerTest, AddAnnotation) 47 | { 48 | const auto annotation_id = m_next_annotation_id++; 49 | ASSERT_EQ(m_layer.annotation_count(), 0uz); 50 | EXPECT_EQ(m_layer.find_annotation(annotation_id), nullptr); 51 | 52 | auto annotation = make_annotation(annotation_id, AnnotationKind::kPoint); 53 | m_layer.add_annotation(std::move(annotation)); 54 | 55 | EXPECT_EQ(m_layer.annotation_count(), 1uz); 56 | EXPECT_NE(m_layer.find_annotation(annotation_id), nullptr); 57 | 58 | EXPECT_THROW(m_layer.add_annotation(nullptr), std::invalid_argument); 59 | } 60 | 61 | TEST_F(AnnotationLayerTest, RemoveAnnotation) 62 | { 63 | const auto annotations = prepare_annotations(); 64 | ASSERT_EQ(m_layer.annotation_count(), 3uz); 65 | 66 | const auto removed_annotation = m_layer.remove_annotation(annotations.point_id); 67 | ASSERT_NE(removed_annotation, nullptr); 68 | EXPECT_EQ(removed_annotation->id, annotations.point_id); 69 | EXPECT_EQ(m_layer.annotation_count(), 2uz); 70 | 71 | EXPECT_EQ(m_layer.remove_annotation(annotations.invalid_id), nullptr); 72 | EXPECT_EQ(m_layer.annotation_count(), 2uz); 73 | } 74 | 75 | TEST_F(AnnotationLayerTest, FindAnnotation) 76 | { 77 | const auto annotations = prepare_annotations(); 78 | 79 | EXPECT_NE(m_layer.find_annotation(annotations.point_id), nullptr); 80 | EXPECT_NE(m_layer.find_annotation(annotations.rect_id), nullptr); 81 | EXPECT_NE(m_layer.find_annotation(annotations.ellipse_id), nullptr); 82 | EXPECT_EQ(m_layer.find_annotation(annotations.invalid_id), nullptr); 83 | 84 | EXPECT_NE(std::as_const(m_layer).find_annotation(annotations.point_id), nullptr); 85 | EXPECT_NE(std::as_const(m_layer).find_annotation(annotations.rect_id), nullptr); 86 | EXPECT_NE(std::as_const(m_layer).find_annotation(annotations.ellipse_id), nullptr); 87 | EXPECT_EQ(std::as_const(m_layer).find_annotation(annotations.invalid_id), nullptr); 88 | } 89 | 90 | TEST_F(AnnotationLayerTest, AnnotationAt) 91 | { 92 | const auto annotations = prepare_annotations(); 93 | 94 | EXPECT_EQ(m_layer.annotation_at(0uz).id, annotations.point_id); 95 | EXPECT_EQ(m_layer.annotation_at(1uz).id, annotations.rect_id); 96 | EXPECT_EQ(m_layer.annotation_at(2uz).id, annotations.ellipse_id); 97 | 98 | EXPECT_EQ(std::as_const(m_layer).annotation_at(0uz).id, annotations.point_id); 99 | EXPECT_EQ(std::as_const(m_layer).annotation_at(1uz).id, annotations.rect_id); 100 | EXPECT_EQ(std::as_const(m_layer).annotation_at(2uz).id, annotations.ellipse_id); 101 | 102 | EXPECT_THROW((void) m_layer.annotation_at(3uz), std::out_of_range); 103 | EXPECT_THROW((void) std::as_const(m_layer).annotation_at(3uz), std::out_of_range); 104 | } 105 | 106 | TEST_F(AnnotationLayerTest, AnnotationCount) 107 | { 108 | EXPECT_EQ(m_layer.annotation_count(), 0uz); 109 | 110 | (void) prepare_annotations(); 111 | 112 | EXPECT_EQ(m_layer.annotation_count(), 3uz); 113 | } 114 | 115 | TEST_F(AnnotationLayerTest, BeginEnd) 116 | { 117 | EXPECT_EQ(std::distance(m_layer.begin(), m_layer.end()), 0z); 118 | 119 | (void) prepare_annotations(); 120 | 121 | EXPECT_EQ(std::distance(m_layer.begin(), m_layer.end()), 3z); 122 | } 123 | 124 | } // namespace 125 | } // namespace tactile::tests 126 | -------------------------------------------------------------------------------- /tests/core/layer/layer.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | #include 5 | 6 | import tactile.core.layer; 7 | 8 | namespace tactile::tests { 9 | namespace { 10 | 11 | using testing::Const; 12 | 13 | using LayerTypes = testing::Types; 14 | 15 | template 16 | class LayerTest : public testing::Test 17 | {}; 18 | 19 | TYPED_TEST_SUITE(LayerTest, LayerTypes); 20 | 21 | TYPED_TEST(LayerTest, Info) 22 | { 23 | constexpr LayerID id {42}; 24 | TypeParam layer {id}; 25 | 26 | EXPECT_EQ(layer.info().id(), id); 27 | EXPECT_EQ(layer.info().opacity(), 1.0f); 28 | EXPECT_TRUE(layer.info().visible()); 29 | 30 | EXPECT_EQ(Const(layer).info().id(), id); 31 | EXPECT_EQ(Const(layer).info().opacity(), 1.0f); 32 | EXPECT_TRUE(Const(layer).info().visible()); 33 | } 34 | 35 | } // namespace 36 | } // namespace tactile::tests 37 | -------------------------------------------------------------------------------- /tests/core/layer/layer_info.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import tactile.core.layer; 6 | 7 | namespace tactile::tests { 8 | namespace { 9 | 10 | class LayerInfoTest : public testing::Test 11 | {}; 12 | 13 | TEST_F(LayerInfoTest, Defaults) 14 | { 15 | constexpr LayerID id {42}; 16 | const LayerInfo info {id}; 17 | 18 | EXPECT_EQ(info.id(), id); 19 | EXPECT_EQ(info.opacity(), 1.0f); 20 | EXPECT_TRUE(info.visible()); 21 | } 22 | 23 | TEST_F(LayerInfoTest, SetOpacity) 24 | { 25 | LayerInfo info {LayerID {42}}; 26 | 27 | info.set_opacity(0.5f); 28 | EXPECT_EQ(info.opacity(), 0.5f); 29 | 30 | info.set_opacity(1.1f); 31 | EXPECT_EQ(info.opacity(), 1.0f); 32 | 33 | info.set_opacity(-0.1f); 34 | EXPECT_EQ(info.opacity(), 0.0f); 35 | } 36 | 37 | TEST_F(LayerInfoTest, SetVisible) 38 | { 39 | LayerInfo info {LayerID {42}}; 40 | 41 | info.set_visible(false); 42 | EXPECT_FALSE(info.visible()); 43 | 44 | info.set_visible(true); 45 | EXPECT_TRUE(info.visible()); 46 | } 47 | 48 | } // namespace 49 | } // namespace tactile::tests 50 | -------------------------------------------------------------------------------- /tests/core/layer/tile_layer.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | #include 5 | 6 | import tactile.core.layer; 7 | 8 | namespace tactile::tests { 9 | namespace { 10 | 11 | class TileLayerTest : public testing::Test 12 | { 13 | protected: 14 | TileLayer m_layer {LayerID {1}}; 15 | }; 16 | 17 | } // namespace 18 | } // namespace tactile::tests 19 | -------------------------------------------------------------------------------- /tests/core/numeric/casts.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import std; 6 | import tactile.core.common; 7 | import tactile.core.numeric; 8 | 9 | namespace tactile::tests { 10 | namespace { 11 | 12 | class NumericCastsTest : public testing::Test 13 | {}; 14 | 15 | TEST_F(NumericCastsTest, CheckedCast_SignedToSigned) 16 | { 17 | EXPECT_EQ(checked_cast(i8 {42}), i8 {42}); 18 | 19 | EXPECT_EQ(checked_cast(i16 {kMinI8}), kMinI8); 20 | EXPECT_EQ(checked_cast(i16 {kMaxI8}), kMaxI8); 21 | 22 | EXPECT_EQ(checked_cast(i32 {kMinI16}), kMinI16); 23 | EXPECT_EQ(checked_cast(i32 {kMaxI16}), kMaxI16); 24 | 25 | EXPECT_THROW((void) checked_cast(i32 {kMinI16 - 1}), std::underflow_error); 26 | EXPECT_THROW((void) checked_cast(i32 {kMaxI16 + 1}), std::overflow_error); 27 | } 28 | 29 | TEST_F(NumericCastsTest, CheckedCast_UnsignedToUnsigned) 30 | { 31 | EXPECT_EQ(checked_cast(u8 {42}), u8 {42}); 32 | 33 | EXPECT_EQ(checked_cast(u16 {kMinU8}), kMinU8); 34 | EXPECT_EQ(checked_cast(u16 {kMaxU8}), kMaxU8); 35 | 36 | EXPECT_EQ(checked_cast(u32 {kMinU16}), kMinU16); 37 | EXPECT_EQ(checked_cast(u32 {kMaxU16}), kMaxU16); 38 | 39 | EXPECT_THROW((void) checked_cast(u32 {kMaxU16 + 1}), std::overflow_error); 40 | } 41 | 42 | TEST_F(NumericCastsTest, CheckedCast_SignedToUnsigned) 43 | { 44 | EXPECT_EQ(checked_cast(i8 {42}), u8 {42}); 45 | 46 | EXPECT_EQ(checked_cast(i16 {kMinU8}), kMinU8); 47 | EXPECT_EQ(checked_cast(i16 {kMaxU8}), kMaxU8); 48 | 49 | EXPECT_EQ(checked_cast(i32 {kMinU16}), kMinU16); 50 | EXPECT_EQ(checked_cast(i32 {kMaxU16}), kMaxU16); 51 | 52 | EXPECT_THROW((void) checked_cast(i16 {-1}), std::underflow_error); 53 | EXPECT_THROW((void) checked_cast(i16 {kMaxU8 + 1}), std::overflow_error); 54 | } 55 | 56 | TEST_F(NumericCastsTest, CheckedCast_UnsignedToSigned) 57 | { 58 | EXPECT_EQ(checked_cast(u8 {42}), i8 {42}); 59 | 60 | EXPECT_EQ(checked_cast(u16 {0}), i8 {0}); 61 | EXPECT_EQ(checked_cast(u16 {kMaxI8}), kMaxI8); 62 | 63 | EXPECT_EQ(checked_cast(u32 {0}), i16 {0}); 64 | EXPECT_EQ(checked_cast(u32 {kMaxI16}), kMaxI16); 65 | 66 | EXPECT_THROW((void) checked_cast(u16 {kMaxI8 + 1}), std::overflow_error); 67 | } 68 | 69 | TEST_F(NumericCastsTest, ToSigned) 70 | { 71 | static_assert(std::same_as); 72 | static_assert(std::same_as); 73 | static_assert(std::same_as); 74 | static_assert(std::same_as); 75 | static_assert(std::same_as); 76 | 77 | EXPECT_EQ(to_signed(0u), 0); 78 | EXPECT_EQ(to_signed(u8 {100u}), i8 {100}); 79 | EXPECT_EQ(to_signed(u16 {18u}), i16 {18}); 80 | EXPECT_EQ(to_signed(u32 {42u}), i32 {42}); 81 | EXPECT_EQ(to_signed(usize {10'000u}), isize {10'000}); 82 | 83 | EXPECT_THROW((void) to_signed(u8 {128u}), std::overflow_error); 84 | EXPECT_THROW((void) to_signed(u16 {32'768u}), std::overflow_error); 85 | } 86 | 87 | TEST_F(NumericCastsTest, ToUnsigned) 88 | { 89 | static_assert(std::same_as); 90 | static_assert(std::same_as); 91 | static_assert(std::same_as); 92 | static_assert(std::same_as); 93 | static_assert(std::same_as); 94 | 95 | EXPECT_EQ(to_unsigned(0), 0u); 96 | EXPECT_EQ(to_unsigned(i8 {100}), u8 {100}); 97 | EXPECT_EQ(to_unsigned(i16 {18}), u16 {18}); 98 | EXPECT_EQ(to_unsigned(i32 {42}), u32 {42}); 99 | EXPECT_EQ(to_unsigned(isize {10'000}), usize {10'000}); 100 | 101 | EXPECT_THROW((void) to_unsigned(i8 {-1}), std::underflow_error); 102 | } 103 | 104 | TEST_F(NumericCastsTest, SaturateCast_SignedToSigned) 105 | { 106 | EXPECT_EQ(saturate_cast(i8 {42}), i8 {42}); 107 | 108 | EXPECT_EQ(saturate_cast(i16 {kMinI8}), kMinI8); 109 | EXPECT_EQ(saturate_cast(i16 {kMaxI8}), kMaxI8); 110 | 111 | EXPECT_EQ(saturate_cast(i32 {kMinI16}), kMinI16); 112 | EXPECT_EQ(saturate_cast(i32 {kMaxI16}), kMaxI16); 113 | 114 | EXPECT_EQ(saturate_cast(i32 {kMinI16 - 1}), kMinI16); 115 | EXPECT_EQ(saturate_cast(i32 {kMaxI16 + 1}), kMaxI16); 116 | } 117 | 118 | TEST_F(NumericCastsTest, SaturateCast_UnsignedToUnsigned) 119 | { 120 | EXPECT_EQ(saturate_cast(u8 {42}), u8 {42}); 121 | 122 | EXPECT_EQ(saturate_cast(u16 {kMinU8}), kMinU8); 123 | EXPECT_EQ(saturate_cast(u16 {kMaxU8}), kMaxU8); 124 | 125 | EXPECT_EQ(saturate_cast(u32 {kMinU16}), kMinU16); 126 | EXPECT_EQ(saturate_cast(u32 {kMaxU16}), kMaxU16); 127 | 128 | EXPECT_EQ(saturate_cast(u32 {kMaxU16 + 1}), kMaxU16); 129 | } 130 | 131 | TEST_F(NumericCastsTest, SaturateCast_SignedToUnsigned) 132 | { 133 | EXPECT_EQ(saturate_cast(i8 {42}), u8 {42}); 134 | 135 | EXPECT_EQ(saturate_cast(i16 {kMinU8}), kMinU8); 136 | EXPECT_EQ(saturate_cast(i16 {kMaxU8}), kMaxU8); 137 | 138 | EXPECT_EQ(saturate_cast(i32 {kMinU16}), kMinU16); 139 | EXPECT_EQ(saturate_cast(i32 {kMaxU16}), kMaxU16); 140 | 141 | EXPECT_EQ(saturate_cast(i16 {-1}), kMinU8); 142 | EXPECT_EQ(saturate_cast(i16 {kMaxU8 + 1}), kMaxU8); 143 | } 144 | 145 | TEST_F(NumericCastsTest, SaturateCast_UnsignedToSigned) 146 | { 147 | EXPECT_EQ(saturate_cast(u8 {42}), i8 {42}); 148 | 149 | EXPECT_EQ(saturate_cast(u16 {0}), i8 {0}); 150 | EXPECT_EQ(saturate_cast(u16 {kMaxI8}), kMaxI8); 151 | 152 | EXPECT_EQ(saturate_cast(u32 {0}), i16 {0}); 153 | EXPECT_EQ(saturate_cast(u32 {kMaxI16}), kMaxI16); 154 | 155 | EXPECT_EQ(saturate_cast(u16 {kMaxI8 + 1}), kMaxI8); 156 | } 157 | 158 | } // namespace 159 | } // namespace tactile::tests 160 | -------------------------------------------------------------------------------- /tests/core/numeric/checked.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import std; 6 | import tactile.core.numeric; 7 | 8 | namespace tactile::tests { 9 | namespace { 10 | 11 | class CheckedNumericsTest : public testing::Test 12 | {}; 13 | 14 | TEST_F(CheckedNumericsTest, CheckedAdd_SignedInt) 15 | { 16 | // Valid 17 | EXPECT_EQ(checked_add(1'234, 4'321).value(), 5'555); 18 | EXPECT_EQ(checked_add(150, -300).value(), -150); 19 | EXPECT_EQ(checked_add(kMaxI32, i32 {0u}).value(), kMaxI32); 20 | EXPECT_EQ(checked_add(kMaxI64, kMinI64).value(), -1); 21 | 22 | // Overflow 23 | EXPECT_EQ(checked_add(kMaxI32, kMaxI32).error_or(Error::kUnknown), 24 | Error::kArithmeticOverflow); 25 | EXPECT_EQ(checked_add(kMaxI32, i32 {1}).error_or(Error::kUnknown), 26 | Error::kArithmeticOverflow); 27 | 28 | // Underflow 29 | EXPECT_EQ(checked_add(kMinI32, i32 {-1}).error_or(Error::kUnknown), 30 | Error::kArithmeticUnderflow); 31 | } 32 | 33 | TEST_F(CheckedNumericsTest, CheckedAdd_UnsignedInt) 34 | { 35 | // Valid 36 | EXPECT_EQ(checked_add(1'234u, 4'321u).value(), 5'555u); 37 | EXPECT_EQ(checked_add(kMaxU16, u16 {0u}).value(), kMaxU16); 38 | 39 | // Overflow 40 | EXPECT_EQ(checked_add(kMaxU8, kMaxU8).error_or(Error::kUnknown), 41 | Error::kArithmeticOverflow); 42 | EXPECT_EQ(checked_add(kMaxU64, u64 {1u}).error_or(Error::kUnknown), 43 | Error::kArithmeticOverflow); 44 | } 45 | 46 | TEST_F(CheckedNumericsTest, CheckedSub_SignedInt) 47 | { 48 | // Valid 49 | EXPECT_EQ(checked_sub(987, 123).value(), 864); 50 | EXPECT_EQ(checked_sub(100, -10).value(), 110); 51 | EXPECT_EQ(checked_sub(-50, 20).value(), -70); 52 | EXPECT_EQ(checked_sub(kMinI32, i32 {0}).value(), kMinI32); 53 | 54 | // Overflow 55 | EXPECT_EQ(checked_sub(kMaxI32, i32 {-1}).error_or(Error::kUnknown), 56 | Error::kArithmeticOverflow); 57 | 58 | // Underflow 59 | EXPECT_EQ(checked_sub(kMinI16, i16 {1}).error_or(Error::kUnknown), 60 | Error::kArithmeticUnderflow); 61 | } 62 | 63 | TEST_F(CheckedNumericsTest, CheckedSub_UnsignedInt) 64 | { 65 | // Valid 66 | EXPECT_EQ(checked_sub(150u, 100u).value(), 50u); 67 | EXPECT_EQ(checked_sub(kMaxU32, kMaxU32).value(), kMinU32); 68 | EXPECT_EQ(checked_sub(kMinU8, kMinU8).value(), kMinU8); 69 | 70 | // Underflow 71 | EXPECT_EQ(checked_sub(kMinU64, u64 {1u}).error_or(Error::kUnknown), 72 | Error::kArithmeticUnderflow); 73 | } 74 | 75 | TEST_F(CheckedNumericsTest, CheckedMul_SignedInt) 76 | { 77 | // Valid 78 | EXPECT_EQ(checked_mul(25, 4).value(), 100); 79 | EXPECT_EQ(checked_mul(100, -10).value(), -1'000); 80 | EXPECT_EQ(checked_mul(999, 0).value(), 0); 81 | EXPECT_EQ(checked_mul(kMaxI16, i16 {1}).value(), kMaxI16); 82 | 83 | // Underflow 84 | EXPECT_EQ(checked_mul(kMinI32, i32 {2}).error_or(Error::kUnknown), 85 | Error::kArithmeticUnderflow); 86 | 87 | // Overflow 88 | EXPECT_EQ(checked_mul(kMaxI8, i8 {2}).error_or(Error::kUnknown), 89 | Error::kArithmeticOverflow); 90 | } 91 | 92 | TEST_F(CheckedNumericsTest, CheckedMul_UnsignedInt) 93 | { 94 | // Valid 95 | EXPECT_EQ(checked_mul(25u, 4u).value(), 100u); 96 | EXPECT_EQ(checked_mul(999u, 0u).value(), 0u); 97 | EXPECT_EQ(checked_mul(kMaxU16, u16 {1u}).value(), kMaxU16); 98 | 99 | // Overflow 100 | EXPECT_EQ(checked_mul(kMaxU16, u16 {2u}).error_or(Error::kUnknown), 101 | Error::kArithmeticOverflow); 102 | } 103 | 104 | TEST_F(CheckedNumericsTest, CheckedDiv_SignedInt) 105 | { 106 | // Valid 107 | EXPECT_EQ(checked_div(100, 4).value(), 25); 108 | EXPECT_EQ(checked_div(400, -2).value(), -200); 109 | EXPECT_EQ(checked_div(-50, 5).value(), -10); 110 | EXPECT_EQ(checked_div(999, 1).value(), 999); 111 | EXPECT_EQ(checked_div(1, 2).value(), 0); 112 | 113 | // Division by zero 114 | EXPECT_EQ(checked_div(42, 0).error_or(Error::kUnknown), 115 | Error::kArithmeticInvalidValue); 116 | } 117 | 118 | TEST_F(CheckedNumericsTest, CheckedDiv_UnsignedInt) 119 | { 120 | // Valid 121 | EXPECT_EQ(checked_div(100u, 4u).value(), 25u); 122 | EXPECT_EQ(checked_div(999u, 1u).value(), 999u); 123 | EXPECT_EQ(checked_div(1u, 2u).value(), 0u); 124 | 125 | // Division by zero 126 | EXPECT_EQ(checked_div(42u, 0u).error_or(Error::kUnknown), 127 | Error::kArithmeticInvalidValue); 128 | } 129 | 130 | } // namespace 131 | } // namespace tactile::tests 132 | -------------------------------------------------------------------------------- /tests/core/numeric/vec.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | #include 5 | 6 | import std; 7 | import tactile.core.numeric; 8 | 9 | namespace tactile::tests { 10 | namespace { 11 | 12 | using testing::Const; 13 | 14 | static_assert(Int2::size() == 2); 15 | static_assert(Int3::size() == 3); 16 | static_assert(Int4::size() == 4); 17 | 18 | static_assert(Float2::size() == 2); 19 | static_assert(Float3::size() == 3); 20 | static_assert(Float4::size() == 4); 21 | 22 | class VecTest : public testing::Test 23 | {}; 24 | 25 | TEST_F(VecTest, Defaults) 26 | { 27 | constexpr Int4 vec {}; 28 | EXPECT_EQ(vec.x(), 0); 29 | EXPECT_EQ(vec.y(), 0); 30 | EXPECT_EQ(vec.z(), 0); 31 | EXPECT_EQ(vec.w(), 0); 32 | EXPECT_NE(vec.data(), nullptr); 33 | EXPECT_NE(Const(vec).data(), nullptr); 34 | } 35 | 36 | TEST_F(VecTest, Setters) 37 | { 38 | Int4 vec {}; 39 | 40 | vec.set_x(10); 41 | vec.set_y(20); 42 | vec.set_z(30); 43 | vec.set_w(40); 44 | 45 | EXPECT_EQ(vec.x(), 10); 46 | EXPECT_EQ(vec.y(), 20); 47 | EXPECT_EQ(vec.z(), 30); 48 | EXPECT_EQ(vec.w(), 40); 49 | 50 | EXPECT_EQ(vec[0uz], 10); 51 | EXPECT_EQ(vec[1uz], 20); 52 | EXPECT_EQ(vec[2uz], 30); 53 | EXPECT_EQ(vec[3uz], 40); 54 | } 55 | 56 | TEST_F(VecTest, At) 57 | { 58 | constexpr Float4 vec {1.0f, 2.0f, 3.0f, 4.0f}; 59 | 60 | EXPECT_EQ(vec.at(0uz), 1.0f); 61 | EXPECT_EQ(vec.at(1uz), 2.0f); 62 | EXPECT_EQ(vec.at(2uz), 3.0f); 63 | EXPECT_EQ(vec.at(3uz), 4.0f); 64 | 65 | EXPECT_THROW((void) vec.at(4uz), std::out_of_range); 66 | } 67 | 68 | TEST_F(VecTest, Data) 69 | { 70 | Int3 vec {1, 2, 3}; 71 | 72 | EXPECT_EQ(vec.data()[0uz], 1); 73 | EXPECT_EQ(vec.data()[1uz], 2); 74 | EXPECT_EQ(vec.data()[2uz], 3); 75 | 76 | EXPECT_EQ(Const(vec).data()[0uz], 1); 77 | EXPECT_EQ(Const(vec).data()[1uz], 2); 78 | EXPECT_EQ(Const(vec).data()[2uz], 3); 79 | } 80 | 81 | TEST_F(VecTest, Add) 82 | { 83 | constexpr auto vec = Int3 {1, 2, 3} + Int3 {4, 5, 6}; 84 | EXPECT_EQ(vec.x(), 5); 85 | EXPECT_EQ(vec.y(), 7); 86 | EXPECT_EQ(vec.z(), 9); 87 | } 88 | 89 | TEST_F(VecTest, AddAssign) 90 | { 91 | Int3 vec {1, 2, 3}; 92 | 93 | vec += Int3 {10, 20, 30}; 94 | 95 | EXPECT_EQ(vec.x(), 11); 96 | EXPECT_EQ(vec.y(), 22); 97 | EXPECT_EQ(vec.z(), 33); 98 | } 99 | 100 | TEST_F(VecTest, Sub) 101 | { 102 | constexpr auto vec = Int2 {9, 8} - Int2 {1, 2}; 103 | EXPECT_EQ(vec.x(), 8); 104 | EXPECT_EQ(vec.y(), 6); 105 | } 106 | 107 | TEST_F(VecTest, SubAssign) 108 | { 109 | Int3 vec {10, 11, 12}; 110 | 111 | vec -= Int3 {0, -5, 10}; 112 | 113 | EXPECT_EQ(vec.x(), 10); 114 | EXPECT_EQ(vec.y(), 16); 115 | EXPECT_EQ(vec.z(), 2); 116 | } 117 | 118 | TEST_F(VecTest, Mul) 119 | { 120 | constexpr auto vec = Int4 {1, -2, 3, 4} * Int4 {10, 20, 30, -40}; 121 | EXPECT_EQ(vec.x(), 10); 122 | EXPECT_EQ(vec.y(), -40); 123 | EXPECT_EQ(vec.z(), 90); 124 | EXPECT_EQ(vec.w(), -160); 125 | } 126 | 127 | TEST_F(VecTest, MulAssign) 128 | { 129 | Int4 vec {1, 2, 3, 4}; 130 | 131 | vec *= Int4 {4, 0, 1, 2}; 132 | 133 | EXPECT_EQ(vec.x(), 4); 134 | EXPECT_EQ(vec.y(), 0); 135 | EXPECT_EQ(vec.z(), 3); 136 | EXPECT_EQ(vec.w(), 8); 137 | } 138 | 139 | TEST_F(VecTest, MulWithScalar) 140 | { 141 | constexpr auto vec = Int4 {1, 2, 3, 4} * 2; 142 | EXPECT_EQ(vec.x(), 2); 143 | EXPECT_EQ(vec.y(), 4); 144 | EXPECT_EQ(vec.z(), 6); 145 | EXPECT_EQ(vec.w(), 8); 146 | } 147 | 148 | TEST_F(VecTest, MulAssignWithScalar) 149 | { 150 | Int4 vec {1, 2, 3, 4}; 151 | 152 | vec *= -2; 153 | 154 | EXPECT_EQ(vec.x(), -2); 155 | EXPECT_EQ(vec.y(), -4); 156 | EXPECT_EQ(vec.z(), -6); 157 | EXPECT_EQ(vec.w(), -8); 158 | } 159 | 160 | TEST_F(VecTest, Eq) 161 | { 162 | constexpr Int3 a {1, 2, 3}; 163 | constexpr Int3 b {1, 2, 4}; 164 | 165 | EXPECT_EQ(a, a); 166 | EXPECT_NE(a, b); 167 | } 168 | 169 | TEST_F(VecTest, Ord) 170 | { 171 | constexpr Int3 a {1, 2, 3}; 172 | constexpr Int3 b {1, 2, 4}; 173 | 174 | EXPECT_FALSE(a < a); 175 | 176 | EXPECT_LT(a, b); 177 | EXPECT_GT(b, a); 178 | } 179 | 180 | } // namespace 181 | } // namespace tactile::tests 182 | -------------------------------------------------------------------------------- /tests/core/tactile_core_tests.main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import std; 6 | import tactile.core; 7 | 8 | namespace tactile::tests { 9 | namespace { 10 | 11 | [[nodiscard]] 12 | auto run(int argc, char* argv[]) -> int 13 | { 14 | auto& logger = get_logger(); 15 | logger.set_start_time(Logger::clock_type::now()); 16 | logger.set_min_level(LogLevel::kTrace); 17 | logger.set_flush_level(LogLevel::kError); 18 | logger.add_sink(make_unique()); 19 | 20 | const Defer reset_logger {[] { get_logger().reset(); }}; 21 | 22 | testing::InitGoogleTest(&argc, argv); 23 | return RUN_ALL_TESTS(); 24 | } 25 | 26 | } // namespace 27 | } // namespace tactile::tests 28 | 29 | auto main(const int argc, char* argv[]) -> int 30 | { 31 | return tactile::tests::run(argc, argv); 32 | } 33 | -------------------------------------------------------------------------------- /tests/core/util/defer.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import tactile.core.util; 6 | 7 | namespace tactile::tests { 8 | namespace { 9 | 10 | class DeferTest : public testing::Test 11 | {}; 12 | 13 | TEST_F(DeferTest, Defer) 14 | { 15 | int value = 0; 16 | 17 | { 18 | const Defer defer_one {[&value] { value = 1; }}; 19 | 20 | { 21 | const Defer defer_two {[&value] { value = 2; }}; 22 | value = 3; 23 | } 24 | 25 | EXPECT_EQ(value, 2); 26 | } 27 | 28 | EXPECT_EQ(value, 1); 29 | } 30 | 31 | } // namespace 32 | } // namespace tactile::tests 33 | -------------------------------------------------------------------------------- /tests/core/util/validation.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import std; 6 | import tactile.core.util; 7 | 8 | namespace tactile::tests { 9 | namespace { 10 | 11 | class ValidationTest : public testing::Test 12 | {}; 13 | 14 | TEST_F(ValidationTest, RequireNotNull) 15 | { 16 | const auto foo = 42; 17 | EXPECT_EQ(require_not_null(&foo), &foo); 18 | 19 | EXPECT_THROW((void) require_not_null(nullptr), std::invalid_argument); 20 | } 21 | 22 | } // namespace 23 | } // namespace tactile::tests 24 | -------------------------------------------------------------------------------- /tests/editor/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_editor_module_tests CXX) 2 | 3 | add_executable(tactile_editor_tests) 4 | 5 | target_sources(tactile_editor_tests 6 | PRIVATE 7 | "command/command_stack.test.cpp" 8 | "tactile_editor_tests.main.cpp" 9 | ) 10 | 11 | tactile_set_target_properties(tactile_editor_tests) 12 | 13 | target_link_libraries(tactile_editor_tests 14 | PRIVATE 15 | tactile_interface_target 16 | tactile_editor 17 | GTest::gtest 18 | ) 19 | -------------------------------------------------------------------------------- /tests/editor/tactile_editor_tests.main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import std; 6 | import tactile.core; 7 | 8 | namespace tactile::editor::tests { 9 | namespace { 10 | 11 | [[nodiscard]] 12 | auto run(int argc, char* argv[]) -> int 13 | { 14 | auto& logger = get_logger(); 15 | logger.set_start_time(Logger::clock_type::now()); 16 | logger.set_min_level(LogLevel::kTrace); 17 | logger.set_flush_level(LogLevel::kError); 18 | logger.add_sink(make_unique()); 19 | 20 | const Defer reset_logger {[] { get_logger().reset(); }}; 21 | 22 | testing::InitGoogleTest(&argc, argv); 23 | return RUN_ALL_TESTS(); 24 | } 25 | 26 | } // namespace 27 | } // namespace tactile::editor::tests 28 | 29 | auto main(const int argc, char* argv[]) -> int 30 | { 31 | return tactile::editor::tests::run(argc, argv); 32 | } 33 | -------------------------------------------------------------------------------- /tests/zlib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(tactile_zlib_tests_project CXX) 2 | 3 | add_executable(tactile_zlib_tests) 4 | 5 | target_sources(tactile_zlib_tests 6 | PRIVATE 7 | "tactile_zlib_tests.main.cpp" 8 | "zlib_compression_format.test.cpp" 9 | ) 10 | 11 | tactile_set_target_properties(tactile_zlib_tests) 12 | 13 | target_link_libraries(tactile_zlib_tests 14 | PRIVATE 15 | tactile_interface_target 16 | tactile_core 17 | tactile_zlib 18 | GTest::gtest 19 | ) 20 | -------------------------------------------------------------------------------- /tests/zlib/tactile_zlib_tests.main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | 5 | import std; 6 | import tactile.core; 7 | 8 | namespace tactile::tests { 9 | namespace { 10 | 11 | [[nodiscard]] 12 | auto run(int argc, char* argv[]) -> int 13 | { 14 | auto& logger = get_logger(); 15 | logger.set_start_time(Logger::clock_type::now()); 16 | logger.set_min_level(LogLevel::kDebug); 17 | logger.set_flush_level(LogLevel::kError); 18 | logger.add_sink(make_unique()); 19 | 20 | const Defer reset_logger {[] { get_logger().reset(); }}; 21 | 22 | testing::InitGoogleTest(&argc, argv); 23 | return RUN_ALL_TESTS(); 24 | } 25 | 26 | } // namespace 27 | } // namespace tactile::tests 28 | 29 | auto main(const int argc, char* argv[]) -> int 30 | { 31 | return tactile::tests::run(argc, argv); 32 | } 33 | -------------------------------------------------------------------------------- /tests/zlib/zlib_compression_format.test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Albin Johansson 2 | 3 | #include 4 | #include 5 | 6 | import std; 7 | import tactile.core; 8 | import tactile.zlib; 9 | 10 | namespace tactile::tests { 11 | namespace { 12 | 13 | using testing::ContainerEq; 14 | 15 | class ZlibCompressionFormatTest : public testing::Test 16 | {}; 17 | 18 | TEST_F(ZlibCompressionFormatTest, CompressAndDecompressBytes) 19 | { 20 | const ZlibCompressionFormat zlib {}; 21 | 22 | Vector bytes {}; 23 | bytes.resize(64'000uz); 24 | std::iota(bytes.begin(), bytes.end(), u8 {0}); 25 | 26 | const auto compressed_bytes = zlib.compress(bytes); 27 | ASSERT_TRUE(compressed_bytes.has_value()); 28 | 29 | const auto decompressed_bytes = zlib.decompress(*compressed_bytes); 30 | ASSERT_TRUE(decompressed_bytes.has_value()); 31 | EXPECT_THAT(*decompressed_bytes, ContainerEq(bytes)); 32 | } 33 | 34 | } // namespace 35 | } // namespace tactile::tests 36 | -------------------------------------------------------------------------------- /tools/cmake/tactile.cmake: -------------------------------------------------------------------------------- 1 | 2 | function(tactile_set_target_properties target) 3 | set_target_properties(${target} 4 | PROPERTIES 5 | PREFIX "" 6 | POSITION_INDEPENDENT_CODE "ON" 7 | INTERPROCEDURAL_OPTIMIZATION "${TACTILE_ENABLE_LTO}" 8 | RUNTIME_OUTPUT_DIRECTORY "${TACTILE_BINARY_DIR}" 9 | ARCHIVE_OUTPUT_DIRECTORY "${TACTILE_BINARY_DIR}" 10 | LIBRARY_OUTPUT_DIRECTORY "${TACTILE_BINARY_DIR}" 11 | ) 12 | endfunction() 13 | 14 | if (MSVC) 15 | list(APPEND 16 | TACTILE_COMPILE_OPTIONS 17 | "/EHsc" 18 | "/MP" 19 | "/W4" 20 | "/bigobj" 21 | "/permissive-" 22 | "/Zc:preprocessor" 23 | "/Zc:__cplusplus" 24 | ) 25 | else () 26 | list(APPEND 27 | TACTILE_COMPILE_OPTIONS 28 | "-fvisibility=hidden" 29 | "-Wall" 30 | "-Wextra" 31 | "-Wpedantic" 32 | "-Wconversion" 33 | "-Wsign-conversion" 34 | "-Wswitch-enum" 35 | "-Wold-style-cast" 36 | ) 37 | 38 | if (TACTILE_BUILD_TYPE STREQUAL "asan") 39 | list(APPEND 40 | TACTILE_COMPILE_OPTIONS 41 | "-fsanitize=address" 42 | "-fno-sanitize-recover" 43 | "-fno-omit-frame-pointer" 44 | ) 45 | list(APPEND 46 | TACTILE_LINK_OPTIONS 47 | "-fsanitize=address" 48 | "-fno-sanitize-recover" 49 | "-fno-omit-frame-pointer" 50 | ) 51 | endif () 52 | endif () 53 | 54 | message(DEBUG "TACTILE_COMPILE_OPTIONS: ${TACTILE_COMPILE_OPTIONS}") 55 | message(DEBUG "TACTILE_LINK_OPTIONS: ${TACTILE_LINK_OPTIONS}") 56 | 57 | add_library(tactile_interface_target INTERFACE) 58 | 59 | target_compile_features(tactile_interface_target INTERFACE cxx_std_23) 60 | 61 | target_compile_options(tactile_interface_target INTERFACE "${TACTILE_COMPILE_OPTIONS}") 62 | 63 | target_link_options(tactile_interface_target INTERFACE "${TACTILE_LINK_OPTIONS}") 64 | 65 | if (TACTILE_BUILD_TYPE STREQUAL "asan" AND NOT MSVC) 66 | target_link_libraries(tactile_interface_target INTERFACE "-fsanitize=address") 67 | endif () 68 | 69 | if (WIN32) 70 | target_compile_definitions(tactile_interface_target 71 | INTERFACE 72 | "WIN32_LEAN_AND_MEAN" 73 | "NOMINMAX" 74 | ) 75 | endif () 76 | 77 | if (TACTILE_ENABLE_CLION_IMPORT_STD_WORKAROUND) 78 | message(DEBUG "Applying workaround for CLion 'import std;' issue") 79 | 80 | # See https://youtrack.jetbrains.com/issue/CPP-39632/import-std-CLion-cant-resolve-module-std-in-case-of-clang 81 | add_library(tactile_import_std_clion_workaround STATIC) 82 | 83 | target_compile_features(tactile_import_std_clion_workaround PUBLIC cxx_std_23) 84 | 85 | target_sources(tactile_import_std_clion_workaround 86 | PRIVATE FILE_SET "CXX_MODULES" BASE_DIRS "/opt/homebrew/opt/llvm/share/libc++/v1" FILES 87 | "/opt/homebrew/opt/llvm/share/libc++/v1/std.cppm" 88 | "/opt/homebrew/opt/llvm/share/libc++/v1/std.compat.cppm" 89 | ) 90 | endif () 91 | -------------------------------------------------------------------------------- /tools/scripts/homebrew-llvm-fix-libc++-modules-path.diff: -------------------------------------------------------------------------------- 1 | 13c13 2 | < -print-file-name=libc++.modules.json 3 | --- 4 | > -print-file-name=c++/libc++.modules.json 5 | -------------------------------------------------------------------------------- /tools/scripts/install_assets.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Albin Johansson 2 | 3 | import shutil 4 | import sys 5 | import urllib.request 6 | import os.path 7 | 8 | FONT_OUTPUT_DIR = "../../data/fonts" 9 | IMAGE_OUTPUT_DIR = "../../data/images" 10 | CACHE_DIR = "./cache" 11 | 12 | FONTAWESOME_VERSION = "6.5.1" 13 | FONTAWESOME_DIR_NAME = f"fontawesome-free-{FONTAWESOME_VERSION}-desktop" 14 | FONTAWESOME_URL = f"https://use.fontawesome.com/releases/v{FONTAWESOME_VERSION}/{FONTAWESOME_DIR_NAME}.zip" 15 | 16 | FONTAWESOME_CACHE_ARCHIVE = f"{CACHE_DIR}/{FONTAWESOME_DIR_NAME}.zip" 17 | FONTAWESOME_CACHE_DIR = f"{CACHE_DIR}/{FONTAWESOME_DIR_NAME}" 18 | FONTAWESOME_OUTPUT_DIR = f"{FONT_OUTPUT_DIR}/fa" 19 | 20 | FONTAWESOME_CACHE_FONT_FILE = f"{FONTAWESOME_CACHE_DIR}/otfs/Font Awesome 6 Free-Solid-900.otf" 21 | FONTAWESOME_OUTPUT_FONT_FILE = f"{FONTAWESOME_OUTPUT_DIR}/fa-solid-900.ttf" 22 | 23 | FONTAWESOME_CACHE_LICENSE_FILE = f"{FONTAWESOME_CACHE_DIR}/LICENSE.txt" 24 | FONTAWESOME_OUTPUT_LICENSE_FILE = f"{FONTAWESOME_OUTPUT_DIR}/LICENSE.txt" 25 | 26 | ROBOTO_FLAVOR = "roboto-unhinted" 27 | ROBOTO_URL = f"https://github.com/googlefonts/roboto/releases/download/v2.138/{ROBOTO_FLAVOR}.zip" 28 | 29 | ROBOTO_CACHE_ARCHIVE = f"{CACHE_DIR}/{ROBOTO_FLAVOR}.zip" 30 | ROBOTO_CACHE_DIR = f"{CACHE_DIR}/{ROBOTO_FLAVOR}" 31 | ROBOTO_OUTPUT_DIR = f"{FONT_OUTPUT_DIR}/roboto" 32 | 33 | ROBOTO_CACHE_FONT_FILE = f"{ROBOTO_CACHE_DIR}/Roboto-Regular.ttf" 34 | ROBOTO_OUTPUT_FONT_FILE = f"{ROBOTO_OUTPUT_DIR}/Roboto-Regular.ttf" 35 | 36 | ROBOTO_CACHE_LICENSE_FILE = f"{ROBOTO_CACHE_DIR}/LICENSE" 37 | ROBOTO_OUTPUT_LICENSE_FILE = f"{ROBOTO_OUTPUT_DIR}/LICENSE.txt" 38 | 39 | DUMMY_IMAGE_URL = "https://dummyimage.com/96x64.png" 40 | DUMMY_IMAGE_CACHE_FILE = f"{CACHE_DIR}/dummy.png" 41 | DUMMY_IMAGE_OUTPUT_FILE = f"{IMAGE_OUTPUT_DIR}/dummy.png" 42 | 43 | 44 | def download_file(url, outfile): 45 | print(f"Downloading '{url}' to '{outfile}'...") 46 | 47 | opener = urllib.request.build_opener() 48 | opener.addheaders = [("User-agent", "Mozilla/5.0")] 49 | 50 | urllib.request.install_opener(opener) 51 | urllib.request.urlretrieve(url, outfile) 52 | 53 | 54 | def install_font_awesome_font(): 55 | print("Installing FontAwesome font...") 56 | 57 | if os.path.isfile(FONTAWESOME_CACHE_ARCHIVE): 58 | print(f" '{FONTAWESOME_CACHE_ARCHIVE}' already exists") 59 | else: 60 | download_file(FONTAWESOME_URL, FONTAWESOME_CACHE_ARCHIVE) 61 | 62 | if not os.path.isdir(FONTAWESOME_CACHE_DIR): 63 | print(" Unpacking FontAwesome archive...") 64 | shutil.unpack_archive(FONTAWESOME_CACHE_ARCHIVE, CACHE_DIR) 65 | 66 | shutil.copy(FONTAWESOME_CACHE_FONT_FILE, FONTAWESOME_OUTPUT_FONT_FILE) 67 | shutil.copy(FONTAWESOME_CACHE_LICENSE_FILE, FONTAWESOME_OUTPUT_LICENSE_FILE) 68 | 69 | 70 | def install_roboto_font(): 71 | print("Installing Roboto font...") 72 | 73 | if os.path.isfile(ROBOTO_CACHE_ARCHIVE): 74 | print(f" '{ROBOTO_CACHE_ARCHIVE}' already exists") 75 | else: 76 | download_file(ROBOTO_URL, ROBOTO_CACHE_ARCHIVE) 77 | 78 | if not os.path.isdir(ROBOTO_CACHE_ARCHIVE): 79 | print(" Unpacking Roboto archive...") 80 | shutil.unpack_archive(ROBOTO_CACHE_ARCHIVE, ROBOTO_CACHE_DIR) 81 | 82 | shutil.copy(ROBOTO_CACHE_FONT_FILE, ROBOTO_OUTPUT_FONT_FILE) 83 | shutil.copy(ROBOTO_CACHE_LICENSE_FILE, ROBOTO_OUTPUT_LICENSE_FILE) 84 | 85 | 86 | def install_dummy_image(): 87 | print("Installing dummy image...") 88 | 89 | if os.path.isfile(DUMMY_IMAGE_CACHE_FILE): 90 | print(f" '{DUMMY_IMAGE_CACHE_FILE}' already exists") 91 | else: 92 | download_file(DUMMY_IMAGE_URL, DUMMY_IMAGE_CACHE_FILE) 93 | 94 | shutil.copy(DUMMY_IMAGE_CACHE_FILE, DUMMY_IMAGE_OUTPUT_FILE) 95 | 96 | 97 | def main(): 98 | if not os.path.isdir(CACHE_DIR): 99 | os.mkdir(CACHE_DIR) 100 | 101 | if not os.path.isdir(FONTAWESOME_OUTPUT_DIR): 102 | os.makedirs(FONTAWESOME_OUTPUT_DIR, exist_ok=True) 103 | 104 | if not os.path.isdir(ROBOTO_OUTPUT_DIR): 105 | os.makedirs(ROBOTO_OUTPUT_DIR, exist_ok=True) 106 | 107 | if not os.path.isdir(IMAGE_OUTPUT_DIR): 108 | os.makedirs(IMAGE_OUTPUT_DIR, exist_ok=True) 109 | 110 | if "--skip-fonts" not in sys.argv: 111 | install_font_awesome_font() 112 | install_roboto_font() 113 | 114 | install_dummy_image() 115 | 116 | 117 | if __name__ == "__main__": 118 | main() 119 | -------------------------------------------------------------------------------- /tools/vcpkg/toolchains/arm64-osx-homebrew-llvm.cmake: -------------------------------------------------------------------------------- 1 | include_guard(GLOBAL) 2 | 3 | set(CMAKE_OSX_SYSROOT "macosx") 4 | set(CMAKE_C_COMPILER "/opt/homebrew/opt/llvm/bin/clang") 5 | set(CMAKE_CXX_COMPILER "/opt/homebrew/opt/llvm/bin/clang++") 6 | 7 | cmake_path(SET VCPKG_ROOT NORMALIZE "$ENV{VCPKG_ROOT}") 8 | include("${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake") 9 | -------------------------------------------------------------------------------- /tools/vcpkg/triplets/arm64-osx-tactile.cmake: -------------------------------------------------------------------------------- 1 | set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/../toolchains/arm64-osx-homebrew-llvm.cmake") 2 | 3 | set(VCPKG_TARGET_ARCHITECTURE "arm64") 4 | set(VCPKG_CRT_LINKAGE "dynamic") 5 | set(VCPKG_LIBRARY_LINKAGE "static") 6 | 7 | set(VCPKG_CMAKE_SYSTEM_NAME "Darwin") 8 | set(VCPKG_OSX_ARCHITECTURES "arm64") 9 | 10 | if (PORT MATCHES "sdl3") 11 | set(VCPKG_LIBRARY_LINKAGE "dynamic") 12 | endif () 13 | 14 | message("${PORT} is using ${VCPKG_LIBRARY_LINKAGE} linkage") 15 | 16 | set(nlohmann-json_IMPLICIT_CONVERSIONS OFF) 17 | -------------------------------------------------------------------------------- /tools/vcpkg/triplets/x64-linux-tactile.cmake: -------------------------------------------------------------------------------- 1 | set(VCPKG_TARGET_ARCHITECTURE "x64") 2 | set(VCPKG_CRT_LINKAGE "dynamic") 3 | set(VCPKG_LIBRARY_LINKAGE "static") 4 | 5 | set(VCPKG_CMAKE_SYSTEM_NAME "Linux") 6 | 7 | if (PORT MATCHES "sdl3") 8 | set(VCPKG_LIBRARY_LINKAGE "dynamic") 9 | endif () 10 | 11 | message("${PORT} is using ${VCPKG_LIBRARY_LINKAGE} linkage") 12 | 13 | set(nlohmann-json_IMPLICIT_CONVERSIONS OFF) 14 | -------------------------------------------------------------------------------- /tools/vcpkg/triplets/x64-windows-tactile.cmake: -------------------------------------------------------------------------------- 1 | set(VCPKG_TARGET_ARCHITECTURE "x64") 2 | set(VCPKG_CRT_LINKAGE "dynamic") 3 | set(VCPKG_LIBRARY_LINKAGE "static") 4 | 5 | if (PORT MATCHES "sdl3") 6 | set(VCPKG_LIBRARY_LINKAGE "dynamic") 7 | endif () 8 | 9 | message("${PORT} is using ${VCPKG_LIBRARY_LINKAGE} linkage") 10 | 11 | set(nlohmann-json_IMPLICIT_CONVERSIONS OFF) 12 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", 3 | "name": "tactile", 4 | "license": "MIT", 5 | "version-semver": "0.5.0", 6 | "maintainers": [ 7 | "albin-johansson" 8 | ], 9 | "builtin-baseline": "760bfd0c8d7c89ec640aec4df89418b7c2745605", 10 | "dependencies": [ 11 | "boost-container", 12 | "boost-safe-numerics", 13 | "boost-stacktrace", 14 | "entt", 15 | "cppcodec", 16 | "fast-float", 17 | "fmt", 18 | "magic-enum", 19 | "stb" 20 | ], 21 | "features": { 22 | "tests": { 23 | "description": "Enable tests", 24 | "dependencies": [ 25 | "gtest" 26 | ] 27 | }, 28 | "editor": { 29 | "description": "Enable the editor application", 30 | "dependencies": [ 31 | "argparse", 32 | { 33 | "name": "imgui", 34 | "features": [ 35 | "docking-experimental", 36 | "sdl3-binding" 37 | ] 38 | }, 39 | "sdl3", 40 | "tinyfiledialogs" 41 | ] 42 | }, 43 | "opengl": { 44 | "description": "Enable OpenGL renderer", 45 | "dependencies": [ 46 | { 47 | "name": "glad", 48 | "features": [ 49 | "extensions" 50 | ] 51 | }, 52 | { 53 | "name": "imgui", 54 | "features": [ 55 | "opengl3-binding" 56 | ] 57 | } 58 | ] 59 | }, 60 | "vulkan": { 61 | "description": "Enable Vulkan renderer", 62 | "dependencies": [ 63 | "vulkan", 64 | "vulkan-memory-allocator", 65 | { 66 | "name": "sdl3", 67 | "features": [ 68 | "vulkan" 69 | ] 70 | }, 71 | { 72 | "name": "imgui", 73 | "features": [ 74 | "vulkan-binding" 75 | ] 76 | } 77 | ] 78 | }, 79 | "zlib": { 80 | "description": "Enable Zlib compression support", 81 | "dependencies": [ 82 | "zlib" 83 | ] 84 | }, 85 | "zstd": { 86 | "description": "Enable Zstd compression support", 87 | "dependencies": [ 88 | "zstd" 89 | ] 90 | }, 91 | "yaml": { 92 | "description": "Enable Tactile YAML format support", 93 | "dependencies": [ 94 | "yaml-cpp" 95 | ] 96 | }, 97 | "tmj": { 98 | "description": "Enable Tiled TMJ format support", 99 | "dependencies": [ 100 | "nlohmann-json" 101 | ] 102 | }, 103 | "tmx": { 104 | "description": "Enable Tiled TMX format support", 105 | "dependencies": [ 106 | "pugixml" 107 | ] 108 | } 109 | } 110 | } 111 | --------------------------------------------------------------------------------