aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md94
-rw-r--r--Makefile8
-rw-r--r--Map.xcodeproj/project.pbxproj388
-rw-r--r--Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json20
-rw-r--r--Map/Business/Constants.swift (renamed from Map/Logic/Constants.swift)0
-rw-r--r--Map/Business/MapParser/MapParser.swift (renamed from Map/Logic/MapParser/MapParser.swift)0
-rw-r--r--Map/Business/MapParser/Strategies/BlockerParserStrategy.swift (renamed from Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift)0
-rw-r--r--Map/Business/MapParser/Strategies/EdgeParserStrategy.swift (renamed from Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift)0
-rw-r--r--Map/Business/MapParser/Strategies/GroupParserStrategy.swift (renamed from Map/Logic/MapParser/Strategies/GroupParserStrategy.swift)0
-rw-r--r--Map/Business/MapParser/Strategies/NoteParserStrategy.swift (renamed from Map/Logic/MapParser/Strategies/NoteParserStrategy.swift)0
-rw-r--r--Map/Business/MapParser/Strategies/OpportunityParserStrategy.swift (renamed from Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift)0
-rw-r--r--Map/Business/MapParser/Strategies/StageParserStrategy.swift (renamed from Map/Logic/MapParser/Strategies/StageParserStrategy.swift)0
-rw-r--r--Map/Business/MapParser/Strategies/VertexParserStrategy.swift (renamed from Map/Logic/MapParser/Strategies/VertexParserStrategy.swift)0
-rw-r--r--Map/Business/Stage.swift (renamed from Map/Data/Stage.swift)0
-rw-r--r--Map/Data/Debouncer.swift (renamed from Map/Logic/Debouncer.swift)0
-rw-r--r--Map/Data/MapDocument.swift18
-rw-r--r--Map/Info.plist6
-rw-r--r--Map/Presentation/Base Components/EvolutionPicker.swift11
-rw-r--r--Map/Presentation/Base Components/MapRender/MapAxes.swift16
-rw-r--r--Map/Presentation/Base Components/MapRender/MapNotes.swift6
-rw-r--r--Map/Presentation/Base Components/MapRender/MapVertices.swift6
-rw-r--r--Map/Presentation/Base Components/MapTextEditor.swift2
-rw-r--r--Map/Presentation/Base Components/SearchBar.swift6
-rw-r--r--Map/Presentation/Complex Components/MapRender/MapRenderView.swift4
-rw-r--r--Map/Presentation/EvolutionPicker.swift41
-rw-r--r--Map/Presentation/MapEditor.swift10
-rw-r--r--Map/Presentation/Theme/Color+theme.swift17
-rw-r--r--Map/Presentation/Theme/Dimensions.swift46
-rw-r--r--Map/Presentation/Theme/Font+theme.swift41
-rw-r--r--Map/Presentation/Theme/LibertinusSerif-Regular.otfbin0 -> 337132 bytes
-rw-r--r--Map/Presentation/Theme/Ronzino-Medium.otfbin0 -> 61604 bytes
-rw-r--r--Map/Presentation/Theme/Ronzino-Regular.otfbin0 -> 59968 bytes
-rw-r--r--MapTests/DebouncerTests.swift71
-rw-r--r--MapTests/MapDocumentTests.swift81
-rw-r--r--MapTests/MapParserStrategyTests.swift163
-rw-r--r--MapTests/MapTests.swift162
-rw-r--r--MapTests/StageTests.swift119
-rw-r--r--MapUITests/MapUITests.swift273
-rw-r--r--MapUITests/SearchUITests.swift226
-rw-r--r--README.md6
40 files changed, 1427 insertions, 414 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..6ca3848
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,94 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Common Commands
+
+### Development
+- **Build**: `make build` or open the project in Xcode and press the run button
+- **Format code**: `make format`
+- **Lint code**: `make lint`
+- **Run tests**: `make test` or use Xcode's test navigator or ⌘+U
+
+### Distribution
+- **Create full distribution**: `make distribute`
+- **Archive for distribution**: `make archive`
+- **Package app**: `make package`
+
+## Architecture Overview
+
+### Core Structure
+This is a SwiftUI-based macOS application for creating Wardley Maps using a text-based domain-specific language.
+
+**Key Components:**
+- **MapDocument**: Document-based app architecture using SwiftUI's `FileDocument` protocol
+- **MapParser**: Text-to-diagram parsing engine with strategy pattern
+- **MapEditor**: Main editing interface with split view (text editor + visual map)
+- **MapRenderView**: SwiftUI view that renders the visual map
+
+### Data Flow
+1. User types text in `MapTextEditor`
+2. `MapParser` parses text using parser strategies
+3. Parsed data creates `ParsedMap` with vertices, edges, notes, etc.
+4. `MapRenderView` renders the visual representation
+5. Changes are automatically saved via document architecture
+
+### Parser Architecture
+The parser uses a strategy pattern with these components:
+- `MapParserStrategy` protocol for parsing different element types
+- Individual strategies: `VertexParserStrategy`, `EdgeParserStrategy`, `NoteParserStrategy`, etc.
+- `MapBuilder` accumulates parsed elements into final `ParsedMap`
+
+### Key Data Models
+- **Vertex**: Map nodes with position and label
+- **MapEdge**: Connections between vertices (with/without arrowheads)
+- **Note**: Text annotations at specific positions
+- **Blocker**: Obstacles placed in front of vertices
+- **Opportunity**: Evolution arrows showing movement
+- **Stage**: Different evolution frameworks (general, practice, data, etc.)
+
+### UI Architecture
+- **MapEditor**: Main container with adaptive layout (horizontal/vertical split)
+- **MapTextEditor**: Text editing with syntax highlighting and search
+- **MapRenderView**: Visual map rendering with zoom and pan
+- **EvolutionPicker**: Dropdown for selecting evolution framework
+- **SearchBar**: Text search functionality with highlight navigation
+
+## Map Language Reference
+
+The app uses a domain-specific language for creating Wardley Maps:
+
+### Basic Elements
+- **Nodes**: `Name (x,y)` where x,y are 0-100 coordinates
+- **Edges**: `Node -- Node` (line) or `Node -> Node` (arrow)
+- **Blockers**: `[Blocker] Node`
+- **Evolution**: `[Evolution] Node +/-x`
+- **Notes**: `[Note] (x,y) Text`
+- **Groups**: `[Group] NodeA, NodeB, NodeC`
+- **Axes**: `[I] x`, `[II] x`, `[III] x` for custom stage boundaries
+
+### File Format
+- Files use `.wmap` extension with UTI `systems.tranquil.map.wmap`
+- Plain text format parsed line by line
+- Each line can contain one map element
+
+## Development Notes
+
+### Testing
+- Unit tests are in `MapTests/MapTests.swift`
+- UI tests are in `MapUITests/`
+- Run tests with ⌘+U in Xcode
+
+### Auto-Updates
+- Uses Sparkle framework for automatic updates
+- Configuration in `Info.plist` and update commands
+
+### Code Style
+- Uses swift-format for consistent formatting
+- Run `make format` before committing
+- All Swift files include GPL license header
+
+### Key Dependencies
+- SwiftUI for UI framework
+- Sparkle for auto-updates
+- CoreGraphics for rendering calculations \ No newline at end of file
diff --git a/Makefile b/Makefile
index 8d1a3b1..158f050 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,12 @@ sparkle_account := tranquil.systems
distribute: archive package generate_appcast
+build:
+ @xcodebuild build -scheme Map
+
+test:
+ @xcodebuild test -scheme Map
+
package:
@scripts/package.sh "$(project_name)" "$(build_directory)"
@@ -24,4 +30,4 @@ format:
lint:
swift format lint -r .
-.PHONY: package prepare archive generate_appcast package distribute format lint
+.PHONY: package prepare archive generate_appcast package distribute format lint build test
diff --git a/Map.xcodeproj/project.pbxproj b/Map.xcodeproj/project.pbxproj
index fc3a574..aaa6948 100644
--- a/Map.xcodeproj/project.pbxproj
+++ b/Map.xcodeproj/project.pbxproj
@@ -3,53 +3,13 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 60;
+ objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
- B5012E3F2C96232A00AC4D68 /* EvolutionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */; };
- B5012E422C96235E00AC4D68 /* Stage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E412C96235B00AC4D68 /* Stage.swift */; };
- B5012E452C9623C700AC4D68 /* Font+theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E442C9623C500AC4D68 /* Font+theme.swift */; };
- B5012E472C96243C00AC4D68 /* MapTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E462C96243500AC4D68 /* MapTextEditor.swift */; };
- B5012E492C96245B00AC4D68 /* Color+theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E482C96245800AC4D68 /* Color+theme.swift */; };
- B5012E4B2C96246F00AC4D68 /* NSColor+theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4A2C96246D00AC4D68 /* NSColor+theme.swift */; };
- B5012E572C96249400AC4D68 /* NoteParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4D2C96249400AC4D68 /* NoteParserStrategy.swift */; };
- B5012E582C96249400AC4D68 /* BlockerParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4F2C96249400AC4D68 /* BlockerParserStrategy.swift */; };
- B5012E592C96249400AC4D68 /* VertexParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4C2C96249400AC4D68 /* VertexParserStrategy.swift */; };
- B5012E5A2C96249400AC4D68 /* MapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E532C96249400AC4D68 /* MapParser.swift */; };
- B5012E5B2C96249400AC4D68 /* OpportunityParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E502C96249400AC4D68 /* OpportunityParserStrategy.swift */; };
- B5012E5C2C96249400AC4D68 /* EdgeParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4E2C96249400AC4D68 /* EdgeParserStrategy.swift */; };
- B5012E5D2C96249400AC4D68 /* StageParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E512C96249400AC4D68 /* StageParserStrategy.swift */; };
- B5012E5E2C96249400AC4D68 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E552C96249400AC4D68 /* Debouncer.swift */; };
- B5012E622C96254700AC4D68 /* MapRenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E5F2C96254700AC4D68 /* MapRenderView.swift */; };
- B5012E6B2C96255A00AC4D68 /* MapAxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E632C96255A00AC4D68 /* MapAxes.swift */; };
- B5012E6C2C96255A00AC4D68 /* MapVertices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E652C96255A00AC4D68 /* MapVertices.swift */; };
- B5012E6D2C96255A00AC4D68 /* MapStages.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E642C96255A00AC4D68 /* MapStages.swift */; };
- B5012E6E2C96255A00AC4D68 /* MapNotes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E662C96255A00AC4D68 /* MapNotes.swift */; };
- B5012E6F2C96255A00AC4D68 /* MapEdges.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E672C96255A00AC4D68 /* MapEdges.swift */; };
- B5012E702C96255A00AC4D68 /* MapOpportunities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E692C96255A00AC4D68 /* MapOpportunities.swift */; };
- B5012E712C96255A00AC4D68 /* MapBlockers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E682C96255A00AC4D68 /* MapBlockers.swift */; };
B5012E742C9625E200AC4D68 /* Patterns in Frameworks */ = {isa = PBXBuildFile; productRef = B5012E732C9625E200AC4D68 /* Patterns */; };
- B5012E7A2C96F02F00AC4D68 /* Dimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E792C96F02E00AC4D68 /* Dimensions.swift */; };
- B5012E7C2C972B6C00AC4D68 /* GroupParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E7B2C972B6600AC4D68 /* GroupParserStrategy.swift */; };
B5012E7F2C97315800AC4D68 /* ConcaveHull in Frameworks */ = {isa = PBXBuildFile; productRef = B5012E7E2C97315800AC4D68 /* ConcaveHull */; };
- B5012E812C97318600AC4D68 /* MapGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E802C97318300AC4D68 /* MapGroups.swift */; };
- B5012E872C97874600AC4D68 /* MapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E862C97874400AC4D68 /* MapGroup.swift */; };
- B5012E8A2C98235500AC4D68 /* MapCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E892C98235300AC4D68 /* MapCommands.swift */; };
- B5012E8C2C98244000AC4D68 /* ViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E8B2C98243E00AC4D68 /* ViewStyle.swift */; };
- B5012E8E2C9828D000AC4D68 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E8D2C9828CE00AC4D68 /* Constants.swift */; };
B51A45F52C982FAE00870ED5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = B51A45F42C982FAE00870ED5 /* Sparkle */; };
- B51A45F72C982FEA00870ED5 /* UpdateCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51A45F62C982FE700870ED5 /* UpdateCommands.swift */; };
- B54587102C961E9C0067B788 /* MapApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B545870F2C961E9C0067B788 /* MapApp.swift */; };
- B54587122C961E9C0067B788 /* MapDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587112C961E9C0067B788 /* MapDocument.swift */; };
- B54587142C961E9C0067B788 /* MapEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587132C961E9C0067B788 /* MapEditor.swift */; };
- B54587162C961E9E0067B788 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B54587152C961E9E0067B788 /* Assets.xcassets */; };
- B54587192C961E9E0067B788 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B54587182C961E9E0067B788 /* Preview Assets.xcassets */; };
- B54587252C961E9E0067B788 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587242C961E9E0067B788 /* MapTests.swift */; };
- B545872F2C961E9E0067B788 /* MapUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B545872E2C961E9E0067B788 /* MapUITests.swift */; };
- B54587312C961E9E0067B788 /* MapUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */; };
- B5D42DC22C984E870075473D /* FocusedValues+document.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D42DC12C984E7F0075473D /* FocusedValues+document.swift */; };
- B5D42DC42C9851ED0075473D /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D42DC32C9851ED0075473D /* SearchBar.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -58,65 +18,39 @@
containerPortal = B54587042C961E9C0067B788 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B545870B2C961E9C0067B788;
- remoteInfo = Map2;
+ remoteInfo = Map;
};
B545872B2C961E9E0067B788 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B54587042C961E9C0067B788 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B545870B2C961E9C0067B788;
- remoteInfo = Map2;
+ remoteInfo = Map;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvolutionPicker.swift; sourceTree = "<group>"; };
- B5012E412C96235B00AC4D68 /* Stage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stage.swift; sourceTree = "<group>"; };
- B5012E442C9623C500AC4D68 /* Font+theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+theme.swift"; sourceTree = "<group>"; };
- B5012E462C96243500AC4D68 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = "<group>"; };
- B5012E482C96245800AC4D68 /* Color+theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+theme.swift"; sourceTree = "<group>"; };
- B5012E4A2C96246D00AC4D68 /* NSColor+theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+theme.swift"; sourceTree = "<group>"; };
- B5012E4C2C96249400AC4D68 /* VertexParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexParserStrategy.swift; sourceTree = "<group>"; };
- B5012E4D2C96249400AC4D68 /* NoteParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteParserStrategy.swift; sourceTree = "<group>"; };
- B5012E4E2C96249400AC4D68 /* EdgeParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeParserStrategy.swift; sourceTree = "<group>"; };
- B5012E4F2C96249400AC4D68 /* BlockerParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockerParserStrategy.swift; sourceTree = "<group>"; };
- B5012E502C96249400AC4D68 /* OpportunityParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpportunityParserStrategy.swift; sourceTree = "<group>"; };
- B5012E512C96249400AC4D68 /* StageParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageParserStrategy.swift; sourceTree = "<group>"; };
- B5012E532C96249400AC4D68 /* MapParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapParser.swift; sourceTree = "<group>"; };
- B5012E552C96249400AC4D68 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
- B5012E5F2C96254700AC4D68 /* MapRenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRenderView.swift; sourceTree = "<group>"; };
- B5012E632C96255A00AC4D68 /* MapAxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAxes.swift; sourceTree = "<group>"; };
- B5012E642C96255A00AC4D68 /* MapStages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapStages.swift; sourceTree = "<group>"; };
- B5012E652C96255A00AC4D68 /* MapVertices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVertices.swift; sourceTree = "<group>"; };
- B5012E662C96255A00AC4D68 /* MapNotes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapNotes.swift; sourceTree = "<group>"; };
- B5012E672C96255A00AC4D68 /* MapEdges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEdges.swift; sourceTree = "<group>"; };
- B5012E682C96255A00AC4D68 /* MapBlockers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapBlockers.swift; sourceTree = "<group>"; };
- B5012E692C96255A00AC4D68 /* MapOpportunities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapOpportunities.swift; sourceTree = "<group>"; };
- B5012E792C96F02E00AC4D68 /* Dimensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dimensions.swift; sourceTree = "<group>"; };
- B5012E7B2C972B6600AC4D68 /* GroupParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupParserStrategy.swift; sourceTree = "<group>"; };
- B5012E802C97318300AC4D68 /* MapGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapGroups.swift; sourceTree = "<group>"; };
- B5012E862C97874400AC4D68 /* MapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapGroup.swift; sourceTree = "<group>"; };
- B5012E892C98235300AC4D68 /* MapCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapCommands.swift; sourceTree = "<group>"; };
- B5012E8B2C98243E00AC4D68 /* ViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStyle.swift; sourceTree = "<group>"; };
- B5012E8D2C9828CE00AC4D68 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
- B51A45F62C982FE700870ED5 /* UpdateCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCommands.swift; sourceTree = "<group>"; };
B545870C2C961E9C0067B788 /* Map.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Map.app; sourceTree = BUILT_PRODUCTS_DIR; };
- B545870F2C961E9C0067B788 /* MapApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapApp.swift; sourceTree = "<group>"; };
- B54587112C961E9C0067B788 /* MapDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDocument.swift; sourceTree = "<group>"; };
- B54587132C961E9C0067B788 /* MapEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEditor.swift; sourceTree = "<group>"; };
- B54587152C961E9E0067B788 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
- B54587182C961E9E0067B788 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
- B545871A2C961E9E0067B788 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
- B545871B2C961E9E0067B788 /* Map.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Map.entitlements; sourceTree = "<group>"; };
B54587202C961E9E0067B788 /* MapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MapTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- B54587242C961E9E0067B788 /* MapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTests.swift; sourceTree = "<group>"; };
- B545872A2C961E9E0067B788 /* Map2UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Map2UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- B545872E2C961E9E0067B788 /* MapUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITests.swift; sourceTree = "<group>"; };
- B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITestsLaunchTests.swift; sourceTree = "<group>"; };
- B5D42DC12C984E7F0075473D /* FocusedValues+document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FocusedValues+document.swift"; sourceTree = "<group>"; };
- B5D42DC32C9851ED0075473D /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
+ B545872A2C961E9E0067B788 /* MapUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MapUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ B59830752E17E7C700F5FBF6 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = B545870B2C961E9C0067B788 /* Map */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ B598304E2E17E7C700F5FBF6 /* Map */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (B59830752E17E7C700F5FBF6 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Map; sourceTree = "<group>"; };
+ B59830772E17E7CC00F5FBF6 /* MapTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MapTests; sourceTree = "<group>"; };
+ B598307C2E17E7D600F5FBF6 /* MapUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MapUITests; sourceTree = "<group>"; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
/* Begin PBXFrameworksBuildPhase section */
B54587092C961E9C0067B788 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -145,131 +79,12 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- B5012E3C2C96222E00AC4D68 /* Data */ = {
- isa = PBXGroup;
- children = (
- B5D42DC12C984E7F0075473D /* FocusedValues+document.swift */,
- B5012E412C96235B00AC4D68 /* Stage.swift */,
- B54587112C961E9C0067B788 /* MapDocument.swift */,
- );
- path = Data;
- sourceTree = "<group>";
- };
- B5012E3D2C96223800AC4D68 /* Presentation */ = {
- isa = PBXGroup;
- children = (
- B5012E8B2C98243E00AC4D68 /* ViewStyle.swift */,
- B5012E882C98234F00AC4D68 /* Commands */,
- B5012E612C96254700AC4D68 /* Complex Components */,
- B5012E432C9623BC00AC4D68 /* Theme */,
- B5012E402C96232E00AC4D68 /* Base Components */,
- B54587132C961E9C0067B788 /* MapEditor.swift */,
- );
- path = Presentation;
- sourceTree = "<group>";
- };
- B5012E402C96232E00AC4D68 /* Base Components */ = {
- isa = PBXGroup;
- children = (
- B5012E6A2C96255A00AC4D68 /* MapRender */,
- B5012E462C96243500AC4D68 /* MapTextEditor.swift */,
- B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */,
- B5D42DC32C9851ED0075473D /* SearchBar.swift */,
- );
- path = "Base Components";
- sourceTree = "<group>";
- };
- B5012E432C9623BC00AC4D68 /* Theme */ = {
- isa = PBXGroup;
- children = (
- B5012E792C96F02E00AC4D68 /* Dimensions.swift */,
- B5012E4A2C96246D00AC4D68 /* NSColor+theme.swift */,
- B5012E482C96245800AC4D68 /* Color+theme.swift */,
- B5012E442C9623C500AC4D68 /* Font+theme.swift */,
- );
- path = Theme;
- sourceTree = "<group>";
- };
- B5012E522C96249400AC4D68 /* Strategies */ = {
- isa = PBXGroup;
- children = (
- B5012E7B2C972B6600AC4D68 /* GroupParserStrategy.swift */,
- B5012E4C2C96249400AC4D68 /* VertexParserStrategy.swift */,
- B5012E4D2C96249400AC4D68 /* NoteParserStrategy.swift */,
- B5012E4E2C96249400AC4D68 /* EdgeParserStrategy.swift */,
- B5012E4F2C96249400AC4D68 /* BlockerParserStrategy.swift */,
- B5012E502C96249400AC4D68 /* OpportunityParserStrategy.swift */,
- B5012E512C96249400AC4D68 /* StageParserStrategy.swift */,
- );
- path = Strategies;
- sourceTree = "<group>";
- };
- B5012E542C96249400AC4D68 /* MapParser */ = {
- isa = PBXGroup;
- children = (
- B5012E522C96249400AC4D68 /* Strategies */,
- B5012E532C96249400AC4D68 /* MapParser.swift */,
- );
- path = MapParser;
- sourceTree = "<group>";
- };
- B5012E562C96249400AC4D68 /* Logic */ = {
- isa = PBXGroup;
- children = (
- B5012E8D2C9828CE00AC4D68 /* Constants.swift */,
- B5012E542C96249400AC4D68 /* MapParser */,
- B5012E552C96249400AC4D68 /* Debouncer.swift */,
- );
- path = Logic;
- sourceTree = "<group>";
- };
- B5012E602C96254700AC4D68 /* MapRender */ = {
- isa = PBXGroup;
- children = (
- B5012E5F2C96254700AC4D68 /* MapRenderView.swift */,
- );
- path = MapRender;
- sourceTree = "<group>";
- };
- B5012E612C96254700AC4D68 /* Complex Components */ = {
- isa = PBXGroup;
- children = (
- B5012E602C96254700AC4D68 /* MapRender */,
- );
- path = "Complex Components";
- sourceTree = "<group>";
- };
- B5012E6A2C96255A00AC4D68 /* MapRender */ = {
- isa = PBXGroup;
- children = (
- B5012E802C97318300AC4D68 /* MapGroups.swift */,
- B5012E862C97874400AC4D68 /* MapGroup.swift */,
- B5012E632C96255A00AC4D68 /* MapAxes.swift */,
- B5012E642C96255A00AC4D68 /* MapStages.swift */,
- B5012E652C96255A00AC4D68 /* MapVertices.swift */,
- B5012E662C96255A00AC4D68 /* MapNotes.swift */,
- B5012E672C96255A00AC4D68 /* MapEdges.swift */,
- B5012E682C96255A00AC4D68 /* MapBlockers.swift */,
- B5012E692C96255A00AC4D68 /* MapOpportunities.swift */,
- );
- path = MapRender;
- sourceTree = "<group>";
- };
- B5012E882C98234F00AC4D68 /* Commands */ = {
- isa = PBXGroup;
- children = (
- B51A45F62C982FE700870ED5 /* UpdateCommands.swift */,
- B5012E892C98235300AC4D68 /* MapCommands.swift */,
- );
- path = Commands;
- sourceTree = "<group>";
- };
B54587032C961E9C0067B788 = {
isa = PBXGroup;
children = (
- B545870E2C961E9C0067B788 /* Map */,
- B54587232C961E9E0067B788 /* MapTests */,
- B545872D2C961E9E0067B788 /* MapUITests */,
+ B598304E2E17E7C700F5FBF6 /* Map */,
+ B59830772E17E7CC00F5FBF6 /* MapTests */,
+ B598307C2E17E7D600F5FBF6 /* MapUITests */,
B545870D2C961E9C0067B788 /* Products */,
);
sourceTree = "<group>";
@@ -279,51 +94,11 @@
children = (
B545870C2C961E9C0067B788 /* Map.app */,
B54587202C961E9E0067B788 /* MapTests.xctest */,
- B545872A2C961E9E0067B788 /* Map2UITests.xctest */,
+ B545872A2C961E9E0067B788 /* MapUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
- B545870E2C961E9C0067B788 /* Map */ = {
- isa = PBXGroup;
- children = (
- B5012E562C96249400AC4D68 /* Logic */,
- B5012E3C2C96222E00AC4D68 /* Data */,
- B5012E3D2C96223800AC4D68 /* Presentation */,
- B545870F2C961E9C0067B788 /* MapApp.swift */,
- B54587152C961E9E0067B788 /* Assets.xcassets */,
- B545871A2C961E9E0067B788 /* Info.plist */,
- B545871B2C961E9E0067B788 /* Map.entitlements */,
- B54587172C961E9E0067B788 /* Preview Content */,
- );
- path = Map;
- sourceTree = "<group>";
- };
- B54587172C961E9E0067B788 /* Preview Content */ = {
- isa = PBXGroup;
- children = (
- B54587182C961E9E0067B788 /* Preview Assets.xcassets */,
- );
- path = "Preview Content";
- sourceTree = "<group>";
- };
- B54587232C961E9E0067B788 /* MapTests */ = {
- isa = PBXGroup;
- children = (
- B54587242C961E9E0067B788 /* MapTests.swift */,
- );
- path = MapTests;
- sourceTree = "<group>";
- };
- B545872D2C961E9E0067B788 /* MapUITests */ = {
- isa = PBXGroup;
- children = (
- B545872E2C961E9E0067B788 /* MapUITests.swift */,
- B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */,
- );
- path = MapUITests;
- sourceTree = "<group>";
- };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -339,13 +114,16 @@
);
dependencies = (
);
+ fileSystemSynchronizedGroups = (
+ B598304E2E17E7C700F5FBF6 /* Map */,
+ );
name = Map;
packageProductDependencies = (
B5012E732C9625E200AC4D68 /* Patterns */,
B5012E7E2C97315800AC4D68 /* ConcaveHull */,
B51A45F42C982FAE00870ED5 /* Sparkle */,
);
- productName = Map2;
+ productName = Map;
productReference = B545870C2C961E9C0067B788 /* Map.app */;
productType = "com.apple.product-type.application";
};
@@ -362,14 +140,17 @@
dependencies = (
B54587222C961E9E0067B788 /* PBXTargetDependency */,
);
+ fileSystemSynchronizedGroups = (
+ B59830772E17E7CC00F5FBF6 /* MapTests */,
+ );
name = MapTests;
- productName = Map2Tests;
+ productName = MapTests;
productReference = B54587202C961E9E0067B788 /* MapTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
- B54587292C961E9E0067B788 /* Map2UITests */ = {
+ B54587292C961E9E0067B788 /* MapUITests */ = {
isa = PBXNativeTarget;
- buildConfigurationList = B545873A2C961E9E0067B788 /* Build configuration list for PBXNativeTarget "Map2UITests" */;
+ buildConfigurationList = B545873A2C961E9E0067B788 /* Build configuration list for PBXNativeTarget "MapUITests" */;
buildPhases = (
B54587262C961E9E0067B788 /* Sources */,
B54587272C961E9E0067B788 /* Frameworks */,
@@ -380,9 +161,12 @@
dependencies = (
B545872C2C961E9E0067B788 /* PBXTargetDependency */,
);
- name = Map2UITests;
- productName = Map2UITests;
- productReference = B545872A2C961E9E0067B788 /* Map2UITests.xctest */;
+ fileSystemSynchronizedGroups = (
+ B598307C2E17E7D600F5FBF6 /* MapUITests */,
+ );
+ name = MapUITests;
+ productName = MapUITests;
+ productReference = B545872A2C961E9E0067B788 /* MapUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
@@ -393,7 +177,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1600;
- LastUpgradeCheck = 1600;
+ LastUpgradeCheck = 1640;
TargetAttributes = {
B545870B2C961E9C0067B788 = {
CreatedOnToolsVersion = 16.0;
@@ -428,7 +212,7 @@
targets = (
B545870B2C961E9C0067B788 /* Map */,
B545871F2C961E9E0067B788 /* MapTests */,
- B54587292C961E9E0067B788 /* Map2UITests */,
+ B54587292C961E9E0067B788 /* MapUITests */,
);
};
/* End PBXProject section */
@@ -438,8 +222,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- B54587192C961E9E0067B788 /* Preview Assets.xcassets in Resources */,
- B54587162C961E9E0067B788 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -464,41 +246,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- B5012E872C97874600AC4D68 /* MapGroup.swift in Sources */,
- B5012E472C96243C00AC4D68 /* MapTextEditor.swift in Sources */,
- B5012E622C96254700AC4D68 /* MapRenderView.swift in Sources */,
- B5012E492C96245B00AC4D68 /* Color+theme.swift in Sources */,
- B54587122C961E9C0067B788 /* MapDocument.swift in Sources */,
- B54587102C961E9C0067B788 /* MapApp.swift in Sources */,
- B5012E8C2C98244000AC4D68 /* ViewStyle.swift in Sources */,
- B5D42DC42C9851ED0075473D /* SearchBar.swift in Sources */,
- B5012E8E2C9828D000AC4D68 /* Constants.swift in Sources */,
- B5012E7C2C972B6C00AC4D68 /* GroupParserStrategy.swift in Sources */,
- B5012E6B2C96255A00AC4D68 /* MapAxes.swift in Sources */,
- B5012E6C2C96255A00AC4D68 /* MapVertices.swift in Sources */,
- B5012E6D2C96255A00AC4D68 /* MapStages.swift in Sources */,
- B5012E7A2C96F02F00AC4D68 /* Dimensions.swift in Sources */,
- B5012E6E2C96255A00AC4D68 /* MapNotes.swift in Sources */,
- B5012E6F2C96255A00AC4D68 /* MapEdges.swift in Sources */,
- B5012E702C96255A00AC4D68 /* MapOpportunities.swift in Sources */,
- B5012E712C96255A00AC4D68 /* MapBlockers.swift in Sources */,
- B5012E4B2C96246F00AC4D68 /* NSColor+theme.swift in Sources */,
- B54587142C961E9C0067B788 /* MapEditor.swift in Sources */,
- B5012E3F2C96232A00AC4D68 /* EvolutionPicker.swift in Sources */,
- B5012E572C96249400AC4D68 /* NoteParserStrategy.swift in Sources */,
- B51A45F72C982FEA00870ED5 /* UpdateCommands.swift in Sources */,
- B5012E582C96249400AC4D68 /* BlockerParserStrategy.swift in Sources */,
- B5012E592C96249400AC4D68 /* VertexParserStrategy.swift in Sources */,
- B5012E5A2C96249400AC4D68 /* MapParser.swift in Sources */,
- B5012E5B2C96249400AC4D68 /* OpportunityParserStrategy.swift in Sources */,
- B5012E5C2C96249400AC4D68 /* EdgeParserStrategy.swift in Sources */,
- B5012E5D2C96249400AC4D68 /* StageParserStrategy.swift in Sources */,
- B5D42DC22C984E870075473D /* FocusedValues+document.swift in Sources */,
- B5012E812C97318600AC4D68 /* MapGroups.swift in Sources */,
- B5012E8A2C98235500AC4D68 /* MapCommands.swift in Sources */,
- B5012E5E2C96249400AC4D68 /* Debouncer.swift in Sources */,
- B5012E452C9623C700AC4D68 /* Font+theme.swift in Sources */,
- B5012E422C96235E00AC4D68 /* Stage.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -506,7 +253,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- B54587252C961E9E0067B788 /* MapTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -514,8 +260,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- B54587312C961E9E0067B788 /* MapUITestsLaunchTests.swift in Sources */,
- B545872F2C961E9E0067B788 /* MapUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -569,7 +313,9 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = S68NHQVJXW;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -632,7 +378,9 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = S68NHQVJXW;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -662,8 +410,8 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
- DEVELOPMENT_TEAM = S68NHQVJXW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -679,7 +427,8 @@
PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 6.0;
};
name = Debug;
};
@@ -692,8 +441,8 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
- DEVELOPMENT_TEAM = S68NHQVJXW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -709,7 +458,8 @@
PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 6.0;
};
name = Release;
};
@@ -719,15 +469,15 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = S68NHQVJXW;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2Tests;
+ PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.MapTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Map2";
+ SWIFT_VERSION = 6.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Map";
};
name = Debug;
};
@@ -737,15 +487,15 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = S68NHQVJXW;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2Tests;
+ PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.MapTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Map2";
+ SWIFT_VERSION = 6.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Map";
};
name = Release;
};
@@ -754,14 +504,14 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = S68NHQVJXW;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2UITests;
+ PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.MapUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TEST_TARGET_NAME = Map2;
+ SWIFT_VERSION = 6.0;
+ TEST_TARGET_NAME = Map;
};
name = Debug;
};
@@ -770,14 +520,14 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = S68NHQVJXW;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2UITests;
+ PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.MapUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TEST_TARGET_NAME = Map2;
+ SWIFT_VERSION = 6.0;
+ TEST_TARGET_NAME = Map;
};
name = Release;
};
@@ -811,7 +561,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- B545873A2C961E9E0067B788 /* Build configuration list for PBXNativeTarget "Map2UITests" */ = {
+ B545873A2C961E9E0067B788 /* Build configuration list for PBXNativeTarget "MapUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B545873B2C961E9E0067B788 /* Debug */,
diff --git a/Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json
new file mode 100644
index 0000000..97650a1
--- /dev/null
+++ b/Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Map/Logic/Constants.swift b/Map/Business/Constants.swift
index 1c6f54d..1c6f54d 100644
--- a/Map/Logic/Constants.swift
+++ b/Map/Business/Constants.swift
diff --git a/Map/Logic/MapParser/MapParser.swift b/Map/Business/MapParser/MapParser.swift
index e94de77..e94de77 100644
--- a/Map/Logic/MapParser/MapParser.swift
+++ b/Map/Business/MapParser/MapParser.swift
diff --git a/Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift b/Map/Business/MapParser/Strategies/BlockerParserStrategy.swift
index 4bd2ce4..4bd2ce4 100644
--- a/Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift
+++ b/Map/Business/MapParser/Strategies/BlockerParserStrategy.swift
diff --git a/Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift b/Map/Business/MapParser/Strategies/EdgeParserStrategy.swift
index cc29b49..cc29b49 100644
--- a/Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift
+++ b/Map/Business/MapParser/Strategies/EdgeParserStrategy.swift
diff --git a/Map/Logic/MapParser/Strategies/GroupParserStrategy.swift b/Map/Business/MapParser/Strategies/GroupParserStrategy.swift
index 96de365..96de365 100644
--- a/Map/Logic/MapParser/Strategies/GroupParserStrategy.swift
+++ b/Map/Business/MapParser/Strategies/GroupParserStrategy.swift
diff --git a/Map/Logic/MapParser/Strategies/NoteParserStrategy.swift b/Map/Business/MapParser/Strategies/NoteParserStrategy.swift
index d06c768..d06c768 100644
--- a/Map/Logic/MapParser/Strategies/NoteParserStrategy.swift
+++ b/Map/Business/MapParser/Strategies/NoteParserStrategy.swift
diff --git a/Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift b/Map/Business/MapParser/Strategies/OpportunityParserStrategy.swift
index 25e6cc4..25e6cc4 100644
--- a/Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift
+++ b/Map/Business/MapParser/Strategies/OpportunityParserStrategy.swift
diff --git a/Map/Logic/MapParser/Strategies/StageParserStrategy.swift b/Map/Business/MapParser/Strategies/StageParserStrategy.swift
index 6e4a341..6e4a341 100644
--- a/Map/Logic/MapParser/Strategies/StageParserStrategy.swift
+++ b/Map/Business/MapParser/Strategies/StageParserStrategy.swift
diff --git a/Map/Logic/MapParser/Strategies/VertexParserStrategy.swift b/Map/Business/MapParser/Strategies/VertexParserStrategy.swift
index 0ca6fc1..0ca6fc1 100644
--- a/Map/Logic/MapParser/Strategies/VertexParserStrategy.swift
+++ b/Map/Business/MapParser/Strategies/VertexParserStrategy.swift
diff --git a/Map/Data/Stage.swift b/Map/Business/Stage.swift
index 4f737a1..4f737a1 100644
--- a/Map/Data/Stage.swift
+++ b/Map/Business/Stage.swift
diff --git a/Map/Logic/Debouncer.swift b/Map/Data/Debouncer.swift
index 8d40e99..8d40e99 100644
--- a/Map/Logic/Debouncer.swift
+++ b/Map/Data/Debouncer.swift
diff --git a/Map/Data/MapDocument.swift b/Map/Data/MapDocument.swift
index 1b43efb..3c6d352 100644
--- a/Map/Data/MapDocument.swift
+++ b/Map/Data/MapDocument.swift
@@ -16,7 +16,7 @@ import SwiftUI
import UniformTypeIdentifiers
extension UTType {
- static var exampleText: UTType {
+ static var wmap: UTType {
UTType(importedAs: "systems.tranquil.map.wmap")
}
}
@@ -24,11 +24,23 @@ extension UTType {
struct MapDocument: FileDocument {
var text: String
- init(text: String = "Hello, world!") {
+ init(
+ text: String = """
+ Anchor (60, 5) [x]
+ Node A (78, 23)
+ Node B (40, 30) [square]
+ Node C (65, 70)
+
+ Anchor -> Node A
+ Anchor -> Node B
+ Node A -> Node C
+ Node B -> Node C
+ """
+ ) {
self.text = text
}
- static var readableContentTypes: [UTType] { [.exampleText] }
+ static var readableContentTypes: [UTType] { [.wmap] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
diff --git a/Map/Info.plist b/Map/Info.plist
index e862423..07f3108 100644
--- a/Map/Info.plist
+++ b/Map/Info.plist
@@ -2,6 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
+ <key>UIAppFonts</key>
+ <array>
+ <string>Ronzino-Regular.otf</string>
+ <string>Ronzino-Medium.otf</string>
+ <string>LibertinusSerif-Regular.otf</string>
+ </array>
<key>SUEnableInstallerLauncherService</key>
<true/>
<key>SUPublicEDKey</key>
diff --git a/Map/Presentation/Base Components/EvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker.swift
index 2768540..2df4a7f 100644
--- a/Map/Presentation/Base Components/EvolutionPicker.swift
+++ b/Map/Presentation/Base Components/EvolutionPicker.swift
@@ -21,21 +21,22 @@ struct EvolutionPicker: View {
var body: some View {
Picker("Evolution", selection: $selectedEvolution) {
ForEach(StageType.types) { stage in
- Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0)
+ Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0)
}
Divider()
ForEach(StageType.characteristics) { stage in
- Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0).font(.Theme.body)
+ Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0).font(
+ .Theme.Body.regular)
}
Divider()
ForEach(StageType.properties) { stage in
- Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0)
+ Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0)
}
Divider()
ForEach(StageType.custom) { stage in
- Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0)
+ Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0)
}
- }.font(.Theme.body).padding(.horizontal, 8.0).padding(.vertical, 4.0)
+ }.font(.Theme.Body.regular).padding(.horizontal, 8.0).padding(.vertical, 4.0)
}
}
diff --git a/Map/Presentation/Base Components/MapRender/MapAxes.swift b/Map/Presentation/Base Components/MapRender/MapAxes.swift
index 4f64135..1c0a147 100644
--- a/Map/Presentation/Base Components/MapRender/MapAxes.swift
+++ b/Map/Presentation/Base Components/MapRender/MapAxes.swift
@@ -36,11 +36,11 @@ struct MapAxes: View {
}.stroke(Color.Map.axisColor, lineWidth: lineWidth * 2)
// Y Labels
- Text("Visible").font(.Theme.axisLabel).foregroundColor(.Map.labelColor).rotationEffect(
+ Text("Visible").font(.Theme.Map.axisLabel).foregroundColor(.Map.labelColor).rotationEffect(
Angle(degrees: -90.0)
)
.offset(CGSize(width: -35.0, height: 0.0))
- Text("Invisible").font(.Theme.axisLabel).foregroundColor(.Map.labelColor).rotationEffect(
+ Text("Invisible").font(.Theme.Map.axisLabel).foregroundColor(.Map.labelColor).rotationEffect(
Angle(degrees: -90.0)
)
.offset(CGSize(width: -40.0, height: mapSize.height - 20))
@@ -48,36 +48,36 @@ struct MapAxes: View {
// X Labels
Text("Uncharted")
- .font(.Theme.axisLabel)
+ .font(.Theme.Map.axisLabel)
.foregroundColor(.Map.labelColor)
.frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading)
.offset(CGSize(width: 0.0, height: -stageHeight / 4.0))
Text("Industrialised")
- .font(.Theme.axisLabel)
+ .font(.Theme.Map.axisLabel)
.foregroundColor(.Map.labelColor)
.frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading)
.offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 4.0))
Text(evolution.i)
- .font(.Theme.axisLabel)
+ .font(.Theme.Map.axisLabel)
.foregroundColor(.Map.labelColor)
.frame(width: w(stages[0]), height: stageHeight, alignment: .topLeading)
.offset(CGSize(width: 0.0, height: mapSize.height + padding))
Text(evolution.ii)
- .font(.Theme.axisLabel)
+ .font(.Theme.Map.axisLabel)
.foregroundColor(.Map.labelColor)
.frame(width: w(stages[1]) - w(stages[0]), height: stageHeight, alignment: .topLeading)
.offset(CGSize(width: w(stages[0]), height: mapSize.height + padding))
Text(evolution.iii)
- .font(.Theme.axisLabel)
+ .font(.Theme.Map.axisLabel)
.foregroundColor(.Map.labelColor)
.frame(width: w(stages[2]) - w(stages[1]), height: stageHeight, alignment: .topLeading)
.offset(CGSize(width: w(stages[1]), height: mapSize.height + padding))
Text(evolution.iv)
- .font(.Theme.axisLabel)
+ .font(.Theme.Map.axisLabel)
.foregroundColor(.Map.labelColor)
.frame(width: mapSize.width - w(stages[2]), height: stageHeight, alignment: .topLeading)
.offset(CGSize(width: w(stages[2]), height: mapSize.height + padding))
diff --git a/Map/Presentation/Base Components/MapRender/MapNotes.swift b/Map/Presentation/Base Components/MapRender/MapNotes.swift
index 0119154..16c61ba 100644
--- a/Map/Presentation/Base Components/MapRender/MapNotes.swift
+++ b/Map/Presentation/Base Components/MapRender/MapNotes.swift
@@ -24,8 +24,10 @@ struct MapNotes: View {
var body: some View {
ForEach(notes, id: \.id) { note in
- Text(note.text.replacingOccurrences(of: "\\n", with: "\n")).font(.Theme.note)
- .padding(2.0)
+ Text(note.text.replacingOccurrences(of: "\\n", with: "\n"))
+ .font(.Theme.Map.note)
+ .lineSpacing(Dimensions.LineHeight.Map.note - Dimensions.FontSize.Map.note)
+ .padding(Dimensions.Spacing.cozy)
.background(.white)
.foregroundColor(.Map.labelColor)
.border(Color.Map.vertexColor, width: lineWidth)
diff --git a/Map/Presentation/Base Components/MapRender/MapVertices.swift b/Map/Presentation/Base Components/MapRender/MapVertices.swift
index d8e3b88..1f6cf1a 100644
--- a/Map/Presentation/Base Components/MapRender/MapVertices.swift
+++ b/Map/Presentation/Base Components/MapRender/MapVertices.swift
@@ -28,7 +28,11 @@ struct MapVertices: View {
ForEach(vertices, id: \.id) { vertex in
ZStack(alignment: .topLeading) {
getVertexShape(vertex).fill(Color.Map.vertexColor)
- Text(vertex.label.replacingOccurrences(of: "\\n", with: "\n")).font(.Theme.vertexLabel)
+ Text(vertex.label.replacingOccurrences(of: "\\n", with: "\n"))
+ .font(.Theme.Map.vertexLabel)
+ .lineSpacing(
+ Dimensions.LineHeight.Map.vertexLabel - Dimensions.FontSize.Map.vertexLabel
+ )
.foregroundColor(.Map.labelColor)
.shadow(color: .white, radius: 0, x: -0.5, y: -0.5)
.shadow(color: .white, radius: 0, x: 0.5, y: 0.5)
diff --git a/Map/Presentation/Base Components/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor.swift
index ff98203..3c5c824 100644
--- a/Map/Presentation/Base Components/MapTextEditor.swift
+++ b/Map/Presentation/Base Components/MapTextEditor.swift
@@ -150,7 +150,7 @@ extension MapTextEditorController: NSTextStorageDelegate {
}
}
- private func colorizeText(textStorage: NSTextStorage) {
+ nonisolated private func colorizeText(textStorage: NSTextStorage) {
let range = NSMakeRange(0, textStorage.length)
var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
diff --git a/Map/Presentation/Base Components/SearchBar.swift b/Map/Presentation/Base Components/SearchBar.swift
index e5182c5..cd2c378 100644
--- a/Map/Presentation/Base Components/SearchBar.swift
+++ b/Map/Presentation/Base Components/SearchBar.swift
@@ -48,19 +48,19 @@ struct SearchBar: View {
Spacer()
Button(action: onPrevious) {
Image(systemName: "chevron.left")
- .font(.Theme.smallControl)
+ .font(.Theme.SmallControl.regular)
}.keyboardShortcut(
"g", modifiers: EventModifiers([.command, .shift])
).help("Find Previous (⇧⌘G)")
Button(action: onNext) {
Image(systemName: "chevron.right")
- .font(.Theme.smallControl)
+ .font(.Theme.SmallControl.regular)
}.keyboardShortcut(
"g", modifiers: EventModifiers([.command])
).help("Find Next (⌘G)")
Button(action: onDismiss) {
Text("Done")
- .font(.Theme.smallControl)
+ .font(.Theme.SmallControl.regular)
}.keyboardShortcut(.escape, modifiers: EventModifiers())
.help("Done (⎋)")
}.padding(4.0)
diff --git a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
index 1ca1ffe..780290b 100644
--- a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
+++ b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
@@ -30,8 +30,8 @@ struct MapRenderView: View {
MapParser.parse(content: document.text)
}
- let mapSize = Dimensions.mapSize
- let padding = Dimensions.mapPadding
+ let mapSize = Dimensions.Map.size
+ let padding = Dimensions.Map.padding
let lineWidth = CGFloat(0.5)
let vertexSize = CGSize(width: 25.0, height: 25.0)
diff --git a/Map/Presentation/EvolutionPicker.swift b/Map/Presentation/EvolutionPicker.swift
deleted file mode 100644
index 0f4c954..0000000
--- a/Map/Presentation/EvolutionPicker.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-import SwiftUI
-
-struct EvolutionPicker: View {
-
- @EnvironmentObject private var store: AppStore
-
- private var selectedEvolution: Binding<StageType> {
- Binding(
- get: { store.state.selectedEvolution },
- set: { evolution in
- store.send(.selectEvolution(evolution: evolution))
- }
- )
- }
-
- var body: some View {
- Picker("Evolution", selection: selectedEvolution) {
- ForEach(StageType.types) { stage in
- Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
- }
- Divider()
- ForEach(StageType.characteristics) { stage in
- Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0).font(.theme.body)
- }
- Divider()
- ForEach(StageType.properties) { stage in
- Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
- }
- Divider()
- ForEach(StageType.custom) { stage in
- Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
- }
- }.font(.theme.body).padding(.horizontal, 8.0).padding(.vertical, 4.0)
- }
-}
-
-struct EvolutionPicker_Previews: PreviewProvider {
- static var previews: some View {
- EvolutionPicker()
- }
-}
diff --git a/Map/Presentation/MapEditor.swift b/Map/Presentation/MapEditor.swift
index 5cda8f9..1e25ce8 100644
--- a/Map/Presentation/MapEditor.swift
+++ b/Map/Presentation/MapEditor.swift
@@ -102,8 +102,8 @@ struct MapEditor: View {
MapRenderView(
document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex
).scaleEffect(zoom, anchor: .center).frame(
- width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom,
- height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom)
+ width: (Dimensions.Map.size.width + 2 * Dimensions.Map.padding) * zoom,
+ height: (Dimensions.Map.size.height + 2 * Dimensions.Map.padding) * zoom)
}.background(Color.UI.background)
.gesture(
MagnificationGesture()
@@ -125,16 +125,16 @@ struct MapEditor: View {
value: $zoom, in: zoomRange, step: 0.1,
label: {
Text(formatZoom(zoom))
- .font(.Theme.smallControl)
+ .font(.Theme.SmallControl.regular)
},
minimumValueLabel: {
Image(systemName: "minus.magnifyingglass")
- .font(.Theme.smallControl)
+ .font(.Theme.SmallControl.regular)
.help("Zoom Out (⌘-)")
},
maximumValueLabel: {
Image(systemName: "plus.magnifyingglass")
- .font(.Theme.smallControl)
+ .font(.Theme.SmallControl.regular)
.help("Zoom In (⌘+)")
}
).frame(width: 200).padding(.trailing, 10.0)
diff --git a/Map/Presentation/Theme/Color+theme.swift b/Map/Presentation/Theme/Color+theme.swift
index feac76a..ee80e82 100644
--- a/Map/Presentation/Theme/Color+theme.swift
+++ b/Map/Presentation/Theme/Color+theme.swift
@@ -16,6 +16,9 @@ import SwiftUI
extension Color {
struct Theme {
+
+ // MARK: - Base Palette
+
static let darkSlate = Color("Dark Slate")
static let jasperRed = Color("Jasper Red")
static let olympicBlue = Color("Olympic Blue")
@@ -28,6 +31,15 @@ extension Color {
static let darkerNeutralGray = Color("Darker Neutral Gray")
}
+ // MARK: - General
+
+ struct UI {
+ static let foreground = Color("Foreground")
+ static let background = Color("Background")
+ }
+
+ // MARK: - The Map
+
struct Map {
static let labelColor = Color.Theme.darkSlate
static let axisColor = Color.Theme.darkSlate
@@ -44,9 +56,4 @@ extension Color {
Color.Theme.hermosaPink,
]
}
-
- struct UI {
- static let foreground = Color("Foreground")
- static let background = Color("Background")
- }
}
diff --git a/Map/Presentation/Theme/Dimensions.swift b/Map/Presentation/Theme/Dimensions.swift
index 99a6207..2e1fd69 100644
--- a/Map/Presentation/Theme/Dimensions.swift
+++ b/Map/Presentation/Theme/Dimensions.swift
@@ -15,6 +15,48 @@
import Foundation
struct Dimensions {
- static let mapSize = CGSize(width: 1300.0, height: 1000.0)
- static let mapPadding: CGFloat = 42.0
+
+ // MARK: - Fonts
+
+ struct FontSize {
+ static let body: CGFloat = 16
+ static let title: CGFloat = 32
+ static let caption: CGFloat = 14
+ static let smallControl: CGFloat = 9
+
+ struct Map {
+ static let note: CGFloat = 12
+ static let axisLabel: CGFloat = 14
+ static let vertexLabel: CGFloat = 12
+ }
+ }
+
+ struct LineHeight {
+ static let body: CGFloat = 18
+ static let title: CGFloat = 38
+ static let caption: CGFloat = 18
+ static let smallControl: CGFloat = 18
+
+ struct Map {
+ static let note: CGFloat = 18
+ static let axisLabel: CGFloat = 18
+ static let vertexLabel: CGFloat = 18
+ }
+ }
+
+ // MARK: - Spacing
+
+ struct Spacing {
+ static let coziest: CGFloat = 3.0
+ static let cozy: CGFloat = 5.0
+ static let regular: CGFloat = 8.0
+ static let loose: CGFloat = 12.0
+ static let indulgent: CGFloat = 16.0
+ }
+
+ // MARK: - The Map
+ struct Map {
+ static let size = CGSize(width: 1300.0, height: 1000.0)
+ static let padding: CGFloat = 42.0
+ }
}
diff --git a/Map/Presentation/Theme/Font+theme.swift b/Map/Presentation/Theme/Font+theme.swift
index 1a9abb8..2fb56f7 100644
--- a/Map/Presentation/Theme/Font+theme.swift
+++ b/Map/Presentation/Theme/Font+theme.swift
@@ -15,6 +15,19 @@
import SwiftUI
extension Font {
+
+ static func libertinus(size: CGFloat) -> Font {
+ return .custom("Libertinus Serif", size: size)
+ }
+
+ static func ronzino(size: CGFloat) -> Font {
+ return .custom("Ronzino", size: size)
+ }
+
+ static func ronzinoMedium(size: CGFloat) -> Font {
+ return .custom("RonzinoMedium", size: size)
+ }
+
public struct Theme {
// Map
@@ -22,8 +35,30 @@ extension Font {
static let axisLabel = Font.system(size: 14, design: .serif)
static let vertexLabel = Font.system(size: 12, design: .serif)
- // UI
- static let smallControl = Font.system(size: 9)
- static let body = Font.system(size: 13)
+ struct Body {
+ static let regular = Font.ronzino(size: Dimensions.FontSize.body).weight(.regular)
+ static let emphasized = Font.ronzino(size: Dimensions.FontSize.body).weight(.medium)
+ }
+
+ struct Title {
+ static let emphasized = Font.libertinus(size: Dimensions.FontSize.title).weight(.medium)
+ }
+
+ struct Caption {
+ static let regular = Font.ronzino(size: Dimensions.FontSize.caption).weight(.regular)
+ static let emphasized = Font.ronzino(size: Dimensions.FontSize.caption).weight(.medium)
+ }
+
+ struct SmallControl {
+ static let regular = Font.ronzino(size: Dimensions.FontSize.smallControl).weight(.regular)
+ }
+
+ struct Map {
+ static let note = Font.libertinus(size: Dimensions.FontSize.Map.note).weight(.regular)
+ static let axisLabel = Font.libertinus(size: Dimensions.FontSize.Map.axisLabel).weight(
+ .regular)
+ static let vertexLabel = Font.libertinus(size: Dimensions.FontSize.Map.vertexLabel).weight(
+ .regular)
+ }
}
}
diff --git a/Map/Presentation/Theme/LibertinusSerif-Regular.otf b/Map/Presentation/Theme/LibertinusSerif-Regular.otf
new file mode 100644
index 0000000..508f3c0
--- /dev/null
+++ b/Map/Presentation/Theme/LibertinusSerif-Regular.otf
Binary files differ
diff --git a/Map/Presentation/Theme/Ronzino-Medium.otf b/Map/Presentation/Theme/Ronzino-Medium.otf
new file mode 100644
index 0000000..0be8dd5
--- /dev/null
+++ b/Map/Presentation/Theme/Ronzino-Medium.otf
Binary files differ
diff --git a/Map/Presentation/Theme/Ronzino-Regular.otf b/Map/Presentation/Theme/Ronzino-Regular.otf
new file mode 100644
index 0000000..8b70471
--- /dev/null
+++ b/Map/Presentation/Theme/Ronzino-Regular.otf
Binary files differ
diff --git a/MapTests/DebouncerTests.swift b/MapTests/DebouncerTests.swift
new file mode 100644
index 0000000..9ac36cd
--- /dev/null
+++ b/MapTests/DebouncerTests.swift
@@ -0,0 +1,71 @@
+// Copyright (C) 2024 Rubén Beltrán del Río
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import Foundation
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see https://map.tranquil.systems.
+import Testing
+
+@testable import Map
+
+struct DebouncerTests {
+
+ @Test func testDebouncerExecutesAfterDelay() async throws {
+ let debouncer = Debouncer(seconds: 0.1)
+ var executed = false
+
+ debouncer.debounce {
+ executed = true
+ }
+
+ #expect(executed == false)
+
+ try await Task.sleep(nanoseconds: 150_000_000) // 0.15 seconds
+
+ #expect(executed == true)
+ }
+
+ @Test func testDebouncerCancelsReplacement() async throws {
+ let debouncer = Debouncer(seconds: 0.1)
+ var firstExecuted = false
+ var secondExecuted = false
+
+ debouncer.debounce {
+ firstExecuted = true
+ }
+
+ debouncer.debounce {
+ secondExecuted = true
+ }
+
+ try await Task.sleep(nanoseconds: 150_000_000) // 0.15 seconds
+
+ #expect(firstExecuted == false)
+ #expect(secondExecuted == true)
+ }
+
+ @Test func testDebouncerMultipleReplacements() async throws {
+ let debouncer = Debouncer(seconds: 0.1)
+ var counter = 0
+
+ for i in 1...5 {
+ debouncer.debounce {
+ counter += i
+ }
+ }
+
+ try await Task.sleep(nanoseconds: 150_000_000) // 0.15 seconds
+
+ #expect(counter == 5)
+ }
+
+}
diff --git a/MapTests/MapDocumentTests.swift b/MapTests/MapDocumentTests.swift
new file mode 100644
index 0000000..7ae93fc
--- /dev/null
+++ b/MapTests/MapDocumentTests.swift
@@ -0,0 +1,81 @@
+// Copyright (C) 2024 Rubén Beltrán del Río
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import SwiftUI
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see https://map.tranquil.systems.
+import Testing
+import UniformTypeIdentifiers
+
+@testable import Map
+
+struct MapDocumentTests {
+
+ @Test func testMapDocumentInitialization() async throws {
+ let document = MapDocument()
+ #expect(
+ document.text == """
+ Anchor (60, 5) [x]
+ Node A (78, 23)
+ Node B (40, 30) [square]
+ Node C (65, 70)
+
+ Anchor -> Node A
+ Anchor -> Node B
+ Node A -> Node C
+ Node B -> Node C
+ """)
+ }
+
+ @Test func testMapDocumentInitializationWithText() async throws {
+ let testText = "Test map content"
+ let document = MapDocument(text: testText)
+ #expect(document.text == testText)
+ }
+
+ @Test func testMapDocumentReadableContentTypes() async throws {
+ let contentTypes = MapDocument.readableContentTypes
+ #expect(contentTypes.count == 1)
+ #expect(contentTypes.first == UTType.wmap)
+ }
+
+ @Test func testMapDocumentTextProperty() async throws {
+ let document = MapDocument(text: "Test content")
+ #expect(document.text == "Test content")
+
+ // Test that the document can handle different text content
+ let mapContent = "Node A (10, 20)\nNode B (30, 40)\nNode A -> Node B"
+ let mapDocument = MapDocument(text: mapContent)
+ #expect(mapDocument.text == mapContent)
+ }
+
+ @Test func testMapDocumentWithEmptyContent() async throws {
+ let document = MapDocument(text: "")
+ #expect(document.text == "")
+ }
+
+ @Test func testMapDocumentExportAsImage() async throws {
+ let document = MapDocument(text: "Node A (10, 20)\nNode B (30, 40)")
+
+ await MainActor.run {
+ let image = document.exportAsImage(withEvolution: .general)
+ #expect(image != nil)
+ #expect(image!.size.width > 0)
+ #expect(image!.size.height > 0)
+ }
+ }
+
+ @Test func testUTTypeExampleText() async throws {
+ let utType = UTType.wmap
+ #expect(utType.identifier == "systems.tranquil.map.wmap")
+ }
+}
diff --git a/MapTests/MapParserStrategyTests.swift b/MapTests/MapParserStrategyTests.swift
new file mode 100644
index 0000000..c9f92e8
--- /dev/null
+++ b/MapTests/MapParserStrategyTests.swift
@@ -0,0 +1,163 @@
+// Copyright (C) 2024 Rubén Beltrán del Río
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import CoreGraphics
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see https://map.tranquil.systems.
+import Testing
+
+@testable import Map
+
+struct MapParserStrategyTests {
+
+ @Test func testVertexParserStrategyCanHandle() async throws {
+ let strategy = VertexParserStrategy()
+
+ #expect(strategy.canHandle(line: "Node (10, 20)"))
+ #expect(strategy.canHandle(line: "NodeWithDecimals (30.5, 40.2)"))
+ #expect(strategy.canHandle(line: "Multi Word Node (0, 100)"))
+ #expect(strategy.canHandle(line: "Node C (50, 60) [square]"))
+
+ #expect(!strategy.canHandle(line: "Node A -> Node B"))
+ #expect(!strategy.canHandle(line: "[Note] (10, 20) Test"))
+ #expect(!strategy.canHandle(line: "Invalid line"))
+ }
+
+ @Test func testVertexParserStrategyHandle() async throws {
+ let strategy = VertexParserStrategy()
+ let vertices: [String: Vertex] = [:]
+
+ let (type, object) = strategy.handle(index: 0, line: "Test Node (25, 75)", vertices: vertices)
+
+ #expect(String(describing: type) == String(describing: Vertex.self))
+ let vertex = object as! Vertex
+ #expect(vertex.id == 0)
+ #expect(vertex.label == "Test Node")
+ #expect(vertex.position.x == 25.0)
+ #expect(vertex.position.y == 75.0)
+ #expect(vertex.shape == .circle)
+ }
+
+ @Test func testVertexParserStrategyHandleWithShape() async throws {
+ let strategy = VertexParserStrategy()
+ let vertices: [String: Vertex] = [:]
+
+ let (type, object) = strategy.handle(
+ index: 1, line: "Shaped Node (10, 20) [triangle]", vertices: vertices)
+
+ #expect(String(describing: type) == String(describing: Vertex.self))
+ let vertex = object as! Vertex
+ #expect(vertex.shape == .triangle)
+ }
+
+ @Test func testEdgeParserStrategyCanHandle() async throws {
+ let strategy = EdgeParserStrategy()
+
+ #expect(strategy.canHandle(line: "Node A -- Node B"))
+ #expect(strategy.canHandle(line: "Node A -> Node B"))
+ #expect(strategy.canHandle(line: "Multi Word A -> Multi Word B"))
+
+ #expect(!strategy.canHandle(line: "Node A (10, 20)"))
+ #expect(!strategy.canHandle(line: "[Note] (10, 20) Test"))
+ #expect(!strategy.canHandle(line: "Invalid line"))
+ }
+
+ @Test func testEdgeParserStrategyHandle() async throws {
+ let strategy = EdgeParserStrategy()
+ let vertices: [String: Vertex] = [
+ "Node A": Vertex(id: 0, label: "Node A", position: CGPoint(x: 10, y: 20)),
+ "Node B": Vertex(id: 1, label: "Node B", position: CGPoint(x: 30, y: 40)),
+ ]
+
+ let (type, object) = strategy.handle(index: 2, line: "Node A -> Node B", vertices: vertices)
+
+ #expect(String(describing: type) == String(describing: MapEdge.self))
+ let edge = object as! MapEdge
+ #expect(edge.id == 2)
+ #expect(edge.origin.x == 10.0)
+ #expect(edge.origin.y == 20.0)
+ #expect(edge.destination.x == 30.0)
+ #expect(edge.destination.y == 40.0)
+ #expect(edge.arrowhead == true)
+ }
+
+ @Test func testEdgeParserStrategyHandleWithMissingVertex() async throws {
+ let strategy = EdgeParserStrategy()
+ let vertices: [String: Vertex] = [
+ "Node A": Vertex(id: 0, label: "Node A", position: CGPoint(x: 10, y: 20))
+ ]
+
+ let (type, object) = strategy.handle(index: 2, line: "Node A -> Node B", vertices: vertices)
+
+ #expect(String(describing: type) == String(describing: NSObject.self))
+ #expect(object is NSObject)
+ }
+
+ @Test func testNoteParserStrategyCanHandle() async throws {
+ let strategy = NoteParserStrategy()
+
+ #expect(strategy.canHandle(line: "[Note] (10, 20) Test note"))
+ #expect(strategy.canHandle(line: "[Note] (50.5, 60.2) Multi word note"))
+
+ #expect(!strategy.canHandle(line: "Node A (10, 20)"))
+ #expect(!strategy.canHandle(line: "Node A -> Node B"))
+ #expect(!strategy.canHandle(line: "Invalid line"))
+ }
+
+ @Test func testBlockerParserStrategyCanHandle() async throws {
+ let strategy = BlockerParserStrategy()
+
+ #expect(strategy.canHandle(line: "[Blocker] Node A"))
+ #expect(strategy.canHandle(line: "[Blocker] Multi Word Node"))
+
+ #expect(!strategy.canHandle(line: "Node A (10, 20)"))
+ #expect(!strategy.canHandle(line: "[Note] (10, 20) Test"))
+ #expect(!strategy.canHandle(line: "Invalid line"))
+ }
+
+ @Test func testOpportunityParserStrategyCanHandle() async throws {
+ let strategy = OpportunityParserStrategy()
+
+ #expect(strategy.canHandle(line: "[Evolution] Node A +15"))
+ #expect(strategy.canHandle(line: "[Evolution] Node B -10"))
+ #expect(strategy.canHandle(line: "[Evolution] Multi Word Node +5.5"))
+
+ #expect(!strategy.canHandle(line: "Node A (10, 20)"))
+ #expect(!strategy.canHandle(line: "[Blocker] Node A"))
+ #expect(!strategy.canHandle(line: "Invalid line"))
+ }
+
+ @Test func testStageParserStrategyCanHandle() async throws {
+ let strategy = StageParserStrategy()
+
+ #expect(strategy.canHandle(line: "[I] 25"))
+ #expect(strategy.canHandle(line: "[II] 50.5"))
+ #expect(strategy.canHandle(line: "[III] 75"))
+
+ #expect(!strategy.canHandle(line: "[IV] 100"))
+ #expect(!strategy.canHandle(line: "Node A (10, 20)"))
+ #expect(!strategy.canHandle(line: "Invalid line"))
+ }
+
+ @Test func testGroupParserStrategyCanHandle() async throws {
+ let strategy = GroupParserStrategy()
+
+ #expect(strategy.canHandle(line: "[Group] Node A, Node B"))
+ #expect(strategy.canHandle(line: "[Group] Node A, Node B, Node C"))
+ #expect(strategy.canHandle(line: "[Group] Multi Word A, Multi Word B"))
+
+ #expect(!strategy.canHandle(line: "Node A (10, 20)"))
+ #expect(!strategy.canHandle(line: "[Note] (10, 20) Test"))
+ #expect(!strategy.canHandle(line: "Invalid line"))
+ }
+
+}
diff --git a/MapTests/MapTests.swift b/MapTests/MapTests.swift
index 5a8f781..0dbf63b 100644
--- a/MapTests/MapTests.swift
+++ b/MapTests/MapTests.swift
@@ -1,11 +1,169 @@
+import CoreGraphics
import Testing
@testable import Map
struct MapTests {
- @Test func testExample() async throws {
- // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+ @Test func testMapParserWithEmptyInput() async throws {
+ let result = MapParser.parse(content: "")
+ #expect(result.vertices.isEmpty)
+ #expect(result.edges.isEmpty)
+ #expect(result.notes.isEmpty)
+ #expect(result.blockers.isEmpty)
+ #expect(result.opportunities.isEmpty)
+ #expect(result.groups.isEmpty)
+ }
+
+ @Test func testMapParserWithSingleVertex() async throws {
+ let content = "Node A (10, 20)"
+ let result = MapParser.parse(content: content)
+
+ #expect(result.vertices.count == 1)
+ let vertex = result.vertices.first!
+ #expect(vertex.label == "Node A")
+ #expect(vertex.position.x == 10.0)
+ #expect(vertex.position.y == 20.0)
+ #expect(vertex.shape == .circle)
+ }
+
+ @Test func testMapParserWithVertexWithShape() async throws {
+ let content = "Node B (30, 40) [square]"
+ let result = MapParser.parse(content: content)
+
+ #expect(result.vertices.count == 1)
+ let vertex = result.vertices.first!
+ #expect(vertex.label == "Node B")
+ #expect(vertex.position.x == 30.0)
+ #expect(vertex.position.y == 40.0)
+ #expect(vertex.shape == .square)
+ }
+
+ @Test func testMapParserWithEdge() async throws {
+ let content = """
+ Node A (10, 20)
+ Node B (30, 40)
+ Node A -- Node B
+ """
+ let result = MapParser.parse(content: content)
+
+ #expect(result.vertices.count == 2)
+ #expect(result.edges.count == 1)
+ let edge = result.edges.first!
+ #expect(edge.origin.x == 10.0)
+ #expect(edge.origin.y == 20.0)
+ #expect(edge.destination.x == 30.0)
+ #expect(edge.destination.y == 40.0)
+ #expect(edge.arrowhead == false)
+ }
+
+ @Test func testMapParserWithArrowEdge() async throws {
+ let content = """
+ Node A (10, 20)
+ Node B (30, 40)
+ Node A -> Node B
+ """
+ let result = MapParser.parse(content: content)
+
+ #expect(result.edges.count == 1)
+ let edge = result.edges.first!
+ #expect(edge.arrowhead == true)
+ }
+
+ @Test func testMapParserWithNote() async throws {
+ let content = "[Note] (50, 60) This is a test note"
+ let result = MapParser.parse(content: content)
+
+ #expect(result.notes.count == 1)
+ let note = result.notes.first!
+ #expect(note.position.x == 50.0)
+ #expect(note.position.y == 60.0)
+ #expect(note.text == "This is a test note")
+ }
+
+ @Test func testMapParserWithBlocker() async throws {
+ let content = """
+ Node A (10, 20)
+ [Blocker] Node A
+ """
+ let result = MapParser.parse(content: content)
+
+ #expect(result.blockers.count == 1)
+ let blocker = result.blockers.first!
+ #expect(blocker.position.x == 10.0)
+ #expect(blocker.position.y == 20.0)
+ }
+
+ @Test func testMapParserWithOpportunity() async throws {
+ let content = """
+ Node A (10, 20)
+ [Evolution] Node A +15
+ """
+ let result = MapParser.parse(content: content)
+
+ #expect(result.opportunities.count == 1)
+ let opportunity = result.opportunities.first!
+ #expect(opportunity.origin.x == 10.0)
+ #expect(opportunity.origin.y == 20.0)
+ #expect(opportunity.destination.x == 25.0)
+ #expect(opportunity.destination.y == 20.0)
+ }
+
+ @Test func testMapParserWithGroup() async throws {
+ let content = """
+ Node A (10, 20)
+ Node B (30, 40)
+ Node C (50, 60)
+ [Group] Node A, Node B, Node C
+ """
+ let result = MapParser.parse(content: content)
+
+ #expect(result.groups.count == 1)
+ let group = result.groups.first!
+ #expect(group.count == 3)
+ #expect(group.map { $0.label }.contains("Node A"))
+ #expect(group.map { $0.label }.contains("Node B"))
+ #expect(group.map { $0.label }.contains("Node C"))
+ }
+
+ @Test func testMapParserWithStages() async throws {
+ let content = """
+ [I] 15
+ [II] 35
+ [III] 80
+ """
+ let result = MapParser.parse(content: content)
+
+ #expect(result.stages.count == 3)
+ #expect(result.stages[0] == 15.0)
+ #expect(result.stages[1] == 35.0)
+ #expect(result.stages[2] == 80.0)
+ }
+
+ @Test func testMapParserWithComplexMap() async throws {
+ let content = """
+ Node A (10, 20)
+ Node B (30, 40) [square]
+ Node C (50, 60)
+ Node A -> Node B
+ Node B -- Node C
+ [Note] (70, 80) Complex map example
+ [Blocker] Node A
+ [Evolution] Node B +10
+ [Group] Node A, Node C
+ [I] 25
+ [II] 75
+ """
+ let result = MapParser.parse(content: content)
+
+ #expect(result.vertices.count == 3)
+ #expect(result.edges.count == 2)
+ #expect(result.notes.count == 1)
+ #expect(result.blockers.count == 1)
+ #expect(result.opportunities.count == 1)
+ #expect(result.groups.count == 1)
+ #expect(result.stages[0] == 25.0)
+ #expect(result.stages[1] == 75.0)
}
}
diff --git a/MapTests/StageTests.swift b/MapTests/StageTests.swift
new file mode 100644
index 0000000..05812fe
--- /dev/null
+++ b/MapTests/StageTests.swift
@@ -0,0 +1,119 @@
+// Copyright (C) 2024 Rubén Beltrán del Río
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see https://map.tranquil.systems.
+import Testing
+
+@testable import Map
+
+struct StageTests {
+
+ @Test func testStageTypesGrouping() async throws {
+ #expect(StageType.types.count == 4)
+ #expect(StageType.types.contains(.general))
+ #expect(StageType.types.contains(.practice))
+ #expect(StageType.types.contains(.data))
+ #expect(StageType.types.contains(.knowledge))
+
+ #expect(StageType.characteristics.count == 3)
+ #expect(StageType.characteristics.contains(.ubiquity))
+ #expect(StageType.characteristics.contains(.certainty))
+ #expect(StageType.characteristics.contains(.publicationTypes))
+
+ #expect(StageType.properties.count == 12)
+ #expect(StageType.properties.contains(.market))
+ #expect(StageType.properties.contains(.efficiency))
+
+ #expect(StageType.custom.count == 1)
+ #expect(StageType.custom.contains(.behavior))
+ }
+
+ @Test func testStageTypeIdentifiable() async throws {
+ #expect(StageType.general.id == "general")
+ #expect(StageType.practice.id == "practice")
+ #expect(StageType.behavior.id == "behavior")
+ }
+
+ @Test func testStageCreationGeneral() async throws {
+ let stage = Stage.stages(.general)
+ #expect(stage.i == "Genesis")
+ #expect(stage.ii == "Custom")
+ #expect(stage.iii == "Product (+rental)")
+ #expect(stage.iv == "Commodity (+utility)")
+ }
+
+ @Test func testStageCreationPractice() async throws {
+ let stage = Stage.stages(.practice)
+ #expect(stage.i == "Novel")
+ #expect(stage.ii == "Emerging")
+ #expect(stage.iii == "Good")
+ #expect(stage.iv == "Best")
+ }
+
+ @Test func testStageCreationData() async throws {
+ let stage = Stage.stages(.data)
+ #expect(stage.i == "Unmodelled")
+ #expect(stage.ii == "Divergent")
+ #expect(stage.iii == "Convergent")
+ #expect(stage.iv == "Modelled")
+ }
+
+ @Test func testStageCreationKnowledge() async throws {
+ let stage = Stage.stages(.knowledge)
+ #expect(stage.i == "Concept")
+ #expect(stage.ii == "Hypothesis")
+ #expect(stage.iii == "Theory")
+ #expect(stage.iv == "Accepted")
+ }
+
+ @Test func testStageCreationBehavior() async throws {
+ let stage = Stage.stages(.behavior)
+ #expect(stage.i == "Uncertain when to use")
+ #expect(stage.ii == "Learning when to use")
+ #expect(stage.iii == "Learning through use")
+ #expect(stage.iv == "Known / common usage")
+ }
+
+ @Test func testStageTitleGeneral() async throws {
+ let title = Stage.title(.general)
+ #expect(title == "Activities")
+ }
+
+ @Test func testStageTitlePractice() async throws {
+ let title = Stage.title(.practice)
+ #expect(title == "Practice")
+ }
+
+ @Test func testStageTitleBehavior() async throws {
+ let title = Stage.title(.behavior)
+ #expect(title == "Behavior")
+ }
+
+ @Test func testAllStageTypesHaveTitles() async throws {
+ for stageType in StageType.allCases {
+ let title = Stage.title(stageType)
+ #expect(!title.isEmpty)
+ }
+ }
+
+ @Test func testAllStageTypesHaveStages() async throws {
+ for stageType in StageType.allCases {
+ let stage = Stage.stages(stageType)
+ #expect(!stage.i.isEmpty)
+ #expect(!stage.ii.isEmpty)
+ #expect(!stage.iii.isEmpty)
+ #expect(!stage.iv.isEmpty)
+ }
+ }
+
+}
diff --git a/MapUITests/MapUITests.swift b/MapUITests/MapUITests.swift
index 2f6c35d..540d928 100644
--- a/MapUITests/MapUITests.swift
+++ b/MapUITests/MapUITests.swift
@@ -3,34 +3,287 @@ import XCTest
final class MapUITests: XCTestCase {
override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the class.
-
- // In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
-
- // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
+ Task { @MainActor in
+ // Close any test documents we may have created to avoid leaving them open
+ let app = XCUIApplication()
+ if app.state == .runningForeground {
+ // Close all windows except the first one (if any existed before tests)
+ let windowCount = app.windows.count
+ if windowCount > 1 {
+ for _ in 1..<windowCount {
+ app.typeKey("w", modifierFlags: .command)
+ }
+ }
+ }
+ }
+ }
+
+ @MainActor
+ private func createNewDocument(app: XCUIApplication) {
+ // Create a new document to ensure we start with a clean slate
+ app.typeKey("n", modifierFlags: .command)
+
+ // Wait for the new document window to appear
+ let newWindow = app.windows.element(boundBy: app.windows.count - 1)
+ XCTAssertTrue(newWindow.waitForExistence(timeout: 3.0))
}
@MainActor
- func testExample() throws {
- // UI tests must launch the application that they test.
+ func testAppLaunchesSuccessfully() throws {
let app = XCUIApplication()
app.launch()
- // Use XCTAssert and related functions to verify your tests produce the correct results.
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Verify the app window appears
+ XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 5.0))
+
+ // Verify the text editor is present
+ XCTAssertTrue(app.textViews.firstMatch.exists)
+
+ // Verify the evolution picker is present
+ XCTAssertTrue(app.popUpButtons.firstMatch.exists)
+ }
+
+ @MainActor
+ func testMapEditorTextInput() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ let textView = app.textViews.firstMatch
+ XCTAssertTrue(textView.waitForExistence(timeout: 5.0))
+
+ // Clear existing text and input new map content
+ textView.click()
+ textView.typeKey("a", modifierFlags: .command) // Select all
+ textView.typeText("Node A (10, 20)\nNode B (30, 40)\nNode A -> Node B")
+
+ // Verify the text was entered
+ XCTAssertTrue(textView.value as? String != nil)
+ }
+
+ @MainActor
+ func testEvolutionPickerFunctionality() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Look specifically for the evolution picker in the toolbar, not just any popup
+ let toolbar = app.toolbars.firstMatch
+ XCTAssertTrue(toolbar.waitForExistence(timeout: 5.0))
+
+ let evolutionPicker = toolbar.popUpButtons.firstMatch
+ XCTAssertTrue(evolutionPicker.waitForExistence(timeout: 5.0))
+
+ // Store the original selection to restore it later
+ let originalSelection = evolutionPicker.value as? String
+
+ // Click the evolution picker
+ evolutionPicker.click()
+
+ // Verify dropdown menu appears with options
+ let menu = app.menus.firstMatch
+ XCTAssertTrue(menu.waitForExistence(timeout: 2.0))
+
+ // Check that there are menu items
+ XCTAssertTrue(menu.menuItems.count > 0)
+
+ // Test selecting a different option temporarily
+ if menu.menuItems["Practice"].exists {
+ menu.menuItems["Practice"].click()
+
+ // Verify the selection changed
+ XCTAssertEqual(evolutionPicker.value as? String, "Practice")
+
+ // Restore original selection if we had one and it's different
+ if let original = originalSelection, original != "Practice" {
+ evolutionPicker.click()
+ let restoreMenu = app.menus.firstMatch
+ if restoreMenu.waitForExistence(timeout: 2.0) && restoreMenu.menuItems[original].exists {
+ restoreMenu.menuItems[original].click()
+ }
+ }
+ } else {
+ // If Practice doesn't exist, just close the menu
+ app.typeKey(XCUIKeyboardKey.escape, modifierFlags: [])
+ }
+ }
+
+ @MainActor
+ func testZoomSliderFunctionality() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Find the zoom slider
+ let slider = app.sliders.firstMatch
+ XCTAssertTrue(slider.waitForExistence(timeout: 5.0))
+
+ // Get initial value and verify slider exists
+ XCTAssertTrue(slider.exists, "Slider should exist")
+
+ // Adjust the slider
+ slider.adjust(toNormalizedSliderPosition: 0.8)
+
+ // For zoom sliders, we can verify the change by checking if the adjustment was successful
+ // Since macOS sliders sometimes don't expose their values properly in UI tests,
+ // we'll just verify that the slider can be interacted with
+ XCTAssertTrue(slider.exists, "Slider should still exist after adjustment")
+ }
+
+ @MainActor
+ func testMenuBarCommands() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document first to have a window available
+ createNewDocument(app: app)
+
+ // Test File menu
+ let fileMenu = app.menuBars.menuBarItems["File"]
+ if fileMenu.exists {
+ fileMenu.click()
+
+ // Look for New Document command
+ let newDocumentItem = app.menus.menuItems["New Document"]
+ if newDocumentItem.exists {
+ XCTAssertTrue(newDocumentItem.isEnabled)
+ }
+
+ // Press Escape to close menu instead of clicking on window
+ app.typeKey(XCUIKeyboardKey.escape, modifierFlags: [])
+ }
+ }
+
+ @MainActor
+ func testKeyboardShortcuts() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ let window = app.windows.firstMatch
+ XCTAssertTrue(window.waitForExistence(timeout: 5.0))
+
+ // Test zoom in shortcut (Cmd+Plus)
+ window.typeKey("+", modifierFlags: .command)
+
+ // Test zoom out shortcut (Cmd+Minus)
+ window.typeKey("-", modifierFlags: .command)
+
+ // Test search shortcut (Cmd+F)
+ window.typeKey("f", modifierFlags: .command)
+
+ // Check if search bar appears
+ let searchField = app.textFields["Search"]
+ if searchField.waitForExistence(timeout: 2.0) {
+ XCTAssertTrue(searchField.exists)
+
+ // Type in search field
+ searchField.typeText("Node")
+
+ // Press Escape to close search
+ searchField.typeKey(XCUIKeyboardKey.escape, modifierFlags: [])
+ }
+ }
+
+ @MainActor
+ func testSplitViewResizing() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ let window = app.windows.firstMatch
+ XCTAssertTrue(window.waitForExistence(timeout: 5.0))
+
+ // Find split view divider
+ let splitViews = app.splitGroups
+ if splitViews.count > 0 {
+ let splitView = splitViews.firstMatch
+ XCTAssertTrue(splitView.exists)
+
+ // Test that we can interact with the split view
+ // Note: Actual dragging of split view dividers is complex in UI tests
+ // This test mainly verifies the split view structure exists
+ }
+ }
+
+ @MainActor
+ func testDocumentSaveLoad() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ let textView = app.textViews.firstMatch
+ XCTAssertTrue(textView.waitForExistence(timeout: 5.0))
+
+ // Enter test content
+ textView.click()
+ textView.typeKey("a", modifierFlags: .command)
+ textView.typeText("Test Map Content\nNode A (10, 20)")
+
+ // Test Save command (Cmd+S) - this will open save dialog for new document
+ app.typeKey("s", modifierFlags: .command)
+
+ // If save dialog appears, dismiss it to avoid saving test files
+ let saveDialog = app.sheets.firstMatch
+ if saveDialog.waitForExistence(timeout: 2.0) {
+ let cancelButton = saveDialog.buttons["Cancel"]
+ if cancelButton.exists {
+ cancelButton.click()
+ }
+ }
+ }
+
+ @MainActor
+ func testMapRenderingArea() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Add some map content
+ let textView = app.textViews.firstMatch
+ XCTAssertTrue(textView.waitForExistence(timeout: 5.0))
+
+ textView.click()
+ textView.typeKey("a", modifierFlags: .command)
+ textView.typeText("Node A (10, 20)\nNode B (30, 40)\nNode A -> Node B")
+
+ // Verify the map rendering area exists
+ let scrollViews = app.scrollViews
+ XCTAssertTrue(scrollViews.count > 0)
+
+ // The map rendering should be in a scroll view
+ let mapScrollView = scrollViews.element(boundBy: scrollViews.count - 1)
+ XCTAssertTrue(mapScrollView.exists)
}
@MainActor
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
- // This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
+
}
diff --git a/MapUITests/SearchUITests.swift b/MapUITests/SearchUITests.swift
new file mode 100644
index 0000000..1891678
--- /dev/null
+++ b/MapUITests/SearchUITests.swift
@@ -0,0 +1,226 @@
+// Copyright (C) 2024 Rubén Beltrán del Río
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see https://map.tranquil.systems.
+import XCTest
+
+final class SearchUITests: XCTestCase {
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ override func tearDownWithError() throws {
+ Task { @MainActor in
+ // Close any test documents we may have created to avoid leaving them open
+ let app = XCUIApplication()
+ if app.state == .runningForeground {
+ // Close all windows except the first one (if any existed before tests)
+ let windowCount = app.windows.count
+ if windowCount > 1 {
+ for _ in 1..<windowCount {
+ app.typeKey("w", modifierFlags: .command)
+ }
+ }
+ }
+ }
+ }
+
+ @MainActor
+ private func createNewDocument(app: XCUIApplication) {
+ // Create a new document to ensure we start with a clean slate
+ app.typeKey("n", modifierFlags: .command)
+
+ // Wait for the new document window to appear
+ let newWindow = app.windows.element(boundBy: app.windows.count - 1)
+ XCTAssertTrue(newWindow.waitForExistence(timeout: 3.0))
+ }
+
+ @MainActor
+ func testSearchBarAppearance() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ let window = app.windows.firstMatch
+ XCTAssertTrue(window.waitForExistence(timeout: 5.0))
+
+ // Trigger search with Cmd+F
+ window.typeKey("f", modifierFlags: .command)
+
+ // Verify search bar appears (SearchBar uses a TextField with placeholder "Search")
+ let searchField = app.textFields["Search"]
+ XCTAssertTrue(searchField.waitForExistence(timeout: 2.0))
+
+ // Verify search navigation buttons appear - the search bar should contain multiple buttons
+ // We'll check for the Done button which should be most reliable
+ let doneButton = app.buttons["Done"]
+ XCTAssertTrue(doneButton.waitForExistence(timeout: 2.0))
+ }
+
+ @MainActor
+ func testSearchFunctionality() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Add test content with searchable terms
+ let textView = app.textViews.firstMatch
+ XCTAssertTrue(textView.waitForExistence(timeout: 5.0))
+
+ textView.click()
+ textView.typeKey("a", modifierFlags: .command)
+ textView.typeText("Node A (10, 20)\nNode B (30, 40)\nNode C (50, 60)\nNode A -> Node B")
+
+ // Open search
+ app.typeKey("f", modifierFlags: .command)
+
+ let searchField = app.textFields["Search"]
+ XCTAssertTrue(searchField.waitForExistence(timeout: 2.0))
+
+ // Search for "Node"
+ searchField.typeText("Node")
+
+ // Verify search results are highlighted (this is visual, hard to test directly)
+ // We can at least verify the search field contains our text
+ XCTAssertEqual(searchField.value as? String, "Node")
+ }
+
+ @MainActor
+ func testSearchNavigation() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Add test content with multiple instances of search term
+ let textView = app.textViews.firstMatch
+ XCTAssertTrue(textView.waitForExistence(timeout: 5.0))
+
+ textView.click()
+ textView.typeKey("a", modifierFlags: .command)
+ textView.typeText("Test Test Test\nAnother Test line\nFinal Test here")
+
+ // Open search
+ app.typeKey("f", modifierFlags: .command)
+
+ let searchField = app.textFields["Search"]
+ XCTAssertTrue(searchField.waitForExistence(timeout: 2.0))
+
+ // Search for "Test"
+ searchField.typeText("Test")
+
+ // Try navigation with chevron buttons
+ let nextButton = app.buttons.containing(.image, identifier: "chevron.right").firstMatch
+ let previousButton = app.buttons.containing(.image, identifier: "chevron.left").firstMatch
+
+ if nextButton.waitForExistence(timeout: 2.0) {
+ nextButton.click()
+ }
+
+ if previousButton.waitForExistence(timeout: 2.0) {
+ previousButton.click()
+ }
+ }
+
+ @MainActor
+ func testSearchDismissal() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ let window = app.windows.firstMatch
+ XCTAssertTrue(window.waitForExistence(timeout: 5.0))
+
+ // Open search
+ window.typeKey("f", modifierFlags: .command)
+
+ let searchField = app.textFields["Search"]
+ XCTAssertTrue(searchField.waitForExistence(timeout: 2.0))
+
+ // Type something
+ searchField.typeText("test")
+
+ // Dismiss with Escape
+ searchField.typeKey(XCUIKeyboardKey.escape, modifierFlags: [])
+
+ // Verify search bar is dismissed
+ XCTAssertFalse(searchField.waitForExistence(timeout: 1.0))
+ }
+
+ @MainActor
+ func testSearchWithEmptyResults() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Add test content
+ let textView = app.textViews.firstMatch
+ XCTAssertTrue(textView.waitForExistence(timeout: 5.0))
+
+ textView.click()
+ textView.typeKey("a", modifierFlags: .command)
+ textView.typeText("Node A (10, 20)\nNode B (30, 40)")
+
+ // Open search
+ app.typeKey("f", modifierFlags: .command)
+
+ let searchField = app.textFields["Search"]
+ XCTAssertTrue(searchField.waitForExistence(timeout: 2.0))
+
+ // Search for something that doesn't exist
+ searchField.typeText("XYZ123")
+
+ // Verify search field still exists and contains our search term
+ XCTAssertEqual(searchField.value as? String, "XYZ123")
+ }
+
+ @MainActor
+ func testSearchCaseSensitivity() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Create new document to avoid interfering with existing files
+ createNewDocument(app: app)
+
+ // Add test content with mixed case
+ let textView = app.textViews.firstMatch
+ XCTAssertTrue(textView.waitForExistence(timeout: 5.0))
+
+ textView.click()
+ textView.typeKey("a", modifierFlags: .command)
+ textView.typeText("Node A (10, 20)\nnode B (30, 40)\nNODE C (50, 60)")
+
+ // Open search
+ app.typeKey("f", modifierFlags: .command)
+
+ let searchField = app.textFields["Search"]
+ XCTAssertTrue(searchField.waitForExistence(timeout: 2.0))
+
+ // Search for "node" (lowercase)
+ searchField.typeText("node")
+
+ // The search should be case-insensitive based on the implementation
+ XCTAssertEqual(searchField.value as? String, "node")
+ }
+
+}
diff --git a/README.md b/README.md
index e64c16a..8726c84 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,11 @@ A wardley mapping tool: Write some text, get a diagram
## Building
-Open the project in Xcode and press buithe run button
+Open the project in Xcode and press the run button (or `make build`)
+
+## Building
+
+Open the project in Xcode and press the test button (or `make test`)
## Formatting and Linting